From f79c416c58557d2f946aa5332989afdfa8c021cd Mon Sep 17 00:00:00 2001 From: soryu Date: Fri, 2 Jan 2026 22:13:28 +0000 Subject: Add defined user input dialogue to LLM edit --- makima/frontend/src/components/files/CliInput.tsx | 254 +++++++++++++++++++++- 1 file changed, 245 insertions(+), 9 deletions(-) (limited to 'makima/frontend/src/components/files/CliInput.tsx') diff --git a/makima/frontend/src/components/files/CliInput.tsx b/makima/frontend/src/components/files/CliInput.tsx index c1e6b6d..ff2b0a4 100644 --- a/makima/frontend/src/components/files/CliInput.tsx +++ b/makima/frontend/src/components/files/CliInput.tsx @@ -1,5 +1,12 @@ import { useState, useCallback, useRef, useEffect } from "react"; -import { chatWithFile, type BodyElement, type LlmModel, type ChatMessage } from "../../lib/api"; +import { + chatWithFile, + type BodyElement, + type LlmModel, + type ChatMessage, + type UserQuestion, + type UserAnswer, +} from "../../lib/api"; import { SimpleMarkdown } from "../SimpleMarkdown"; interface CliInputProps { @@ -9,9 +16,10 @@ interface CliInputProps { interface Message { id: string; - type: "user" | "assistant" | "error"; + type: "user" | "assistant" | "error" | "question"; content: string; toolCalls?: { name: string; success: boolean; message: string }[]; + questions?: UserQuestion[]; } const MODEL_OPTIONS: { value: LlmModel; label: string }[] = [ @@ -28,6 +36,13 @@ export function CliInput({ fileId, onUpdate }: CliInputProps) { const [model, setModel] = useState("claude-opus"); // Track conversation history for context continuity const [conversationHistory, setConversationHistory] = useState([]); + // Track pending questions from the LLM + const [pendingQuestions, setPendingQuestions] = useState(null); + // Track user's answers to questions + const [userAnswers, setUserAnswers] = useState>(new Map()); + // Track custom input for each question + const [customInputs, setCustomInputs] = useState>(new Map()); + const inputRef = useRef(null); const messagesRef = useRef(null); @@ -66,13 +81,14 @@ export function CliInput({ fileId, onUpdate }: CliInputProps) { ...prev, { id: assistantMsgId, - type: "assistant", + 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, }, ]); @@ -83,6 +99,18 @@ export function CliInput({ fileId, onUpdate }: CliInputProps) { { role: "assistant", content: response.response }, ]); + // Handle pending questions + if (response.pendingQuestions?.length) { + setPendingQuestions(response.pendingQuestions); + // Initialize answers map + const initialAnswers = new Map(); + 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) { @@ -103,9 +131,148 @@ export function CliInput({ fileId, onUpdate }: CliInputProps) { [input, loading, fileId, model, onUpdate, conversationHistory] ); + // 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); + + // 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(); + 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]); + + // 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 ( @@ -121,10 +288,10 @@ export function CliInput({ fileId, onUpdate }: CliInputProps) { {msg.type === "user" && (
> - {msg.content} + {msg.content}
)} - {msg.type === "assistant" && ( + {(msg.type === "assistant" || msg.type === "question") && (
{msg.toolCalls && msg.toolCalls.length > 0 && ( @@ -153,12 +320,75 @@ export function CliInput({ fileId, onUpdate }: CliInputProps) {
)} + {/* Pending Questions UI */} + {pendingQuestions && pendingQuestions.length > 0 && ( +
+
+ Questions from AI +
+ {pendingQuestions.map((q) => ( +
+
{q.question}
+
+ {q.options.map((option) => { + const isSelected = (userAnswers.get(q.id) || []).includes(option); + return ( + + ); + })} +
+ {q.allowCustom && ( + 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]" + /> + )} +
+ ))} +
+ + +
+
+ )} + {/* Input Bar */}