summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/files/CliInput.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2025-12-23 14:43:23 +0000
committersoryu <soryu@soryu.co>2025-12-23 14:47:18 +0000
commit555061b179b8ec034cb70f9a2dd6c823ced0f637 (patch)
tree0545b4395dab6d957884d8d36bf15b8da529dc1f /makima/frontend/src/components/files/CliInput.tsx
parenta32dc56d2e5447ef8988cb98b8686476cc94e70c (diff)
downloadsoryu-555061b179b8ec034cb70f9a2dd6c823ced0f637.tar.gz
soryu-555061b179b8ec034cb70f9a2dd6c823ced0f637.zip
Add file body and initial tool call system
Diffstat (limited to 'makima/frontend/src/components/files/CliInput.tsx')
-rw-r--r--makima/frontend/src/components/files/CliInput.tsx168
1 files changed, 168 insertions, 0 deletions
diff --git a/makima/frontend/src/components/files/CliInput.tsx b/makima/frontend/src/components/files/CliInput.tsx
new file mode 100644
index 0000000..b20eb27
--- /dev/null
+++ b/makima/frontend/src/components/files/CliInput.tsx
@@ -0,0 +1,168 @@
+import { useState, useCallback, useRef, useEffect } from "react";
+import { chatWithFile, type BodyElement } from "../../lib/api";
+
+interface CliInputProps {
+ fileId: string;
+ onUpdate: (body: BodyElement[], summary: string | null) => void;
+}
+
+interface Message {
+ id: string;
+ type: "user" | "assistant" | "error";
+ content: string;
+ toolCalls?: { name: string; success: boolean; message: string }[];
+}
+
+export function CliInput({ fileId, onUpdate }: CliInputProps) {
+ const [input, setInput] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [messages, setMessages] = useState<Message[]>([]);
+ const [expanded, setExpanded] = useState(false);
+ 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]);
+
+ 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 {
+ const response = await chatWithFile(fileId, userMessage);
+
+ // Add assistant response
+ const assistantMsgId = (Date.now() + 1).toString();
+ setMessages((prev) => [
+ ...prev,
+ {
+ id: assistantMsgId,
+ type: "assistant",
+ content: response.response,
+ toolCalls: response.toolCalls.map((tc) => ({
+ name: tc.name,
+ success: tc.result.success,
+ message: tc.result.message,
+ })),
+ },
+ ]);
+
+ // 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, onUpdate]
+ );
+
+ const clearMessages = useCallback(() => {
+ setMessages([]);
+ }, []);
+
+ 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">{msg.content}</span>
+ </div>
+ )}
+ {msg.type === "assistant" && (
+ <div className="pl-4 space-y-1">
+ <div className="text-[#75aafc]">{msg.content}</div>
+ {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>
+ )}
+
+ {/* Input Bar */}
+ <form onSubmit={handleSubmit} className="flex items-center gap-2 p-3">
+ <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..." : "Add a heading, chart, or summary..."}
+ disabled={loading}
+ 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()}
+ 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>
+ );
+}