diff --git a/app/src/electron/handlers/chatHandlers.ts b/app/src/electron/handlers/chatHandlers.ts index b0395a4..1e6477a 100644 --- a/app/src/electron/handlers/chatHandlers.ts +++ b/app/src/electron/handlers/chatHandlers.ts @@ -62,4 +62,37 @@ export function setupChatHandlers(chatService: ChatService) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } }); + + // Get checklist for a message + ipcMain.handle(IPC_CHANNELS.GET_CHECKLIST, async (event, sessionId: string, messageId: string) => { + try { + const checklist = await chatService.getChecklist(sessionId, messageId); + return { success: true, data: checklist }; + } catch (error) { + console.error('[IPC] Get checklist error:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }); + + // Save checklist for a message + ipcMain.handle(IPC_CHANNELS.SAVE_CHECKLIST, async (event, sessionId: string, messageId: string, tasks: any[]) => { + try { + const result = await chatService.saveChecklist(sessionId, messageId, tasks); + return { success: true, data: result }; + } catch (error) { + console.error('[IPC] Save checklist error:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }); + + // Update checklist item + ipcMain.handle(IPC_CHANNELS.UPDATE_CHECKLIST_ITEM, async (event, sessionId: string, messageId: string, itemId: string, isCompleted: boolean) => { + try { + const result = await chatService.updateChecklistItem(sessionId, messageId, itemId, isCompleted); + return { success: true, data: result }; + } catch (error) { + console.error('[IPC] Update checklist item error:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }); } \ No newline at end of file diff --git a/app/src/electron/preload.ts b/app/src/electron/preload.ts index 08da5e2..cc2d48c 100644 --- a/app/src/electron/preload.ts +++ b/app/src/electron/preload.ts @@ -28,6 +28,9 @@ const IPC_CHANNELS = { GET_CHAT_HISTORY: 'get-chat-history', CREATE_CHAT_SESSION: 'create-chat-session', DELETE_CHAT_SESSION: 'delete-chat-session', + GET_CHECKLIST: 'get-checklist', + SAVE_CHECKLIST: 'save-checklist', + UPDATE_CHECKLIST_ITEM: 'update-checklist-item', // Dock Controls TOGGLE_DOCK: 'toggle-dock', @@ -120,6 +123,9 @@ contextBridge.exposeInMainWorld('electron', { getHistory: (sessionId: string) => ipcRenderer.invoke(IPC_CHANNELS.GET_CHAT_HISTORY, sessionId), createSession: (context?: string) => ipcRenderer.invoke(IPC_CHANNELS.CREATE_CHAT_SESSION, context), deleteSession: (sessionId: string) => ipcRenderer.invoke(IPC_CHANNELS.DELETE_CHAT_SESSION, sessionId), + getChecklist: (sessionId: string, messageId: string) => ipcRenderer.invoke(IPC_CHANNELS.GET_CHECKLIST, sessionId, messageId), + saveChecklist: (sessionId: string, messageId: string, tasks: any[]) => ipcRenderer.invoke(IPC_CHANNELS.SAVE_CHECKLIST, sessionId, messageId, tasks), + updateChecklistItem: (sessionId: string, messageId: string, itemId: string, isCompleted: boolean) => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_CHECKLIST_ITEM, sessionId, messageId, itemId, isCompleted), open: (context?: string) => ipcRenderer.invoke(IPC_CHANNELS.OPEN_CHAT_WINDOW, context), showDailyPlanNotification: () => ipcRenderer.invoke(IPC_CHANNELS.SHOW_DAILY_PLAN_NOTIFICATION) }, diff --git a/app/src/electron/services/ChatService.ts b/app/src/electron/services/ChatService.ts index eb2fcc1..e14fcf8 100644 --- a/app/src/electron/services/ChatService.ts +++ b/app/src/electron/services/ChatService.ts @@ -31,11 +31,13 @@ export class ChatService { // Get recent activity logs for context (last 2 hours) const recentLogs = await this.getRecentActivityContext(); - // Send message to Flask API with activity context + // Send message to Flask API with activity context and mode const response = await this.pythonServerService.apiRequest('POST', '/generate', { message: request.message, session_id: sessionId, - activity_context: recentLogs + activity_context: recentLogs, + mode: request.mode || 'general', + detective_mode: request.detectiveMode || 'teaching' }); if (!response.ok) { @@ -198,4 +200,63 @@ export class ChatService { return []; } } + + async getChecklist(sessionId: string, messageId: string): Promise { + try { + const response = await this.pythonServerService.apiRequest('GET', `/checklist/${sessionId}/${messageId}`); + + if (!response.ok) { + throw new Error(response.error || 'Failed to get checklist'); + } + + const data = response.data; + if (!data.success) { + throw new Error(data.error || 'API returned error'); + } + + return data.data || []; + + } catch (error) { + this.logger.error('Error getting checklist:', error); + return []; + } + } + + async saveChecklist(sessionId: string, messageId: string, tasks: any[]): Promise { + try { + const response = await this.pythonServerService.apiRequest('POST', `/checklist/${sessionId}/${messageId}`, { + tasks: tasks + }); + + if (!response.ok) { + throw new Error(response.error || 'Failed to save checklist'); + } + + const data = response.data; + return data.success || false; + + } catch (error) { + this.logger.error('Error saving checklist:', error); + return false; + } + } + + async updateChecklistItem(sessionId: string, messageId: string, itemId: string, isCompleted: boolean): Promise { + try { + const response = await this.pythonServerService.apiRequest('PATCH', `/checklist/${sessionId}/${messageId}/item/${itemId}`, { + isCompleted: isCompleted + }); + + if (!response.ok) { + throw new Error(response.error || 'Failed to update checklist item'); + } + + const data = response.data; + return data.success || false; + + } catch (error) { + this.logger.error('Error updating checklist item:', error); + return false; + } + } } \ No newline at end of file diff --git a/app/src/shared/constants.ts b/app/src/shared/constants.ts index 687f82c..a73867a 100644 --- a/app/src/shared/constants.ts +++ b/app/src/shared/constants.ts @@ -29,6 +29,9 @@ export const IPC_CHANNELS = { GET_CHAT_HISTORY: 'get-chat-history', CREATE_CHAT_SESSION: 'create-chat-session', DELETE_CHAT_SESSION: 'delete-chat-session', + GET_CHECKLIST: 'get-checklist', + SAVE_CHECKLIST: 'save-checklist', + UPDATE_CHECKLIST_ITEM: 'update-checklist-item', // Dock Controls TOGGLE_DOCK: 'toggle-dock', diff --git a/app/src/shared/types.ts b/app/src/shared/types.ts index 9141bea..298a4b0 100644 --- a/app/src/shared/types.ts +++ b/app/src/shared/types.ts @@ -105,12 +105,17 @@ export interface LLMProvider { } // Chat-related types +export type ChatMode = 'blueprint' | 'builder' | 'detective' | 'reviewer' | 'general'; +export type DetectiveMode = 'teaching' | 'quick-fix'; + export interface ChatMessage { id: string; text: string; sender: 'user' | 'assistant'; timestamp: number; sessionId: string; + mode?: ChatMode; + checklistId?: string; } export interface ChatSession { @@ -126,6 +131,8 @@ export interface ChatRequest { message: string; sessionId?: string; context?: string; + mode?: ChatMode; + detectiveMode?: DetectiveMode; } export interface ChatResponse { @@ -134,6 +141,21 @@ export interface ChatResponse { messageId: string; } +export interface ChecklistItem { + id: string; + taskText: string; + isCompleted: boolean; + position: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Checklist { + messageId: string; + sessionId: string; + items: ChecklistItem[]; +} + export interface GamificationData { points: number; level: number; diff --git a/app/src/ui/components/common/ChecklistRenderer.tsx b/app/src/ui/components/common/ChecklistRenderer.tsx new file mode 100644 index 0000000..37fb4b6 --- /dev/null +++ b/app/src/ui/components/common/ChecklistRenderer.tsx @@ -0,0 +1,257 @@ +import React, { useState, useEffect } from 'react'; +import { ChecklistItem, Checklist } from '../../../shared/types'; +import { MarkdownRenderer } from './MarkdownRenderer'; + +interface ChecklistRendererProps { + content: string; + messageId: string; + sessionId: string; + onChecklistUpdate?: (checklist: Checklist) => void; +} + +export const ChecklistRenderer: React.FC = ({ + content, + messageId, + sessionId, + onChecklistUpdate +}) => { + const [checklist, setChecklist] = useState([]); + const [loading, setLoading] = useState(false); + const [hasChecklist, setHasChecklist] = useState(false); + + useEffect(() => { + parseAndLoadChecklist(); + }, [content, messageId, sessionId]); + + const parseAndLoadChecklist = async () => { + // Parse markdown content for checklist items + const checklistItems = parseMarkdownChecklist(content); + + if (checklistItems.length > 0) { + setHasChecklist(true); + + // Try to load existing checklist from backend + try { + const result = await window.electron?.chat?.getChecklist(sessionId, messageId); + + if (result?.success && result.data && result.data.length > 0) { + // Use backend data if available + setChecklist(result.data); + } else { + // Save new checklist to backend + const newItems = checklistItems.map((item, idx) => ({ + id: '', + taskText: item.text, + isCompleted: item.completed, + position: idx + })); + + await saveChecklistToBackend(newItems); + + // Reload from backend to get IDs + const reloadResult = await window.electron?.chat?.getChecklist(sessionId, messageId); + if (reloadResult?.success && reloadResult.data) { + setChecklist(reloadResult.data); + } else { + setChecklist(newItems); + } + } + } catch (error) { + console.error('Error loading checklist:', error); + // Fallback to parsed items + setChecklist(checklistItems.map((item, idx) => ({ + id: `temp-${idx}`, + taskText: item.text, + isCompleted: item.completed, + position: idx + }))); + } + } + }; + + const parseMarkdownChecklist = (markdown: string): Array<{ text: string; completed: boolean }> => { + const lines = markdown.split('\n'); + const items: Array<{ text: string; completed: boolean }> = []; + + for (const line of lines) { + // Match markdown checklist format: - [ ] or - [x] or - [X] + const uncheckedMatch = line.match(/^[-*]\s+\[\s\]\s+(.+)$/); + const checkedMatch = line.match(/^[-*]\s+\[[xX]\]\s+(.+)$/); + + if (uncheckedMatch) { + items.push({ text: uncheckedMatch[1].trim(), completed: false }); + } else if (checkedMatch) { + items.push({ text: checkedMatch[1].trim(), completed: true }); + } + } + + return items; + }; + + const saveChecklistToBackend = async (items: ChecklistItem[]) => { + try { + const tasks = items.map(item => ({ + task_text: item.taskText, + is_completed: item.isCompleted, + position: item.position + })); + + await window.electron?.chat?.saveChecklist(sessionId, messageId, tasks); + } catch (error) { + console.error('Error saving checklist:', error); + } + }; + + const handleToggle = async (item: ChecklistItem) => { + setLoading(true); + + try { + const newCompletedState = !item.isCompleted; + + // Update backend + const result = await window.electron?.chat?.updateChecklistItem( + sessionId, + messageId, + item.id, + newCompletedState + ); + + if (result?.success) { + // Update local state + const updatedChecklist = checklist.map(i => + i.id === item.id ? { ...i, isCompleted: newCompletedState } : i + ); + setChecklist(updatedChecklist); + + // Notify parent + if (onChecklistUpdate) { + onChecklistUpdate({ + messageId, + sessionId, + items: updatedChecklist + }); + } + } + } catch (error) { + console.error('Error updating checklist item:', error); + } finally { + setLoading(false); + } + }; + + const getProgress = () => { + if (checklist.length === 0) return 0; + const completed = checklist.filter(item => item.isCompleted).length; + return Math.round((completed / checklist.length) * 100); + }; + + if (!hasChecklist) { + // Render normal markdown if no checklist detected + return ; + } + + const nonChecklistContent = removeChecklistFromMarkdown(content); + const progress = getProgress(); + + return ( +
+ {/* Non-checklist content */} + {nonChecklistContent && ( + + )} + + {/* Progress bar */} + {checklist.length > 0 && ( +
+
+ Progress + {progress}% +
+
+
+
+
+ )} + + {/* Checklist items */} +
+ {checklist.map((item, index) => ( +
+ + +
+ +
+
+ ))} +
+ + {/* Completion message */} + {progress === 100 && ( +
+
+ + + +

+ Great job! You've completed all tasks +

+
+
+ )} +
+ ); +}; + +function removeChecklistFromMarkdown(markdown: string): string { + const lines = markdown.split('\n'); + const nonChecklistLines = lines.filter(line => { + return !line.match(/^[-*]\s+\[[xX\s]\]/); + }); + return nonChecklistLines.join('\n').trim(); +} diff --git a/app/src/ui/components/common/MarkdownRenderer.tsx b/app/src/ui/components/common/MarkdownRenderer.tsx index b04767c..91ebfa5 100644 --- a/app/src/ui/components/common/MarkdownRenderer.tsx +++ b/app/src/ui/components/common/MarkdownRenderer.tsx @@ -6,110 +6,211 @@ interface MarkdownRendererProps { } export function MarkdownRenderer({ content, className = '' }: MarkdownRendererProps) { - const parseMarkdown = (text: string) => { - const parts: React.ReactNode[] = []; - let currentIndex = 0; + const parseMarkdown = (text: string): React.ReactNode[] => { + const lines = text.split('\n'); + const result: React.ReactNode[] = []; let key = 0; + let inCodeBlock = false; + let codeBlockLines: string[] = []; + let codeLanguage = ''; - // First, handle code blocks (```) - const codeBlockRegex = /```(\w+)?\n?([\s\S]*?)```/g; - let match; - - while ((match = codeBlockRegex.exec(text)) !== null) { - const [fullMatch, language, code] = match; - const beforeMatch = text.slice(currentIndex, match.index); - - // Add text before the code block - if (beforeMatch) { - parts.push(...parseInlineFormatting(beforeMatch, key)); - key += beforeMatch.length; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Handle code blocks + if (line.trim().startsWith('```')) { + if (!inCodeBlock) { + // Starting a code block + inCodeBlock = true; + codeLanguage = line.trim().substring(3).trim(); + codeBlockLines = []; + } else { + // Ending a code block + inCodeBlock = false; + + // Determine common indentation to strip + const nonEmptyLines = codeBlockLines.filter(l => l.trim().length > 0); + let minIndent = 0; + + if (nonEmptyLines.length > 0) { + minIndent = nonEmptyLines.reduce((min, line) => { + const match = line.match(/^(\s*)/); + return Math.min(min, match ? match[1].length : 0); + }, Infinity); + } + + const formattedLines = minIndent > 0 && minIndent !== Infinity + ? codeBlockLines.map(line => line.length >= minIndent ? line.substring(minIndent) : line) + : codeBlockLines; + + result.push( +
+ {codeLanguage && ( +
+ {codeLanguage} +
+ )} +
+                {formattedLines.join('\n')}
+              
+
+ ); + codeBlockLines = []; + codeLanguage = ''; + } + continue; + } + + if (inCodeBlock) { + codeBlockLines.push(line); + continue; } + + // Parse the line with inline formatting + const parsedLine = parseInlineFormatting(line, key); - // Add the code block - parts.push( -
- {language && ( -
- {language} + // Check for headers + if (line.startsWith('#### ')) { + result.push(

{parseInlineFormatting(line.substring(5), key)}

); + } else if (line.startsWith('### ')) { + result.push(

{parseInlineFormatting(line.substring(4), key)}

); + } else if (line.startsWith('## ')) { + result.push(

{parseInlineFormatting(line.substring(3), key)}

); + } else if (line.startsWith('# ')) { + result.push(

{parseInlineFormatting(line.substring(2), key)}

); + } + // Check for lists + else if (line.trim().startsWith('* ') || line.trim().startsWith('- ')) { + result.push( +
+ + {parseInlineFormatting(line.trim().substring(2), key)} +
+ ); + } + // Check for numbered lists + else if (/^\d+\.\s/.test(line.trim())) { + const match = line.trim().match(/^(\d+)\.\s(.+)$/); + if (match) { + result.push( +
+ {match[1]}. + {parseInlineFormatting(match[2], key)}
- )} -
-            {code.trim()}
-          
-
- ); - key++; - - currentIndex = match.index + fullMatch.length; + ); + } + } + // Empty line + else if (line.trim() === '') { + result.push(
); + } + // Regular paragraph + else { + result.push(

{parsedLine}

); + } } - - // Add remaining text after the last code block - const remainingText = text.slice(currentIndex); - if (remainingText) { - parts.push(...parseInlineFormatting(remainingText, key)); + + return result; + }; + + const parseInlineFormatting = (text: string, startKey: number): React.ReactNode[] => { + const parts: React.ReactNode[] = []; + let remaining = text; + let key = startKey; + + // Process text with multiple inline formats + while (remaining.length > 0) { + // Try to match inline code first (highest priority) + const inlineCodeMatch = remaining.match(/`([^`]+)`/); + if (inlineCodeMatch && inlineCodeMatch.index !== undefined) { + // Add text before the match + if (inlineCodeMatch.index > 0) { + parts.push(...parseTextFormatting(remaining.substring(0, inlineCodeMatch.index), key++)); + } + // Add the inline code + parts.push( + + {inlineCodeMatch[1]} + + ); + remaining = remaining.substring(inlineCodeMatch.index + inlineCodeMatch[0].length); + continue; + } + + // No more special formatting, process remaining text + parts.push(...parseTextFormatting(remaining, key++)); + break; } - + return parts; }; - const parseInlineFormatting = (text: string, startKey: number) => { + const parseTextFormatting = (text: string, startKey: number): React.ReactNode[] => { const parts: React.ReactNode[] = []; - let currentIndex = 0; + let remaining = text; let key = startKey; - // Handle inline code (`code`) - const inlineCodeRegex = /`([^`]+)`/g; - let match; - - while ((match = inlineCodeRegex.exec(text)) !== null) { - const [fullMatch, code] = match; - const beforeMatch = text.slice(currentIndex, match.index); - - // Add text before the inline code - if (beforeMatch) { + while (remaining.length > 0) { + // Try bold (**text** or __text__) + const boldMatch = remaining.match(/\*\*(.+?)\*\*|__(.+?)__/); + if (boldMatch && boldMatch.index !== undefined) { + if (boldMatch.index > 0) { + parts.push(...parseItalic(remaining.substring(0, boldMatch.index), key++)); + } parts.push( - {beforeMatch} + + {boldMatch[1] || boldMatch[2]} + ); - key++; + remaining = remaining.substring(boldMatch.index + boldMatch[0].length); + continue; } - - // Add the inline code - parts.push( - - {code} - - ); - key++; - - currentIndex = match.index + fullMatch.length; - } - - // Add remaining text after the last inline code - const remainingText = text.slice(currentIndex); - if (remainingText) { - parts.push( - {remainingText} - ); + + // No more bold, check for italic + parts.push(...parseItalic(remaining, key++)); + break; } - + return parts; }; - const renderContent = () => { - // If no code blocks or inline code, return plain text - if (!content.includes('```') && !content.includes('`')) { - return content; + const parseItalic = (text: string, startKey: number): React.ReactNode[] => { + const parts: React.ReactNode[] = []; + let remaining = text; + let key = startKey; + + while (remaining.length > 0) { + // Try italic (*text* or _text_) - but not ** or __ + const italicMatch = remaining.match(/(? 0) { + parts.push({remaining.substring(0, italicMatch.index)}); + } + parts.push( + + {italicMatch[1] || italicMatch[2]} + + ); + remaining = remaining.substring(italicMatch.index + italicMatch[0].length); + continue; + } + + // No more formatting + if (remaining) { + parts.push({remaining}); + } + break; } - - return parseMarkdown(content); + + return parts; }; return ( -
- {renderContent()} +
+ {parseMarkdown(content)}
); } \ No newline at end of file diff --git a/app/src/ui/components/common/index.ts b/app/src/ui/components/common/index.ts index eaff56a..f86f44c 100644 --- a/app/src/ui/components/common/index.ts +++ b/app/src/ui/components/common/index.ts @@ -4,4 +4,5 @@ export { Input, type InputProps } from './Input'; export { Slider, type SliderProps } from './Slider'; export { StatusBadge, type StatusBadgeProps } from './StatusBadge'; export { Card, type CardProps } from './Card'; -export { MarkdownRenderer } from './MarkdownRenderer'; \ No newline at end of file +export { MarkdownRenderer } from './MarkdownRenderer'; +export { ChecklistRenderer } from './ChecklistRenderer'; \ No newline at end of file diff --git a/app/src/ui/global.d.ts b/app/src/ui/global.d.ts index f9e691a..6b12bf0 100644 --- a/app/src/ui/global.d.ts +++ b/app/src/ui/global.d.ts @@ -38,6 +38,9 @@ declare global { getHistory: (sessionId: string) => Promise<{ success: boolean; data?: any[]; error?: string }>; createSession: (context?: string) => Promise<{ success: boolean; data?: any; error?: string }>; deleteSession: (sessionId: string) => Promise<{ success: boolean; data?: any; error?: string }>; + getChecklist: (sessionId: string, messageId: string) => Promise<{ success: boolean; data?: any[]; error?: string }>; + saveChecklist: (sessionId: string, messageId: string, tasks: any[]) => Promise<{ success: boolean; data?: any; error?: string }>; + updateChecklistItem: (sessionId: string, messageId: string, itemId: string, isCompleted: boolean) => Promise<{ success: boolean; data?: any; error?: string }>; open: (context?: string) => Promise<{ success: boolean; error?: string }>; showDailyPlanNotification: () => Promise<{ success: boolean; error?: string }>; }; diff --git a/app/src/ui/pages/ChatWindow/ChatWindow.tsx b/app/src/ui/pages/ChatWindow/ChatWindow.tsx index 3318ff3..3394329 100644 --- a/app/src/ui/pages/ChatWindow/ChatWindow.tsx +++ b/app/src/ui/pages/ChatWindow/ChatWindow.tsx @@ -1,11 +1,13 @@ import React, { useState, useEffect, useRef } from 'react'; -import { MarkdownRenderer } from '../../components/common'; +import { MarkdownRenderer, ChecklistRenderer } from '../../components/common'; +import { ChatMode, DetectiveMode } from '../../../shared/types'; interface Message { id: string; text: string; sender: 'user' | 'assistant'; timestamp: Date; + mode?: ChatMode; } interface ChatSession { @@ -28,6 +30,9 @@ export const ChatWindow: React.FC = ({ context }) => { const [currentSessionId, setCurrentSessionId] = useState(null); const [sessions, setSessions] = useState([]); const [showSessions, setShowSessions] = useState(false); + const [currentMode, setCurrentMode] = useState('general'); + const [detectiveMode, setDetectiveMode] = useState('teaching'); + const [isCodingWorkflow, setIsCodingWorkflow] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); @@ -93,8 +98,29 @@ export const ChatWindow: React.FC = ({ context }) => { id: msg.id, text: msg.text, sender: msg.sender, - timestamp: new Date(msg.timestamp) + timestamp: new Date(msg.timestamp), + mode: msg.mode })); + + // Check if this session has workflow messages + const hasWorkflowMessages = chatMessages.some((msg: Message) => + msg.mode && msg.mode !== 'general' + ); + + // Restore workflow state if session had workflow messages + if (hasWorkflowMessages) { + setIsCodingWorkflow(true); + // Set mode to the most recent workflow message's mode, or default to blueprint + const lastWorkflowMessage = chatMessages.reverse().find((msg: Message) => + msg.mode && msg.mode !== 'general' + ); + setCurrentMode(lastWorkflowMessage?.mode || 'blueprint'); + chatMessages.reverse(); // Restore original order + } else { + setIsCodingWorkflow(false); + setCurrentMode('general'); + } + setMessages(chatMessages); setCurrentSessionId(sessionId); setShowSessions(false); @@ -126,7 +152,8 @@ export const ChatWindow: React.FC = ({ context }) => { id: Date.now().toString(), text: inputText.trim(), sender: 'user', - timestamp: new Date() + timestamp: new Date(), + mode: currentMode }; setMessages(prev => [...prev, userMessage]); @@ -138,7 +165,9 @@ export const ChatWindow: React.FC = ({ context }) => { const result = await window.electron?.chat?.sendMessage({ message: userMessage.text, sessionId: currentSessionId, - context: context || 'general' + context: context || 'general', + mode: currentMode, + detectiveMode: currentMode === 'detective' ? detectiveMode : undefined }); if (result?.success) { @@ -149,7 +178,8 @@ export const ChatWindow: React.FC = ({ context }) => { id: response.messageId, text: response.message, sender: 'assistant', - timestamp: new Date() + timestamp: new Date(), + mode: currentMode }; setMessages(prev => [...prev, assistantMessage]); @@ -183,21 +213,113 @@ export const ChatWindow: React.FC = ({ context }) => { const getQuickActions = () => { if (context === 'daily-plan') { return [ - { text: "Help me prioritize my tasks for today", icon: "📋" }, - { text: "What should I focus on first?", icon: "🎯" } + { text: "Help me prioritize my tasks for today" }, + { text: "What should I focus on first?" } ]; } return [ - { text: "I'm feeling overwhelmed, help me break this down", icon: "🧘" }, - { text: "I keep getting distracted, what can I do?", icon: "⚡" } + { text: "I'm feeling overwhelmed, help me break this down" }, + { text: "I keep getting distracted, what can I do?" } ]; }; // Check if we have any actual conversation (excluding welcome messages) const hasConversation = messages.some(msg => msg.sender === 'user'); + + // Filter messages by current mode - show both user and AI messages for the selected mode + const filteredMessages = messages.filter(msg => { + // If we're in coding workflow, only show messages from the current mode + if (isCodingWorkflow) { + return msg.mode === currentMode; + } + // If we're in general mode, show general messages or messages without a mode + return msg.mode === 'general' || !msg.mode; + }); + + const tabs: Array<{ id: ChatMode; label: string; description: string }> = [ + { id: 'blueprint', label: 'Blueprint', description: 'Planning & Architecture' }, + { id: 'builder', label: 'Builder', description: 'Active Coding' }, + { id: 'detective', label: 'Detective', description: 'Debugging' }, + { id: 'reviewer', label: 'Reviewer', description: 'Testing & Polish' }, + ]; + + const handleEnterCodingWorkflow = () => { + setIsCodingWorkflow(true); + setCurrentMode('blueprint'); + }; + + const handleExitCodingWorkflow = () => { + // Create a new session when exiting workflow to avoid confusion + createNewSession(context || 'general'); + setIsCodingWorkflow(false); + setCurrentMode('general'); + }; return (
+ {/* Tab Navigation - Only show in coding workflow mode */} + {isCodingWorkflow && ( +
+
+ {/* Left: Tabs */} +
+ {tabs.map((tab) => ( + + ))} +
+ +
+ {/* Center/Right: Detective mode toggle */} + {currentMode === 'detective' && ( +
+ + +
+ )} + + {/* Far Right: Exit coding workflow button */} + +
+
+
+ )} + {/* Header with input and session controls in same row */}
@@ -277,18 +399,34 @@ export const ChatWindow: React.FC = ({ context }) => {
)} - {/* Quick Actions (only show if no conversation has started) */} - {!hasConversation && !isLoading && ( + {/* Quick Actions and Coding Workflow Button - Only show in general mode when no conversation */} + {!hasConversation && !isLoading && !isCodingWorkflow && (
+ {/* Coding Workflow Button */} +
+ +
+ + {/* Quick Actions */}
{getQuickActions().map((action, index) => ( ))}
@@ -297,28 +435,45 @@ export const ChatWindow: React.FC = ({ context }) => { {/* Messages */}
- {messages.length === 0 && !isLoading && ( -
-
- 💭 + {filteredMessages.length === 0 && !isLoading && ( +
+
+ {isCodingWorkflow ? ( + + + + ) : ( + + + + )}
-

- {context === 'daily-plan' - ? "Ready to plan your day? Share your goals and I'll help you stay focused!" - : "Hi! I'm Tether, your ADHD-friendly assistant. How can I help you focus today?"} +

+ {isCodingWorkflow ? ( + <> + {currentMode === 'blueprint' && "Ready to plan your project? Share your idea and I'll break it down into manageable tasks"} + {currentMode === 'builder' && "Let's build something! What would you like to code today?"} + {currentMode === 'detective' && "Got a bug? Paste your error and let's solve it together"} + {currentMode === 'reviewer' && "Ready to polish your project? Let's add tests, docs, and reflection"} + + ) : ( + context === 'daily-plan' + ? "Ready to plan your day? Share your goals and I'll help you stay focused" + : "Hi! I'm Tether, your ADHD-friendly assistant. How can I help you focus today?" + )}

)}
- {messages.map((message) => ( + {filteredMessages.map((message) => (
{message.sender === 'user' ? (
You
-
+
= ({ context }) => {
T
-
- +
+ {/* Mode badge */} + {message.mode && message.mode !== 'general' && ( +
+ + {tabs.find(t => t.id === message.mode)?.label} + +
+ )} + + {/* Render checklist for Blueprint mode or regular markdown */} + {message.mode === 'blueprint' && message.text.includes('- [ ]') ? ( + + ) : ( + + )} +

{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}

diff --git a/server/routes/conversation.py b/server/routes/conversation.py index cb59809..d84fdac 100644 --- a/server/routes/conversation.py +++ b/server/routes/conversation.py @@ -29,7 +29,8 @@ def get_conversation_history(session_id: str): "text": msg.get("content", ""), "sender": "user" if msg.get("type") == "human" else "assistant", "timestamp": msg.get("timestamp", 0), - "sessionId": session_id + "sessionId": session_id, + "mode": msg.get("mode", "general") }) return jsonify({ diff --git a/server/routes/session.py b/server/routes/session.py index 1b2f1b5..30d71cc 100644 --- a/server/routes/session.py +++ b/server/routes/session.py @@ -162,6 +162,8 @@ def generate_response(): message = data.get("message") session_id = data.get("session_id") activity_context = data.get("activity_context", []) + mode = data.get("mode", "general") + detective_mode = data.get("detective_mode", "teaching") if not message: return jsonify({ @@ -175,8 +177,8 @@ def generate_response(): "error": "Session ID is required" }), 400 - # Generate response with activity context - result = rag_service.generate_response(message, session_id, activity_context) + # Generate response with activity context and mode + result = rag_service.generate_response(message, session_id, activity_context, mode, detective_mode) if result["success"]: # Format response for ChatService compatibility @@ -191,6 +193,126 @@ def generate_response(): else: return jsonify(result), 500 + except Exception as e: + return jsonify({ + "success": False, + "error": str(e) + }), 500 + + +@session_bp.route("/checklist//", methods=["GET"]) +def get_checklist(session_id: str, message_id: int): + """Get checklist items for a specific message""" + from app import rag_service + + if rag_service is None: + return jsonify({ + "error": "RAG service not initialized" + }), 500 + + try: + items = rag_service.conversation_repo.get_checklist(session_id, message_id) + return jsonify({ + "success": True, + "data": items + }), 200 + + except Exception as e: + return jsonify({ + "success": False, + "error": str(e) + }), 500 + + +@session_bp.route("/checklist//", methods=["POST"]) +def save_checklist(session_id: str, message_id: int): + """Save checklist items for a Blueprint mode message""" + from app import rag_service + + if rag_service is None: + return jsonify({ + "error": "RAG service not initialized" + }), 500 + + try: + data = request.get_json() + + if not data: + return jsonify({ + "success": False, + "error": "No JSON data provided" + }), 400 + + tasks = data.get("tasks", []) + + if not tasks: + return jsonify({ + "success": False, + "error": "Tasks are required" + }), 400 + + success = rag_service.conversation_repo.save_checklist(session_id, message_id, tasks) + + if success: + return jsonify({ + "success": True, + "message": "Checklist saved successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": "Failed to save checklist" + }), 500 + + except Exception as e: + return jsonify({ + "success": False, + "error": str(e) + }), 500 + + +@session_bp.route("/checklist///item/", methods=["PATCH"]) +def update_checklist_item(session_id: str, message_id: int, item_id: int): + """Update the completion status of a checklist item""" + from app import rag_service + + if rag_service is None: + return jsonify({ + "error": "RAG service not initialized" + }), 500 + + try: + data = request.get_json() + + if not data: + return jsonify({ + "success": False, + "error": "No JSON data provided" + }), 400 + + is_completed = data.get("isCompleted") + + if is_completed is None: + return jsonify({ + "success": False, + "error": "isCompleted field is required" + }), 400 + + success = rag_service.conversation_repo.update_checklist_item( + session_id, message_id, item_id, is_completed + ) + + if success: + return jsonify({ + "success": True, + "message": "Checklist item updated successfully" + }), 200 + else: + return jsonify({ + "success": False, + "error": "Failed to update checklist item" + }), 500 + except Exception as e: return jsonify({ "success": False, diff --git a/server/schema.sql b/server/schema.sql index 30fca3c..617f7ca 100644 --- a/server/schema.sql +++ b/server/schema.sql @@ -17,11 +17,26 @@ CREATE TABLE IF NOT EXISTS messages ( session_id TEXT NOT NULL, message_type TEXT NOT NULL, -- 'human', 'ai', 'system', 'tool' content TEXT NOT NULL, + mode TEXT DEFAULT 'general', -- 'blueprint', 'builder', 'detective', 'reviewer', 'general' metadata JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE ); +-- Checklists table to store Blueprint mode micro-tasks +CREATE TABLE IF NOT EXISTS checklists ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + message_id INTEGER NOT NULL, + task_text TEXT NOT NULL, + is_completed BOOLEAN DEFAULT 0, + position INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE, + FOREIGN KEY (message_id) REFERENCES messages (id) ON DELETE CASCADE +); + -- LangGraph checkpoints table (used by SqliteSaver) -- This table structure is expected by LangGraph's SqliteSaver CREATE TABLE IF NOT EXISTS checkpoints ( @@ -55,6 +70,11 @@ CREATE INDEX IF NOT EXISTS idx_sessions_active ON sessions (is_active); CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages (session_id); CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages (created_at); CREATE INDEX IF NOT EXISTS idx_messages_type ON messages (message_type); +CREATE INDEX IF NOT EXISTS idx_messages_mode ON messages (mode); + +CREATE INDEX IF NOT EXISTS idx_checklists_session_id ON checklists (session_id); +CREATE INDEX IF NOT EXISTS idx_checklists_message_id ON checklists (message_id); +CREATE INDEX IF NOT EXISTS idx_checklists_position ON checklists (session_id, message_id, position); CREATE INDEX IF NOT EXISTS idx_checkpoints_thread_id ON checkpoints (thread_id); CREATE INDEX IF NOT EXISTS idx_checkpoints_timestamp ON checkpoints (thread_id, checkpoint_ns, checkpoint_id); @@ -68,4 +88,11 @@ CREATE TRIGGER IF NOT EXISTS update_sessions_updated_at FOR EACH ROW BEGIN UPDATE sessions SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; +END; + +CREATE TRIGGER IF NOT EXISTS update_checklists_updated_at + AFTER UPDATE ON checklists + FOR EACH ROW +BEGIN + UPDATE checklists SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; \ No newline at end of file diff --git a/server/services/conversation_repository.py b/server/services/conversation_repository.py index a887fef..f699d00 100644 --- a/server/services/conversation_repository.py +++ b/server/services/conversation_repository.py @@ -20,6 +20,7 @@ def __init__(self, db_path: str = "conversations.db"): Note: Database must be initialized separately before creating the repository """ self.db_path = db_path + self._run_migrations() def create_session(self, session_id: str = None, name: str = None, metadata: Dict[str, Any] = None) -> str: """Create a new conversation session""" @@ -105,7 +106,7 @@ def add_message(self, session_id: str, message: BaseMessage): ) return cursor.lastrowid - def add_message_simple(self, session_id: str, message_type: str, content: str, metadata: Dict[str, Any] = None) -> int: + def add_message_simple(self, session_id: str, message_type: str, content: str, metadata: Dict[str, Any] = None, mode: str = "general") -> int: """Add a message to a session with simple parameters""" # Validate message type valid_types = ["human", "ai", "system", "tool", "user", "assistant"] @@ -127,8 +128,8 @@ def add_message_simple(self, session_id: str, message_type: str, content: str, m # Insert message cursor = conn.execute( - "INSERT INTO messages (session_id, message_type, content, metadata) VALUES (?, ?, ?, ?)", - (session_id, message_type, content, json.dumps(metadata) if metadata else None) + "INSERT INTO messages (session_id, message_type, content, metadata, mode) VALUES (?, ?, ?, ?, ?)", + (session_id, message_type, content, json.dumps(metadata) if metadata else None, mode) ) return cursor.lastrowid @@ -162,8 +163,10 @@ def get_message_history(self, session_id: str, limit: int = 100) -> List[Dict[st return [ { + "id": row["id"], "type": row["message_type"], "content": row["content"], + "mode": row["mode"] if "mode" in row.keys() else "general", "metadata": json.loads(row["metadata"]) if row["metadata"] else {}, "timestamp": row["created_at"] } @@ -284,4 +287,132 @@ def _row_to_message(self, row) -> Optional[BaseMessage]: print(f"Error converting row to message: {e}") return None - return None \ No newline at end of file + return None + + def _run_migrations(self): + """Run database migrations for schema changes""" + with sqlite3.connect(self.db_path) as conn: + # Check if mode column exists in messages table + cursor = conn.execute("PRAGMA table_info(messages)") + columns = [row[1] for row in cursor.fetchall()] + + if "mode" not in columns: + # Add mode column to messages table + conn.execute("ALTER TABLE messages ADD COLUMN mode TEXT DEFAULT 'general'") + print("Migration: Added 'mode' column to messages table") + + def save_checklist(self, session_id: str, message_id: int, tasks: List[Dict[str, Any]]) -> bool: + """ + Save a checklist for a Blueprint mode message + + Args: + session_id: Session ID + message_id: Message ID + tasks: List of task dictionaries with 'task_text' and optional 'is_completed', 'position' + + Returns: + True if successful + """ + try: + with sqlite3.connect(self.db_path) as conn: + # First, delete existing items for this message to prevent duplicates + conn.execute( + "DELETE FROM checklists WHERE session_id = ? AND message_id = ?", + (session_id, message_id) + ) + + for idx, task in enumerate(tasks): + task_text = task.get('task_text', task.get('text', '')) + is_completed = task.get('is_completed', False) + position = task.get('position', idx) + + conn.execute( + """INSERT INTO checklists (session_id, message_id, task_text, is_completed, position) + VALUES (?, ?, ?, ?, ?)""", + (session_id, message_id, task_text, is_completed, position) + ) + return True + except Exception as e: + print(f"Error saving checklist: {e}") + return False + + def get_checklist(self, session_id: str, message_id: int) -> List[Dict[str, Any]]: + """ + Get checklist items for a specific message + + Args: + session_id: Session ID + message_id: Message ID + + Returns: + List of checklist items + """ + with sqlite3.connect(self.db_path) as conn: + conn.row_factory = sqlite3.Row + cursor = conn.execute( + """SELECT * FROM checklists + WHERE session_id = ? AND message_id = ? + ORDER BY position ASC""", + (session_id, message_id) + ) + rows = cursor.fetchall() + + return [ + { + "id": row["id"], + "taskText": row["task_text"], + "isCompleted": bool(row["is_completed"]), + "position": row["position"], + "createdAt": row["created_at"], + "updatedAt": row["updated_at"] + } + for row in rows + ] + + def update_checklist_item(self, session_id: str, message_id: int, item_id: int, is_completed: bool) -> bool: + """ + Update the completion status of a checklist item + + Args: + session_id: Session ID + message_id: Message ID + item_id: Checklist item ID + is_completed: New completion status + + Returns: + True if successful + """ + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.execute( + """UPDATE checklists + SET is_completed = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? AND session_id = ? AND message_id = ?""", + (is_completed, item_id, session_id, message_id) + ) + return cursor.rowcount > 0 + except Exception as e: + print(f"Error updating checklist item: {e}") + return False + + def delete_checklist(self, session_id: str, message_id: int) -> bool: + """ + Delete all checklist items for a message + + Args: + session_id: Session ID + message_id: Message ID + + Returns: + True if successful + """ + try: + with sqlite3.connect(self.db_path) as conn: + conn.execute( + "DELETE FROM checklists WHERE session_id = ? AND message_id = ?", + (session_id, message_id) + ) + return True + except Exception as e: + print(f"Error deleting checklist: {e}") + return False \ No newline at end of file diff --git a/server/services/rag_service.py b/server/services/rag_service.py index c591833..95f3937 100644 --- a/server/services/rag_service.py +++ b/server/services/rag_service.py @@ -32,6 +32,8 @@ def __init__(self, vector_store_path: str = "vector_store", google_api_key: str self.conversation_repo = ConversationRepository(db_path) self.db_path = db_path self._current_activity_context = None + self._current_mode = "general" + self._current_detective_mode = "teaching" # Load vector store self.load_vector_store(vector_store_path) @@ -62,8 +64,21 @@ def retrieve(query: str): return retrieve - def _create_system_prompt(self, activity_summary: str = "", docs_content: str = "") -> str: - """Create the unified ADHD-focused system prompt""" + def _create_system_prompt(self, mode: str = "general", activity_summary: str = "", docs_content: str = "", detective_mode: str = "teaching") -> str: + """Create mode-specific system prompts""" + if mode == "blueprint": + return self._create_blueprint_prompt(activity_summary, docs_content) + elif mode == "builder": + return self._create_builder_prompt(activity_summary, docs_content) + elif mode == "detective": + return self._create_detective_prompt(activity_summary, docs_content, detective_mode) + elif mode == "reviewer": + return self._create_reviewer_prompt(activity_summary, docs_content) + else: + return self._create_general_prompt(activity_summary, docs_content) + + def _create_general_prompt(self, activity_summary: str = "", docs_content: str = "") -> str: + """Create the unified ADHD-focused system prompt for general mode""" context_section = "" if docs_content: context_section = f"RETRIEVED RESEARCH CONTEXT:\n{docs_content}\n\n" @@ -111,6 +126,276 @@ def _create_system_prompt(self, activity_summary: str = "", docs_content: str = IMPORTANT: You have access to the user's recent activity history above. When they ask about what they were doing (yesterday, today, recently), use this information directly - don't search for it.{' Always prioritize emotional support when users express overwhelm.' if docs_content else ''}""" + + def _create_blueprint_prompt(self, activity_summary: str = "", docs_content: str = "") -> str: + """Create system prompt for Blueprint (Planning & Architecture) mode""" + context_section = "" + if docs_content: + context_section = f"RETRIEVED RESEARCH CONTEXT:\n{docs_content}\n\n" + + return f"""You are Tether in BLUEPRINT MODE - a specialized planning assistant for novice coders with ADHD. + + YOUR MISSION: Help users overcome Task Initiation paralysis by breaking down vague ideas into clear, actionable plans. + + CORE RULES FOR THIS MODE: + 1. **NEVER generate code** - This is planning-only mode. If they ask for code, gently redirect them to use the Builder tab. + 2. **Always output tasks in markdown checklist format** using `- [ ] Task description` syntax + 3. **Break projects into 5-10 micro-tasks** - small enough to feel achievable + 4. **Ask clarifying questions** before creating the plan to understand requirements + 5. **Encourage pseudocode and logic flow discussion** - but no actual code + + ADHD-SPECIFIC APPROACH: + - Validate any feelings of overwhelm about starting + - Keep each micro-task to 15-30 minutes of work + - Use clear, specific task descriptions (no vague goals) + - Number tasks to show clear progression + - Add estimated time for each task if helpful + - Celebrate the act of planning itself + + OUTPUT FORMAT: + When creating a project breakdown, use this structure: + + ## Project: [Name] + + ### Tasks Checklist: + - [ ] Task 1: Clear, specific description + - [ ] Task 2: Another specific task + - [ ] Task 3: Continue the list + + ### Notes: + - Any additional context or considerations + - Pseudocode suggestions if relevant + + RESPONSE STRATEGY: + 1. First, ask 2-3 clarifying questions about requirements + 2. Validate their project idea + 3. Break it into micro-tasks + 4. Suggest the logical order (but remind them they can tackle tasks in any order) + 5. Emphasize that they can check off tasks as they complete them + + {context_section}{activity_summary} + + Remember: Your job is to make starting feel manageable, not to solve the coding problem. Guide them to think through the logic BEFORE coding.""" + + def _create_builder_prompt(self, activity_summary: str = "", docs_content: str = "") -> str: + """Create system prompt for Builder (Active Coding) mode""" + context_section = "" + if docs_content: + context_section = f"RETRIEVED RESEARCH CONTEXT:\n{docs_content}\n\n" + + return f"""You are Tether in BUILDER MODE - a teaching-focused coding mentor for novice programmers with ADHD. + + YOUR MISSION: Help users write code while learning the concepts, not just copying solutions. + + TEACHING PHILOSOPHY: + 1. **Explain WHY, not just WHAT** - Every code suggestion should include reasoning + 2. **Break code into tiny chunks** - One concept at a time + 3. **Encourage experimentation** - Suggest they try variations + 4. **Celebrate small wins** - Acknowledge every working piece of code + 5. **Connect to their existing knowledge** - Use analogies and real-world examples + + ADHD-SPECIFIC ADAPTATIONS: + - Keep explanations concise but complete (aim for 3-4 sentence explanations) + - Use clear headers and formatting for easy scanning + - Provide working code examples, not pseudocode + - Acknowledge when something is genuinely tricky + - Remind them to save their work and commit often + - Suggest break points for longer coding sessions + + CODE PRESENTATION FORMAT: + When showing code, always include: + 1. A brief "What this does" explanation + 2. The code itself with helpful comments + 3. A "Why this approach" explanation + 4. (Optional) "Try experimenting with" suggestions + + RESPONSE GUIDELINES: + - If they share broken code: Point out what's working first, then the issue + - If they ask "how do I...": Break it into 2-3 sub-steps + - If they seem stuck: Suggest the smallest possible next step + - If they're making progress: Celebrate it and suggest the next challenge + - Always verify they understand before moving to the next concept + + ENCOURAGEMENT: + - Remind them that all programmers look up syntax + - Normalize making mistakes as part of learning + - Frame debugging as a skill, not a failure + - Acknowledge when they're thinking like a programmer + + {context_section}{activity_summary} + + Remember: You're building their confidence AND their skills. Every response should teach something new while making them feel capable.""" + + def _create_detective_prompt(self, activity_summary: str = "", docs_content: str = "", detective_mode: str = "teaching") -> str: + """Create system prompt for Detective (Debugging) mode""" + context_section = "" + if docs_content: + context_section = f"RETRIEVED RESEARCH CONTEXT:\n{docs_content}\n\n" + + mode_specific = "" + if detective_mode == "teaching": + mode_specific = """ + TEACHING MODE PROTOCOL: + Use the "Rubber Duck Debugging" approach: + 1. **Ask before answering**: "What did you expect to happen?" vs "What actually happened?" + 2. **Guide their reasoning**: Help them discover the issue themselves + 3. **Teach debugging methodology**: Show them how to isolate the problem + 4. **Build problem-solving skills**: Explain how you'd approach this type of bug + 5. **Only provide the solution after** they've thought through it with your guidance + + Question Framework: + - "What line do you think the error is happening on?" + - "What does the error message tell us?" + - "What was the last thing that worked?" + - "If you add a print/console.log here, what do you expect to see?" + """ + else: # quick-fix mode + mode_specific = """ + QUICK-FIX MODE PROTOCOL: + Provide direct solutions with brief explanations: + 1. **Identify the issue** clearly and concisely + 2. **Show the fix** with corrected code + 3. **Explain why** it was failing (1-2 sentences) + 4. **Suggest prevention**: How to avoid this in the future + + This mode is for when they're frustrated or time-pressured, so be efficient but still educational. + """ + + return f"""You are Tether in DETECTIVE MODE - a debugging companion for novice coders with ADHD. + + YOUR MISSION: Help users debug their code while managing the emotional frustration that comes with broken code. + + {mode_specific} + + ADHD-SPECIFIC EMOTIONAL SUPPORT: + 1. **Validate frustration first**: "Debugging is genuinely frustrating, especially with ADHD executive dysfunction" + 2. **Manage overwhelm**: If the error is complex, break investigation into small steps + 3. **Celebrate debugging wins**: Finding a bug is success, not just fixing it + 4. **Prevent spiraling**: If they're stuck >20min, suggest a break or different approach + 5. **Normalize the struggle**: Remind them that all developers debug constantly + + ERROR ANALYSIS FRAMEWORK: + When they share an error: + 1. Acknowledge their emotional state first + 2. Parse the error message into plain English + 3. Identify the most likely cause + 4. (In teaching mode) Ask guiding questions + 5. (In quick-fix mode) Provide the solution + 6. Explain how to prevent this error type + + COMMON ADHD DEBUGGING PITFALLS: + - Typos from rushing (validate empathetically, not judgmentally) + - Missing semicolons/brackets from distraction + - Logic errors from working memory challenges + - Copy-paste errors from hyperfocus on the wrong problem + + RESPONSE STRUCTURE: + ``` + [Emotional validation] + + **Error Analysis:** + [Plain English explanation of what went wrong] + + **The Issue:** + [Specific problem and location] + + **The Fix:** + [Solution - either guided questions or direct fix depending on mode] + + **Prevention:** + [How to avoid this next time] + ``` + + {context_section}{activity_summary} + + Remember: The goal isn't just fixing the code - it's building their debugging confidence and skills while managing ADHD-related frustration.""" + + def _create_reviewer_prompt(self, activity_summary: str = "", docs_content: str = "") -> str: + """Create system prompt for Reviewer (Testing & Polish) mode""" + context_section = "" + if docs_content: + context_section = f"RETRIEVED RESEARCH CONTEXT:\n{docs_content}\n\n" + + return f"""You are Tether in REVIEWER MODE - a completion coach for novice coders with ADHD. + + YOUR MISSION: Help users cross the finish line by guiding them through testing, documentation, and reflection. + + THE ADHD "CLOSING LOOPS" CHALLENGE: + ADHD brains struggle with finishing tasks. You're here to make completion feel achievable and rewarding, not like busywork. + + THE "DEFINITION OF DONE" FRAMEWORK: + A project isn't finished until: + 1. It works (basic functionality) + 2. It's tested (at least simple checks) + 3. It's documented (comments or README) + 4. It's reflected upon (what did you learn?) + + YOUR APPROACH: + 1. **Celebrate the build**: Acknowledge their working code first + 2. **Make completion feel worthwhile**: Explain WHY each step matters + 3. **Keep it simple**: No need for perfect tests, just basic ones + 4. **Guide, don't overwhelm**: One completion task at a time + 5. **Generate learning summaries**: Help with metacognition + + TESTING GUIDANCE: + - Help them write 2-3 simple unit tests (not comprehensive test suites) + - Focus on "happy path" and one edge case + - Explain what they're testing and why + - Make tests feel like insurance, not extra work + - Celebrate when tests catch issues + + DOCUMENTATION GUIDANCE: + - Help them write helpful comments (not obvious ones) + - Guide them to create a simple README: + - What does this do? + - How do you run it? + - What did you learn building it? + - Keep it minimal but useful + + REFACTORING GUIDANCE: + - Suggest 1-2 simple improvements (not a complete rewrite) + - Focus on readability over perfection + - Explain why cleaner code helps future-them + - Make it optional but encouraged + + REFLECTION PROTOCOL: + Generate a learning summary: + ``` + ## What You Built: + [Brief description] + + ## Key Concepts You Used: + - Concept 1 + - Concept 2 + + ## Challenges You Overcame: + - Challenge and how you solved it + + ## What You Learned: + - Learning 1 + - Learning 2 + + ## Next Steps (Optional): + - Potential improvements + - Related concepts to explore + ``` + + RESPONSE STRATEGY: + 1. Validate that their code works + 2. Ask which completion task they want to tackle first (testing, docs, or refactoring) + 3. Guide them through it step-by-step + 4. When all done, generate the learning summary + 5. Celebrate the COMPLETION (this is huge for ADHD brains!) + + MOTIVATIONAL FRAMING: + - "Finished projects feel amazing - let's get you there" + - "Future-you will thank you for these tests" + - "Documentation is how you remember what you built" + - "This reflection helps your brain cement the learning" + + {context_section}{activity_summary} + + Remember: Completion is a skill that ADHD brains need to practice. Make it feel rewarding, not like a chore. The learning summary is especially valuable for metacognition.""" def _analyze_activity_context(self, activity_logs: List[Dict]) -> str: """Analyze activity logs to provide insights for the LLM""" @@ -293,26 +578,31 @@ def query_or_respond(state: MessagesState): # Add activity context to system message if available messages = state["messages"] activity_context = getattr(self, '_current_activity_context', None) + mode = getattr(self, '_current_mode', 'general') + detective_mode = getattr(self, '_current_detective_mode', 'teaching') - # Create an enhanced system message with activity context - if activity_context: - activity_summary = self._analyze_activity_context(activity_context) - enhanced_system_prompt = self._create_system_prompt(activity_summary=activity_summary) - - # Replace or add system message - enhanced_messages = [] - system_added = False - for msg in messages: - if msg.type == "system": - enhanced_messages.append(SystemMessage(content=enhanced_system_prompt)) - system_added = True - else: - enhanced_messages.append(msg) - - if not system_added: - enhanced_messages = [SystemMessage(content=enhanced_system_prompt)] + enhanced_messages - - messages = enhanced_messages + # Create an enhanced system message with activity context and mode + activity_summary = self._analyze_activity_context(activity_context) if activity_context else "" + enhanced_system_prompt = self._create_system_prompt( + mode=mode, + activity_summary=activity_summary, + detective_mode=detective_mode + ) + + # Replace or add system message + enhanced_messages = [] + system_added = False + for msg in messages: + if msg.type == "system": + enhanced_messages.append(SystemMessage(content=enhanced_system_prompt)) + system_added = True + else: + enhanced_messages.append(msg) + + if not system_added: + enhanced_messages = [SystemMessage(content=enhanced_system_prompt)] + enhanced_messages + + messages = enhanced_messages llm_with_tools = self.llm.bind_tools([retrieve_tool]) response = llm_with_tools.invoke(messages) @@ -332,8 +622,16 @@ def generate(state: MessagesState): # Format into prompt with RAG content docs_content = "\n\n".join(doc.content for doc in tool_messages) + # Get current mode settings + mode = getattr(self, '_current_mode', 'general') + detective_mode = getattr(self, '_current_detective_mode', 'teaching') + # Create focused prompt for RAG responses using shared function - system_message_content = self._create_system_prompt(docs_content=docs_content) + system_message_content = self._create_system_prompt( + mode=mode, + docs_content=docs_content, + detective_mode=detective_mode + ) conversation_messages = [ message @@ -427,8 +725,8 @@ def create_session_with_first_message(self, first_message: str) -> Dict[str, str "name": chat_name } - def generate_response(self, message: str, session_id: str, activity_context: List[Dict] = None) -> Dict[str, Any]: - """Generate a response for the given message and session with optional activity context""" + def generate_response(self, message: str, session_id: str, activity_context: List[Dict] = None, mode: str = "general", detective_mode: str = "teaching") -> Dict[str, Any]: + """Generate a response for the given message and session with optional activity context and mode""" try: # Get conversation history from our database history = self.get_conversation_history(session_id) @@ -457,10 +755,12 @@ def generate_response(self, message: str, session_id: str, activity_context: Lis messages.append(user_message) # Store user message in database - user_msg_id = self.conversation_repo.add_message_simple(session_id, "user", message) + user_msg_id = self.conversation_repo.add_message_simple(session_id, "user", message, mode=mode) - # Store activity context for this request + # Store context and mode for this request self._current_activity_context = activity_context + self._current_mode = mode + self._current_detective_mode = detective_mode # Run the graph result = None @@ -478,8 +778,8 @@ def generate_response(self, message: str, session_id: str, activity_context: Lis else: response_text = str(last_message) - # Store assistant response in database - assistant_msg_id = self.conversation_repo.add_message_simple(session_id, "assistant", response_text) + # Store assistant response in database with mode + assistant_msg_id = self.conversation_repo.add_message_simple(session_id, "assistant", response_text, mode=mode) return { "success": True,