From 857e717e6343fa5c2ae96664bdc64741d5ba6830 Mon Sep 17 00:00:00 2001 From: soryu Date: Sun, 17 May 2026 21:22:34 +0100 Subject: chore: remove LLM module + all dependent surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wholesale removal of the LLM integration layer. ~14,200 LOC deleted across backend and frontend. All chat-driven UIs go with it. ## Backend - Delete `src/llm/` (7,400 LOC): claude/groq clients, contract_tools, contract_evaluator, discuss_tools, mesh_tools, phase_guidance, task_output, templates, markdown round-trip, tools, transcript_analyzer. - Delete handlers wholly dependent on LLM: - `chat.rs` (file-level LLM chat at /files/{id}/chat) - `mesh_chat.rs` (mesh & task LLM chat + history) - `templates.rs` (/contract-types listing) - Strip LLM uses from `mesh_daemon.rs`: - `compute_action_directive` (used phase_guidance::check_deliverables_met to nudge supervisors with "all tasks done" messages). The auto-PR path below still fires when all tasks finish, so no behaviour lost. - `crate::llm::markdown_to_body` → inline 1-line replacement that wraps markdown content in a single BodyElement::Markdown. The editor re-parses on display, so round-trip is preserved. - Drop routes: /files/{id}/chat, /mesh/chat, /mesh/chat/history, /mesh/tasks/{id}/chat, /contract-types. - Drop the matching openapi registrations. ## Frontend - Delete components that were LLM-only: - `mesh/UnifiedMeshChatInput.tsx` - `listen/DiscussContractModal.tsx` - `listen/TranscriptAnalysisPanel.tsx` - `listen/ContractPickerModal.tsx` - `files/CliInput.tsx` - Delete the entire /listen page (its primary value-add was voice → LLM analysis → contract creation; without LLM the page is just a transcript display with no obvious user purpose). - Delete `hooks/useMeshChatHistory.ts` and `lib/listenApi.ts` (transcript-analysis API client to the already-Phase-5-removed listen handlers). - Strip api.ts of LLM exports: LlmModel, ChatMessage/Request/Response, UserQuestion/Answer, chatWithFile, MeshChat* types & functions, getMeshChatHistory, clearMeshChatHistory, chatWithMeshContext, ContractTypeTemplate, listContractTypes, chatWithContract, getContractChatHistory, clearContractChatHistory, discussContract, PhaseDefinition, DeliverableDefinition. - mesh.tsx: drop UnifiedMeshChatInput render + the chatContext memo + handleTaskUpdatedFromCli (only consumer was the input). - files.tsx: drop CliInput render + handleGenerateFromElement + handleBodyUpdate + handleClearFocus + suggestedPrompt state (all CliInput-only). - NavStrip: drop the /listen link. - main.tsx: drop the /listen route. ## Net diff: 37 files changed, 58 insertions, 14,281 deletions. Co-Authored-By: Claude Opus 4.7 (1M context) --- makima/frontend/src/components/NavStrip.tsx | 1 - makima/frontend/src/components/files/CliInput.tsx | 483 ------------------- .../src/components/listen/ContractPickerModal.tsx | 133 ----- .../src/components/listen/ControlPanel.tsx | 200 -------- .../src/components/listen/DiscussContractModal.tsx | 287 ----------- .../src/components/listen/SpeakerPanel.tsx | 61 --- .../components/listen/TranscriptAnalysisPanel.tsx | 339 ------------- .../src/components/listen/TranscriptPanel.tsx | 85 ---- .../src/components/mesh/UnifiedMeshChatInput.tsx | 536 --------------------- 9 files changed, 2125 deletions(-) delete mode 100644 makima/frontend/src/components/files/CliInput.tsx delete mode 100644 makima/frontend/src/components/listen/ContractPickerModal.tsx delete mode 100644 makima/frontend/src/components/listen/ControlPanel.tsx delete mode 100644 makima/frontend/src/components/listen/DiscussContractModal.tsx delete mode 100644 makima/frontend/src/components/listen/SpeakerPanel.tsx delete mode 100644 makima/frontend/src/components/listen/TranscriptAnalysisPanel.tsx delete mode 100644 makima/frontend/src/components/listen/TranscriptPanel.tsx delete mode 100644 makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx (limited to 'makima/frontend/src/components') 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([]); - const [expanded, setExpanded] = useState(false); - const [model, setModel] = useState("claude-opus"); - // Track conversation history for context continuity - const [conversationHistory, setConversationHistory] = useState([]); - // Track pending questions from the LLM - const [pendingQuestions, setPendingQuestions] = useState(null); - // Track user's answers to questions - const [userAnswers, setUserAnswers] = useState>(new Map()); - // Track custom input for each question - const [customInputs, setCustomInputs] = useState>(new Map()); - - const inputRef = useRef(null); - const messagesRef = useRef(null); - - // 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(); - 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(); - 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 ( -
- {/* Messages Panel (expandable) */} - {expanded && messages.length > 0 && ( -
- {messages.map((msg) => ( -
- {msg.type === "user" && ( -
- > - {msg.content} -
- )} - {(msg.type === "assistant" || msg.type === "question") && ( -
- - {msg.toolCalls && msg.toolCalls.length > 0 && ( -
- {msg.toolCalls.map((tc, i) => ( -
- - {tc.success ? "+" : "x"} - {" "} - {tc.name}: {tc.message} -
- ))} -
- )} -
- )} - {msg.type === "error" && ( -
{msg.content}
- )} -
- ))} -
- )} - - {/* Pending Questions UI */} - {pendingQuestions && pendingQuestions.length > 0 && ( -
-
- Questions from AI -
- {pendingQuestions.map((q) => ( -
-
{q.question}
-
- {q.options.map((option) => { - const isSelected = (userAnswers.get(q.id) || []).includes(option); - return ( - - ); - })} -
- {q.allowCustom && ( - handleCustomInputChange(q.id, e.target.value)} - placeholder="Or type a custom answer..." - className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-xs px-2 py-1 outline-none focus:border-[#3f6fb3] placeholder-[#555]" - /> - )} -
- ))} -
- - -
-
- )} - - {/* Input Bar */} -
- - - {/* Focus Badge */} - {focusedElement && ( - - )} - - > - 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 && ( - - )} - -
-
- ); -} 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(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 ( -
-
-
-

- Select Contract -

- -
- -
- {loading ? ( -
- Loading... -
- ) : ( - <> - - - - - {contracts.map((contract) => ( - - ))} - - {contracts.length === 0 && ( -
- No contracts available -
- )} - - )} -
-
-
- ); -} 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 ( -
- {/* Logo button */} -
- - - {statusText} - -
- - {/* Status indicators */} -
- {/* Microphone status */} -
-
- - {micStatus === "ready" || micStatus === "recording" - ? "MIC READY" - : micStatus === "requesting" - ? "REQUESTING..." - : micStatus === "denied" - ? "MIC DENIED" - : micStatus === "error" - ? "MIC ERROR" - : "MIC IDLE"} -
- {isListening && ( -
-
-
- )} -
- - {/* Connection status */} -
-
- - {isConnected - ? "CONNECTED" - : connectionStatus === "connecting" - ? "LOADING MODELS..." - : "DISCONNECTED"} -
- {connectionStatus === "connecting" && ( -
-
-
- )} -
-
- - {/* Error display */} - {error && ( -
- {error} -
- )} - - {/* Buttons */} -
- - -
- - setIsModalOpen(false)} - contracts={contracts} - selectedContractId={selectedContractId} - onSelect={onContractChange} - onDiscussContract={onDiscussContract} - loading={contractsLoading} - /> -
- ); -} 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 ( -
-
-
{message.content}
- - {!isUser && ( -
- -
- )} - - {message.toolCalls && message.toolCalls.length > 0 && ( -
- {message.toolCalls.map((tc, i) => ( -
- {tc.result.success ? "+" : "-"} {tc.name}: {tc.result.message} -
- ))} -
- )} -
-
- ); -} - -export function DiscussContractModal({ - isOpen, - onClose, - transcriptContext, - onContractCreated, -}: DiscussContractModalProps) { - const [messages, setMessages] = useState([]); - const [input, setInput] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [createdContract, setCreatedContract] = useState(null); - - const { speak, isSpeaking, cancel } = useSpeakWebSocket(); - const messagesEndRef = useRef(null); - const modalRef = useRef(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 ( -
-
- {/* Header */} -
-

- Discuss Contract with Makima -

- -
- - {/* Messages */} -
- {messages.map((message) => ( - handleSpeak(message.content)} - isSpeaking={isSpeaking} - /> - ))} -
- - {isLoading && ( -
- Makima is thinking... -
- )} - - {error && ( -
- {error} -
- )} -
- - {/* Contract Created Banner */} - {createdContract && ( -
-
- Contract "{createdContract.name}" created successfully! -
-
- )} - - {/* Input */} -
-
- 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" - /> - -
-
-
-
- ); -} 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 ( -
-
- SPEAKERS// -
- - {speakers.length === 0 ? ( -
- Waiting for speech... -
- ) : ( -
- {speakers.map((speaker, index) => ( -
- - {SPEAKER_SYMBOLS[index % SPEAKER_SYMBOLS.length]} - -
-
- {speaker.label} -
-
- {speaker.isActive ? "speaking" : "idle"} -
-
- {speaker.isActive && ( -
- )} -
- ))} -
- )} -
- ); -} 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("idle"); - const [analysis, setAnalysis] = useState(null); - const [error, setError] = useState(null); - const [successMessage, setSuccessMessage] = useState(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) || {}; - - return ( -
- {/* Header */} -
-
- TRANSCRIPT ANALYSIS// -
- {onClose && ( - - )} -
- - {/* Error display */} - {error && ( -
- {error} -
- )} - - {/* Success message */} - {successMessage && ( -
- {successMessage} -
- )} - - {/* Initial state - Show analyze button */} - {state === "idle" && ( -
-

- Transcript saved. Analyze to extract requirements, decisions, and action items. -

- -
- )} - - {/* Analyzing state */} - {state === "analyzing" && ( -
-
-

Analyzing transcript...

-
- )} - - {/* Analysis results */} - {(state === "analyzed" || state === "creating" || state === "updating") && analysis && ( -
- {/* Suggested Contract Info */} - {(analysis.suggestedContractName || analysis.suggestedDescription) && ( -
-
- Suggested Contract -
- {analysis.suggestedContractName && ( -
- {analysis.suggestedContractName} -
- )} - {analysis.suggestedDescription && ( -
- {analysis.suggestedDescription} -
- )} -
- )} - - {/* Requirements */} - {Object.keys(groupedRequirements).length > 0 && ( -
-
- Requirements ({analysis.requirements.length}) -
- {Object.entries(groupedRequirements).map(([category, reqs]) => ( -
-
- {category} -
-
    - {reqs.map((req, idx) => ( -
  • - - - {req.text} - {req.speaker && ( - ({req.speaker}) - )} -
  • - ))} -
-
- ))} -
- )} - - {/* Decisions */} - {analysis.decisions.length > 0 && ( -
-
- Decisions ({analysis.decisions.length}) -
-
    - {analysis.decisions.map((decision, idx) => ( -
  • -
    - - - {decision.text} -
    - {decision.context && ( -
    - Context: {decision.context} -
    - )} -
  • - ))} -
-
- )} - - {/* Action Items */} - {analysis.actionItems.length > 0 && ( -
-
- Action Items ({analysis.actionItems.length}) -
-
    - {analysis.actionItems.map((item, idx) => ( -
  • -
    - - -
    - {item.text} -
    - {item.assignee && ( - - @{item.assignee} - - )} - {item.priority && ( - - {item.priority} - - )} -
    -
    -
    -
  • - ))} -
-
- )} - - {/* Key Topics */} - {analysis.keyTopics.length > 0 && ( -
-
- Key Topics -
-
- {analysis.keyTopics.map((topic, idx) => ( - - {topic} - - ))} -
-
- )} - - {/* Speaker Statistics */} - {analysis.speakerSummary.length > 0 && ( -
-
- Speaker Statistics -
-
- {analysis.speakerSummary.map((speaker, idx) => ( -
- - {speaker.speaker} - -
-
-
- - {speaker.contributionPercentage.toFixed(0)}% - -
- ))} -
-
- )} - - {/* Action Buttons */} -
- - {selectedContractId && selectedContractId !== contractId && ( - - )} -
-
- )} -
- ); -} 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(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 ( -
-
- TRANSCRIPT// - {!autoScroll && ( - - )} -
- -
- {transcripts.length === 0 ? ( -
- Transcriptions will appear here... -
- ) : ( - transcripts.map((entry) => ( -
-
- - [{entry.start.toFixed(2)}s - {entry.end.toFixed(2)}s] - - - {entry.speaker} - - {entry.isFinal && ( - [FINAL] - )} -
-

{entry.text}

-
- )) - )} -
-
- ); -} 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(DEFAULT_MODEL); - - // Pending questions state - const [pendingQuestions, setPendingQuestions] = useState< - UserQuestion[] | null - >(null); - const [userAnswers, setUserAnswers] = useState>( - new Map() - ); - const [customInputs, setCustomInputs] = useState>( - new Map() - ); - - // Command history for arrow key navigation - const [commandHistory, setCommandHistory] = useState([]); - const [historyIndex, setHistoryIndex] = useState(-1); - const [savedInput, setSavedInput] = useState(""); - - const inputRef = useRef(null); - const messagesRef = useRef(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) => { - 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(); - 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(); - 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 ( -
- {/* Error Display */} - {historyError && ( -
- {historyError} -
- )} - - {/* Messages Panel (expandable) */} - {expanded && messages.length > 0 && ( -
- {messages.map((msg) => ( -
- {msg.role === "user" && ( -
- > - - {msg.content} - - {msg.contextType !== "mesh" && ( - - [{msg.contextType}] - - )} -
- )} - {msg.role === "assistant" && ( -
- - {msg.toolCalls && msg.toolCalls.length > 0 && ( -
- {msg.toolCalls.map((tc, i) => ( -
- - {tc.result.success ? "+" : "x"} - {" "} - {tc.name}: {tc.result.message} -
- ))} -
- )} -
- )} - {msg.role === "error" && ( -
{msg.content}
- )} -
- ))} -
- )} - - {/* Pending Questions UI */} - {pendingQuestions && pendingQuestions.length > 0 && ( -
-
- Questions from AI -
- {pendingQuestions.map((q) => ( -
-
- {q.question} -
-
- {q.options.map((option) => { - const isSelected = (userAnswers.get(q.id) || []).includes( - option - ); - return ( - - ); - })} -
- {q.allowCustom && ( - handleCustomInputChange(q.id, e.target.value)} - placeholder="Or type a custom answer..." - className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-xs px-2 py-1 outline-none focus:border-[#3f6fb3] placeholder-[#555]" - /> - )} -
- ))} -
- - -
-
- )} - - {/* Input Bar */} -
- - - [{getContextLabel(context)}] - - > - 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 && ( - - )} - -
-
- ); -} -- cgit v1.2.3