# Claude CLI Chatbox Integration Plan ## Conversational Document Search & Analysis **Document Version:** 1.0 **Created:** 2025-11-13 **Status:** Integration Plan (Ready for Implementation) **Implementation Time:** 45-60 minutes **Complexity:** Medium **Dependencies:** Vue 3 frontend, Express.js backend, Anthropic Claude API --- ## Executive Summary Integrate Claude API into NaviDocs to enable: 1. **Conversational search** - "Tell me about my engine warranty" instead of filtering/searching 2. **Document analysis** - "Summarize my maintenance costs" or "What's still under warranty?" 3. **Online research** - "Find current prices for Yanmar engine parts" (search online for pricing) 4. **Q&A chatbox** - Live chat interface in web app for boat-specific questions This integration provides a modern, AI-powered alternative to traditional search while keeping all document context local to NaviDocs. --- ## 1. Architecture Overview ### 1.1 System Components ``` ┌─────────────────────────────────────────────────────────────┐ │ NaviDocs Web App (Vue 3) │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Chatbox Component (Right sidebar) │ │ │ │ - User types: "What warranties do I have?" │ │ │ │ - Responses stream in real-time │ │ │ │ - References: "Warranty Card - Nov 13" with links │ │ │ └────────────────────────────────────────────────────────┘ │ └──────────────────────┬────────────────────────────────────────┘ │ HTTPS ↓ ┌─────────────────────────────────────────────────────────────┐ │ NaviDocs Backend (Express.js + SQLite) │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ /api/chat/message (POST) │ │ │ │ - Receives user message + boat_id │ │ │ │ - Query SQLite: documents for this boat │ │ │ │ - Query Meilisearch: relevant documents │ │ │ │ - Format context for Claude │ │ │ │ - Call Claude API with documents + question │ │ │ │ - Stream response back to frontend │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Chat History Table (SQLite) │ │ │ │ - Store conversation thread per boat │ │ │ │ - Include references to documents │ │ │ │ - Enable context carry-over to Claude │ │ │ └────────────────────────────────────────────────────────┘ │ └──────────────────────┬────────────────────────────────────────┘ │ HTTPS (API key in Authorization header) ↓ ┌──────────────────────────┐ │ Anthropic Claude API │ │ (claude-3-5-sonnet) │ │ or claude-opus-4 │ └──────────────────────────┘ ``` ### 1.2 Data Flow: Message to Response ``` User (Chatbox): "What's my monthly maintenance cost?" ↓ Frontend: POST /api/chat/message { boat_id: 42, message: "What's my monthly maintenance cost?", thread_id: "thread_xyz" } ↓ Backend: 1. Verify user owns boat_id 2. Query SQLite: SELECT * FROM documents WHERE boat_id=42 3. Query Meilisearch: search("monthly", "maintenance", "cost") 4. Extract relevant document snippets 5. Build system prompt with NaviDocs context 6. Call Claude API with: - System prompt: "You are a boat management AI..." - Document context: "The following are documents from the boat..." - Messages: [{ role: "user", content: "What's my monthly..." }] 7. Stream response token-by-token to frontend 8. Store conversation in chat_history table ↓ Frontend (Streaming): Receive tokens in real-time Display as user types: "Based on your documents..." ↓ User sees: "Based on your documents, your average monthly maintenance cost is €180 across fuel (€80), lubricants (€40), and misc repairs (€60). References: - Maintenance Log - Oct 2025 (€150 in repairs) - Fuel Receipts - Oct 2025 (€80) - Service Invoice - Oct 13 (€60 oil change)" ``` --- ## 2. Claude API Integration Setup ### 2.1 API Key Management ```bash # .env file ANTHROPIC_API_KEY=sk-ant-v1-xxxxxxxxxxxxxxxxxxxxx ANTHROPIC_API_VERSION=2023-06-01 ANTHROPIC_MODEL=claude-3-5-sonnet-20241022 # or claude-opus-4 ANTHROPIC_MAX_TOKENS=2048 # Optional: for tracking costs ANTHROPIC_BUDGET_MONTHLY=500 # USD ``` ### 2.2 Installation ```bash # Add Anthropic SDK to NaviDocs backend npm install @anthropic-ai/sdk # Verify installation npm list @anthropic-ai/sdk # @navidocs@1.0.0 /home/setup/navidocs # └── @anthropic-ai/sdk@0.24.0 ``` ### 2.3 API Client Setup ```javascript // services/claude.js const Anthropic = require('@anthropic-ai/sdk'); class ClaudeService { constructor(apiKey) { this.client = new Anthropic({ apiKey }); this.model = process.env.ANTHROPIC_MODEL || 'claude-3-5-sonnet-20241022'; this.maxTokens = parseInt(process.env.ANTHROPIC_MAX_TOKENS) || 2048; } /** * Generate response for boat-specific question * @param {Object} context - { boat_id, documents, chatHistory } * @param {string} userMessage - User's question * @returns {AsyncIterable} Token stream */ async *generateResponse(context, userMessage) { const { boat_id, documents, boatName, chatHistory } = context; // Build system prompt with boat context const systemPrompt = this.buildSystemPrompt(boat_id, boatName, documents); // Build message history const messages = this.buildMessageHistory(chatHistory, userMessage); // Stream response from Claude const stream = await this.client.messages.create({ model: this.model, max_tokens: this.maxTokens, system: systemPrompt, messages: messages, stream: true }); for await (const event of stream) { if (event.type === 'content_block_delta') { yield event.delta.text; // Yield each text token } } } /** * Build system prompt with boat-specific context */ buildSystemPrompt(boat_id, boatName, documents) { const docSummary = this.summarizeDocuments(documents); return `You are NaviDocs, an AI assistant for boat management and documentation. You are helping the owner of "${boatName}" (Boat ID: ${boat_id}). INSTRUCTIONS: 1. Answer questions about their boat's documentation, maintenance, warranties, and equipment 2. Reference specific documents when answering: "According to your Warranty Card from Nov 13..." 3. If information isn't in their documents, suggest what documents they might need 4. Be concise and practical - focus on actionable information 5. Use their document data to provide personalized insights 6. Maintain conversational tone - not robotic BOAT DOCUMENTS AVAILABLE: ${docSummary} IMPORTANT CONSTRAINTS: - Do NOT invent information about their boat or documents - Do NOT provide legal or financial advice - If asked about pricing/market rates, offer to search online - Always cite the document source when providing specific information - If information is missing, ask user to upload relevant documents CONVERSATION CONTEXT: You have access to this boat's maintenance history, warranties, and equipment documentation. Use this context to provide highly relevant, personalized responses.`; } /** * Summarize documents into context for Claude */ summarizeDocuments(documents) { if (!documents || documents.length === 0) { return 'No documents uploaded yet.'; } let summary = ''; const grouped = this.groupDocumentsByType(documents); for (const [type, docs] of Object.entries(grouped)) { summary += `\n**${type}:**\n`; for (const doc of docs.slice(0, 3)) { // Limit to 3 per type summary += `- ${doc.name} (${doc.upload_date})\n`; } if (docs.length > 3) { summary += `- ... and ${docs.length - 3} more\n`; } } return summary; } /** * Group documents by type */ groupDocumentsByType(documents) { return documents.reduce((acc, doc) => { const type = doc.document_type || 'Other'; if (!acc[type]) acc[type] = []; acc[type].push(doc); return acc; }, {}); } /** * Build message history for Claude (include previous context) */ buildMessageHistory(chatHistory, userMessage) { const messages = []; // Add previous messages (limit to last 5 exchanges to save tokens) if (chatHistory && chatHistory.length > 0) { const recentHistory = chatHistory.slice(-10); // 5 exchanges = 10 messages for (const entry of recentHistory) { messages.push({ role: entry.role, content: entry.message }); } } // Add current user message messages.push({ role: 'user', content: userMessage }); return messages; } /** * Get document references from Claude response * Extract which documents were cited/relevant */ extractDocumentReferences(response, documents) { const references = []; for (const doc of documents) { // Simple matching: if document name appears in response if (response.toLowerCase().includes(doc.name.toLowerCase())) { references.push({ doc_id: doc.id, name: doc.name, type: doc.document_type, download_url: doc.download_url }); } } return references; } } module.exports = new ClaudeService(process.env.ANTHROPIC_API_KEY); ``` --- ## 3. Backend API Endpoint ### 3.1 Chat Message Handler ```javascript // routes/api/chat.js const express = require('express'); const router = express.Router(); const db = require('../../db'); const meilisearch = require('../../meilisearch'); const claudeService = require('../../services/claude'); const { authenticate, authorize } = require('../../middleware/auth'); /** * POST /api/chat/message * Send message to Claude, stream response */ router.post('/message', authenticate, async (req, res) => { const { boat_id, message, thread_id } = req.body; const userId = req.user.id; // Validate request if (!boat_id || !message) { return res.status(400).json({ error: 'boat_id and message required' }); } // Verify user owns this boat const boat = await db.get( 'SELECT * FROM boats WHERE id = ? AND owner_id = ?', [boat_id, userId] ); if (!boat) { return res.status(403).json({ error: 'Access denied' }); } try { // Get chat thread (or create new one) let thread = null; if (thread_id) { thread = await db.get( 'SELECT * FROM chat_threads WHERE id = ? AND boat_id = ?', [thread_id, boat_id] ); } if (!thread) { const result = await db.run( 'INSERT INTO chat_threads (boat_id, created_at) VALUES (?, ?)', [boat_id, new Date().toISOString()] ); thread = { id: result.lastID, boat_id }; } // Fetch all documents for this boat (context) const documents = await db.all( 'SELECT id, document_name as name, document_type, upload_date, file_path FROM documents WHERE boat_id = ?', [boat_id] ); // Fetch recent chat history (last 5 exchanges) const chatHistory = await db.all( `SELECT role, message FROM chat_messages WHERE thread_id = ? ORDER BY created_at DESC LIMIT 10`, [thread.id] ); // Store user message await db.run( `INSERT INTO chat_messages (thread_id, role, message, created_at) VALUES (?, ?, ?, ?)`, [thread.id, 'user', message, new Date().toISOString()] ); // Prepare context for Claude const context = { boat_id, boatName: boat.boat_name, documents, chatHistory: chatHistory.reverse() // Chronological order }; // Set response headers for streaming res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); let fullResponse = ''; try { // Stream response from Claude for await (const token of claudeService.generateResponse(context, message)) { res.write(`data: ${JSON.stringify({ token })}\n\n`); fullResponse += token; } // Extract document references from response const references = claudeService.extractDocumentReferences(fullResponse, documents); // Store assistant response and references await db.run( `INSERT INTO chat_messages (thread_id, role, message, references, created_at) VALUES (?, ?, ?, ?, ?)`, [thread.id, 'assistant', fullResponse, JSON.stringify(references), new Date().toISOString()] ); // Signal completion res.write(`data: ${JSON.stringify({ done: true, thread_id: thread.id })}\n\n`); res.end(); } catch (streamError) { console.error('Stream error:', streamError); res.write(`data: ${JSON.stringify({ error: 'Stream interrupted' })}\n\n`); res.end(); } } catch (error) { console.error('Chat error:', error); res.status(500).json({ error: 'Failed to process message' }); } }); /** * GET /api/chat/threads/:boat_id * Get all chat threads for a boat */ router.get('/threads/:boat_id', authenticate, async (req, res) => { const { boat_id } = req.params; const userId = req.user.id; // Verify user owns this boat const boat = await db.get( 'SELECT * FROM boats WHERE id = ? AND owner_id = ?', [boat_id, userId] ); if (!boat) { return res.status(403).json({ error: 'Access denied' }); } const threads = await db.all( `SELECT id, created_at, (SELECT message FROM chat_messages WHERE thread_id = chat_threads.id ORDER BY created_at DESC LIMIT 1) as last_message, (SELECT COUNT(*) FROM chat_messages WHERE thread_id = chat_threads.id) as message_count FROM chat_threads WHERE boat_id = ? ORDER BY created_at DESC`, [boat_id] ); res.json(threads); }); /** * GET /api/chat/threads/:thread_id/messages * Get all messages in a thread */ router.get('/threads/:thread_id/messages', authenticate, async (req, res) => { const { thread_id } = req.params; const messages = await db.all( `SELECT cm.*, ct.boat_id FROM chat_messages cm JOIN chat_threads ct ON cm.thread_id = ct.id WHERE cm.thread_id = ? ORDER BY cm.created_at ASC`, [thread_id] ); // Verify user owns the boat for this thread if (messages.length > 0) { const boat = await db.get( 'SELECT * FROM boats WHERE id = ? AND owner_id = ?', [messages[0].boat_id, req.user.id] ); if (!boat) { return res.status(403).json({ error: 'Access denied' }); } } res.json(messages); }); /** * DELETE /api/chat/threads/:thread_id * Delete a chat thread */ router.delete('/threads/:thread_id', authenticate, async (req, res) => { const { thread_id } = req.params; // Verify ownership const thread = await db.get( 'SELECT ct.* FROM chat_threads ct WHERE ct.id = ? AND ct.boat_id IN (SELECT id FROM boats WHERE owner_id = ?)', [thread_id, req.user.id] ); if (!thread) { return res.status(403).json({ error: 'Access denied' }); } // Delete thread and all messages await db.run('DELETE FROM chat_messages WHERE thread_id = ?', [thread_id]); await db.run('DELETE FROM chat_threads WHERE id = ?', [thread_id]); res.json({ success: true }); }); module.exports = router; ``` ### 3.2 Database Schema ```sql -- Chat conversations per boat CREATE TABLE chat_threads ( id INTEGER PRIMARY KEY AUTOINCREMENT, boat_id INTEGER NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, title VARCHAR(255), FOREIGN KEY (boat_id) REFERENCES boats(id) ); -- Individual messages in chat CREATE TABLE chat_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER NOT NULL, role VARCHAR(20), -- 'user' or 'assistant' message TEXT NOT NULL, references JSON, -- Array of { doc_id, name, type, download_url } created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, tokens_used INTEGER, -- For cost tracking FOREIGN KEY (thread_id) REFERENCES chat_threads(id) ); -- Track API costs CREATE TABLE claude_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, boat_id INTEGER NOT NULL, message_id INTEGER, input_tokens INTEGER, output_tokens INTEGER, cost_usd DECIMAL(10, 4), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (boat_id) REFERENCES boats(id), FOREIGN KEY (message_id) REFERENCES chat_messages(id) ); ``` --- ## 4. Frontend: Chatbox Component ### 4.1 Vue 3 Chatbox Component ```vue