import { SendIcon, StopCircleIcon, X, ChevronDown, ChevronUp, AlertTriangle, AlertOctagon, FileText, Check, Loader2, Package, FileX, SendToBack, Database, ChevronsUpDown, ChevronsDownUp, BarChart2, Paperclip, } from "lucide-react"; import type React from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import { useSettings } from "@/hooks/useSettings"; import { IpcClient } from "@/ipc/ipc_client"; import { chatInputValueAtom, chatMessagesAtom, selectedChatIdAtom, } from "@/atoms/chatAtoms"; import { atom, useAtom, useSetAtom, useAtomValue } from "jotai"; import { useStreamChat } from "@/hooks/useStreamChat"; import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { Button } from "@/components/ui/button"; import { useProposal } from "@/hooks/useProposal"; import { ActionProposal, Proposal, SuggestedAction, FileChange, SqlQuery, } from "@/lib/schemas"; import type { Message } from "@/ipc/ipc_types"; import { isPreviewOpenAtom } from "@/atoms/viewAtoms"; import { useRunApp } from "@/hooks/useRunApp"; import { AutoApproveSwitch } from "../AutoApproveSwitch"; import { usePostHog } from "posthog-js/react"; import { CodeHighlight } from "./CodeHighlight"; import { TokenBar } from "./TokenBar"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "../ui/tooltip"; import { useNavigate } from "@tanstack/react-router"; import { useVersions } from "@/hooks/useVersions"; import { useAttachments } from "@/hooks/useAttachments"; import { AttachmentsList } from "./AttachmentsList"; import { DragDropOverlay } from "./DragDropOverlay"; import { showError, showExtraFilesToast } from "@/lib/toast"; import { ChatInputControls } from "../ChatInputControls"; const showTokenBarAtom = atom(false); export function ChatInput({ chatId }: { chatId?: number }) { const posthog = usePostHog(); const [inputValue, setInputValue] = useAtom(chatInputValueAtom); const textareaRef = useRef(null); const { settings } = useSettings(); const appId = useAtomValue(selectedAppIdAtom); const { refreshVersions } = useVersions(appId); const { streamMessage, isStreaming, setIsStreaming, error, setError } = useStreamChat(); const [showError, setShowError] = useState(true); const [isApproving, setIsApproving] = useState(false); // State for approving const [isRejecting, setIsRejecting] = useState(false); // State for rejecting const [, setMessages] = useAtom(chatMessagesAtom); const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom); const [showTokenBar, setShowTokenBar] = useAtom(showTokenBarAtom); // Use the attachments hook const { attachments, fileInputRef, isDraggingOver, handleAttachmentClick, handleFileChange, removeAttachment, handleDragOver, handleDragLeave, handleDrop, clearAttachments, } = useAttachments(); // Use the hook to fetch the proposal const { proposalResult, isLoading: isProposalLoading, error: proposalError, refreshProposal, } = useProposal(chatId); const { proposal, messageId } = proposalResult ?? {}; const adjustHeight = () => { const textarea = textareaRef.current; if (textarea) { textarea.style.height = "0px"; const scrollHeight = textarea.scrollHeight; textarea.style.height = `${scrollHeight + 4}px`; } }; useEffect(() => { adjustHeight(); }, [inputValue]); useEffect(() => { if (error) { setShowError(true); } }, [error]); const fetchChatMessages = useCallback(async () => { if (!chatId) { setMessages([]); return; } const chat = await IpcClient.getInstance().getChat(chatId); setMessages(chat.messages); }, [chatId, setMessages]); const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSubmit(); } }; const handleSubmit = async () => { if ( (!inputValue.trim() && attachments.length === 0) || isStreaming || !chatId ) { return; } const currentInput = inputValue; setInputValue(""); // Send message with attachments and clear them after sending await streamMessage({ prompt: currentInput, chatId, attachments, redo: false, }); clearAttachments(); posthog.capture("chat:submit"); }; const handleCancel = () => { if (chatId) { IpcClient.getInstance().cancelChatStream(chatId); } setIsStreaming(false); }; const dismissError = () => { setShowError(false); }; const handleApprove = async () => { if (!chatId || !messageId || isApproving || isRejecting || isStreaming) return; console.log( `Approving proposal for chatId: ${chatId}, messageId: ${messageId}`, ); setIsApproving(true); posthog.capture("chat:approve"); try { const result = await IpcClient.getInstance().approveProposal({ chatId, messageId, }); if (result.extraFiles) { showExtraFilesToast({ files: result.extraFiles, error: result.extraFilesError, }); } } catch (err) { console.error("Error approving proposal:", err); setError((err as Error)?.message || "An error occurred while approving"); } finally { setIsApproving(false); setIsPreviewOpen(true); refreshVersions(); // Keep same as handleReject refreshProposal(); fetchChatMessages(); } }; const handleReject = async () => { if (!chatId || !messageId || isApproving || isRejecting || isStreaming) return; console.log( `Rejecting proposal for chatId: ${chatId}, messageId: ${messageId}`, ); setIsRejecting(true); posthog.capture("chat:reject"); try { await IpcClient.getInstance().rejectProposal({ chatId, messageId, }); } catch (err) { console.error("Error rejecting proposal:", err); setError((err as Error)?.message || "An error occurred while rejecting"); } finally { setIsRejecting(false); // Keep same as handleApprove refreshProposal(); fetchChatMessages(); } }; if (!settings) { return null; // Or loading state } return ( <> {error && showError && (
{error}
)} {/* Display loading or error state for proposal */} {isProposalLoading && (
Loading proposal...
)} {proposalError && (
Error loading proposal: {proposalError}
)}
{/* Only render ChatInputActions if proposal is loaded */} {proposal && proposalResult?.chatId === chatId && ( )} {/* Use the AttachmentsList component */} {/* Use the DragDropOverlay component */}