diff options
Diffstat (limited to 'makima/frontend/src')
| -rw-r--r-- | makima/frontend/src/components/NavStrip.tsx | 1 | ||||
| -rw-r--r-- | makima/frontend/src/components/files/CliInput.tsx | 483 | ||||
| -rw-r--r-- | makima/frontend/src/components/listen/ContractPickerModal.tsx | 133 | ||||
| -rw-r--r-- | makima/frontend/src/components/listen/ControlPanel.tsx | 200 | ||||
| -rw-r--r-- | makima/frontend/src/components/listen/DiscussContractModal.tsx | 287 | ||||
| -rw-r--r-- | makima/frontend/src/components/listen/SpeakerPanel.tsx | 61 | ||||
| -rw-r--r-- | makima/frontend/src/components/listen/TranscriptAnalysisPanel.tsx | 339 | ||||
| -rw-r--r-- | makima/frontend/src/components/listen/TranscriptPanel.tsx | 85 | ||||
| -rw-r--r-- | makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx | 536 | ||||
| -rw-r--r-- | makima/frontend/src/hooks/useMeshChatHistory.ts | 133 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 466 | ||||
| -rw-r--r-- | makima/frontend/src/lib/listenApi.ts | 168 | ||||
| -rw-r--r-- | makima/frontend/src/main.tsx | 9 | ||||
| -rw-r--r-- | makima/frontend/src/routes/files.tsx | 88 | ||||
| -rw-r--r-- | makima/frontend/src/routes/listen.tsx | 277 | ||||
| -rw-r--r-- | makima/frontend/src/routes/mesh.tsx | 37 |
16 files changed, 31 insertions, 3272 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx index 7c5dad1..36ced19 100644 --- a/makima/frontend/src/components/NavStrip.tsx +++ b/makima/frontend/src/components/NavStrip.tsx @@ -17,7 +17,6 @@ interface NavLink { } const NAV_LINKS: NavLink[] = [ - { label: "Listen", href: "/listen" }, { label: "Directives", href: "/directives", requiresAuth: true }, { label: "Orders", href: "/orders", requiresAuth: true }, // /contracts has been removed in Phase 5; the legacy nav entry is gone. 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> - ); -} diff --git a/makima/frontend/src/components/listen/ContractPickerModal.tsx b/makima/frontend/src/components/listen/ContractPickerModal.tsx deleted file mode 100644 index f3c72d0..0000000 --- a/makima/frontend/src/components/listen/ContractPickerModal.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { useEffect, useRef } from "react"; -import type { ContractOption } from "./ControlPanel"; - -interface ContractPickerModalProps { - isOpen: boolean; - onClose: () => void; - contracts: ContractOption[]; - selectedContractId: string | null; - onSelect: (contractId: string | null) => void; - onDiscussContract: () => void; - loading?: boolean; -} - -export function ContractPickerModal({ - isOpen, - onClose, - contracts, - selectedContractId, - onSelect, - onDiscussContract, - loading, -}: ContractPickerModalProps) { - const modalRef = useRef<HTMLDivElement>(null); - - useEffect(() => { - if (!isOpen) return; - - function handleKeyDown(e: KeyboardEvent) { - if (e.key === "Escape") { - onClose(); - } - } - - function handleClickOutside(e: MouseEvent) { - if (modalRef.current && !modalRef.current.contains(e.target as Node)) { - onClose(); - } - } - - document.addEventListener("keydown", handleKeyDown); - document.addEventListener("mousedown", handleClickOutside); - - return () => { - document.removeEventListener("keydown", handleKeyDown); - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [isOpen, onClose]); - - if (!isOpen) return null; - - const handleSelect = (contractId: string | null) => { - onSelect(contractId); - onClose(); - }; - - return ( - <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"> - <div - ref={modalRef} - className="panel p-4 w-[300px] max-h-[400px] flex flex-col gap-3" - > - <div className="flex items-center justify-between"> - <h2 className="font-mono text-sm text-[#dbe7ff] uppercase tracking-wide"> - Select Contract - </h2> - <button - onClick={onClose} - className="font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors" - > - [X] - </button> - </div> - - <div className="flex-1 overflow-y-auto flex flex-col gap-1 min-h-0"> - {loading ? ( - <div className="font-mono text-xs text-[#9bc3ff] text-center py-4"> - Loading... - </div> - ) : ( - <> - <button - onClick={() => handleSelect(null)} - className={`w-full text-left px-3 py-2 font-mono text-xs border transition-colors ${ - selectedContractId === null - ? "bg-[#0f3c78]/50 border-[#3f6fb3] text-[#dbe7ff]" - : "bg-[#0d1b2d] border-[#0f3c78] text-[#9bc3ff] hover:border-[#3f6fb3] hover:text-[#dbe7ff]" - }`} - > - <span className="uppercase tracking-wide">Ephemeral</span> - <span className="block text-[10px] text-[#75aafc] mt-0.5"> - Transcript not saved - </span> - </button> - - <button - onClick={() => { - onClose(); - onDiscussContract(); - }} - className="w-full text-left px-3 py-2 font-mono text-xs border bg-[#0d1b2d] border-[#3f6fb3] text-[#dbe7ff] hover:bg-[#153667] transition-colors" - > - <span className="uppercase tracking-wide">Discuss Contract</span> - <span className="block text-[10px] text-[#75aafc] mt-0.5"> - Chat with Makima to define a new contract - </span> - </button> - - {contracts.map((contract) => ( - <button - key={contract.id} - onClick={() => handleSelect(contract.id)} - className={`w-full text-left px-3 py-2 font-mono text-xs border transition-colors ${ - selectedContractId === contract.id - ? "bg-[#0f3c78]/50 border-[#3f6fb3] text-[#dbe7ff]" - : "bg-[#0d1b2d] border-[#0f3c78] text-[#9bc3ff] hover:border-[#3f6fb3] hover:text-[#dbe7ff]" - }`} - > - <span className="block truncate">{contract.name}</span> - </button> - ))} - - {contracts.length === 0 && ( - <div className="font-mono text-xs text-[#9bc3ff] text-center py-4"> - No contracts available - </div> - )} - </> - )} - </div> - </div> - </div> - ); -} diff --git a/makima/frontend/src/components/listen/ControlPanel.tsx b/makima/frontend/src/components/listen/ControlPanel.tsx deleted file mode 100644 index ab2bcee..0000000 --- a/makima/frontend/src/components/listen/ControlPanel.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { useState } from "react"; -import { Logo } from "../Logo"; -import type { MicrophoneStatus } from "../../hooks/useMicrophone"; -import type { ConnectionStatus } from "../../hooks/useWebSocket"; -import { ContractPickerModal } from "./ContractPickerModal"; - -export interface ContractOption { - id: string; - name: string; -} - -interface ControlPanelProps { - isListening: boolean; - isConnected: boolean; - micStatus: MicrophoneStatus; - micVolume: number; - hasTranscripts: boolean; - onToggle: () => void; - onNew: () => void; - error?: string | null; - // Contract selection - contracts: ContractOption[]; - selectedContractId: string | null; - onContractChange: (contractId: string | null) => void; - onDiscussContract: () => void; - contractsLoading?: boolean; - // Connection status for loading state - connectionStatus?: ConnectionStatus; -} - -function getStatusText(isListening: boolean, micStatus: MicrophoneStatus): string { - if (isListening) return "Listening..."; - - switch (micStatus) { - case "requesting": - return "Requesting permission..."; - case "ready": - return "Click to start"; - case "denied": - return "Permission denied - click to retry"; - case "error": - return "Error - click to retry"; - default: - return "Click to start"; - } -} - -export function ControlPanel({ - isListening, - isConnected, - micStatus, - micVolume, - hasTranscripts, - onToggle, - onNew, - error, - contracts, - selectedContractId, - onContractChange, - onDiscussContract, - contractsLoading, - connectionStatus, -}: ControlPanelProps) { - const [isModalOpen, setIsModalOpen] = useState(false); - const statusText = getStatusText(isListening, micStatus); - const isRequesting = micStatus === "requesting"; - - const selectedContract = contracts.find((c) => c.id === selectedContractId); - - return ( - <div className="panel p-4 flex flex-col items-center justify-center gap-3"> - {/* Logo button */} - <div className="flex flex-col items-center gap-2"> - <Logo - size={100} - listening={isListening || isRequesting} - onClick={isRequesting ? undefined : onToggle} - className={isRequesting ? "opacity-50" : "cursor-pointer"} - noHoverAnimation - /> - <span className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase text-center"> - {statusText} - </span> - </div> - - {/* Status indicators */} - <div className="font-mono text-xs text-center flex flex-col gap-1"> - {/* Microphone status */} - <div - className={`inline-flex flex-col gap-1 px-2 py-1 border ${ - micStatus === "ready" || micStatus === "recording" - ? "border-[#3f6fb3] text-[#75aafc]" - : micStatus === "denied" || micStatus === "error" - ? "border-red-400/50 text-red-400" - : "border-[rgba(117,170,252,0.25)] text-[#9bc3ff]" - }`} - > - <div className="inline-flex items-center gap-1.5"> - <span - className={`w-1.5 h-1.5 rounded-full ${ - micStatus === "ready" || micStatus === "recording" - ? "bg-[#75aafc]" - : micStatus === "denied" || micStatus === "error" - ? "bg-red-400" - : "bg-[#3f6fb3]" - }`} - /> - {micStatus === "ready" || micStatus === "recording" - ? "MIC READY" - : micStatus === "requesting" - ? "REQUESTING..." - : micStatus === "denied" - ? "MIC DENIED" - : micStatus === "error" - ? "MIC ERROR" - : "MIC IDLE"} - </div> - {isListening && ( - <div className="w-full h-1.5 bg-[#0f1c2f] overflow-hidden"> - <div - className="h-full bg-[#75aafc] transition-all duration-75" - style={{ width: `${micVolume * 100}%` }} - /> - </div> - )} - </div> - - {/* Connection status */} - <div - className={`inline-flex flex-col gap-1 px-2 py-1 border ${ - isConnected - ? "border-[#3f6fb3] text-[#75aafc]" - : connectionStatus === "connecting" - ? "border-[#3f6fb3] text-[#9bc3ff]" - : "border-[rgba(117,170,252,0.25)] text-[#9bc3ff]" - }`} - > - <div className="inline-flex items-center gap-1.5"> - <span - className={`w-1.5 h-1.5 rounded-full ${ - isConnected ? "bg-[#75aafc]" : "bg-[#3f6fb3]" - }`} - /> - {isConnected - ? "CONNECTED" - : connectionStatus === "connecting" - ? "LOADING MODELS..." - : "DISCONNECTED"} - </div> - {connectionStatus === "connecting" && ( - <div className="w-full h-1.5 bg-[#0f1c2f] overflow-hidden"> - <div - className="h-full w-1/3 bg-[#75aafc]" - style={{ - animation: "loading-slide 1.5s ease-in-out infinite", - }} - /> - </div> - )} - </div> - </div> - - {/* Error display */} - {error && ( - <div className="font-mono text-xs text-red-400 text-center px-2 py-1 border border-red-400/50 bg-red-400/10 max-w-[250px]"> - {error} - </div> - )} - - {/* Buttons */} - <div className="flex gap-2"> - <button - onClick={onNew} - className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0d1b2d] border border-[#0f3c78] hover:border-[#3f6fb3] transition-colors uppercase tracking-wide" - title={hasTranscripts ? "Save and start new session" : "Start new session"} - > - New - </button> - <button - onClick={() => setIsModalOpen(true)} - disabled={isListening || contractsLoading} - className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0d1b2d] border border-[#0f3c78] hover:border-[#3f6fb3] transition-colors disabled:opacity-50 disabled:cursor-not-allowed uppercase tracking-wide" - title={selectedContract ? `Saving to: ${selectedContract.name}` : "Transcript not saved"} - > - {selectedContract ? "Contract" : "Ephemeral"} - </button> - </div> - - <ContractPickerModal - isOpen={isModalOpen} - onClose={() => setIsModalOpen(false)} - contracts={contracts} - selectedContractId={selectedContractId} - onSelect={onContractChange} - onDiscussContract={onDiscussContract} - loading={contractsLoading} - /> - </div> - ); -} diff --git a/makima/frontend/src/components/listen/DiscussContractModal.tsx b/makima/frontend/src/components/listen/DiscussContractModal.tsx deleted file mode 100644 index 984f505..0000000 --- a/makima/frontend/src/components/listen/DiscussContractModal.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import { useState, useEffect, useRef, useCallback } from "react"; -import { useSpeakWebSocket } from "../../hooks/useSpeakWebSocket"; -import { - discussContract, - type CreatedContractInfo, - type ContractToolCallInfo, - type ChatMessage, -} from "../../lib/api"; - -interface Message { - id: string; - role: "user" | "assistant"; - content: string; - timestamp: Date; - toolCalls?: ContractToolCallInfo[]; -} - -interface DiscussContractModalProps { - isOpen: boolean; - onClose: () => void; - transcriptContext?: string; - onContractCreated: (contract: CreatedContractInfo) => void; -} - -interface ChatBubbleProps { - message: Message; - onSpeak: () => void; - isSpeaking: boolean; -} - -function ChatBubble({ message, onSpeak, isSpeaking }: ChatBubbleProps) { - const isUser = message.role === "user"; - - return ( - <div className={`flex ${isUser ? "justify-end" : "justify-start"}`}> - <div - className={`max-w-[80%] p-3 font-mono text-sm ${ - isUser - ? "bg-[#0f3c78] text-[#dbe7ff]" - : "bg-[#0d1b2d] border border-[rgba(117,170,252,0.25)] text-[#dbe7ff]" - }`} - > - <div className="whitespace-pre-wrap">{message.content}</div> - - {!isUser && ( - <div className="mt-2 flex items-center gap-2"> - <button - onClick={onSpeak} - className="text-[10px] text-[#9bc3ff] hover:text-[#dbe7ff] uppercase" - > - {isSpeaking ? "[Stop]" : "[Speak]"} - </button> - </div> - )} - - {message.toolCalls && message.toolCalls.length > 0 && ( - <div className="mt-2 pt-2 border-t border-[rgba(117,170,252,0.25)]"> - {message.toolCalls.map((tc, i) => ( - <div key={i} className="text-[10px] text-[#75aafc]"> - {tc.result.success ? "+" : "-"} {tc.name}: {tc.result.message} - </div> - ))} - </div> - )} - </div> - </div> - ); -} - -export function DiscussContractModal({ - isOpen, - onClose, - transcriptContext, - onContractCreated, -}: DiscussContractModalProps) { - const [messages, setMessages] = useState<Message[]>([]); - const [input, setInput] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState<string | null>(null); - const [createdContract, setCreatedContract] = useState<CreatedContractInfo | null>(null); - - const { speak, isSpeaking, cancel } = useSpeakWebSocket(); - const messagesEndRef = useRef<HTMLDivElement>(null); - const modalRef = useRef<HTMLDivElement>(null); - - // Auto-scroll to bottom on new messages - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages]); - - // Initial greeting when modal opens - useEffect(() => { - if (isOpen && messages.length === 0) { - const greeting = transcriptContext - ? "I've reviewed your session transcript. What would you like to build based on this discussion?" - : "Hello! I'm Makima. Tell me about what you'd like to build, and I'll help you create a contract for it."; - - setMessages([{ - id: crypto.randomUUID(), - role: "assistant", - content: greeting, - timestamp: new Date(), - }]); - } - }, [isOpen, transcriptContext, messages.length]); - - // Handle escape key and click outside - useEffect(() => { - if (!isOpen) return; - - function handleKeyDown(e: KeyboardEvent) { - if (e.key === "Escape") { - onClose(); - } - } - - function handleClickOutside(e: MouseEvent) { - if (modalRef.current && !modalRef.current.contains(e.target as Node)) { - onClose(); - } - } - - document.addEventListener("keydown", handleKeyDown); - document.addEventListener("mousedown", handleClickOutside); - - return () => { - document.removeEventListener("keydown", handleKeyDown); - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [isOpen, onClose]); - - // Reset state when modal closes - useEffect(() => { - if (!isOpen) { - setMessages([]); - setInput(""); - setIsLoading(false); - setError(null); - setCreatedContract(null); - } - }, [isOpen]); - - const handleSend = useCallback(async () => { - if (!input.trim() || isLoading) return; - - const userMessage: Message = { - id: crypto.randomUUID(), - role: "user", - content: input, - timestamp: new Date(), - }; - - setMessages(prev => [...prev, userMessage]); - setInput(""); - setIsLoading(true); - setError(null); - - try { - // Build history from existing messages (excluding the greeting) - const history: ChatMessage[] = messages.map(m => ({ - role: m.role, - content: m.content, - })); - - const response = await discussContract( - input, - undefined, // model - history, - transcriptContext - ); - - const assistantMessage: Message = { - id: crypto.randomUUID(), - role: "assistant", - content: response.response, - timestamp: new Date(), - toolCalls: response.toolCalls, - }; - - setMessages(prev => [...prev, assistantMessage]); - - if (response.createdContract) { - setCreatedContract(response.createdContract); - onContractCreated(response.createdContract); - } - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to get response"); - } finally { - setIsLoading(false); - } - }, [input, isLoading, messages, transcriptContext, onContractCreated]); - - const handleSpeak = useCallback((text: string) => { - if (isSpeaking) { - cancel(); - } else { - speak(text); - } - }, [isSpeaking, cancel, speak]); - - const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSend(); - } - }, [handleSend]); - - if (!isOpen) return null; - - return ( - <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"> - <div - ref={modalRef} - className="panel w-full max-w-2xl h-[600px] flex flex-col mx-4" - > - {/* Header */} - <div className="flex items-center justify-between p-4 border-b border-[rgba(117,170,252,0.25)]"> - <h2 className="font-mono text-sm text-[#dbe7ff] uppercase tracking-wide"> - Discuss Contract with Makima - </h2> - <button - onClick={onClose} - className="font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors" - > - [X] - </button> - </div> - - {/* Messages */} - <div className="flex-1 overflow-y-auto p-4 space-y-4"> - {messages.map((message) => ( - <ChatBubble - key={message.id} - message={message} - onSpeak={() => handleSpeak(message.content)} - isSpeaking={isSpeaking} - /> - ))} - <div ref={messagesEndRef} /> - - {isLoading && ( - <div className="flex items-center gap-2 text-[#9bc3ff]"> - <span className="animate-pulse font-mono text-sm">Makima is thinking...</span> - </div> - )} - - {error && ( - <div className="font-mono text-xs text-red-400 px-2 py-2 border border-red-400/50 bg-red-400/10"> - {error} - </div> - )} - </div> - - {/* Contract Created Banner */} - {createdContract && ( - <div className="p-3 bg-green-400/10 border-t border-green-400/50"> - <div className="font-mono text-xs text-green-400"> - Contract "{createdContract.name}" created successfully! - </div> - </div> - )} - - {/* Input */} - <div className="p-4 border-t border-[rgba(117,170,252,0.25)]"> - <div className="flex gap-2"> - <input - type="text" - value={input} - onChange={(e) => setInput(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Describe your project..." - disabled={isLoading || !!createdContract} - className="flex-1 px-3 py-2 bg-[#0d1b2d] border border-[#0f3c78] text-[#dbe7ff] font-mono text-sm focus:border-[#3f6fb3] outline-none disabled:opacity-50" - /> - <button - onClick={handleSend} - disabled={isLoading || !input.trim() || !!createdContract} - className="px-4 py-2 bg-[#0f3c78] text-[#dbe7ff] font-mono text-xs uppercase hover:bg-[#153667] disabled:opacity-50 disabled:cursor-not-allowed transition-colors" - > - Send - </button> - </div> - </div> - </div> - </div> - ); -} diff --git a/makima/frontend/src/components/listen/SpeakerPanel.tsx b/makima/frontend/src/components/listen/SpeakerPanel.tsx deleted file mode 100644 index cb43992..0000000 --- a/makima/frontend/src/components/listen/SpeakerPanel.tsx +++ /dev/null @@ -1,61 +0,0 @@ -interface Speaker { - id: string; - label: string; - isActive: boolean; -} - -interface SpeakerPanelProps { - speakers: Speaker[]; -} - -const SPEAKER_SYMBOLS = ["///", ":::", "***", "###", "+++", "---", "===", "%%%"]; - -export function SpeakerPanel({ speakers }: SpeakerPanelProps) { - return ( - <div className="panel h-full p-4 flex flex-col"> - <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase mb-3 pb-2 border-b border-dashed border-[rgba(117,170,252,0.35)]"> - SPEAKERS// - </div> - - {speakers.length === 0 ? ( - <div className="flex-1 flex items-center justify-center text-[#9bc3ff] text-sm font-mono opacity-60"> - <span>Waiting for speech...</span> - </div> - ) : ( - <div className="flex-1 flex flex-col gap-3"> - {speakers.map((speaker, index) => ( - <div - key={speaker.id} - className={`flex items-center gap-3 p-3 border ${ - speaker.isActive - ? "border-[#3f6fb3] bg-[#0f1c2f]" - : "border-[rgba(117,170,252,0.25)] bg-[#0b1423]" - } transition-colors`} - > - <span - className={`font-mono text-2xl tracking-tighter ${ - speaker.isActive - ? "text-[#75aafc] animate-pulse" - : "text-[#3f6fb3]" - }`} - > - {SPEAKER_SYMBOLS[index % SPEAKER_SYMBOLS.length]} - </span> - <div className="flex-1"> - <div className="font-mono text-sm text-[#dbe7ff]"> - {speaker.label} - </div> - <div className="font-mono text-xs text-[#9bc3ff]"> - {speaker.isActive ? "speaking" : "idle"} - </div> - </div> - {speaker.isActive && ( - <div className="w-2 h-2 rounded-full bg-[#75aafc] animate-pulse" /> - )} - </div> - ))} - </div> - )} - </div> - ); -} diff --git a/makima/frontend/src/components/listen/TranscriptAnalysisPanel.tsx b/makima/frontend/src/components/listen/TranscriptAnalysisPanel.tsx deleted file mode 100644 index 89d56a8..0000000 --- a/makima/frontend/src/components/listen/TranscriptAnalysisPanel.tsx +++ /dev/null @@ -1,339 +0,0 @@ -import { useState, useCallback } from "react"; -import { - analyzeTranscript, - createContractFromTranscript, - updateContractFromTranscript, - type TranscriptAnalysisResult, - type CreateContractResponse, -} from "../../lib/listenApi"; - -interface TranscriptAnalysisPanelProps { - fileId: string; - contractId: string; - selectedContractId: string | null; - onContractCreated?: (response: CreateContractResponse) => void; - onContractUpdated?: (response: CreateContractResponse) => void; - onClose?: () => void; -} - -type AnalysisState = "idle" | "analyzing" | "analyzed" | "creating" | "updating"; - -export function TranscriptAnalysisPanel({ - fileId, - contractId, - selectedContractId, - onContractCreated, - onContractUpdated, - onClose, -}: TranscriptAnalysisPanelProps) { - const [state, setState] = useState<AnalysisState>("idle"); - const [analysis, setAnalysis] = useState<TranscriptAnalysisResult | null>(null); - const [error, setError] = useState<string | null>(null); - const [successMessage, setSuccessMessage] = useState<string | null>(null); - - const handleAnalyze = useCallback(async () => { - setState("analyzing"); - setError(null); - setSuccessMessage(null); - - try { - const response = await analyzeTranscript(fileId); - setAnalysis(response.analysis); - setState("analyzed"); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to analyze transcript"); - setState("idle"); - } - }, [fileId]); - - const handleCreateContract = useCallback(async () => { - if (!analysis) return; - - setState("creating"); - setError(null); - - try { - const response = await createContractFromTranscript(fileId, { - name: analysis.suggestedContractName, - description: analysis.suggestedDescription, - includeRequirements: true, - includeDecisions: true, - includeActionItems: true, - }); - setSuccessMessage(`Created contract "${response.contractName}"`); - onContractCreated?.(response); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to create contract"); - setState("analyzed"); - } - }, [fileId, analysis, onContractCreated]); - - const handleUpdateContract = useCallback(async () => { - if (!analysis || !selectedContractId) return; - - setState("updating"); - setError(null); - - try { - const response = await updateContractFromTranscript(fileId, selectedContractId, { - description: analysis.suggestedDescription, - includeRequirements: true, - includeDecisions: true, - includeActionItems: true, - }); - setSuccessMessage(`Updated contract "${response.contractName}"`); - onContractUpdated?.(response); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to update contract"); - setState("analyzed"); - } - }, [fileId, analysis, selectedContractId, onContractUpdated]); - - // Group requirements by category - const groupedRequirements = analysis?.requirements.reduce((acc, req) => { - const category = req.category || "General"; - if (!acc[category]) { - acc[category] = []; - } - acc[category].push(req); - return acc; - }, {} as Record<string, typeof analysis.requirements>) || {}; - - return ( - <div className="panel p-4 flex flex-col gap-4"> - {/* Header */} - <div className="flex justify-between items-center border-b border-dashed border-[rgba(117,170,252,0.35)] pb-2"> - <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase"> - TRANSCRIPT ANALYSIS// - </div> - {onClose && ( - <button - onClick={onClose} - className="font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors" - > - [X] - </button> - )} - </div> - - {/* Error display */} - {error && ( - <div className="font-mono text-xs text-red-400 px-2 py-2 border border-red-400/50 bg-red-400/10"> - {error} - </div> - )} - - {/* Success message */} - {successMessage && ( - <div className="font-mono text-xs text-green-400 px-2 py-2 border border-green-400/50 bg-green-400/10"> - {successMessage} - </div> - )} - - {/* Initial state - Show analyze button */} - {state === "idle" && ( - <div className="flex flex-col items-center gap-3 py-4"> - <p className="font-mono text-sm text-[#9bc3ff] text-center"> - Transcript saved. Analyze to extract requirements, decisions, and action items. - </p> - <button - onClick={handleAnalyze} - className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0d1b2d] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase tracking-wide" - > - Analyze Transcript - </button> - </div> - )} - - {/* Analyzing state */} - {state === "analyzing" && ( - <div className="flex flex-col items-center gap-3 py-8"> - <div className="w-6 h-6 border-2 border-[#3f6fb3] border-t-[#75aafc] rounded-full animate-spin" /> - <p className="font-mono text-sm text-[#9bc3ff]">Analyzing transcript...</p> - </div> - )} - - {/* Analysis results */} - {(state === "analyzed" || state === "creating" || state === "updating") && analysis && ( - <div className="flex flex-col gap-4 overflow-y-auto max-h-[60vh]"> - {/* Suggested Contract Info */} - {(analysis.suggestedContractName || analysis.suggestedDescription) && ( - <div className="border border-[rgba(117,170,252,0.25)] p-3"> - <div className="font-mono text-xs text-[#75aafc] uppercase tracking-wide mb-2"> - Suggested Contract - </div> - {analysis.suggestedContractName && ( - <div className="font-mono text-sm text-[#dbe7ff] mb-1"> - {analysis.suggestedContractName} - </div> - )} - {analysis.suggestedDescription && ( - <div className="font-mono text-xs text-[#9bc3ff]"> - {analysis.suggestedDescription} - </div> - )} - </div> - )} - - {/* Requirements */} - {Object.keys(groupedRequirements).length > 0 && ( - <div className="border border-[rgba(117,170,252,0.25)] p-3"> - <div className="font-mono text-xs text-[#75aafc] uppercase tracking-wide mb-2"> - Requirements ({analysis.requirements.length}) - </div> - {Object.entries(groupedRequirements).map(([category, reqs]) => ( - <div key={category} className="mb-3 last:mb-0"> - <div className="font-mono text-[10px] text-[#3f6fb3] uppercase tracking-wider mb-1"> - {category} - </div> - <ul className="space-y-1"> - {reqs.map((req, idx) => ( - <li key={idx} className="font-mono text-xs text-[#dbe7ff] flex gap-2"> - <span className="text-[#3f6fb3]">-</span> - <span>{req.text}</span> - {req.speaker && ( - <span className="text-[#9bc3ff] text-[10px]">({req.speaker})</span> - )} - </li> - ))} - </ul> - </div> - ))} - </div> - )} - - {/* Decisions */} - {analysis.decisions.length > 0 && ( - <div className="border border-[rgba(117,170,252,0.25)] p-3"> - <div className="font-mono text-xs text-[#75aafc] uppercase tracking-wide mb-2"> - Decisions ({analysis.decisions.length}) - </div> - <ul className="space-y-2"> - {analysis.decisions.map((decision, idx) => ( - <li key={idx} className="font-mono text-xs text-[#dbe7ff]"> - <div className="flex gap-2"> - <span className="text-[#3f6fb3]">-</span> - <span>{decision.text}</span> - </div> - {decision.context && ( - <div className="ml-4 text-[10px] text-[#9bc3ff] mt-1"> - Context: {decision.context} - </div> - )} - </li> - ))} - </ul> - </div> - )} - - {/* Action Items */} - {analysis.actionItems.length > 0 && ( - <div className="border border-[rgba(117,170,252,0.25)] p-3"> - <div className="font-mono text-xs text-[#75aafc] uppercase tracking-wide mb-2"> - Action Items ({analysis.actionItems.length}) - </div> - <ul className="space-y-2"> - {analysis.actionItems.map((item, idx) => ( - <li key={idx} className="font-mono text-xs text-[#dbe7ff]"> - <div className="flex gap-2 items-start"> - <span className="text-[#3f6fb3]">-</span> - <div className="flex-1"> - <span>{item.text}</span> - <div className="flex gap-2 mt-1"> - {item.assignee && ( - <span className="text-[10px] px-1.5 py-0.5 bg-[#0f1c2f] border border-[#3f6fb3] text-[#9bc3ff]"> - @{item.assignee} - </span> - )} - {item.priority && ( - <span - className={`text-[10px] px-1.5 py-0.5 border ${ - item.priority === "high" - ? "border-red-400/50 text-red-400 bg-red-400/10" - : item.priority === "medium" - ? "border-yellow-400/50 text-yellow-400 bg-yellow-400/10" - : "border-[#3f6fb3] text-[#9bc3ff] bg-[#0f1c2f]" - }`} - > - {item.priority} - </span> - )} - </div> - </div> - </div> - </li> - ))} - </ul> - </div> - )} - - {/* Key Topics */} - {analysis.keyTopics.length > 0 && ( - <div className="border border-[rgba(117,170,252,0.25)] p-3"> - <div className="font-mono text-xs text-[#75aafc] uppercase tracking-wide mb-2"> - Key Topics - </div> - <div className="flex flex-wrap gap-2"> - {analysis.keyTopics.map((topic, idx) => ( - <span - key={idx} - className="font-mono text-[10px] px-2 py-1 bg-[#0f1c2f] border border-[#3f6fb3] text-[#9bc3ff]" - > - {topic} - </span> - ))} - </div> - </div> - )} - - {/* Speaker Statistics */} - {analysis.speakerSummary.length > 0 && ( - <div className="border border-[rgba(117,170,252,0.25)] p-3"> - <div className="font-mono text-xs text-[#75aafc] uppercase tracking-wide mb-2"> - Speaker Statistics - </div> - <div className="space-y-2"> - {analysis.speakerSummary.map((speaker, idx) => ( - <div key={idx} className="flex items-center gap-3"> - <span className="font-mono text-xs text-[#dbe7ff] min-w-[100px]"> - {speaker.speaker} - </span> - <div className="flex-1 h-2 bg-[#0f1c2f] overflow-hidden"> - <div - className="h-full bg-[#3f6fb3]" - style={{ width: `${speaker.contributionPercentage}%` }} - /> - </div> - <span className="font-mono text-[10px] text-[#9bc3ff] min-w-[40px] text-right"> - {speaker.contributionPercentage.toFixed(0)}% - </span> - </div> - ))} - </div> - </div> - )} - - {/* Action Buttons */} - <div className="flex gap-2 pt-2 border-t border-dashed border-[rgba(117,170,252,0.35)]"> - <button - onClick={handleCreateContract} - disabled={state === "creating" || state === "updating"} - className="flex-1 px-3 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0d1b2d] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase tracking-wide disabled:opacity-50 disabled:cursor-not-allowed" - > - {state === "creating" ? "Creating..." : "Create New Contract"} - </button> - {selectedContractId && selectedContractId !== contractId && ( - <button - onClick={handleUpdateContract} - disabled={state === "creating" || state === "updating"} - className="flex-1 px-3 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0d1b2d] border border-[#0f3c78] hover:border-[#3f6fb3] transition-colors uppercase tracking-wide disabled:opacity-50 disabled:cursor-not-allowed" - > - {state === "updating" ? "Updating..." : "Add to Current Contract"} - </button> - )} - </div> - </div> - )} - </div> - ); -} diff --git a/makima/frontend/src/components/listen/TranscriptPanel.tsx b/makima/frontend/src/components/listen/TranscriptPanel.tsx deleted file mode 100644 index 662c94f..0000000 --- a/makima/frontend/src/components/listen/TranscriptPanel.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useRef, useEffect, useState, useCallback } from "react"; -import type { TranscriptEntry } from "../../types/messages"; - -interface TranscriptPanelProps { - transcripts: TranscriptEntry[]; -} - -export function TranscriptPanel({ transcripts }: TranscriptPanelProps) { - const containerRef = useRef<HTMLDivElement>(null); - const [autoScroll, setAutoScroll] = useState(true); - - // Auto-scroll when new transcripts arrive - useEffect(() => { - if (autoScroll && containerRef.current) { - containerRef.current.scrollTop = containerRef.current.scrollHeight; - } - }, [transcripts, autoScroll]); - - // Detect manual scroll - const handleScroll = useCallback(() => { - if (!containerRef.current) return; - - const { scrollTop, scrollHeight, clientHeight } = containerRef.current; - const isAtBottom = scrollHeight - scrollTop - clientHeight < 50; - - setAutoScroll(isAtBottom); - }, []); - - const scrollToBottom = useCallback(() => { - if (containerRef.current) { - containerRef.current.scrollTop = containerRef.current.scrollHeight; - setAutoScroll(true); - } - }, []); - - return ( - <div className="panel h-full flex flex-col"> - <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase p-4 pb-2 border-b border-dashed border-[rgba(117,170,252,0.35)] flex justify-between items-center"> - <span>TRANSCRIPT//</span> - {!autoScroll && ( - <button - onClick={scrollToBottom} - className="px-2 py-1 text-[10px] bg-[#0f1c2f] border border-[#3f6fb3] hover:bg-[#153667] transition-colors" - > - Scroll to bottom - </button> - )} - </div> - - <div - ref={containerRef} - onScroll={handleScroll} - className="flex-1 overflow-y-auto p-4 space-y-3" - > - {transcripts.length === 0 ? ( - <div className="text-center text-[#9bc3ff] text-sm font-mono opacity-60 py-8"> - Transcriptions will appear here... - </div> - ) : ( - transcripts.map((entry) => ( - <div - key={entry.id} - className={`font-mono text-sm ${ - entry.isFinal ? "opacity-100" : "opacity-70" - }`} - > - <div className="flex items-baseline gap-2 mb-1"> - <span className="text-[#75aafc] text-xs"> - [{entry.start.toFixed(2)}s - {entry.end.toFixed(2)}s] - </span> - <span className="text-[#9bc3ff] text-xs font-bold"> - {entry.speaker} - </span> - {entry.isFinal && ( - <span className="text-[#3f6fb3] text-[10px]">[FINAL]</span> - )} - </div> - <p className="m-0 text-[#dbe7ff] leading-relaxed">{entry.text}</p> - </div> - )) - )} - </div> - </div> - ); -} diff --git a/makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx b/makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx deleted file mode 100644 index 5caa3c4..0000000 --- a/makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx +++ /dev/null @@ -1,536 +0,0 @@ -import { useState, useCallback, useRef, useEffect } from "react"; -import { - type LlmModel, - type UserQuestion, - type UserAnswer, - type MeshChatContext, -} from "../../lib/api"; -import { useMeshChatHistory } from "../../hooks/useMeshChatHistory"; -import { SimpleMarkdown } from "../SimpleMarkdown"; - -interface UnifiedMeshChatInputProps { - context: MeshChatContext; - onUpdate?: () => void; -} - -const MODEL_OPTIONS: { value: LlmModel; label: string }[] = [ - { value: "claude-opus", label: "Claude Opus" }, - { value: "claude-sonnet", label: "Claude Sonnet" }, - { value: "groq", label: "Groq Kimi" }, -]; - -const DEFAULT_MODEL: LlmModel = "claude-opus"; - -// LocalStorage keys -const STORAGE_KEY_MODEL = "makima-mesh-chat-model"; -const STORAGE_KEY_CMD_HISTORY = "makima-mesh-chat-cmd-history"; -const MAX_CMD_HISTORY = 100; - -function loadModel(): LlmModel { - try { - const modelStr = localStorage.getItem(STORAGE_KEY_MODEL); - return (modelStr as LlmModel) || DEFAULT_MODEL; - } catch { - return DEFAULT_MODEL; - } -} - -function saveModel(model: LlmModel): void { - try { - localStorage.setItem(STORAGE_KEY_MODEL, model); - } catch { - // Ignore storage errors - } -} - -function loadCommandHistory(): string[] { - try { - const historyJson = localStorage.getItem(STORAGE_KEY_CMD_HISTORY); - return historyJson ? JSON.parse(historyJson) : []; - } catch { - return []; - } -} - -function saveCommandHistory(history: string[]): void { - try { - localStorage.setItem( - STORAGE_KEY_CMD_HISTORY, - JSON.stringify(history.slice(-MAX_CMD_HISTORY)) - ); - } catch { - // Ignore storage errors - } -} - -function getPlaceholder(context: MeshChatContext): string { - switch (context.type) { - case "mesh": - return "Create task, list tasks, check status..."; - case "task": - return "Create subtask, run task, check status..."; - case "subtask": - return "Update plan, check siblings, merge..."; - default: - return "Ask anything..."; - } -} - -function getContextLabel(context: MeshChatContext): string { - switch (context.type) { - case "mesh": - return "mesh"; - case "task": - return `task:${context.taskId?.slice(0, 8)}`; - case "subtask": - return `subtask:${context.taskId?.slice(0, 8)}`; - default: - return "chat"; - } -} - -export function UnifiedMeshChatInput({ - context, - onUpdate, -}: UnifiedMeshChatInputProps) { - const { - messages, - loading: historyLoading, - error: historyError, - sending, - clearHistory, - sendMessage, - } = useMeshChatHistory(); - - const [input, setInput] = useState(""); - const [expanded, setExpanded] = useState(false); - const [model, setModel] = useState<LlmModel>(DEFAULT_MODEL); - - // Pending questions state - const [pendingQuestions, setPendingQuestions] = useState< - UserQuestion[] | null - >(null); - const [userAnswers, setUserAnswers] = useState<Map<string, string[]>>( - new Map() - ); - const [customInputs, setCustomInputs] = useState<Map<string, string>>( - new Map() - ); - - // Command history for arrow key navigation - const [commandHistory, setCommandHistory] = useState<string[]>([]); - const [historyIndex, setHistoryIndex] = useState(-1); - const [savedInput, setSavedInput] = useState(""); - - const inputRef = useRef<HTMLInputElement>(null); - const messagesRef = useRef<HTMLDivElement>(null); - - // Load model preference on mount - useEffect(() => { - setModel(loadModel()); - setCommandHistory(loadCommandHistory()); - }, []); - - // Expand when messages exist - useEffect(() => { - if (messages.length > 0) { - setExpanded(true); - } - }, [messages.length]); - - // Auto-scroll to bottom when messages change - useEffect(() => { - if (messagesRef.current) { - messagesRef.current.scrollTop = messagesRef.current.scrollHeight; - } - }, [messages]); - - // Handle model change - const handleModelChange = useCallback((newModel: LlmModel) => { - setModel(newModel); - saveModel(newModel); - }, []); - - // Handle keyboard navigation for command history - const handleKeyDown = useCallback( - (e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === "ArrowUp") { - e.preventDefault(); - if (commandHistory.length === 0) return; - - if (historyIndex === -1) { - setSavedInput(input); - setHistoryIndex(commandHistory.length - 1); - setInput(commandHistory[commandHistory.length - 1]); - } else if (historyIndex > 0) { - setHistoryIndex(historyIndex - 1); - setInput(commandHistory[historyIndex - 1]); - } - } else if (e.key === "ArrowDown") { - e.preventDefault(); - if (historyIndex === -1) return; - - if (historyIndex < commandHistory.length - 1) { - setHistoryIndex(historyIndex + 1); - setInput(commandHistory[historyIndex + 1]); - } else { - setHistoryIndex(-1); - setInput(savedInput); - } - } - }, - [commandHistory, historyIndex, input, savedInput] - ); - - const handleSubmit = useCallback( - async (e: React.FormEvent) => { - e.preventDefault(); - if (!input.trim() || sending) return; - - const userMessage = input.trim(); - - // Update command history - const newHistory = - commandHistory[commandHistory.length - 1] !== userMessage - ? [...commandHistory, userMessage] - : commandHistory; - setCommandHistory(newHistory); - saveCommandHistory(newHistory); - - // Reset navigation state - setHistoryIndex(-1); - setSavedInput(""); - - setInput(""); - setExpanded(true); - - // Send message via hook (uses DB-persisted history) - const response = await sendMessage(userMessage, context, model); - - if (response) { - // Handle 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()); - } - - // Notify parent that something may have been updated - // Always refresh when tool calls were made (state may have changed) - if (response.toolCalls && response.toolCalls.length > 0) { - onUpdate?.(); - } - } - - inputRef.current?.focus(); - }, - [input, sending, context, model, sendMessage, onUpdate, commandHistory] - ); - - // 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) { - if (currentAnswers.includes(option)) { - newMap.set( - questionId, - currentAnswers.filter((a) => a !== option) - ); - } else { - newMap.set(questionId, [...currentAnswers, option]); - } - } else { - 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 || sending) return; - - // Build answers array - const answers: UserAnswer[] = pendingQuestions.map((q) => { - const selectedOptions = userAnswers.get(q.id) || []; - const customInput = customInputs.get(q.id)?.trim(); - 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()); - - // Send answers as the next message - const response = await sendMessage(answerText, context, model); - - if (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()); - } - - // Notify parent that something may have been updated - if (response.toolCalls && response.toolCalls.length > 0) { - onUpdate?.(); - } - } - }, [ - pendingQuestions, - userAnswers, - customInputs, - sending, - context, - model, - sendMessage, - onUpdate, - ]); - - // Cancel answering questions - const handleCancelQuestions = useCallback(() => { - setPendingQuestions(null); - setUserAnswers(new Map()); - setCustomInputs(new Map()); - }, []); - - const handleClearHistory = useCallback(async () => { - await clearHistory(); - setPendingQuestions(null); - setUserAnswers(new Map()); - setCustomInputs(new Map()); - }, [clearHistory]); - - const loading = sending || historyLoading; - - return ( - <div className="border-t border-[rgba(117,170,252,0.35)] bg-[#0d1b2d]"> - {/* Error Display */} - {historyError && ( - <div className="px-3 py-2 bg-red-900/20 text-red-400 text-xs font-mono"> - {historyError} - </div> - )} - - {/* 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.role === "user" && ( - <div className="flex gap-2"> - <span className="text-[#9bc3ff]">></span> - <span className="text-white/80 whitespace-pre-wrap"> - {msg.content} - </span> - {msg.contextType !== "mesh" && ( - <span className="text-[#555] text-[10px]"> - [{msg.contextType}] - </span> - )} - </div> - )} - {msg.role === "assistant" && ( - <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.result.success - ? "text-green-500" - : "text-red-400" - } - > - {tc.result.success ? "+" : "x"} - </span>{" "} - {tc.name}: {tc.result.message} - </div> - ))} - </div> - )} - </div> - )} - {msg.role === "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) => handleModelChange(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> - <span className="text-[#555] font-mono text-[10px]"> - [{getContextLabel(context)}] - </span> - <span className="text-[#9bc3ff] font-mono text-sm">></span> - <input - ref={inputRef} - type="text" - value={input} - onChange={(e) => setInput(e.target.value)} - onKeyDown={handleKeyDown} - placeholder={ - loading - ? "Processing..." - : pendingQuestions - ? "Answer questions above first..." - : getPlaceholder(context) - } - 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={handleClearHistory} - 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> - ); -} diff --git a/makima/frontend/src/hooks/useMeshChatHistory.ts b/makima/frontend/src/hooks/useMeshChatHistory.ts deleted file mode 100644 index 82c576d..0000000 --- a/makima/frontend/src/hooks/useMeshChatHistory.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { useState, useCallback, useEffect } from "react"; -import { - getMeshChatHistory, - clearMeshChatHistory, - chatWithMeshContext, - type MeshChatMessageRecord, - type MeshChatContext, - type MeshChatResponse, - type LlmModel, -} from "../lib/api"; - -export interface MeshChatState { - conversationId: string | null; - messages: MeshChatMessageRecord[]; - loading: boolean; - error: string | null; - sending: boolean; -} - -export function useMeshChatHistory() { - const [state, setState] = useState<MeshChatState>({ - conversationId: null, - messages: [], - loading: true, - error: null, - sending: false, - }); - - const fetchHistory = useCallback(async () => { - setState((prev) => ({ ...prev, loading: true, error: null })); - try { - const response = await getMeshChatHistory(); - setState((prev) => ({ - ...prev, - conversationId: response.conversationId, - messages: response.messages, - loading: false, - })); - } catch (e) { - setState((prev) => ({ - ...prev, - error: e instanceof Error ? e.message : "Failed to fetch chat history", - loading: false, - })); - } - }, []); - - const clearHistory = useCallback(async (): Promise<boolean> => { - setState((prev) => ({ ...prev, loading: true, error: null })); - try { - const response = await clearMeshChatHistory(); - setState({ - conversationId: response.conversationId, - messages: [], - loading: false, - error: null, - sending: false, - }); - return true; - } catch (e) { - setState((prev) => ({ - ...prev, - error: e instanceof Error ? e.message : "Failed to clear chat history", - loading: false, - })); - return false; - } - }, []); - - const sendMessage = useCallback( - async ( - message: string, - context: MeshChatContext, - model?: LlmModel - ): Promise<MeshChatResponse | null> => { - setState((prev) => ({ ...prev, sending: true, error: null })); - - // Optimistically add user message (will be refetched after response) - const tempUserMessage: MeshChatMessageRecord = { - id: `temp-${Date.now()}`, - conversationId: state.conversationId || "", - role: "user", - content: message, - contextType: context.type, - contextTaskId: context.taskId || null, - toolCalls: null, - pendingQuestions: null, - createdAt: new Date().toISOString(), - }; - - setState((prev) => ({ - ...prev, - messages: [...prev.messages, tempUserMessage], - })); - - try { - const response = await chatWithMeshContext(message, context, model); - - // Refetch to get the actual saved messages (with proper IDs) - await fetchHistory(); - - setState((prev) => ({ ...prev, sending: false })); - return response; - } catch (e) { - // Remove optimistic message on error - setState((prev) => ({ - ...prev, - messages: prev.messages.filter((m) => m.id !== tempUserMessage.id), - error: e instanceof Error ? e.message : "Failed to send message", - sending: false, - })); - return null; - } - }, - [state.conversationId, fetchHistory] - ); - - // Initial fetch on mount - useEffect(() => { - fetchHistory(); - }, [fetchHistory]); - - return { - conversationId: state.conversationId, - messages: state.messages, - loading: state.loading, - error: state.error, - sending: state.sending, - fetchHistory, - clearHistory, - sendMessage, - }; -} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index b34f786..b7b904e 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -220,51 +220,9 @@ export class VersionConflictError extends Error { } } -// Available LLM models -export type LlmModel = "claude-sonnet" | "claude-opus" | "groq"; - -// Chat API types -export interface ChatMessage { - role: "user" | "assistant"; - content: string; -} - -export interface ChatRequest { - message: string; - model?: LlmModel; - history?: ChatMessage[]; - focusedElementIndex?: number; -} - -export interface ToolCallInfo { - name: string; - result: { - success: boolean; - message: string; - }; -} - -// User question types for interactive LLM tool -export interface UserQuestion { - id: string; - question: string; - options: string[]; - allowMultiple: boolean; - allowCustom: boolean; -} - -export interface UserAnswer { - id: string; - answers: string[]; -} - -export interface ChatResponse { - response: string; - toolCalls: ToolCallInfo[]; - updatedBody: BodyElement[]; - updatedSummary: string | null; - pendingQuestions?: UserQuestion[]; -} +// (LlmModel, ChatMessage, ChatRequest, UserQuestion, UserAnswer, +// ChatResponse, ToolCallInfo removed alongside the LLM module — +// see Chat API function block further down + the deleted /llm/ tree.) // File API functions export async function listFiles(): Promise<FileListResponse> { @@ -323,34 +281,7 @@ export async function deleteFile(id: string): Promise<void> { } } -// Chat API function -export async function chatWithFile( - id: string, - message: string, - model?: LlmModel, - history?: ChatMessage[], - focusedElementIndex?: number -): Promise<ChatResponse> { - const body: ChatRequest = { message }; - if (model) { - body.model = model; - } - if (history && history.length > 0) { - body.history = history; - } - if (focusedElementIndex !== undefined) { - body.focusedElementIndex = focusedElementIndex; - } - const res = await authFetch(`${API_BASE}/api/v1/files/${id}/chat`, { - method: "POST", - body: JSON.stringify(body), - }); - if (!res.ok) { - const errorText = await res.text(); - throw new Error(`Chat failed: ${errorText || res.statusText}`); - } - return res.json(); -} +// chatWithFile removed — handler `/api/v1/files/{id}/chat` is gone. // Version history types export type VersionSource = "user" | "llm" | "system"; @@ -1327,186 +1258,9 @@ export async function getDaemonReauthStatus( return res.json(); } -// ============================================================================= -// Mesh Chat Types for Task Orchestration -// ============================================================================= - -export interface MeshChatMessage { - role: "user" | "assistant"; - content: string; -} - -export interface MeshChatRequest { - message: string; - model?: LlmModel; - history?: MeshChatMessage[]; -} - -export interface MeshToolCallInfo { - name: string; - result: { - success: boolean; - message: string; - }; -} - -export interface MeshChatResponse { - response: string; - toolCalls: MeshToolCallInfo[]; - pendingQuestions?: UserQuestion[]; -} - -// Mesh Chat API functions - -// Top-level mesh chat (no specific task context) -export async function chatWithMesh( - message: string, - model?: LlmModel, - history?: MeshChatMessage[] -): Promise<MeshChatResponse> { - const body: MeshChatRequest = { message }; - if (model) { - body.model = model; - } - if (history && history.length > 0) { - body.history = history; - } - const res = await authFetch(`${API_BASE}/api/v1/mesh/chat`, { - method: "POST", - body: JSON.stringify(body), - }); - if (!res.ok) { - const errorText = await res.text(); - throw new Error(`Mesh chat failed: ${errorText || res.statusText}`); - } - return res.json(); -} - -// Task-scoped mesh chat -export async function chatWithTask( - taskId: string, - message: string, - model?: LlmModel, - history?: MeshChatMessage[] -): Promise<MeshChatResponse> { - const body: MeshChatRequest = { message }; - if (model) { - body.model = model; - } - if (history && history.length > 0) { - body.history = history; - } - const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/chat`, { - method: "POST", - body: JSON.stringify(body), - }); - if (!res.ok) { - const errorText = await res.text(); - throw new Error(`Mesh chat failed: ${errorText || res.statusText}`); - } - return res.json(); -} - -// ============================================================================= -// Mesh Chat History Types -// ============================================================================= - -export type MeshChatContextType = "mesh" | "task" | "subtask"; - -export interface MeshChatContext { - type: MeshChatContextType; - taskId?: string; - parentTaskId?: string; -} - -export interface MeshChatMessageRecord { - id: string; - conversationId: string; - role: "user" | "assistant" | "error"; - content: string; - contextType: MeshChatContextType; - contextTaskId: string | null; - toolCalls: MeshToolCallInfo[] | null; - pendingQuestions: UserQuestion[] | null; - createdAt: string; -} - -export interface MeshChatHistoryResponse { - conversationId: string; - messages: MeshChatMessageRecord[]; -} - -export interface MeshChatWithContextRequest { - message: string; - model?: LlmModel; - contextType?: MeshChatContextType; - contextTaskId?: string; -} - -// ============================================================================= -// Mesh Chat History API Functions -// ============================================================================= - -/** - * Get the current chat history from the database - */ -export async function getMeshChatHistory(): Promise<MeshChatHistoryResponse> { - const res = await authFetch(`${API_BASE}/api/v1/mesh/chat/history`); - if (!res.ok) { - throw new Error(`Failed to get chat history: ${res.statusText}`); - } - return res.json(); -} - -/** - * Clear chat history (archives current conversation, starts new one) - */ -export async function clearMeshChatHistory(): Promise<{ success: boolean; conversationId: string }> { - const res = await authFetch(`${API_BASE}/api/v1/mesh/chat/history`, { - method: "DELETE", - }); - if (!res.ok) { - throw new Error(`Failed to clear chat history: ${res.statusText}`); - } - return res.json(); -} - -/** - * Chat with mesh using context (new approach with DB history) - */ -export async function chatWithMeshContext( - message: string, - context: MeshChatContext, - model?: LlmModel -): Promise<MeshChatResponse> { - const body: MeshChatWithContextRequest = { - message, - contextType: context.type, - }; - - if (model) { - body.model = model; - } - - // Set contextTaskId based on context type - if (context.type === "task" && context.taskId) { - body.contextTaskId = context.taskId; - } else if (context.type === "subtask" && context.taskId) { - body.contextTaskId = context.taskId; - } - - // Use top-level endpoint (it now loads history from DB) - const res = await authFetch(`${API_BASE}/api/v1/mesh/chat`, { - method: "POST", - body: JSON.stringify(body), - }); - - if (!res.ok) { - const errorText = await res.text(); - throw new Error(`Mesh chat failed: ${errorText || res.statusText}`); - } - return res.json(); -} +// (Mesh chat types + chatWithMesh / chatWithTask / getMeshChatHistory / +// clearMeshChatHistory / chatWithMeshContext / MeshChatContext etc. +// removed alongside the LLM module — backend handlers gone.) // ============================================================================= // API Key Management @@ -1770,58 +1524,10 @@ export function getDefaultPhase(contractType: ContractType): ContractPhase { return "research"; } -// ============================================================================= -// Contract Type Templates -// ============================================================================= - -/** Contract type template returned by the API */ -export interface ContractTypeTemplate { - /** Template identifier (e.g., "simple", "specification") */ - id: string; - /** Display name */ - name: string; - /** Description of the contract type workflow */ - description: string; - /** Ordered list of phases for this contract type */ - phases: ContractPhase[]; - /** Default starting phase */ - defaultPhase: ContractPhase; - /** Whether this is a built-in type (always available) */ - isBuiltin: boolean; - /** Optional mapping from phase ID to display name */ - phaseNames?: Record<string, string>; -} - -/** Response from list contract types endpoint */ -export interface ListContractTypesResponse { - contractTypes: ContractTypeTemplate[]; -} - -/** Phase definition for custom templates */ -export interface PhaseDefinition { - id: string; - name: string; - order: number; -} - -/** Deliverable definition for custom templates */ -export interface DeliverableDefinition { - id: string; - name: string; - priority: "required" | "recommended" | "optional"; -} - -/** - * List available contract types. - * Returns built-in types only (simple, specification, execute). - */ -export async function listContractTypes(): Promise<ListContractTypesResponse> { - const res = await authFetch(`${API_BASE}/api/v1/contract-types`); - if (!res.ok) { - throw new Error(`Failed to list contract types: ${res.statusText}`); - } - return res.json(); -} +// (ContractTypeTemplate / ListContractTypesResponse / PhaseDefinition / +// DeliverableDefinition / listContractTypes removed alongside the LLM +// module — the templates handler was the only consumer and the +// endpoint /api/v1/contract-types is gone.) export interface ContractRepository { id: string; @@ -2194,153 +1900,9 @@ export async function removeTaskFromContract( } } -// ============================================================================= -// Contract Chat Types and API -// ============================================================================= - -export interface ContractChatRequest { - message: string; - model?: LlmModel; - history?: ChatMessage[]; -} - -export interface ContractToolCallInfo { - name: string; - result: { - success: boolean; - message: string; - }; -} - -export interface ContractChatResponse { - response: string; - toolCalls: ContractToolCallInfo[]; - pendingQuestions?: UserQuestion[]; -} - -/** - * Chat with a contract using LLM-powered management tools. - */ -export async function chatWithContract( - contractId: string, - message: string, - model?: LlmModel, - history?: ChatMessage[] -): Promise<ContractChatResponse> { - const body: ContractChatRequest = { message }; - if (model) { - body.model = model; - } - if (history && history.length > 0) { - body.history = history; - } - const res = await authFetch(`${API_BASE}/api/v1/contracts/${contractId}/chat`, { - method: "POST", - body: JSON.stringify(body), - }); - if (!res.ok) { - const errorText = await res.text(); - throw new Error(`Contract chat failed: ${errorText || res.statusText}`); - } - return res.json(); -} - -// Contract chat history types -export interface ContractChatMessage { - id: string; - conversationId: string; - role: "user" | "assistant" | "error"; - content: string; - toolCalls?: unknown; - pendingQuestions?: unknown; - createdAt: string; -} - -export interface ContractChatHistoryResponse { - contractId: string; - conversationId: string; - messages: ContractChatMessage[]; -} - -/** Get contract chat history */ -export async function getContractChatHistory( - contractId: string -): Promise<ContractChatHistoryResponse> { - const res = await authFetch( - `${API_BASE}/api/v1/contracts/${contractId}/chat/history` - ); - if (!res.ok) { - throw new Error(`Failed to fetch contract chat history: ${res.statusText}`); - } - return res.json(); -} - -/** Clear contract chat history (starts a new conversation) */ -export async function clearContractChatHistory( - contractId: string -): Promise<void> { - const res = await authFetch( - `${API_BASE}/api/v1/contracts/${contractId}/chat/history`, - { method: "DELETE" } - ); - if (!res.ok) { - throw new Error(`Failed to clear contract chat history: ${res.statusText}`); - } -} - -// ============================================================================= -// Contract Discussion Types and API -// ============================================================================= - -export interface DiscussContractRequest { - message: string; - model?: LlmModel; - history?: ChatMessage[]; - transcriptContext?: string; -} - -export interface CreatedContractInfo { - id: string; - name: string; - description: string | null; - contractType: string; - initialPhase: string; -} - -export interface DiscussContractResponse { - response: string; - toolCalls: ContractToolCallInfo[]; - createdContract?: CreatedContractInfo; - pendingQuestions?: UserQuestion[]; -} - -/** - * Discuss a potential contract with Makima. - * This is an ephemeral conversation that can result in contract creation. - */ -export async function discussContract( - message: string, - model?: LlmModel, - history?: ChatMessage[], - transcriptContext?: string -): Promise<DiscussContractResponse> { - const body: DiscussContractRequest = { message }; - if (model) body.model = model; - if (history && history.length > 0) body.history = history; - if (transcriptContext) body.transcriptContext = transcriptContext; - - const res = await authFetch(`${API_BASE}/api/v1/contracts/discuss`, { - method: "POST", - body: JSON.stringify(body), - }); - - if (!res.ok) { - const errorText = await res.text(); - throw new Error(`Discussion failed: ${errorText || res.statusText}`); - } - - return res.json(); -} +// (Contract Chat / Contract Discussion API removed alongside the LLM +// module — legacy contracts chat handlers were already Phase-5 +// removed; these were the dead frontend half.) // ============================================================================= // Template Types and API diff --git a/makima/frontend/src/lib/listenApi.ts b/makima/frontend/src/lib/listenApi.ts deleted file mode 100644 index 187ebe0..0000000 --- a/makima/frontend/src/lib/listenApi.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { API_BASE } from './api'; -import { supabase } from './supabase'; - -// ============================================================================= -// Authentication helper (same pattern as api.ts) -// ============================================================================= - -/** Storage key for API key */ -const API_KEY_STORAGE_KEY = "makima_api_key"; - -/** Get stored API key from localStorage */ -function getStoredApiKey(): string | null { - if (typeof window === "undefined") return null; - return localStorage.getItem(API_KEY_STORAGE_KEY); -} - -/** Get auth headers for API requests */ -async function getAuthHeaders(): Promise<HeadersInit> { - const headers: HeadersInit = { - "Content-Type": "application/json", - }; - - // Try Supabase session first - if (supabase) { - const { data: { session } } = await supabase.auth.getSession(); - if (session?.access_token) { - headers["Authorization"] = `Bearer ${session.access_token}`; - return headers; - } - } - - // Fall back to API key if available - const apiKey = getStoredApiKey(); - if (apiKey) { - headers["X-Makima-API-Key"] = apiKey; - } - - return headers; -} - -// ============================================================================= -// Transcript Analysis Types -// ============================================================================= - -export interface TranscriptAnalysisResult { - requirements: Array<{ - text: string; - speaker: string; - timestamp: number; - confidence: number; - category?: string; - }>; - decisions: Array<{ - text: string; - speaker: string; - timestamp: number; - confidence: number; - context?: string; - }>; - actionItems: Array<{ - text: string; - speaker: string; - timestamp: number; - assignee?: string; - priority?: string; - }>; - keyTopics: string[]; - suggestedContractName?: string; - suggestedDescription?: string; - speakerSummary: Array<{ - speaker: string; - wordCount: number; - speakingTimeSeconds: number; - contributionPercentage: number; - }>; -} - -export interface AnalyzeResponse { - fileId: string; - analysis: TranscriptAnalysisResult; -} - -export interface CreateContractResponse { - contractId: string; - contractName: string; - filesCreated: Array<{ id: string; name: string; fileType: string }>; - tasksCreated: Array<{ id: string; name: string }>; -} - -export interface CreateContractOptions { - name?: string; - description?: string; - includeRequirements?: boolean; - includeDecisions?: boolean; - includeActionItems?: boolean; -} - -// ============================================================================= -// Listen API Functions -// ============================================================================= - -/** - * Analyze a transcript file to extract requirements, decisions, and action items. - */ -export async function analyzeTranscript(fileId: string): Promise<AnalyzeResponse> { - const response = await fetch(`${API_BASE}/api/v1/listen/analyze`, { - method: 'POST', - headers: await getAuthHeaders(), - body: JSON.stringify({ fileId }), - }); - - if (!response.ok) { - const error = await response.json().catch(() => ({ message: 'Failed to analyze transcript' })); - throw new Error(error.message || 'Failed to analyze transcript'); - } - - return response.json(); -} - -/** - * Create a contract from a transcript analysis. - */ -export async function createContractFromTranscript( - fileId: string, - options?: CreateContractOptions -): Promise<CreateContractResponse> { - const response = await fetch(`${API_BASE}/api/v1/listen/create-contract`, { - method: 'POST', - headers: await getAuthHeaders(), - body: JSON.stringify({ - fileId, - ...options, - }), - }); - - if (!response.ok) { - const error = await response.json().catch(() => ({ message: 'Failed to create contract' })); - throw new Error(error.message || 'Failed to create contract'); - } - - return response.json(); -} - -/** - * Update an existing contract with transcript analysis. - */ -export async function updateContractFromTranscript( - fileId: string, - contractId: string, - options?: Omit<CreateContractOptions, 'name'> -): Promise<CreateContractResponse> { - const response = await fetch(`${API_BASE}/api/v1/listen/update-contract`, { - method: 'POST', - headers: await getAuthHeaders(), - body: JSON.stringify({ - fileId, - contractId, - ...options, - }), - }); - - if (!response.ok) { - const error = await response.json().catch(() => ({ message: 'Failed to update contract' })); - throw new Error(error.message || 'Failed to update contract'); - } - - return response.json(); -} diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx index c1c6c35..8665ecc 100644 --- a/makima/frontend/src/main.tsx +++ b/makima/frontend/src/main.tsx @@ -10,7 +10,6 @@ import { PhaseConfirmationNotification } from "./components/PhaseConfirmationNot import { QuickSwitcher } from "./components/QuickSwitcher"; import { ProtectedRoute } from "./components/ProtectedRoute"; import HomePage from "./routes/_index"; -import ListenPage from "./routes/listen"; import FilesPage from "./routes/files"; import OrdersPage from "./routes/orders"; import MeshPage from "./routes/mesh"; @@ -36,14 +35,6 @@ createRoot(document.getElementById("root")!).render( <Route path="/" element={<HomePage />} /> <Route path="/login" element={<LoginPage />} /> <Route - path="/listen" - element={ - <ProtectedRoute> - <ListenPage /> - </ProtectedRoute> - } - /> - <Route path="/files" element={ <ProtectedRoute> diff --git a/makima/frontend/src/routes/files.tsx b/makima/frontend/src/routes/files.tsx index b232aa0..5d757a4 100644 --- a/makima/frontend/src/routes/files.tsx +++ b/makima/frontend/src/routes/files.tsx @@ -3,7 +3,6 @@ import { useParams, useNavigate } from "react-router"; import { Masthead } from "../components/Masthead"; import { FileList } from "../components/files/FileList"; import { FileDetail, type FocusedElement } from "../components/files/FileDetail"; -import { CliInput } from "../components/files/CliInput"; import { ConflictNotification } from "../components/files/ConflictNotification"; import { UpdateNotification } from "../components/files/UpdateNotification"; import { useFiles } from "../hooks/useFiles"; @@ -57,7 +56,6 @@ function FilesPageContent() { const [remoteUpdate, setRemoteUpdate] = useState<FileUpdateEvent | null>(null); const [remoteFileData, setRemoteFileData] = useState<FileDetailType | null>(null); const [focusedElement, setFocusedElement] = useState<FocusedElement | null>(null); - const [suggestedPrompt, setSuggestedPrompt] = useState<string | null>(null); const [createdTask, setCreatedTask] = useState<Task | null>(null); // Contract selection modal state for task creation const [showContractModal, setShowContractModal] = useState(false); @@ -246,18 +244,8 @@ function FilesPageContent() { [editFile, fileDetail, updateHasLocalChanges] ); - const handleBodyUpdate = useCallback( - (body: BodyElement[], summary: string | null) => { - if (fileDetail) { - setFileDetail({ - ...fileDetail, - body, - summary, - }); - } - }, - [fileDetail] - ); + // handleBodyUpdate was the CliInput callback for AI-driven body + // rewrites. CliInput is gone with the LLM module. const handleBodyElementUpdate = useCallback( async (index: number, element: BodyElement) => { @@ -423,9 +411,7 @@ function FilesPageContent() { setFocusedElement(element); }, []); - const handleClearFocus = useCallback(() => { - setFocusedElement(null); - }, []); + // handleClearFocus was passed to CliInput; both gone now. // Convert element to a different type const handleConvertElement = useCallback( @@ -506,55 +492,10 @@ function FilesPageContent() { [fileDetail, id, editFile, updateHasLocalChanges, focusedElement] ); - // Generate from element - focus on it and pre-fill a prompt - const handleGenerateFromElement = useCallback( - (index: number, action: string) => { - if (!fileDetail) return; - - const element = fileDetail.body[index]; - if (!element) return; - - // Get preview text - let preview = ""; - switch (element.type) { - case "heading": - case "paragraph": - preview = element.text.slice(0, 50); - break; - case "code": - preview = element.content.slice(0, 50); - break; - case "list": - preview = element.items[0]?.slice(0, 40) || ""; - break; - default: - preview = "Element"; - } - - // Focus on the element - setFocusedElement({ - index, - type: element.type, - preview: preview + (preview.length >= 50 ? "..." : ""), - }); - - // Set suggested prompt based on action - let prompt = ""; - switch (action) { - case "elaborate": - prompt = "Elaborate and expand on this content"; - break; - case "summarize": - prompt = "Summarize this content"; - break; - case "extract_actions": - prompt = "Extract action items from this content"; - break; - } - setSuggestedPrompt(prompt); - }, - [fileDetail] - ); + // handleGenerateFromElement was an LLM elaborate/summarise/extract + // affordance that piped a suggested prompt into CliInput. Both + // CliInput and the LLM module are gone; the handler + its prop on + // FileDetail are removed. // Create a mesh task from an element - shows contract selection modal const handleCreateTaskFromElement = useCallback( @@ -762,7 +703,6 @@ function FilesPageContent() { onBodyElementDelete={handleBodyElementDelete} onBodyElementDuplicate={handleBodyElementDuplicate} onConvertElement={handleConvertElement} - onGenerateFromElement={handleGenerateFromElement} onCreateTaskFromElement={handleCreateTaskFromElement} onEditingChange={updateIsActivelyEditing} hasPendingRemoteUpdate={!!remoteUpdate} @@ -779,16 +719,10 @@ function FilesPageContent() { onClearVersionSelection={clearSelectedVersion} /> </div> - <div className="shrink-0"> - <CliInput - fileId={id} - onUpdate={handleBodyUpdate} - focusedElement={focusedElement} - onClearFocus={handleClearFocus} - suggestedPrompt={suggestedPrompt} - onClearSuggestedPrompt={() => setSuggestedPrompt(null)} - /> - </div> + {/* CliInput (file-level LLM chat) removed alongside the LLM + module. The file detail view is now a pure read/edit + surface; AI-driven file editing was the primary value of + this composer and went with the LLM removal. */} </div> ) : id && detailLoading ? ( <div className="panel h-full flex items-center justify-center"> diff --git a/makima/frontend/src/routes/listen.tsx b/makima/frontend/src/routes/listen.tsx deleted file mode 100644 index a53cbd9..0000000 --- a/makima/frontend/src/routes/listen.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import { useState, useCallback, useMemo, useEffect, useRef } from "react"; -import { Masthead } from "../components/Masthead"; -import { SpeakerPanel } from "../components/listen/SpeakerPanel"; -import { TranscriptPanel } from "../components/listen/TranscriptPanel"; -import { ControlPanel, type ContractOption } from "../components/listen/ControlPanel"; -import { TranscriptAnalysisPanel } from "../components/listen/TranscriptAnalysisPanel"; -import { DiscussContractModal } from "../components/listen/DiscussContractModal"; -import { useMicrophone } from "../hooks/useMicrophone"; -import { useWebSocket } from "../hooks/useWebSocket"; -import { listContracts, type CreatedContractInfo } from "../lib/api"; -import { useAuth } from "../contexts/AuthContext"; - -export default function ListenPage() { - const [isListening, setIsListening] = useState(false); - const [activeSpeaker, setActiveSpeaker] = useState<string | null>(null); - const [permissionRequested, setPermissionRequested] = useState(false); - const isListeningRef = useRef(false); - - // Contract selection state - const [contracts, setContracts] = useState<ContractOption[]>([]); - const [selectedContractId, setSelectedContractId] = useState<string | null>(null); - const [contractsLoading, setContractsLoading] = useState(true); - const { session, isAuthenticated } = useAuth(); - - // Saved transcript state for analysis - const [savedTranscript, setSavedTranscript] = useState<{ - fileId: string; - contractId: string; - } | null>(null); - - // Discuss contract modal state - const [isDiscussModalOpen, setIsDiscussModalOpen] = useState(false); - - // Fetch contracts on mount - useEffect(() => { - if (!isAuthenticated) { - setContractsLoading(false); - return; - } - - async function fetchContracts() { - try { - const response = await listContracts(); - setContracts( - response.contracts.map((c) => ({ - id: c.id, - name: c.name, - })) - ); - } catch (err) { - console.error("Failed to fetch contracts:", err); - } finally { - setContractsLoading(false); - } - } - fetchContracts(); - }, [isAuthenticated]); - - // Keep ref in sync with state for use in callbacks - useEffect(() => { - isListeningRef.current = isListening; - }, [isListening]); - - const ws = useWebSocket({ - onTranscript: (transcript) => { - // Track active speaker - if (!transcript.isFinal) { - setActiveSpeaker(transcript.speaker); - } - }, - onStopped: () => { - setIsListening(false); - setActiveSpeaker(null); - }, - onTranscriptSaved: (fileId, contractId) => { - // Store the saved transcript info for analysis - setSavedTranscript({ fileId, contractId }); - }, - }); - - const wsRef = useRef(ws); - useEffect(() => { - wsRef.current = ws; - }, [ws]); - - const handleAudioData = useCallback((samples: Float32Array) => { - if (wsRef.current.isConnected && isListeningRef.current) { - wsRef.current.sendAudio(samples); - } - }, []); - - const mic = useMicrophone({ - sampleRate: 16000, - onAudioData: handleAudioData, - }); - - // Request microphone permission on page load - useEffect(() => { - if (!permissionRequested) { - setPermissionRequested(true); - mic.requestPermission(); - } - }, [permissionRequested, mic.requestPermission]); - - // Derive unique speakers from transcripts - const speakers = useMemo(() => { - const speakerSet = new Set<string>(); - ws.transcripts.forEach((t) => speakerSet.add(t.speaker)); - - return Array.from(speakerSet).map((speaker) => ({ - id: speaker, - label: speaker, - isActive: speaker === activeSpeaker, - })); - }, [ws.transcripts, activeSpeaker]); - - // Clear active speaker after a delay - useEffect(() => { - if (activeSpeaker) { - const timer = setTimeout(() => setActiveSpeaker(null), 1500); - return () => clearTimeout(timer); - } - }, [activeSpeaker]); - - const handleToggle = useCallback(async () => { - if (isListening) { - // Stop listening - mic.stop(); - ws.stopSession("user_stopped"); - setIsListening(false); - setActiveSpeaker(null); - return; - } - - // If permission was denied or errored, try requesting again - if (mic.status === "denied" || mic.status === "error") { - const permitted = await mic.requestPermission(); - if (!permitted) { - return; - } - } - - // Start listening - start the microphone - const micStarted = await mic.start(); - if (!micStarted) { - // Microphone permission denied or error - return; - } - - // Microphone started, now connect to WebSocket - const connected = await ws.connect(); - if (!connected) { - // Connection failed - stop the microphone - mic.stop(); - return; - } - - // Both microphone and WebSocket are ready - start the session - // Pass contract_id and auth token if available - const authToken = session?.access_token || null; - ws.startSession(mic.sampleRate, mic.channels, selectedContractId, authToken); - setIsListening(true); - }, [isListening, mic, ws, selectedContractId, session]); - - const handleNew = useCallback(() => { - // Stop current session - backend auto-saves transcript on disconnect - mic.stop(); - if (ws.isConnected) { - ws.stopSession("new_session"); - } - ws.clearTranscripts(); - ws.disconnect(); - setIsListening(false); - setActiveSpeaker(null); - setSavedTranscript(null); - }, [mic, ws]); - - const handleCloseAnalysis = useCallback(() => { - setSavedTranscript(null); - }, []); - - // Get current transcript context for discussion - const transcriptContext = useMemo(() => { - if (ws.transcripts.length === 0) return undefined; - return ws.transcripts - .map(t => `[${t.speaker}]: ${t.text}`) - .join("\n"); - }, [ws.transcripts]); - - const handleOpenDiscussModal = useCallback(() => { - setIsDiscussModalOpen(true); - }, []); - - const handleContractCreated = useCallback((contract: CreatedContractInfo) => { - // Add to contracts list and select it - setContracts(prev => [ - { id: contract.id, name: contract.name }, - ...prev, - ]); - setSelectedContractId(contract.id); - // Close the modal after a short delay to show success - setTimeout(() => setIsDiscussModalOpen(false), 2000); - }, []); - - const error = ws.error || mic.error; - - return ( - <div className="relative z-10 h-screen flex flex-col overflow-hidden"> - <Masthead showTicker={false} showNav /> - - <main className="flex-1 p-4 md:p-6 grid grid-cols-1 md:grid-cols-[300px_1fr] grid-rows-[minmax(0,1fr)_auto] md:grid-rows-[minmax(0,1fr)_auto] gap-4 min-h-0 overflow-hidden"> - {/* Speaker Panel - top left on desktop, hidden on mobile */} - <div className="hidden md:block row-span-1 min-h-0 overflow-hidden"> - <SpeakerPanel speakers={speakers} /> - </div> - - {/* Transcript Panel - right side, spans 2 rows on desktop */} - <div className="md:row-span-2 min-h-0 overflow-hidden"> - <TranscriptPanel transcripts={ws.transcripts} /> - </div> - - {/* Control Panel - bottom left on desktop */} - <div className="md:col-start-1 md:row-start-2 shrink-0"> - <ControlPanel - isListening={isListening} - isConnected={ws.isConnected} - micStatus={mic.status} - micVolume={mic.volume} - hasTranscripts={ws.transcripts.length > 0} - onToggle={handleToggle} - onNew={handleNew} - error={error} - contracts={contracts} - selectedContractId={selectedContractId} - onContractChange={setSelectedContractId} - onDiscussContract={handleOpenDiscussModal} - contractsLoading={contractsLoading} - connectionStatus={ws.status} - /> - </div> - </main> - - {/* Transcript Analysis Panel - shown after recording stops and transcript is saved */} - {savedTranscript && !isListening && ( - <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"> - <div className="w-full max-w-2xl mx-4 max-h-[90vh] overflow-hidden"> - <TranscriptAnalysisPanel - fileId={savedTranscript.fileId} - contractId={savedTranscript.contractId} - selectedContractId={selectedContractId} - onContractCreated={(response) => { - // Refresh contracts list and select the new contract - setContracts((prev) => [ - { id: response.contractId, name: response.contractName }, - ...prev, - ]); - setSelectedContractId(response.contractId); - }} - onContractUpdated={() => { - // Keep the current selection - }} - onClose={handleCloseAnalysis} - /> - </div> - </div> - )} - - {/* Discuss Contract Modal */} - <DiscussContractModal - isOpen={isDiscussModalOpen} - onClose={() => setIsDiscussModalOpen(false)} - transcriptContext={transcriptContext} - onContractCreated={handleContractCreated} - /> - </div> - ); -} diff --git a/makima/frontend/src/routes/mesh.tsx b/makima/frontend/src/routes/mesh.tsx index f210227..362b2e0 100644 --- a/makima/frontend/src/routes/mesh.tsx +++ b/makima/frontend/src/routes/mesh.tsx @@ -4,11 +4,10 @@ import { Masthead } from "../components/Masthead"; import { TaskList } from "../components/mesh/TaskList"; import { TaskDetail } from "../components/mesh/TaskDetail"; import { TaskOutput } from "../components/mesh/TaskOutput"; -import { UnifiedMeshChatInput } from "../components/mesh/UnifiedMeshChatInput"; import { ContractCompleteQuestion } from "../components/mesh/ContractCompleteQuestion"; import { useTasks } from "../hooks/useTasks"; import { useTaskSubscription, type TaskUpdateEvent, type TaskOutputEvent } from "../hooks/useTaskSubscription"; -import type { TaskWithSubtasks, MeshChatContext, ContractSummary, ContractWithRelations, DaemonDirectory, TaskSummary, RepositoryHistoryEntry } from "../lib/api"; +import type { TaskWithSubtasks, ContractSummary, ContractWithRelations, DaemonDirectory, TaskSummary, RepositoryHistoryEntry } from "../lib/api"; import { startTask as startTaskApi, stopTask as stopTaskApi, getTaskOutput, listContracts, getContract, getDaemonDirectories, continueTask as continueTaskApi, resumeSupervisor, branchTask, getRepositorySuggestions, getTaskDiff } from "../lib/api"; import { DirectoryInput } from "../components/mesh/DirectoryInput"; import { useAuth } from "../contexts/AuthContext"; @@ -650,28 +649,8 @@ export default function MeshPage() { setShowRepoSuggestions(false); }, []); - // Callback when task is updated via CLI - const handleTaskUpdatedFromCli = useCallback(async () => { - if (id) { - const updated = await fetchTask(id); - if (updated) { - setTaskDetail(updated); - } - } - // Also refresh the task list - fetchTasks(); - }, [id, fetchTask, fetchTasks]); - - // Calculate chat context based on current view - const chatContext: MeshChatContext = useMemo(() => { - if (!id) { - return { type: "mesh" }; - } - if (taskDetail?.parentTaskId) { - return { type: "subtask", taskId: id, parentTaskId: taskDetail.parentTaskId }; - } - return { type: "task", taskId: id }; - }, [id, taskDetail?.parentTaskId]); + // handleTaskUpdatedFromCli + chatContext drove the deleted + // UnifiedMeshChatInput; gone with the LLM module. // Handle resizing of the split panel const handleResizeStart = useCallback((e: MouseEvent) => { @@ -904,13 +883,9 @@ export default function MeshPage() { </div> )} - {/* Mesh Chat Input - always rendered to persist state across navigation */} - <div className="shrink-0"> - <UnifiedMeshChatInput - context={chatContext} - onUpdate={id ? handleTaskUpdatedFromCli : fetchTasks} - /> - </div> + {/* UnifiedMeshChatInput removed alongside the LLM module. The + mesh page is now a pure task viewer; spawning / editing + tasks via natural language chat went with LLM removal. */} </div> </main> |
