summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/files/CliInput.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/files/CliInput.tsx')
-rw-r--r--makima/frontend/src/components/files/CliInput.tsx483
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]">&gt;</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">&times;</span>
- </button>
- )}
-
- <span className="text-[#9bc3ff] font-mono text-sm">&gt;</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>
- );
-}