summaryrefslogtreecommitdiff
path: root/makima/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src')
-rw-r--r--makima/frontend/src/components/NavStrip.tsx1
-rw-r--r--makima/frontend/src/components/files/CliInput.tsx483
-rw-r--r--makima/frontend/src/components/listen/ContractPickerModal.tsx133
-rw-r--r--makima/frontend/src/components/listen/ControlPanel.tsx200
-rw-r--r--makima/frontend/src/components/listen/DiscussContractModal.tsx287
-rw-r--r--makima/frontend/src/components/listen/SpeakerPanel.tsx61
-rw-r--r--makima/frontend/src/components/listen/TranscriptAnalysisPanel.tsx339
-rw-r--r--makima/frontend/src/components/listen/TranscriptPanel.tsx85
-rw-r--r--makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx536
-rw-r--r--makima/frontend/src/hooks/useMeshChatHistory.ts133
-rw-r--r--makima/frontend/src/lib/api.ts466
-rw-r--r--makima/frontend/src/lib/listenApi.ts168
-rw-r--r--makima/frontend/src/main.tsx9
-rw-r--r--makima/frontend/src/routes/files.tsx88
-rw-r--r--makima/frontend/src/routes/listen.tsx277
-rw-r--r--makima/frontend/src/routes/mesh.tsx37
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]">&gt;</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">&times;</span>
- </button>
- )}
-
- <span className="text-[#9bc3ff] font-mono text-sm">&gt;</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]">&gt;</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">&gt;</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>