import React from "react"; import { useNavigate, useNavigation } from "react-router"; import { useDispatch, useSelector } from "react-redux"; import { useMutation } from "@tanstack/react-query"; import posthog from "posthog-js"; import { RootState } from "#/store"; import { addFile, removeFile, setInitialQuery, } from "#/state/initial-query-slice"; import OpenHands from "#/api/open-hands"; import { useAuth } from "#/context/auth-context"; import { useSettings } from "#/context/settings-context"; import { SuggestionBubble } from "#/components/features/suggestions/suggestion-bubble"; import { SUGGESTIONS } from "#/utils/suggestions"; import { convertImageToBase64 } from "#/utils/convert-image-to-base-64"; import { ChatInput } from "#/components/features/chat/chat-input"; import { getRandomKey } from "#/utils/get-random-key"; import { cn } from "#/utils/utils"; import { AttachImageLabel } from "../features/images/attach-image-label"; import { ImageCarousel } from "../features/images/image-carousel"; import { UploadImageInput } from "../features/images/upload-image-input"; import { LoadingSpinner } from "./loading-spinner"; interface TaskFormProps { ref: React.RefObject; } export function TaskForm({ ref }: TaskFormProps) { const dispatch = useDispatch(); const navigation = useNavigation(); const navigate = useNavigate(); const { gitHubToken } = useAuth(); const { settings } = useSettings(); const { selectedRepository, files } = useSelector( (state: RootState) => state.initialQuery, ); const [text, setText] = React.useState(""); const [suggestion, setSuggestion] = React.useState( getRandomKey(SUGGESTIONS["non-repo"]), ); const [inputIsFocused, setInputIsFocused] = React.useState(false); const newConversationMutation = useMutation({ mutationFn: (variables: { q?: string }) => { if (variables.q) dispatch(setInitialQuery(variables.q)); return OpenHands.newConversation({ githubToken: gitHubToken || undefined, selectedRepository: selectedRepository || undefined, args: settings || undefined, }); }, onSuccess: ({ conversation_id: conversationId }, { q }) => { posthog.capture("initial_query_submitted", { entry_point: "task_form", query_character_length: q?.length, has_repository: !!selectedRepository, has_files: files.length > 0, }); navigate(`/conversations/${conversationId}`); }, }); const onRefreshSuggestion = () => { const suggestions = SUGGESTIONS["non-repo"]; // remove current suggestion to avoid refreshing to the same suggestion const suggestionCopy = { ...suggestions }; delete suggestionCopy[suggestion]; const key = getRandomKey(suggestionCopy); setSuggestion(key); }; const onClickSuggestion = () => { const suggestions = SUGGESTIONS["non-repo"]; const value = suggestions[suggestion]; setText(value); }; const placeholder = React.useMemo(() => { if (selectedRepository) { return `What would you like to change in ${selectedRepository}?`; } return "What do you want to build?"; }, [selectedRepository]); const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); const formData = new FormData(event.currentTarget); const q = formData.get("q")?.toString(); newConversationMutation.mutate({ q }); }; return (
{newConversationMutation.isPending ? (
) : ( { if (typeof ref !== "function") ref?.current?.requestSubmit(); }} onChange={(message) => setText(message)} onFocus={() => setInputIsFocused(true)} onBlur={() => setInputIsFocused(false)} onImagePaste={async (imageFiles) => { const promises = imageFiles.map(convertImageToBase64); const base64Images = await Promise.all(promises); base64Images.forEach((base64) => { dispatch(addFile(base64)); }); }} placeholder={placeholder} value={text} maxRows={15} showButton={!!text} className="text-[17px] leading-5 py-[17px]" buttonClassName="pb-[17px]" disabled={navigation.state === "submitting"} /> )}
{ const promises = uploadedFiles.map(convertImageToBase64); const base64Images = await Promise.all(promises); base64Images.forEach((base64) => { dispatch(addFile(base64)); }); }} label={} /> {files.length > 0 && ( dispatch(removeFile(index))} /> )}
); }