From 555061b179b8ec034cb70f9a2dd6c823ced0f637 Mon Sep 17 00:00:00 2001 From: soryu Date: Tue, 23 Dec 2025 14:43:23 +0000 Subject: Add file body and initial tool call system --- .../frontend/src/components/files/BodyRenderer.tsx | 125 +++++++++++++++ makima/frontend/src/components/files/CliInput.tsx | 168 +++++++++++++++++++++ .../frontend/src/components/files/FileDetail.tsx | 91 ++++++++--- 3 files changed, 364 insertions(+), 20 deletions(-) create mode 100644 makima/frontend/src/components/files/BodyRenderer.tsx create mode 100644 makima/frontend/src/components/files/CliInput.tsx (limited to 'makima/frontend/src/components/files') diff --git a/makima/frontend/src/components/files/BodyRenderer.tsx b/makima/frontend/src/components/files/BodyRenderer.tsx new file mode 100644 index 0000000..9d008e2 --- /dev/null +++ b/makima/frontend/src/components/files/BodyRenderer.tsx @@ -0,0 +1,125 @@ +import type { BodyElement } from "../../lib/api"; +import { ChartRenderer } from "../charts/ChartRenderer"; + +interface BodyRendererProps { + elements: BodyElement[]; +} + +export function BodyRenderer({ elements }: BodyRendererProps) { + if (elements.length === 0) { + return ( +
+ No content yet. Use the CLI below to add content. +
+ ); + } + + return ( +
+ {elements.map((element, index) => ( + + ))} +
+ ); +} + +function BodyElementRenderer({ element }: { element: BodyElement }) { + switch (element.type) { + case "heading": + return ; + case "paragraph": + return ; + case "chart": + return ( + + ); + case "image": + return ( + + ); + default: + return null; + } +} + +function HeadingElement({ level, text }: { level: number; text: string }) { + const className = "font-mono text-[#9bc3ff]"; + + switch (level) { + case 1: + return

{text}

; + case 2: + return

{text}

; + case 3: + return

{text}

; + case 4: + return

{text}

; + case 5: + return
{text}
; + case 6: + return
{text}
; + default: + return

{text}

; + } +} + +function ParagraphElement({ text }: { text: string }) { + return

{text}

; +} + +function ChartElement({ + chartType, + data, + title, + config, +}: { + chartType: "line" | "bar" | "pie" | "area"; + data: Record[]; + title?: string; + config?: Record; +}) { + return ( +
+ +
+ ); +} + +function ImageElement({ + src, + alt, + caption, +}: { + src: string; + alt?: string; + caption?: string; +}) { + return ( +
+ {alt + {caption && ( +
+ {caption} +
+ )} +
+ ); +} 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([]); + const [expanded, setExpanded] = useState(false); + const inputRef = useRef(null); + const messagesRef = useRef(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 ( +
+ {/* Messages Panel (expandable) */} + {expanded && messages.length > 0 && ( +
+ {messages.map((msg) => ( +
+ {msg.type === "user" && ( +
+ > + {msg.content} +
+ )} + {msg.type === "assistant" && ( +
+
{msg.content}
+ {msg.toolCalls && msg.toolCalls.length > 0 && ( +
+ {msg.toolCalls.map((tc, i) => ( +
+ + {tc.success ? "+" : "x"} + {" "} + {tc.name}: {tc.message} +
+ ))} +
+ )} +
+ )} + {msg.type === "error" && ( +
{msg.content}
+ )} +
+ ))} +
+ )} + + {/* Input Bar */} +
+ $ + 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 && ( + + )} + +
+
+ ); +} diff --git a/makima/frontend/src/components/files/FileDetail.tsx b/makima/frontend/src/components/files/FileDetail.tsx index 643f35e..ffc67dd 100644 --- a/makima/frontend/src/components/files/FileDetail.tsx +++ b/makima/frontend/src/components/files/FileDetail.tsx @@ -1,5 +1,6 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import type { FileDetail as FileDetailType } from "../../lib/api"; +import { BodyRenderer } from "./BodyRenderer"; interface FileDetailProps { file: FileDetailType; @@ -19,6 +20,13 @@ export function FileDetail({ const [isEditing, setIsEditing] = useState(false); const [name, setName] = useState(file.name); const [description, setDescription] = useState(file.description || ""); + const [transcriptExpanded, setTranscriptExpanded] = useState(false); + + // Update local state when file changes + useEffect(() => { + setName(file.name); + setDescription(file.description || ""); + }, [file.name, file.description]); const handleSave = () => { onSave(file.id, name, description); @@ -116,27 +124,70 @@ export function FileDetail({ )} - {/* Transcript */} -
- {file.transcript.length === 0 ? ( -
- No transcript entries. + {/* Content */} +
+ {/* Summary Section */} + {file.summary && ( +
+

+ Summary +

+

+ {file.summary} +

- ) : ( - file.transcript.map((entry) => ( -
-
- - [{entry.start.toFixed(2)}s - {entry.end.toFixed(2)}s] - - - {entry.speaker} - -
-

{entry.text}

-
- )) )} + + {/* Body Content */} +
+

+ Content +

+ +
+ + {/* Collapsible Transcript Section */} +
+ + + {transcriptExpanded && ( +
+ {file.transcript.length === 0 ? ( +
+ No transcript entries. +
+ ) : ( + file.transcript.map((entry) => ( +
+
+ + [{entry.start.toFixed(2)}s - {entry.end.toFixed(2)}s] + + + {entry.speaker} + +
+

+ {entry.text} +

+
+ )) + )} +
+ )} +
); -- cgit v1.2.3