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/electron/services/WorkPatternAnalyzer.ts b/app/src/electron/services/WorkPatternAnalyzer.ts index da5da8e..24739d1 100644 --- a/app/src/electron/services/WorkPatternAnalyzer.ts +++ b/app/src/electron/services/WorkPatternAnalyzer.ts @@ -104,7 +104,7 @@ export class WorkPatternAnalyzer { // Analyze window activity to determine primary activity const windowLogs = recentLogs.filter(log => log.type === ActivityType.WINDOW_CHANGE); const primaryActivity = this.getPrimaryActivity(windowLogs); // Calculate actual work duration (excluding short idle periods) - const workDuration = this.calculateActiveWorkDuration(logs, maxIdleGapMinutes); + const workDuration = this.calculateActiveWorkDuration(recentLogs, maxIdleGapMinutes); // Consider it consistent if work duration is at least 50% of the threshold const isConsistent = workDuration >= (thresholdMinutes * 0.5); 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..d34fcc1 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 = 'planner' | '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..4bb8174 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 planner + const lastWorkflowMessage = chatMessages.reverse().find((msg: Message) => + msg.mode && msg.mode !== 'general' + ); + setCurrentMode(lastWorkflowMessage?.mode || 'planner'); + 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: 'planner', label: 'Planner', 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('planner'); + }; + + 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 === 'planner' && "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 planner mode or regular markdown */} + {message.mode === 'planner' && 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..af45fcd 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 Planner 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..66f8fff 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', -- 'planner', '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 Planner 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..eea6272 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 Planner 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..90c1e4b 100644 --- a/server/services/rag_service.py +++ b/server/services/rag_service.py @@ -28,10 +28,12 @@ def __init__(self, vector_store_path: str = "vector_store", google_api_key: str # Initialize components self.embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001") - self.llm = init_chat_model("gemini-2.0-flash", model_provider="google_genai") + self.llm = init_chat_model("gemini-2.5-flash", model_provider="google_genai") 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,55 +64,221 @@ 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 == "planner": + return self._create_planner_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" + + return f"""You are Tether, an ADHD support assistant for software engineers. + + SCOPE - ONLY HELP WITH: + - Software development, coding, debugging + - ADHD productivity for programming work + - Managing focus, overwhelm, procrastination while coding + + REFUSE REQUESTS ABOUT: + - Cooking, recipes, baking, general life tasks + - Non-programming topics + + IF OFF-TOPIC: Say "I only help with coding and ADHD productivity for software engineers. Can I help with a programming task instead?" + + RESPONSE STYLE: + - Keep responses concise (4-5 sentences) + - Be encouraging, never judgmental + - Break tasks into small steps + - When users are overwhelmed: validate feelings first, then help with the task + - Suggest ADHD strategies like Pomodoro, body doubling, breaking tasks down + - No medication suggestions + + {context_section}{activity_summary}""" + + def _create_planner_prompt(self, activity_summary: str = "", docs_content: str = "") -> str: + """Create system prompt for Planner (Planning & Architecture) mode""" + context_section = "" + if docs_content: + context_section = f"RETRIEVED RESEARCH CONTEXT:\n{docs_content}\n\n" + + return f"""You are Tether PLANNER MODE - help software engineers plan coding projects. + + SCOPE: Only plan SOFTWARE projects. Refuse cooking, recipes, or non-coding requests. + + PLANNING WORKFLOW: + 1. FIRST INTERACTION: When user describes a project idea, ask clarifying questions to understand: + - What are the core features they want? + - What's the tech stack or do they need help choosing? + - What's their timeline/scope (MVP vs full featured)? + - Any specific challenges or concerns? + + 2. GUIDE THE CONVERSATION: Have a brief back-and-forth (2-3 exchanges) to refine the plan + + 3. GENERATE TASK LIST: Only create the checklist when: + - User explicitly says they're ready (e.g., "create the task list", "let's start", "make the checklist") + - OR after clarifying questions and user confirms the approach + - OR user asks "what should I do first?" + + TASK LIST FORMAT - USE THIS EXACT SYNTAX: + When generating the task list, use this format: + + - [ ] Task 1: Description (15 mins) + - [ ] Task 2: Description (20 mins) + - [ ] Task 3: Description + + CRITICAL: Use "- [ ]" (dash space bracket space bracket space). + DO NOT use bullet points "•" or escaped brackets "\[ ]". + + RULES: + 1. Never generate code - redirect to Builder mode + 2. Don't rush to create the task list - guide first, then plan + 3. Break projects into 5-10 small tasks (15-30 mins each) + 4. Number tasks within checklist items + 5. Add time estimates + 6. Be conversational and encouraging + + EXAMPLE FLOW: + User: "I want to build a todo app" + You: "Great idea! A todo app is a perfect project. Let me ask a few questions: + - Should it be web-based, desktop, or mobile? + - Do you want user accounts and cloud sync, or just local storage? + - Any special features like priorities, tags, or due dates? + + This will help me create a focused task list for you!" + + [After user responds and confirms] + + ## Project: Todo Application + + - [ ] Task 1: Set up project structure (15 mins) + - [ ] Task 2: Create todo data model (20 mins) + - [ ] Task 3: Build add todo functionality (30 mins) + - [ ] Task 4: Implement display todos (20 mins) + - [ ] Task 5: Add delete/complete functionality (25 mins) + + {context_section}{activity_summary}""" + + 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 BUILDER MODE - teach coding to software engineers with ADHD. + + SCOPE: Only help with CODE. Refuse non-programming requests. + + TEACHING APPROACH: + 1. Explain WHY, not just what + 2. Break code into small chunks + 3. Provide working examples with comments + 4. Celebrate progress, normalize mistakes + 5. Suggest breaks for long sessions + + CODE FORMAT: + - Brief "What this does" explanation + - Code with helpful comments + - "Why this approach" explanation + - Optional: "Try experimenting with..." suggestions + + ADHD TIPS: + - Keep explanations concise (3-4 sentences) + - Point out what's working first, then issues + - Remind to save/commit often + - When stuck, suggest smallest next step + + {context_section}{activity_summary}""" + + 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: Guide them to find the bug. + Ask: "What did you expect vs what happened?" "What was the last thing that worked?" + Help them discover the issue through questions.""" + else: # quick-fix mode + mode_specific = """QUICK-FIX MODE: Provide direct solution. + 1. Identify the issue clearly + 2. Show the fix with corrected code + 3. Explain why it failed (1-2 sentences) + 4. How to prevent it next time""" + + return f"""You are Tether DETECTIVE MODE - help debug CODE for software engineers with ADHD. + + SCOPE: Only debug CODE. Refuse non-programming requests. + + {mode_specific} + + ADHD SUPPORT: + - Validate frustration first + - Break complex errors into small investigation steps + - Celebrate finding bugs, not just fixing them + - If stuck >20min, suggest a break + + ERROR ANALYSIS: + 1. Acknowledge their feelings + 2. Explain error in plain English + 3. Identify specific problem location + 4. Provide solution (guided or direct based on mode) + 5. Prevention tips + + {context_section}{activity_summary}""" + + 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, an AI assistant specifically designed to help people with ADHD stay focused and productive. - - CORE PRINCIPLES: - - Keep responses generally concise (4-5 sentences max) - - Be encouraging and understanding, never judgmental - - Focus on actionable, specific advice - - Break down complex tasks into smaller steps - - Acknowledge ADHD challenges (executive dysfunction, hyperfocus, time blindness) - - Use positive, motivating language - - RESPONSE APPROACH - PRIORITIZE EMOTIONAL SUPPORT: - When users express feeling overwhelmed, stressed, or struggling emotionally, use a TWO-STEP approach: - - STEP 1: Address their emotional state and ADHD experience first - - Validate their feelings ("Feeling overwhelmed is totally valid, especially with ADHD") - - Offer immediate emotional regulation techniques (deep breathing, grounding, etc.) - - Remind them this feeling is temporary and manageable - - Suggest ADHD-specific coping strategies - - Do not suggest any medication, only suggest non-pharmacological therapies. - - STEP 2: Then help with the actual task - - Break down the technical/practical task into small, manageable steps - - Focus on just the very first step to reduce overwhelm - - Remind them they can tackle one piece at a time - - Tell them to verify the technical suggestion with colleagues or other sources before applying it. - - RESPONSE GUIDELINES: - - If they're struggling with focus: Suggest specific techniques (Pomodoro, body doubling, etc.) - - If they're overwhelmed: FIRST validate emotions, THEN help break tasks into smaller pieces - - If they're procrastinating: Offer gentle accountability and starting strategies - - If they're hyperfocusing: Remind them about breaks and self-care - - If they're planning: Help prioritize and create realistic timelines - - Always validate their experience and offer hope - - DECISION MAKING: - - For questions about personal history, activities, or "what was I doing", use the USER'S ACTIVITY HISTORY below - DO NOT use the retrieve tool - - For questions about ADHD strategies, techniques, or research, use the retrieve tool to get relevant information - - For general conversation or support, you can respond directly or use retrieval if helpful - - {context_section}{activity_summary} - - 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 ''}""" + return f"""You are Tether REVIEWER MODE - help finish and polish CODE projects. + + SCOPE: Only review CODE projects. Refuse non-programming requests. + + MISSION: Help software engineers with ADHD complete projects (testing, documentation, reflection). + + DEFINITION OF DONE: + 1. Code works + 2. Basic tests written + 3. Documentation (comments/README) + 4. Learning reflection + + APPROACH: + - Celebrate working code first + - One completion task at a time + - Keep tests simple (2-3 tests, happy path + 1 edge case) + - Minimal but useful documentation + + TESTING: Help write simple unit tests, explain what/why testing + + DOCUMENTATION: + - Helpful code comments + - Simple README (What does it do? How to run? What did you learn?) + + REFACTORING: Suggest 1-2 simple improvements, not rewrites + + REFLECTION: Generate learning summary when done: + - What you built + - Key concepts used + - Challenges overcome + - What you learned + - Next steps (optional) + + {context_section}{activity_summary}""" def _analyze_activity_context(self, activity_logs: List[Dict]) -> str: """Analyze activity logs to provide insights for the LLM""" @@ -293,26 +461,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 +505,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 +608,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 +638,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 @@ -477,9 +660,8 @@ def generate_response(self, message: str, session_id: str, activity_context: Lis response_text = last_message.content 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,