diff options
Diffstat (limited to 'makima/frontend/src/components/files')
| -rw-r--r-- | makima/frontend/src/components/files/CliInput.tsx | 483 |
1 files changed, 0 insertions, 483 deletions
diff --git a/makima/frontend/src/components/files/CliInput.tsx b/makima/frontend/src/components/files/CliInput.tsx deleted file mode 100644 index 47e7616..0000000 --- a/makima/frontend/src/components/files/CliInput.tsx +++ /dev/null @@ -1,483 +0,0 @@ -import { useState, useCallback, useRef, useEffect } from "react"; -import { - chatWithFile, - type BodyElement, - type LlmModel, - type ChatMessage, - type UserQuestion, - type UserAnswer, -} from "../../lib/api"; -import { SimpleMarkdown } from "../SimpleMarkdown"; -import type { FocusedElement } from "./FileDetail"; - -interface CliInputProps { - fileId: string; - onUpdate: (body: BodyElement[], summary: string | null) => void; - focusedElement?: FocusedElement | null; - onClearFocus?: () => void; - suggestedPrompt?: string | null; - onClearSuggestedPrompt?: () => void; -} - -interface Message { - id: string; - type: "user" | "assistant" | "error" | "question"; - content: string; - toolCalls?: { name: string; success: boolean; message: string }[]; - questions?: UserQuestion[]; -} - -const MODEL_OPTIONS: { value: LlmModel; label: string }[] = [ - { value: "claude-opus", label: "Claude Opus" }, - { value: "claude-sonnet", label: "Claude Sonnet" }, - { value: "groq", label: "Groq Kimi" }, -]; - -export function CliInput({ fileId, onUpdate, focusedElement, onClearFocus, suggestedPrompt, onClearSuggestedPrompt }: CliInputProps) { - const [input, setInput] = useState(""); - const [loading, setLoading] = useState(false); - const [messages, setMessages] = useState<Message[]>([]); - const [expanded, setExpanded] = useState(false); - const [model, setModel] = useState<LlmModel>("claude-opus"); - // Track conversation history for context continuity - const [conversationHistory, setConversationHistory] = useState<ChatMessage[]>([]); - // Track pending questions from the LLM - const [pendingQuestions, setPendingQuestions] = useState<UserQuestion[] | null>(null); - // Track user's answers to questions - const [userAnswers, setUserAnswers] = useState<Map<string, string[]>>(new Map()); - // Track custom input for each question - const [customInputs, setCustomInputs] = useState<Map<string, string>>(new Map()); - - const inputRef = useRef<HTMLInputElement>(null); - const messagesRef = useRef<HTMLDivElement>(null); - - // Auto-scroll to bottom when messages change - useEffect(() => { - if (messagesRef.current) { - messagesRef.current.scrollTop = messagesRef.current.scrollHeight; - } - }, [messages]); - - // Auto-focus input when an element is focused - useEffect(() => { - if (focusedElement && inputRef.current) { - inputRef.current.focus(); - } - }, [focusedElement]); - - // Handle suggested prompt from generate actions - useEffect(() => { - if (suggestedPrompt) { - setInput(suggestedPrompt); - onClearSuggestedPrompt?.(); - } - }, [suggestedPrompt, onClearSuggestedPrompt]); - - const handleSubmit = useCallback( - async (e: React.FormEvent) => { - e.preventDefault(); - if (!input.trim() || loading) return; - - const userMessage = input.trim(); - setInput(""); - setExpanded(true); - - // Add user message - const userMsgId = Date.now().toString(); - setMessages((prev) => [ - ...prev, - { id: userMsgId, type: "user", content: userMessage }, - ]); - - setLoading(true); - - try { - // Send request with conversation history for context - const response = await chatWithFile( - fileId, - userMessage, - model, - conversationHistory, - focusedElement?.index - ); - - // Add assistant response - const assistantMsgId = (Date.now() + 1).toString(); - setMessages((prev) => [ - ...prev, - { - id: assistantMsgId, - type: response.pendingQuestions?.length ? "question" : "assistant", - content: response.response, - toolCalls: response.toolCalls.map((tc) => ({ - name: tc.name, - success: tc.result.success, - message: tc.result.message, - })), - questions: response.pendingQuestions, - }, - ]); - - // Update conversation history for next request - setConversationHistory((prev) => [ - ...prev, - { role: "user", content: userMessage }, - { role: "assistant", content: response.response }, - ]); - - // Handle pending questions - if (response.pendingQuestions?.length) { - setPendingQuestions(response.pendingQuestions); - // Initialize answers map - const initialAnswers = new Map<string, string[]>(); - response.pendingQuestions.forEach((q) => { - initialAnswers.set(q.id, []); - }); - setUserAnswers(initialAnswers); - setCustomInputs(new Map()); - } - - // Update parent with new body/summary - onUpdate(response.updatedBody, response.updatedSummary); - } catch (err) { - const errorMsgId = (Date.now() + 1).toString(); - setMessages((prev) => [ - ...prev, - { - id: errorMsgId, - type: "error", - content: err instanceof Error ? err.message : "An error occurred", - }, - ]); - } finally { - setLoading(false); - inputRef.current?.focus(); - } - }, - [input, loading, fileId, model, onUpdate, conversationHistory, focusedElement] - ); - - // Handle option selection for a question - const handleOptionToggle = useCallback((questionId: string, option: string, allowMultiple: boolean) => { - setUserAnswers((prev) => { - const newMap = new Map(prev); - const currentAnswers = newMap.get(questionId) || []; - - if (allowMultiple) { - // Toggle option in array - if (currentAnswers.includes(option)) { - newMap.set(questionId, currentAnswers.filter((a) => a !== option)); - } else { - newMap.set(questionId, [...currentAnswers, option]); - } - } else { - // Single select - replace - newMap.set(questionId, [option]); - } - - return newMap; - }); - }, []); - - // Handle custom input change - const handleCustomInputChange = useCallback((questionId: string, value: string) => { - setCustomInputs((prev) => { - const newMap = new Map(prev); - newMap.set(questionId, value); - return newMap; - }); - }, []); - - // Submit answers to questions - const handleSubmitAnswers = useCallback(async () => { - if (!pendingQuestions || loading) return; - - // Build answers array, including custom inputs - const answers: UserAnswer[] = pendingQuestions.map((q) => { - const selectedOptions = userAnswers.get(q.id) || []; - const customInput = customInputs.get(q.id)?.trim(); - - // If there's a custom input, add it to answers - const finalAnswers = customInput - ? [...selectedOptions, customInput] - : selectedOptions; - - return { - id: q.id, - answers: finalAnswers, - }; - }); - - // Format answers as a message - const answerText = answers - .map((a) => { - const question = pendingQuestions.find((q) => q.id === a.id); - return `${question?.question || a.id}: ${a.answers.join(", ")}`; - }) - .join("\n"); - - // Clear pending questions - setPendingQuestions(null); - setUserAnswers(new Map()); - setCustomInputs(new Map()); - - // Add user answer message - const userMsgId = Date.now().toString(); - setMessages((prev) => [ - ...prev, - { id: userMsgId, type: "user", content: `[Answers]\n${answerText}` }, - ]); - - setLoading(true); - - try { - // Send answers as the next message - const response = await chatWithFile( - fileId, - answerText, - model, - conversationHistory, - focusedElement?.index - ); - - // Add assistant response - const assistantMsgId = (Date.now() + 1).toString(); - setMessages((prev) => [ - ...prev, - { - id: assistantMsgId, - type: response.pendingQuestions?.length ? "question" : "assistant", - content: response.response, - toolCalls: response.toolCalls.map((tc) => ({ - name: tc.name, - success: tc.result.success, - message: tc.result.message, - })), - questions: response.pendingQuestions, - }, - ]); - - // Update conversation history - setConversationHistory((prev) => [ - ...prev, - { role: "user", content: answerText }, - { role: "assistant", content: response.response }, - ]); - - // Handle more pending questions - if (response.pendingQuestions?.length) { - setPendingQuestions(response.pendingQuestions); - const initialAnswers = new Map<string, string[]>(); - response.pendingQuestions.forEach((q) => { - initialAnswers.set(q.id, []); - }); - setUserAnswers(initialAnswers); - setCustomInputs(new Map()); - } - - // Update parent with new body/summary - onUpdate(response.updatedBody, response.updatedSummary); - } catch (err) { - const errorMsgId = (Date.now() + 1).toString(); - setMessages((prev) => [ - ...prev, - { - id: errorMsgId, - type: "error", - content: err instanceof Error ? err.message : "An error occurred", - }, - ]); - } finally { - setLoading(false); - } - }, [pendingQuestions, userAnswers, customInputs, loading, fileId, model, conversationHistory, onUpdate, focusedElement]); - - // Cancel answering questions - const handleCancelQuestions = useCallback(() => { - setPendingQuestions(null); - setUserAnswers(new Map()); - setCustomInputs(new Map()); - }, []); - - const clearMessages = useCallback(() => { - setMessages([]); - setConversationHistory([]); - setPendingQuestions(null); - setUserAnswers(new Map()); - setCustomInputs(new Map()); - }, []); - - return ( - <div className="border-t border-[rgba(117,170,252,0.35)] bg-[#0d1b2d]"> - {/* Messages Panel (expandable) */} - {expanded && messages.length > 0 && ( - <div - ref={messagesRef} - className="max-h-48 overflow-y-auto p-3 space-y-2 border-b border-[rgba(117,170,252,0.2)]" - > - {messages.map((msg) => ( - <div key={msg.id} className="font-mono text-xs"> - {msg.type === "user" && ( - <div className="flex gap-2"> - <span className="text-[#9bc3ff]">></span> - <span className="text-white/80 whitespace-pre-wrap">{msg.content}</span> - </div> - )} - {(msg.type === "assistant" || msg.type === "question") && ( - <div className="pl-4 space-y-1"> - <SimpleMarkdown content={msg.content} className="text-[#75aafc]" /> - {msg.toolCalls && msg.toolCalls.length > 0 && ( - <div className="text-[#555] text-[10px] space-y-0.5"> - {msg.toolCalls.map((tc, i) => ( - <div key={i}> - <span - className={ - tc.success ? "text-green-500" : "text-red-400" - } - > - {tc.success ? "+" : "x"} - </span>{" "} - {tc.name}: {tc.message} - </div> - ))} - </div> - )} - </div> - )} - {msg.type === "error" && ( - <div className="pl-4 text-red-400">{msg.content}</div> - )} - </div> - ))} - </div> - )} - - {/* Pending Questions UI */} - {pendingQuestions && pendingQuestions.length > 0 && ( - <div className="p-3 border-b border-[rgba(117,170,252,0.2)] space-y-3"> - <div className="text-[#9bc3ff] font-mono text-xs uppercase tracking-wide"> - Questions from AI - </div> - {pendingQuestions.map((q) => ( - <div key={q.id} className="space-y-2"> - <div className="text-white/90 font-mono text-sm">{q.question}</div> - <div className="flex flex-wrap gap-2"> - {q.options.map((option) => { - const isSelected = (userAnswers.get(q.id) || []).includes(option); - return ( - <button - key={option} - type="button" - onClick={() => handleOptionToggle(q.id, option, q.allowMultiple)} - className={`px-2 py-1 font-mono text-xs border transition-colors ${ - isSelected - ? "bg-[#3f6fb3] border-[#75aafc] text-white" - : "bg-transparent border-[rgba(117,170,252,0.25)] text-[#9bc3ff] hover:border-[#3f6fb3]" - }`} - > - {q.allowMultiple && ( - <span className="mr-1">{isSelected ? "☑" : "☐"}</span> - )} - {option} - </button> - ); - })} - </div> - {q.allowCustom && ( - <input - type="text" - value={customInputs.get(q.id) || ""} - onChange={(e) => handleCustomInputChange(q.id, e.target.value)} - placeholder="Or type a custom answer..." - className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-xs px-2 py-1 outline-none focus:border-[#3f6fb3] placeholder-[#555]" - /> - )} - </div> - ))} - <div className="flex gap-2 pt-2"> - <button - type="button" - onClick={handleSubmitAnswers} - disabled={loading} - className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase" - > - {loading ? "..." : "Submit Answers"} - </button> - <button - type="button" - onClick={handleCancelQuestions} - disabled={loading} - className="px-3 py-1 font-mono text-xs text-[#555] hover:text-[#9bc3ff] transition-colors" - > - Cancel - </button> - </div> - </div> - )} - - {/* Input Bar */} - <form onSubmit={handleSubmit} className="flex items-center gap-2 p-3"> - <select - value={model} - onChange={(e) => setModel(e.target.value as LlmModel)} - disabled={loading || !!pendingQuestions} - className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs px-2 py-1 rounded-none outline-none focus:border-[#3f6fb3] disabled:opacity-50" - > - {MODEL_OPTIONS.map((opt) => ( - <option key={opt.value} value={opt.value}> - {opt.label} - </option> - ))} - </select> - - {/* Focus Badge */} - {focusedElement && ( - <button - type="button" - onClick={onClearFocus} - className="flex items-center gap-1 px-2 py-0.5 font-mono text-[10px] bg-[rgba(117,170,252,0.1)] border border-[rgba(117,170,252,0.3)] text-[#9bc3ff] hover:border-[#75aafc] transition-colors group" - title="Click to clear focus" - > - <span className="text-[#75aafc]">{focusedElement.type}</span> - <span className="text-[#555]">:</span> - <span>{focusedElement.index}</span> - <span className="text-[#555] group-hover:text-red-400 ml-1">×</span> - </button> - )} - - <span className="text-[#9bc3ff] font-mono text-sm">></span> - <input - ref={inputRef} - type="text" - value={input} - onChange={(e) => setInput(e.target.value)} - placeholder={ - loading - ? "Processing..." - : pendingQuestions - ? "Answer questions above first..." - : "Add a heading, chart, or summary..." - } - disabled={loading || !!pendingQuestions} - className="flex-1 bg-transparent border-none outline-none font-mono text-sm text-white placeholder-[#555]" - /> - {messages.length > 0 && ( - <button - type="button" - onClick={clearMessages} - className="text-[#555] hover:text-[#9bc3ff] font-mono text-xs transition-colors" - > - clear - </button> - )} - <button - type="submit" - disabled={loading || !input.trim() || !!pendingQuestions} - className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase" - > - {loading ? "..." : "Send"} - </button> - </form> - </div> - ); -} |
