diff options
| author | soryu <soryu@soryu.co> | 2026-05-17 21:22:34 +0100 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-05-17 21:22:34 +0100 |
| commit | 857e717e6343fa5c2ae96664bdc64741d5ba6830 (patch) | |
| tree | 0f3898d9e2e2a3c312358dbf70c44f4ab1cf3648 /makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx | |
| parent | ce29ae801bcc5a0ba76d5a8d1565242ab267a47d (diff) | |
| download | soryu-857e717e6343fa5c2ae96664bdc64741d5ba6830.tar.gz soryu-857e717e6343fa5c2ae96664bdc64741d5ba6830.zip | |
chore: remove LLM module + all dependent surfacesremove-llm
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) <noreply@anthropic.com>
Diffstat (limited to 'makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx')
| -rw-r--r-- | makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx | 536 |
1 files changed, 0 insertions, 536 deletions
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> - ); -} |
