diff options
| author | soryu <soryu@soryu.co> | 2026-02-03 18:16:43 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-03 18:16:43 +0000 |
| commit | 334db7304209f48261563c0b9d955b3037b55833 (patch) | |
| tree | c141919c73758d7768be8cae66576cd1e17c38ff | |
| parent | 8361916ce67f3d2ba191ebf27cb50e79cb42e39c (diff) | |
| download | soryu-makima/task-task-be1c651d-be1c651d.tar.gz soryu-makima/task-task-be1c651d-be1c651d.zip | |
[WIP] Heartbeat checkpoint - 2026-02-03 18:16:43 UTCmakima/task-task-be1c651d-be1c651d
| -rw-r--r-- | makima/docs/PLAN-discuss-contract.md | 953 |
1 files changed, 953 insertions, 0 deletions
diff --git a/makima/docs/PLAN-discuss-contract.md b/makima/docs/PLAN-discuss-contract.md new file mode 100644 index 0000000..7543bdb --- /dev/null +++ b/makima/docs/PLAN-discuss-contract.md @@ -0,0 +1,953 @@ +# Discuss Contract Feature - Implementation Plan + +## Overview + +Add a "Discuss Contract" feature to the listen page that enables users to have a natural conversation with Makima (in character) to flesh out and ultimately create a contract specification. This feature bridges the gap between raw transcript/requirements and a well-defined contract through interactive dialogue. + +**Goal:** Enable users to talk through their project idea with Makima, refine requirements conversationally, and then have the LLM create a properly-structured contract when the conversation reaches a natural conclusion. + +## Key Design Principles + +1. **Ephemeral conversation** - Does not require an existing contract (creates one at the end) +2. **Character-aware** - Makima responds in character, aware of the makima.jp platform +3. **Contextual** - Can incorporate transcript context from the current session +4. **Audible** - Each response can be spoken using the TTS functionality +5. **Natural conclusion** - The LLM decides when to create the contract via tool use +6. **Reuse existing infrastructure** - Leverage chatWithContract API patterns and useSpeakWebSocket + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Listen Page │ +├─────────────────────────────────────────────────────────────────┤ +│ ControlPanel │ +│ ├── Contract Dropdown │ +│ │ ├── Ephemeral │ +│ │ ├── [existing contracts...] │ +│ │ └── ** "Discuss Contract" ** <-- NEW OPTION │ +│ └── ... │ +├─────────────────────────────────────────────────────────────────┤ +│ DiscussContractModal (NEW) │ +│ ├── Conversation history (chat bubbles) │ +│ ├── Text input │ +│ ├── Speak button per response │ +│ └── Contract created notification │ +└─────────────────────────────────────────────────────────────────┘ + │ + │ POST /api/v1/contracts/discuss + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Backend: discuss_contract_handler │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. Receive message + history + optional transcript context │ +│ 2. Build Makima-character system prompt │ +│ 3. Run agentic loop with discussion tools │ +│ 4. When ready, call create_contract tool │ +│ 5. Return response + tool calls + created contract info │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Implementation Tasks + +--- + +### Task 1: Backend - New Discussion Endpoint + +**File:** `makima/src/server/handlers/contract_discuss.rs` (NEW) + +Create a new handler for ephemeral contract discussions that doesn't require an existing contract. + +#### 1.1 Request/Response Types + +```rust +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DiscussContractRequest { + /// The user's message + pub message: String, + /// Optional model selection (default: claude-sonnet) + #[serde(default)] + pub model: Option<String>, + /// Conversation history for context continuity + #[serde(default)] + pub history: Option<Vec<ChatMessage>>, + /// Optional transcript context from current session + #[serde(default)] + pub transcript_context: Option<String>, +} + +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DiscussContractResponse { + /// Makima's response message + pub response: String, + /// Tool calls that were executed (e.g., create_contract) + pub tool_calls: Vec<ToolCallInfo>, + /// If a contract was created, its details + #[serde(skip_serializing_if = "Option::is_none")] + pub created_contract: Option<CreatedContractInfo>, + /// Pending questions (if LLM needs clarification) + #[serde(skip_serializing_if = "Option::is_none")] + pub pending_questions: Option<Vec<UserQuestion>>, +} + +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreatedContractInfo { + pub id: String, + pub name: String, + pub description: Option<String>, + pub contract_type: String, + pub initial_phase: String, +} +``` + +#### 1.2 System Prompt - Makima Character + +```rust +const DISCUSS_SYSTEM_PROMPT: &str = r#" +You are Makima, an AI assistant on the makima.jp platform. You help users define and create contracts for their projects through natural conversation. + +## Your Personality +- Professional yet personable +- Focused on understanding the user's actual needs +- Ask clarifying questions when requirements are vague +- Guide the conversation toward actionable outcomes +- Comfortable making recommendations based on experience + +## Your Goal +Help the user flesh out their project idea into a well-defined contract. A contract on makima.jp includes: +- A clear name and description +- The right contract type (simple, specification, or execute) +- Understanding of the scope and requirements + +## Contract Types +- **simple**: Quick tasks with minimal planning (plan -> execute phases only) +- **specification**: Full lifecycle projects (research -> specify -> plan -> execute -> review) +- **execute**: Direct implementation when requirements are already clear (execute phase only) + +## Guidelines +1. **Start by understanding**: Ask about what they want to build +2. **Clarify scope**: Is this a quick fix, a new feature, or a full project? +3. **Gather requirements**: What are the must-haves vs nice-to-haves? +4. **Identify context**: Is there existing code? Which repository? +5. **Recommend type**: Suggest the appropriate contract type +6. **Confirm and create**: When the user is satisfied, create the contract + +## When to Create the Contract +Create the contract when: +- You have a clear understanding of what the user wants +- The user has confirmed they're ready to proceed +- You've gathered enough information for a meaningful contract + +Do NOT create the contract if: +- The user is still exploring ideas +- Key information is missing +- The user hasn't indicated readiness + +{transcript_context} +"#; +``` + +#### 1.3 Tool Definitions + +**File:** `makima/src/llm/discuss_tools.rs` (NEW) + +```rust +pub static DISCUSS_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::Lazy::new(|| { + vec![ + Tool { + name: "create_contract".to_string(), + description: "Create a new contract based on the discussion. Only call this when the user has confirmed they're ready to create the contract.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name for the contract" + }, + "description": { + "type": "string", + "description": "Detailed description of what the contract is for" + }, + "contract_type": { + "type": "string", + "enum": ["simple", "specification", "execute"], + "description": "Type of contract workflow" + }, + "repository_url": { + "type": "string", + "description": "Optional repository URL if discussed" + }, + "local_only": { + "type": "boolean", + "description": "If true, tasks won't auto-push or create PRs" + } + }, + "required": ["name", "description", "contract_type"] + }), + }, + Tool { + name: "ask_clarification".to_string(), + description: "Ask the user a clarifying question with multiple choice options.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "question": { + "type": "string", + "description": "The question to ask" + }, + "options": { + "type": "array", + "items": { "type": "string" }, + "description": "Multiple choice options" + }, + "allow_custom": { + "type": "boolean", + "description": "Allow user to provide a custom answer" + } + }, + "required": ["question", "options"] + }), + }, + ] +}); +``` + +#### 1.4 Handler Implementation + +```rust +pub async fn discuss_contract_handler( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Json(request): Json<DiscussContractRequest>, +) -> impl IntoResponse { + // 1. Build system prompt with optional transcript context + let transcript_section = match &request.transcript_context { + Some(ctx) => format!( + "\n## Current Session Context\nThe user has been recording a session. Here's the transcript:\n\n{}\n", + ctx + ), + None => String::new(), + }; + + let system_prompt = DISCUSS_SYSTEM_PROMPT.replace("{transcript_context}", &transcript_section); + + // 2. Initialize LLM client + let llm_client = match model { + LlmModel::ClaudeSonnet => ClaudeClient::from_env(ClaudeModel::Sonnet)?, + // ... + }; + + // 3. Run agentic loop with DISCUSS_TOOLS + run_discuss_agentic_loop( + pool, + &state, + &llm_client, + system_prompt, + &request, + auth.owner_id, + ).await +} +``` + +#### 1.5 Register the Endpoint + +**File:** `makima/src/server/mod.rs` + +Add route: +```rust +.route("/api/v1/contracts/discuss", post(handlers::contract_discuss::discuss_contract_handler)) +``` + +--- + +### Task 2: Frontend - DiscussContractModal Component + +**File:** `makima/frontend/src/components/listen/DiscussContractModal.tsx` (NEW) + +Create a chat modal component for the discussion interface. + +#### 2.1 Types + +```typescript +interface Message { + id: string; + role: "user" | "assistant"; + content: string; + timestamp: Date; + toolCalls?: ToolCallInfo[]; +} + +interface DiscussContractModalProps { + isOpen: boolean; + onClose: () => void; + transcriptContext?: string; // Current session transcript + onContractCreated: (contract: CreatedContractInfo) => void; +} + +interface DiscussContractState { + messages: Message[]; + isLoading: boolean; + error: string | null; + createdContract: CreatedContractInfo | null; +} +``` + +#### 2.2 Component Structure + +```tsx +export function DiscussContractModal({ + isOpen, + onClose, + transcriptContext, + onContractCreated, +}: DiscussContractModalProps) { + const [messages, setMessages] = useState<Message[]>([]); + const [input, setInput] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [createdContract, setCreatedContract] = useState<CreatedContractInfo | null>(null); + + const { speak, isSpeaking, cancel } = useSpeakWebSocket(); + const messagesEndRef = 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) { + // Add initial assistant message + 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]); + + const handleSend = 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); + + try { + const history = 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) { + // Handle error + } finally { + setIsLoading(false); + } + }; + + const handleSpeak = (text: string) => { + if (isSpeaking) { + cancel(); + } else { + speak(text); + } + }; + + return ( + <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"> + <div className="panel w-full max-w-2xl h-[600px] flex flex-col"> + {/* 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="..."> + [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">Makima is thinking...</span> + </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={(e) => e.key === "Enter" && handleSend()} + 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" + /> + <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" + > + Send + </button> + </div> + </div> + </div> + </div> + ); +} +``` + +#### 2.3 ChatBubble Component + +```tsx +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> + ); +} +``` + +--- + +### Task 3: Frontend - API Integration + +**File:** `makima/frontend/src/lib/api.ts` + +Add the discuss contract API function. + +```typescript +// ============================================================================= +// 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(); +} +``` + +--- + +### Task 4: Frontend - Integration into Listen Page + +#### 4.1 Update ContractPickerModal + +**File:** `makima/frontend/src/components/listen/ContractPickerModal.tsx` + +Add "Discuss Contract" option to the picker. + +```tsx +interface ContractPickerModalProps { + isOpen: boolean; + onClose: () => void; + contracts: ContractOption[]; + selectedContractId: string | null; + onSelect: (contractId: string | null) => void; + onDiscussContract: () => void; // NEW + loading?: boolean; +} + +export function ContractPickerModal({ + // ... existing props + onDiscussContract, +}: ContractPickerModalProps) { + return ( + <div className="..."> + {/* ... existing content ... */} + + {/* Ephemeral option */} + <button onClick={() => handleSelect(null)} className="..."> + <span className="uppercase tracking-wide">Ephemeral</span> + <span className="...">Transcript not saved</span> + </button> + + {/* NEW: Discuss Contract option */} + <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> + + {/* Existing contracts */} + {contracts.map((contract) => ( + // ... existing contract buttons + ))} + </div> + ); +} +``` + +#### 4.2 Update ControlPanel + +**File:** `makima/frontend/src/components/listen/ControlPanel.tsx` + +Add prop for discuss contract callback. + +```tsx +interface ControlPanelProps { + // ... existing props + onDiscussContract: () => void; // NEW +} + +export function ControlPanel({ + // ... existing props + onDiscussContract, +}: ControlPanelProps) { + return ( + <div className="..."> + {/* ... existing content ... */} + + <ContractPickerModal + isOpen={isModalOpen} + onClose={() => setIsModalOpen(false)} + contracts={contracts} + selectedContractId={selectedContractId} + onSelect={onContractChange} + onDiscussContract={onDiscussContract} + loading={contractsLoading} + /> + </div> + ); +} +``` + +#### 4.3 Update Listen Route + +**File:** `makima/frontend/src/routes/listen.tsx` + +Integrate the DiscussContractModal. + +```tsx +import { DiscussContractModal } from "../components/listen/DiscussContractModal"; + +export default function ListenPage() { + // ... existing state ... + + // NEW: Discuss contract modal state + const [isDiscussModalOpen, setIsDiscussModalOpen] = useState(false); + + // 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); + }, []); + + return ( + <div className="..."> + {/* ... existing content ... */} + + <ControlPanel + // ... existing props ... + onDiscussContract={handleOpenDiscussModal} + /> + + {/* NEW: Discuss Contract Modal */} + <DiscussContractModal + isOpen={isDiscussModalOpen} + onClose={() => setIsDiscussModalOpen(false)} + transcriptContext={transcriptContext} + onContractCreated={handleContractCreated} + /> + + {/* ... existing TranscriptAnalysisPanel ... */} + </div> + ); +} +``` + +--- + +### Task 5: Integration Testing + +#### 5.1 Backend Tests + +**File:** `makima/src/server/handlers/contract_discuss_test.rs` + +```rust +#[tokio::test] +async fn test_discuss_contract_basic_conversation() { + // Test basic message/response flow +} + +#[tokio::test] +async fn test_discuss_contract_creates_contract() { + // Test that create_contract tool works +} + +#[tokio::test] +async fn test_discuss_contract_with_transcript_context() { + // Test transcript context is included +} +``` + +#### 5.2 Frontend Tests + +- Test modal opens/closes correctly +- Test message sending and receiving +- Test speak button functionality +- Test contract creation flow + +--- + +## UI/UX Flow + +### User Journey + +1. **User records a transcript** (optional) + - Captures meeting or brainstorming session + +2. **User clicks "Contract" dropdown** + - Sees: Ephemeral, existing contracts, "Discuss Contract" option + +3. **User selects "Discuss Contract"** + - Modal opens with greeting from Makima + - If transcript exists, Makima references it + +4. **Conversation flows naturally** + - User describes what they want to build + - Makima asks clarifying questions + - User can click [Speak] to hear Makima's responses + +5. **When ready, user confirms** + - "Yes, let's create the contract" + - Makima calls create_contract tool + +6. **Contract created** + - Success banner appears + - Modal closes, contract selected automatically + - User can continue with normal workflow + +### Example Conversation + +``` +Makima: Hello! I'm Makima. Tell me about what you'd like to build, + and I'll help you create a contract for it. + +User: I want to add dark mode to my React app + +Makima: Great! Dark mode is a common feature. Let me understand your + situation better: + + 1. Is this for an existing React application, or are you + starting fresh? + 2. Do you have a specific design system or color palette in mind? + 3. Should users be able to toggle between light/dark, or should + it follow system preferences? + +User: Existing app. I want both - toggle and follow system. + No specific design yet. + +Makima: Perfect. For an existing app adding dark mode with toggle + AND system preference support, I'd recommend a "simple" contract + since the requirements are clear. + + Here's what I'm thinking: + - **Name:** "Add Dark Mode Support" + - **Description:** Implement theme switching (light/dark mode) + with manual toggle and system preference detection + - **Type:** Simple (plan -> execute) + + Does this look right? Should I create this contract? + +User: Yes, looks good! + +Makima: Done! I've created the contract "Add Dark Mode Support". + You can now add your repository and start planning the + implementation. + + [Contract created: Add Dark Mode Support] +``` + +--- + +## Technical Details + +### API Contract + +#### Request +``` +POST /api/v1/contracts/discuss +Content-Type: application/json +Authorization: Bearer <token> + +{ + "message": "I want to add dark mode to my app", + "model": "claude-sonnet", + "history": [ + { "role": "assistant", "content": "Hello! I'm Makima..." }, + { "role": "user", "content": "I want to add dark mode" } + ], + "transcriptContext": "[Speaker A]: Let's discuss the dark mode feature..." +} +``` + +#### Response +```json +{ + "response": "Great! Let me understand your requirements...", + "toolCalls": [], + "createdContract": null, + "pendingQuestions": null +} +``` + +#### Response (with contract creation) +```json +{ + "response": "Done! I've created the contract...", + "toolCalls": [ + { + "name": "create_contract", + "result": { + "success": true, + "message": "Contract 'Add Dark Mode Support' created" + } + } + ], + "createdContract": { + "id": "uuid-here", + "name": "Add Dark Mode Support", + "description": "Implement theme switching...", + "contractType": "simple", + "initialPhase": "plan" + } +} +``` + +### State Management + +The DiscussContractModal manages its own state: + +```typescript +interface ModalState { + messages: Message[]; // Conversation history + isLoading: boolean; // API call in progress + error: string | null; // Error message + createdContract: CreatedContractInfo | null; // Created contract info +} +``` + +The parent (ListenPage) only needs to know: +- Is modal open? +- When was a contract created? + +### Error Handling + +1. **API errors** - Display in modal, allow retry +2. **Network errors** - Display error, suggest retry +3. **LLM errors** - Display gracefully, offer to restart conversation + +--- + +## Files to Create/Modify + +### New Files +1. `makima/src/server/handlers/contract_discuss.rs` - Backend handler +2. `makima/src/llm/discuss_tools.rs` - Tool definitions +3. `makima/frontend/src/components/listen/DiscussContractModal.tsx` - Chat UI + +### Modified Files +1. `makima/src/server/mod.rs` - Register new route +2. `makima/src/server/openapi.rs` - Add OpenAPI docs +3. `makima/src/llm/mod.rs` - Export discuss tools +4. `makima/frontend/src/lib/api.ts` - Add API function +5. `makima/frontend/src/components/listen/ContractPickerModal.tsx` - Add discuss option +6. `makima/frontend/src/components/listen/ControlPanel.tsx` - Pass callback +7. `makima/frontend/src/routes/listen.tsx` - Integrate modal + +--- + +## Key Design Decisions + +1. **New endpoint vs. existing chatWithContract** + - **Decision:** New endpoint `/api/v1/contracts/discuss` + - **Rationale:** chatWithContract requires an existing contract; this feature creates one + +2. **System prompt character** + - **Decision:** Makima-character aware of makima.jp platform + - **Rationale:** Consistent brand experience, engaging conversation + +3. **Tool for contract creation** + - **Decision:** `create_contract` tool the LLM invokes + - **Rationale:** Natural conversation flow; LLM decides when ready + +4. **Transcript context** + - **Decision:** Pass as optional context in system prompt + - **Rationale:** Seamless integration with listen page recording + +5. **Speak functionality** + - **Decision:** Per-message speak button using useSpeakWebSocket + - **Rationale:** Reuses existing TTS infrastructure + +6. **Conversation history** + - **Decision:** Frontend manages history, sends with each request + - **Rationale:** Stateless backend, consistent with existing chat patterns |
