import { useEffect, useState, useRef, useCallback } from "react" import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts' import { useApiHelpers } from '@/hooks/useApiHelpers' import { useWebSocket } from '@/hooks/useWebSocket' import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { GlobalErrorBoundary } from '@/components/GlobalErrorBoundary' import { OfflineDetector } from '@/components/OfflineDetector' import { setupGlobalErrorHandlers, errorReporting } from '@/utils/errorReporting' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Textarea } from "@/components/ui/textarea" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Badge } from "@/components/ui/badge" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' import Editor from '@monaco-editor/react' import { MemoryCard } from '@/components/MemoryCard' import { MemoryEditModal } from '@/components/MemoryEditModal' import { AdvancedSearch } from '@/components/AdvancedSearch' import { ProjectTabs } from '@/components/ProjectTabs' import { SortControls } from '@/components/SortControls' import { ExportImport } from '@/components/ExportImport' import { ExportImportDialogs } from '@/components/ExportImportDialogs' import { StatisticsDashboard } from '@/components/StatisticsDashboard' import { AIEnhancement } from '@/components/AIEnhancement' import { TaskEnhancement } from '@/components/TaskEnhancement' import { MemoryRelationships } from '@/components/MemoryRelationships' import { TaskManagement } from '@/components/TaskManagement' import { MemoryTreeView } from '@/components/MemoryTreeView' import { KeyboardShortcutsHelp } from '@/components/KeyboardShortcutsHelp' import { TemplateSelector } from '@/components/TemplateSelector' import { GlobalSearch } from '@/components/GlobalSearch' import { CategorySuggestions } from '@/components/CategorySuggestions' import { SearchPresets } from '@/components/SearchPresets' import { TutorialLauncher, OnboardingTutorial } from '@/components/OnboardingTutorial' import { PathConfiguration } from '@/components/PathConfiguration' import { ToastProvider, useToast, toastHelpers } from '@/components/ToastNotifications' import { ProgressProvider, useOperationProgress } from '@/components/ProgressIndicators' import { SettingsDropdown } from '@/components/SettingsDropdown' import { PageLoadingSpinner, RefreshSpinner, ButtonSpinner, MemoryCardsGridSkeleton, MemoryTableSkeleton, EmptyState } from '@/components/LoadingStates' import { BarChart3, Brain, ListTodo, Link, Bot, Menu, X, Settings, Download } from 'lucide-react' import { Memory, MemoryCategory, ViewMode, AdvancedFilters, SortOptions } from '@/types' import { searchMemories, sortMemories } from '@/utils/helpers' // === HELPER FUNCTIONS === function extractTags(memory: Memory): string[] { if (memory.tags && Array.isArray(memory.tags)) { return memory.tags } return [] } function extractVisibleTags(memory: Memory): string[] { if (memory.tags && Array.isArray(memory.tags)) { // Filter out title: and summary: tags from visible display return memory.tags.filter(tag => !tag.startsWith('title:') && !tag.startsWith('summary:') ) } return [] } function extractTitle(content: string, memory?: Memory): string { // Check for LLM-generated title first if (memory) { const tags = extractTags(memory) const titleTag = tags.find(tag => tag.startsWith('title:')) if (titleTag) { return titleTag.substring(6) // Remove 'title:' prefix } } // Enhanced title extraction const lines = content.split('\\n').filter(line => line.trim()) // Look for markdown headers const headerMatch = content.match(/^#{1,6}\\s+(.+)$/m) if (headerMatch) { return headerMatch[1].trim() } // Look for structured patterns const structuredPatterns = [ /^(.+?):\\s*[\\r\\n]/m, // "Title: content" /^"(.+?)"/m, // Quoted titles /^\\*\\*(.+?)\\*\\*/m, // Bold markdown /^__(.+?)__/m, // Bold underscore /^\\[(.+?)\\]/m, // Bracketed content ] for (const pattern of structuredPatterns) { const match = content.match(pattern) if (match && match[1].length < 60 && match[1].length > 5) { return match[1].trim() } } // Development patterns const devPatterns = [ /(?:Phase|Step|Task)\\s+(\\d+)[:\\s]+(.+?)(?:[\\r\\n]|$)/i, /(?:Feature|Bug|Fix)[:\\s]+(.+?)(?:[\\r\\n]|$)/i, /(?:TODO|DONE|WIP)[:\\s]+(.+?)(?:[\\r\\n]|$)/i, /^\\d+[.)\\s]+(.+?)(?:[\\r\\n]|$)/m, // Numbered lists ] for (const pattern of devPatterns) { const match = content.match(pattern) if (match) { const title = (match[2] || match[1]).trim() if (title.length < 60 && title.length > 5) { return title } } } // Extract key phrases from content const sentences = content.split(/[.!?\\n]+/).filter(s => s.trim().length > 10) for (const sentence of sentences.slice(0, 3)) { const cleaned = sentence.trim() // Skip generic patterns if (!cleaned.match(/^(project location|current|status|update|working|running)/i) && cleaned.length > 15 && cleaned.length < 80) { return cleaned } } // Use meaningful keywords const keywords = content.toLowerCase().match(/\\b(dashboard|api|component|feature|bug|fix|update|implement|create|add)\\b/g) if (keywords && keywords.length > 0) { const firstSentence = sentences[0]?.trim() if (firstSentence && firstSentence.length < 100) { return firstSentence } } // Fallback to first meaningful sentence const fallback = sentences[0]?.trim() if (fallback && fallback.length < 100) { return fallback } return content.substring(0, 50) + (content.length > 50 ? '...' : '') } function generateSummary(content: string, memory?: Memory): string { // Check for LLM-generated summary first if (memory) { const tags = extractTags(memory) const summaryTag = tags.find(tag => tag.startsWith('summary:')) if (summaryTag) { return summaryTag.substring(8) // Remove 'summary:' prefix } } // Extract first few sentences for summary const sentences = content.split(/[.!?\\n]+/).filter(s => s.trim().length > 10) const summary = sentences.slice(0, 2).join('. ').trim() if (summary.length > 0) { return summary.length > 200 ? summary.substring(0, 197) + '...' : summary } return content.substring(0, 150) + (content.length > 150 ? '...' : '') } function getTagColor(tag: string): { bg: string; text: string; border: string } { let hash = 0 for (let i = 0; i < tag.length; i++) { hash = tag.charCodeAt(i) + ((hash << 5) - hash) } const hue = Math.abs(hash) % 360 const saturation = 60 + (Math.abs(hash) % 30) // 60-90% const lightness = 25 + (Math.abs(hash) % 15) // 25-40% for dark backgrounds return { bg: `hsl(${hue}, ${saturation}%, ${lightness}%)`, text: `hsl(${hue}, ${Math.max(saturation - 10, 50)}%, 85%)`, // Light text for dark bg border: `hsl(${hue}, ${saturation}%, ${lightness + 15}%)` } } // === MAIN COMPONENT === function AppContent() { const toast = useToast() const progress = useOperationProgress() // === STATE === const [memories, setMemories] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isRefreshing, setIsRefreshing] = useState(false) const [isCreating, setIsCreating] = useState(false) const [isDeleting, setIsDeleting] = useState>(new Set()) const [search, setSearch] = useState("") const [searchFilters, setSearchFilters] = useState({}) const [tagFilter, setTagFilter] = useState("all") const [showAddDialog, setShowAddDialog] = useState(false) const [showMobileSearch, setShowMobileSearch] = useState(false) const [newValue, setNewValue] = useState("") const [newTags, setNewTags] = useState("") const [editingMemory, setEditingMemory] = useState(null) const [editingValue, setEditingValue] = useState("") const [editingTags, setEditingTags] = useState("") const [showEditDialog, setShowEditDialog] = useState(false) const [showEditModal, setShowEditModal] = useState(false) const [isEditLoading, setIsEditLoading] = useState(false) const [currentTab, setCurrentTab] = useState<"dashboard" | "memories" | "tasks" | "relationships" | "ai" | "settings">("memories") const [aiMode, setAiMode] = useState<'memories' | 'tasks'>('memories') const [useAdvancedEditor, setUseAdvancedEditor] = useState(false) const [useAdvancedEditorCreate, setUseAdvancedEditorCreate] = useState(false) const [selectedCategory, setSelectedCategory] = useState("all") const [viewMode, setViewMode] = useState<"cards" | "table" | "tree">("cards") const [llmProvider, setLlmProvider] = useState<"openai" | "anthropic" | "ollama" | "none">("none") const [llmApiKey, setLlmApiKey] = useState("") const [apiKeyEditMode, setApiKeyEditMode] = useState(false) const [isEnhancing, setIsEnhancing] = useState(false) const [enhancingMemories, setEnhancingMemories] = useState>(new Set()) const [currentProject, setCurrentProject] = useState("all") const [selectedMemories, setSelectedMemories] = useState>(new Set()) const [showBulkTagDialog, setShowBulkTagDialog] = useState(false) const [showExportDialog, setShowExportDialog] = useState(false) const [showImportDialog, setShowImportDialog] = useState(false) const [bulkTagInput, setBulkTagInput] = useState("") const [sortOptions, setSortOptions] = useState({ field: 'date', direction: 'desc' }) const [bulkTagAction, setBulkTagAction] = useState<"add" | "remove">("add") const [newCategory, setNewCategory] = useState(undefined) const [newProject, setNewProject] = useState("") const [editingCategory, setEditingCategory] = useState(undefined) const [editingProject, setEditingProject] = useState("") const [showScrollToTop, setShowScrollToTop] = useState(false) const [showKeyboardHelp, setShowKeyboardHelp] = useState(false) const [showTutorial, setShowTutorial] = useState(false) const [showMemoryTemplateSelector, setShowMemoryTemplateSelector] = useState(false) const [showGlobalSearch, setShowGlobalSearch] = useState(false) // === TASK STATE === const [tasks, setTasks] = useState([]) const [isLoadingTasks, setIsLoadingTasks] = useState(false) // === ENHANCEMENT ERROR TRACKING === const [enhancementFailures, setEnhancementFailures] = useState(0) const [enhancementDisabled, setEnhancementDisabled] = useState(false) const MAX_ENHANCEMENT_FAILURES = 3 // === WEBSOCKET STATE === const [wsConnected, setWsConnected] = useState(false) // === API HELPERS === const { apiGet, apiPost, apiPut, apiDelete } = useApiHelpers() // === REFS === const searchInputRef = useRef(null) // Keep ref in sync with memories state to avoid stale closure const memoriesRef = useRef(memories); useEffect(() => { memoriesRef.current = memories; }, [memories]); // Create a ref to hold the loadMemories function const loadMemoriesRef = useRef<(isRefresh?: boolean) => Promise>(); // === DATA MANAGEMENT === const loadMemories = async (isRefresh = false) => { try { if (isRefresh) { setIsRefreshing(true) } else { setIsLoading(true) } // Load all memories by fetching all pages let allMemories = [] let page = 1 let hasMore = true while (hasMore) { const response = await apiGet(`/api/memories?page=${page}&limit=100`) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } const text = await response.text() if (!text.trim()) { break } const data = JSON.parse(text) // Handle both old array format and new paginated format if (Array.isArray(data)) { allMemories = [...allMemories, ...data] hasMore = false // Old format doesn't have pagination } else if (data.data && Array.isArray(data.data)) { allMemories = [...allMemories, ...data.data] hasMore = data.pagination ? data.pagination.hasNext : false page++ } else { hasMore = false } } setMemories(allMemories) } catch (error) { console.error('Failed to load memories:', error) toast.error('Failed to load memories', 'Please check your connection and try again') setMemories([]) } finally { setIsLoading(false) setIsRefreshing(false) } } // Update the ref when loadMemories changes useEffect(() => { loadMemoriesRef.current = loadMemories; }, [loadMemories]); // === TASK MANAGEMENT === const loadTasks = async () => { try { setIsLoadingTasks(true) // Load all tasks by fetching all pages let allTasks = [] let page = 1 let hasMore = true while (hasMore) { const response = await apiGet(`/api/tasks?page=${page}&limit=100`) if (!response.ok) { break } const tasksData = await response.json() // Handle both old array format and new paginated format if (Array.isArray(tasksData)) { allTasks = [...allTasks, ...tasksData] hasMore = false // Old format doesn't have pagination } else if (tasksData.data && Array.isArray(tasksData.data)) { allTasks = [...allTasks, ...tasksData.data] hasMore = tasksData.pagination ? tasksData.pagination.hasNext : false page++ } else { hasMore = false } } setTasks(allTasks) } catch (error) { console.warn('Failed to load tasks:', error) setTasks([]) } finally { setIsLoadingTasks(false) } } // === WEBSOCKET HOOK === const { isConnected: wsIsConnected } = useWebSocket({ onMessage: (message) => { console.log('📡 WebSocket message:', message) if (message.type === 'file_change') { console.log(`📄 Memory file ${message.data.action}: ${message.data.file}`) console.log('Current memories count before refresh:', memoriesRef.current.length) // Refresh memories when files change loadMemoriesRef.current?.(true) } else if (message.type === 'task_change') { console.log(`📋 Task file ${message.data.action}: ${message.data.file}`) // Refresh tasks when task files change loadTasks() } }, onConnect: () => { console.log('🔌 WebSocket connected - Real-time updates enabled') setWsConnected(true) }, onDisconnect: () => { console.log('🔌 WebSocket disconnected') setWsConnected(false) }, reconnectInterval: 5000, maxReconnectAttempts: 20 }) // === EFFECTS === useEffect(() => { loadMemories() loadTasks() loadSettings() // WebSocket is handled by the hook, no manual setup needed }, []) // eslint-disable-line react-hooks/exhaustive-deps // Reset selected category when switching tabs useEffect(() => { setSelectedCategory("all") }, [currentTab]) // === SETTINGS MANAGEMENT === const loadSettings = () => { const savedProvider = localStorage.getItem('llm-provider') as "openai" | "anthropic" | "none" const savedApiKey = localStorage.getItem('llm-api-key') const savedSortOptions = localStorage.getItem('sort-options') if (savedProvider) setLlmProvider(savedProvider) if (savedApiKey) setLlmApiKey(savedApiKey) if (savedSortOptions) { try { const parsed = JSON.parse(savedSortOptions) as SortOptions setSortOptions(parsed) } catch (e) { // Invalid JSON, use defaults console.warn('Invalid sort options in localStorage, using defaults') } } } const saveSettings = () => { localStorage.setItem('llm-provider', llmProvider) localStorage.setItem('llm-api-key', llmApiKey) // Reset enhancement failures when settings change if (llmProvider === 'ollama' && enhancementDisabled) { setEnhancementFailures(0) setEnhancementDisabled(false) toast.success('Ollama enhancement re-enabled', 'You can now try enhancing memories again.') } } const clearApiKey = () => { setLlmApiKey("") localStorage.removeItem('llm-api-key') } const handleSortChange = (newSortOptions: SortOptions) => { setSortOptions(newSortOptions) localStorage.setItem('sort-options', JSON.stringify(newSortOptions)) } const addMemory = async () => { if (!newValue.trim() || isCreating) return const tags = newTags.split(',').map(tag => tag.trim()).filter(Boolean) const category = newCategory || autoAssignCategory(newValue, tags) const memory = { content: newValue, tags: tags, category: category, project: newProject.trim() || undefined } try { setIsCreating(true) await apiPost('/api/memories', memory) setNewValue("") setNewTags("") setNewCategory(undefined) setNewProject("") setShowAddDialog(false) loadMemories(true) // Refresh instead of full reload toast.success('Memory created successfully', 'Your memory has been saved and is ready to use') } catch (error) { console.error('Failed to add memory:', error) toast.error('Failed to create memory', 'Please try again or check your connection') } finally { setIsCreating(false) } } const updateMemory = async () => { if (!editingMemory) return const tags = editingTags.split(',').map(tag => tag.trim()).filter(Boolean) const category = editingCategory || autoAssignCategory(editingValue, tags) const updatedMemory = { content: editingValue, tags: tags, category: category, project: editingProject.trim() || undefined } try { await apiPut(`/api/memories/${editingMemory.id}`, updatedMemory) setShowEditDialog(false) setEditingMemory(null) loadMemories() toast.success('Memory updated', 'Your changes have been saved') } catch (error) { console.error('Failed to update memory:', error) toast.error('Failed to update memory', 'Please try again or check your connection') } } const deleteMemory = async (memoryId: string) => { if (isDeleting.has(memoryId)) return // Show confirmation toast toast.warning('Delete memory?', 'This action cannot be undone', { action: { label: 'Delete', onClick: () => performDelete(memoryId) }, duration: 8000 }) } const performDelete = async (memoryId: string) => { try { setIsDeleting(prev => new Set([...prev, memoryId])) await apiDelete(`/api/memories/${memoryId}`) loadMemories(true) // Refresh instead of full reload toast.success('Memory deleted', 'The memory has been permanently removed') } catch (error) { console.error('Failed to delete memory:', error) toast.error('Failed to delete memory', 'Please try again or check your connection') } finally { setIsDeleting(prev => { const newSet = new Set(prev) newSet.delete(memoryId) return newSet }) } } const handleEdit = (memoryId: string) => { const memory = memories.find(m => m.id === memoryId) if (memory) { setEditingMemory(memory) setEditingValue(memory.content) setEditingTags(extractTags(memory).join(', ')) setEditingCategory(memory.category) setEditingProject(memory.project || "") setShowEditModal(true) } } const handleSaveMemoryFromModal = async (updatedMemory: Memory) => { setIsEditLoading(true) try { await apiPut(`/api/memories/${updatedMemory.id}`, { content: updatedMemory.content, category: updatedMemory.category, priority: updatedMemory.priority, tags: updatedMemory.tags, project: updatedMemory.project }) setShowEditModal(false) setEditingMemory(null) loadMemories() toast.success('Memory updated', 'Your changes have been saved') } catch (error) { console.error('Failed to update memory:', error) toast.error('Failed to update memory', 'Please try again') } finally { setIsEditLoading(false) } } // === CATEGORIZATION === const suggestCategory = (content: string, tags: string[]): MemoryCategory | undefined => { const lowerContent = content.toLowerCase() const lowerTags = tags.map(tag => tag.toLowerCase()) // Check for code-related content if ( lowerTags.some(tag => ["code", "programming", "dev", "tech", "javascript", "typescript", "python", "react", "node", "api", "database", "sql"].includes(tag)) || content.includes("```") || content.includes("function") || content.includes("npm ") || content.includes("git ") || content.includes("const ") || content.includes("import ") || content.includes("export ") || /\b(bug|fix|debug|error|exception|variable|method|class)\b/i.test(content) ) { return 'code' } // Check for work-related content if ( lowerTags.some(tag => ["work", "business", "meeting", "client", "job", "project", "team", "office", "deadline", "task"].includes(tag)) || /\b(meeting|deadline|project|team|client|business|work|office|manager|boss|colleague)\b/i.test(content) ) { return 'work' } // Check for research-related content if ( lowerTags.some(tag => ["research", "study", "analysis", "data", "investigation", "paper", "academic", "science"].includes(tag)) || /\b(research|study|analysis|investigation|findings|hypothesis|methodology|paper|academic)\b/i.test(content) ) { return 'research' } // Check for conversation-related content if ( lowerTags.some(tag => ["conversation", "chat", "discussion", "call", "meeting", "talk"].includes(tag)) || /\b(conversation|discussed|talked|said|mentioned|asked|told|chat|call)\b/i.test(content) || content.includes('"') || content.includes("'") ) { return 'conversations' } // Check for personal content if ( lowerTags.some(tag => ["personal", "me", "my", "self", "private", "family", "friend", "home"].includes(tag)) || /\b(my |I |me |myself|personal|family|friend|home|feel|think|believe|remember)\b/i.test(content) ) { return 'personal' } // Default to undefined (no category) return undefined } const autoAssignCategory = (content: string, tags: string[]) => { return suggestCategory(content, tags) } // === TEMPLATE HANDLING === const handleMemoryTemplate = (template: any, variables: Record) => { // Replace variables in template content let content = template.content for (const [key, value] of Object.entries(variables)) { content = content.replace(new RegExp(`{{${key}}}`, 'g'), value) } // Populate form fields setNewValue(content) setNewTags([...template.tags].join(', ')) setNewCategory(template.category) // Close template selector and open add dialog setShowMemoryTemplateSelector(false) setShowAddDialog(true) } // === GLOBAL SEARCH HANDLING === const handleGlobalSearchSelectMemory = (memory: Memory) => { // Switch to memories tab and focus on the selected memory setCurrentTab('memories') setEditingMemory(memory) setEditingValue(memory.content) setEditingTags((memory.tags || []).join(', ')) setEditingCategory(memory.category) setEditingProject(memory.project || '') setShowEditDialog(true) } const handleGlobalSearchSelectTask = (task: any) => { // Switch to tasks tab - the TaskManagement component will handle task selection setCurrentTab('tasks') // We could emit a custom event for the TaskManagement component to handle setTimeout(() => { document.dispatchEvent(new CustomEvent('selectTask', { detail: task })) }, 100) } // === PROJECT MANAGEMENT === const createProject = async (projectId: string) => { // Projects are created dynamically when memories are assigned to them } const deleteProject = async (projectId: string) => { if (projectId === "all" || projectId === "default") return // Move all memories from this project to default const projectMemories = memories.filter(m => m.project === projectId) for (const memory of projectMemories) { await updateMemoryProject(memory.id, "default") } } const updateMemoryProject = async (memoryId: string, projectId: string) => { const memory = memories.find(m => m.id === memoryId) if (!memory) return try { await apiPut(`/api/memories/${memoryId}`, { ...memory, project: projectId === "default" ? undefined : projectId }) loadMemories() toast.success('Project updated', 'Memory has been moved to the new project') } catch (error) { console.error('Failed to update memory project:', error) toast.error('Failed to update project', 'Please try again or check your connection') } } const moveSelectedMemoriesToProject = async (projectId: string) => { for (const memoryId of selectedMemories) { await updateMemoryProject(memoryId, projectId) } setSelectedMemories(new Set()) } const bulkUpdateCategory = async (category: MemoryCategory) => { const memoryIds = Array.from(selectedMemories) const progressId = progress.startBulkOperation( 'Updating Categories', memoryIds, `Changing category to "${category}" for ${memoryIds.length} memories` ) let completed = 0 try { for (const memoryId of memoryIds) { const memory = memories.find(m => m.id === memoryId) if (!memory) continue progress.updateOperation(progressId, { completed, description: `Updating "${memory.content.substring(0, 40)}..."` }) await apiPut(`/api/memories/${memoryId}`, { ...memory, category }) completed++ progress.updateOperation(progressId, { completed }) } loadMemories() setSelectedMemories(new Set()) progress.completeOperation(progressId, true) toast.success('Categories updated', `Updated ${completed} memories`) } catch (error) { console.error('Failed to bulk update category:', error) progress.completeOperation(progressId, false, `Failed after updating ${completed} memories`) toast.error('Failed to update categories', 'Please try again or check your connection') } } const bulkAddTags = async (tagsToAdd: string[]) => { const memoryIds = Array.from(selectedMemories) const progressId = progress.startBulkOperation( 'Adding Tags', memoryIds, `Adding "${tagsToAdd.join(', ')}" to ${memoryIds.length} memories` ) let completed = 0 try { for (const memoryId of memoryIds) { const memory = memories.find(m => m.id === memoryId) if (!memory) continue progress.updateOperation(progressId, { completed, description: `Adding tags to "${memory.content.substring(0, 40)}..."` }) const currentTags = memory.tags || [] const newTags = [...new Set([...currentTags, ...tagsToAdd])] // Remove duplicates await apiPut(`/api/memories/${memoryId}`, { ...memory, tags: newTags }) completed++ progress.updateOperation(progressId, { completed }) } loadMemories() setSelectedMemories(new Set()) progress.completeOperation(progressId, true) toast.success('Tags added', `Added tags to ${completed} memories`) } catch (error) { console.error('Failed to bulk add tags:', error) progress.completeOperation(progressId, false, `Failed after updating ${completed} memories`) toast.error('Failed to add tags', 'Please try again or check your connection') } } const bulkRemoveTags = async (tagsToRemove: string[]) => { try { for (const memoryId of selectedMemories) { const memory = memories.find(m => m.id === memoryId) if (!memory) continue const currentTags = memory.tags || [] const newTags = currentTags.filter(tag => !tagsToRemove.includes(tag)) await apiPut(`/api/memories/${memoryId}`, { ...memory, tags: newTags }) } loadMemories() setSelectedMemories(new Set()) toast.success('Tags removed', `Removed tags from ${selectedMemories.size} memories`) } catch (error) { console.error('Failed to bulk remove tags:', error) toast.error('Failed to remove tags', 'Please try again or check your connection') } } const handleMemorySelect = (memoryId: string) => { setSelectedMemories(prev => { const newSet = new Set(prev) if (newSet.has(memoryId)) { newSet.delete(memoryId) } else { newSet.add(memoryId) } return newSet }) } // === KEYBOARD SHORTCUTS === useKeyboardShortcuts({ 'Ctrl+N': () => setShowAddDialog(true), 'Ctrl+Shift+N': () => { if (currentTab === 'tasks') { // Trigger new task creation - we'll need to communicate with TaskManagement component document.dispatchEvent(new CustomEvent('createNewTask')) } }, 'Ctrl+K': () => { setShowGlobalSearch(true) }, 'Ctrl+Shift+K': () => { searchInputRef.current?.focus() searchInputRef.current?.select() }, 'Ctrl+R': () => loadMemories(true), 'Escape': () => { setShowAddDialog(false) setShowEditDialog(false) setShowEditModal(false) setShowBulkTagDialog(false) setSelectedMemories(new Set()) setShowKeyboardHelp(false) setShowTutorial(false) setShowGlobalSearch(false) setShowMemoryTemplateSelector(false) }, 'Ctrl+/': () => setShowKeyboardHelp(true), 'Ctrl+1': () => setCurrentTab('memories'), 'Ctrl+2': () => setCurrentTab('tasks'), 'Ctrl+3': () => setCurrentTab('relationships'), 'Ctrl+4': () => setCurrentTab('ai'), 'Ctrl+5': () => setCurrentTab('dashboard'), 'Ctrl+Shift+C': () => setViewMode('cards'), 'Ctrl+Shift+T': () => setViewMode('table'), 'Ctrl+Shift+R': () => setViewMode('tree'), 'Ctrl+A': () => { if (currentTab === 'memories') { const visibleMemoryIds = filteredMemories.map(m => m.id) setSelectedMemories(new Set(visibleMemoryIds)) } else if (currentTab === 'tasks') { // Trigger select all for tasks document.dispatchEvent(new CustomEvent('selectAllTasks')) } }, 'Ctrl+D': () => { setSelectedMemories(new Set()) if (currentTab === 'tasks') { // Trigger clear selection for tasks document.dispatchEvent(new CustomEvent('clearTaskSelection')) } }, 'Delete': () => { if (selectedMemories.size > 0) { toast.warning(`Delete ${selectedMemories.size} memories?`, 'This action cannot be undone', { action: { label: 'Delete All', onClick: () => { selectedMemories.forEach(id => performDelete(id)) setSelectedMemories(new Set()) } }, duration: 8000 }) } } }) const handleImportMemories = async (newMemories: Memory[]) => { const progressId = progress.startBulkOperation( 'Importing Memories', newMemories, `Importing ${newMemories.length} memories from file` ) let completed = 0 try { for (const memory of newMemories) { progress.updateOperation(progressId, { completed, description: `Importing "${memory.content.substring(0, 50)}..."` }) await apiPost('/api/memories', memory) completed++ progress.updateOperation(progressId, { completed }) } loadMemories() progress.completeOperation(progressId, true) toast.success('Data imported successfully', `${completed} memories have been imported`) } catch (error) { console.error('Failed to import memories:', error) progress.completeOperation(progressId, false, `Failed after importing ${completed} memories`) toast.error('Failed to import data', 'Please check the file format and try again') throw error } } // === LLM ENHANCEMENT === const enhanceMemoryWithLLM = async (memory: Memory) => { if (llmProvider === "none") return if (llmProvider !== "ollama" && !llmApiKey) return // Check if enhancement has been disabled due to repeated failures if (enhancementDisabled) { console.log('Enhancement disabled due to repeated failures') return } setEnhancingMemories(prev => new Set([...prev, memory.id])) try { if (llmProvider === "ollama") { // Use Ollama local AI processing const { OllamaClient } = await import('./lib/ollama-client.js') const ollama = new OllamaClient() // Check if Ollama is available if (!(await ollama.isAvailable())) { throw new Error('Ollama server is not running. Please start Ollama and ensure a model is installed.') } // Enhance memory using Ollama const result = await ollama.enhanceMemory(memory.content, { category: memory.category, project: memory.project, tags: memory.tags }) await updateMemoryMetadata(memory.id, result.title, result.summary) return } // External API processing (OpenAI/Anthropic) const prompt = `Please analyze this memory content and provide a concise title (max 50 chars) and summary (max 150 chars). Content: ${memory.content} Respond with JSON format: { "title": "your title here", "summary": "your summary here" }` let response if (llmProvider === "openai") { response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${llmApiKey}` }, body: JSON.stringify({ model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: prompt }], max_tokens: 200, temperature: 0.3 }) }) } else if (llmProvider === "anthropic") { response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': llmApiKey, 'anthropic-version': '2023-06-01' }, body: JSON.stringify({ model: 'claude-3-haiku-20240307', max_tokens: 200, messages: [{ role: 'user', content: prompt }] }) }) } if (!response?.ok) { throw new Error(`API request failed: ${response?.status}`) } const data = await response.json() let content if (llmProvider === "openai") { content = data.choices[0]?.message?.content } else if (llmProvider === "anthropic") { content = data.content[0]?.text } if (content) { try { // Clean the content for JSON parsing const cleanContent = content .replace(/\\/g, '\\\\') // Escape backslashes .replace(/"/g, '\\"') // Escape quotes .replace(/\n/g, '\\n') // Escape newlines .replace(/\r/g, '\\r') // Escape carriage returns .replace(/\t/g, '\\t') // Escape tabs const parsed = JSON.parse(cleanContent) await updateMemoryMetadata(memory.id, parsed.title, parsed.summary) } catch (parseError) { // Fallback: try to extract title and summary with regex const titleMatch = content.match(/"title":\s*"([^"]+)"/); const summaryMatch = content.match(/"summary":\s*"([^"]+)"/); if (titleMatch && summaryMatch) { await updateMemoryMetadata(memory.id, titleMatch[1], summaryMatch[1]) } else { throw new Error('Invalid JSON format and could not extract title/summary') } } } loadMemories() } catch (error) { console.error('LLM enhancement error:', error) // Track Ollama-specific failures if (llmProvider === "ollama" && error.message.includes('Ollama server is not running')) { const newFailureCount = enhancementFailures + 1 setEnhancementFailures(newFailureCount) if (newFailureCount >= MAX_ENHANCEMENT_FAILURES) { setEnhancementDisabled(true) toast.error( 'Ollama Enhancement Disabled', `Enhancement has been disabled after ${MAX_ENHANCEMENT_FAILURES} failed attempts. You can re-enable it in settings once Ollama is running.` ) } else { toast.error( 'Failed to enhance memory', `${error.message} (Attempt ${newFailureCount}/${MAX_ENHANCEMENT_FAILURES})` ) } } else { // For other errors, show the error but don't count as failure toast.error('Failed to enhance memory', `${error.message}`) } } finally { setEnhancingMemories(prev => { const newSet = new Set(prev) newSet.delete(memory.id) return newSet }) } } const updateMemoryMetadata = async (memoryId: string, title: string, summary: string) => { const memory = memories.find(m => m.id === memoryId) if (!memory) return const currentTags = extractTags(memory) // Remove old title/summary tags to prevent duplication const cleanedTags = currentTags.filter(tag => !tag.startsWith('title:') && !tag.startsWith('summary:') ) // Add new title/summary as tags for internal processing const newTags = [ ...cleanedTags, `title:${title}`, `summary:${summary}` ] await apiPut(`/api/memories/${memoryId}`, { content: memory.content, tags: newTags }) } const enhanceAllMemories = async () => { if (llmProvider === "none") { toast.warning("LLM configuration required", "Please configure LLM settings first") return } if (llmProvider !== "ollama" && !llmApiKey) { toast.warning("API key required", "Please configure your API key first") return } // Show confirmation toast toast.warning(`Enhance all ${memories.length} memories?`, 'Add AI-generated titles and summaries', { action: { label: 'Enhance All', onClick: () => performEnhanceAll() }, duration: 10000 }) } const performEnhanceAll = async () => { const memoriesToEnhance = memories.filter(memory => { const currentTags = extractTags(memory) return !currentTags.some(tag => tag.startsWith('title:')) }) if (memoriesToEnhance.length === 0) { toast.info('All memories already enhanced', 'No memories need AI enhancement') return } const progressId = progress.startBulkOperation( 'Enhancing Memories', memoriesToEnhance, `Adding AI-generated titles and summaries to ${memoriesToEnhance.length} memories` ) setIsEnhancing(true) let completed = 0 try { for (const memory of memoriesToEnhance) { progress.updateOperation(progressId, { completed, description: `Enhancing "${memory.content.substring(0, 50)}..."` }) await enhanceMemoryWithLLM(memory) completed++ progress.updateOperation(progressId, { completed }) await new Promise(resolve => setTimeout(resolve, 1000)) // Rate limiting } progress.completeOperation(progressId, true) toast.success('Enhancement complete!', `Enhanced ${completed} memories with AI-generated content`) } catch (error) { progress.completeOperation(progressId, false, `Failed after enhancing ${completed} memories`) toast.error('Enhancement failed', `Completed ${completed} of ${memoriesToEnhance.length} memories`) } finally { setIsEnhancing(false) } } // === DATA PROCESSING === const allTags = Array.from(new Set( memories.flatMap(memory => extractTags(memory)) )).sort() // Calculate active tasks (exclude 'done' status) const activeTasks = tasks.filter(task => task.status !== 'done') const activeTaskCount = activeTasks.length // Generate categories based on current tab const getCategories = () => { if (currentTab === "memories") { return [ { id: "all", name: "All Memories", icon: null, count: memories.length }, { id: "personal", name: "Personal", icon: null, count: memories.filter(memory => { const tags = extractTags(memory) return tags.some(tag => ["personal", "me", "my", "self", "private"].includes(tag.toLowerCase())) || memory.content.toLowerCase().includes("my ") }).length }, { id: "work", name: "Work & Business", icon: null, count: memories.filter(memory => { const tags = extractTags(memory) return tags.some(tag => ["work", "business", "meeting", "client", "job"].includes(tag.toLowerCase())) || memory.content.toLowerCase().includes("work") || memory.content.toLowerCase().includes("meeting") }).length }, { id: "code", name: "Code & Tech", icon: null, count: memories.filter(memory => { const tags = extractTags(memory) return tags.some(tag => ["code", "programming", "dev", "tech", "javascript", "typescript", "python", "react", "node"].includes(tag.toLowerCase())) || memory.content.includes("```") || memory.content.includes("npm ") || memory.content.includes("function") }).length }, { id: "ideas", name: "Ideas & Plans", icon: null, count: memories.filter(memory => { const tags = extractTags(memory) return tags.some(tag => ["idea", "brainstorm", "concept", "inspiration", "plan", "roadmap", "todo"].includes(tag.toLowerCase())) || memory.content.toLowerCase().includes("idea") || memory.content.toLowerCase().includes("plan") }).length }, { id: "conversations", name: "Conversations", icon: null, count: memories.filter(memory => { const tags = extractTags(memory) return tags.some(tag => ["conversation", "chat", "discussion", "call", "meeting", "talk"].includes(tag.toLowerCase())) || memory.content.toLowerCase().includes("conversation") || memory.content.toLowerCase().includes("discussed") }).length }, { id: "connections", name: "Connected", icon: null, count: memories.filter(memory => { const tags = extractTags(memory) return tags.length > 0 && memories.some(other => other.id !== memory.id && extractTags(other).some(tag => tags.includes(tag)) ) }).length }, { id: "untagged", name: "Untagged", icon: null, count: memories.filter(memory => extractTags(memory).length === 0).length } ] } else if (currentTab === "tasks") { return [ { id: "all", name: "All Tasks", icon: "📋", count: activeTasks.length }, { id: "personal", name: "Personal", icon: "👤", count: tasks.filter(task => task.category === "personal" && task.status !== "done").length }, { id: "work", name: "Work", icon: "💼", count: tasks.filter(task => task.category === "work" && task.status !== "done").length }, { id: "code", name: "Code", icon: "💻", count: tasks.filter(task => task.category === "code" && task.status !== "done").length }, { id: "research", name: "Research", icon: "🔬", count: tasks.filter(task => task.category === "research" && task.status !== "done").length }, { id: "todo", name: "To Do", icon: "⏳", count: tasks.filter(task => task.status === "todo").length }, { id: "in_progress", name: "In Progress", icon: "🔄", count: tasks.filter(task => task.status === "in_progress").length }, { id: "done", name: "Done", icon: "✅", count: tasks.filter(task => task.status === "done").length }, { id: "blocked", name: "Blocked", icon: "🚫", count: tasks.filter(task => task.status === "blocked").length } ] } // Default to empty array for other tabs return [] } const categories = getCategories() // Combined filtering using new search function and legacy category filter const filtered = searchMemories(memories, search, searchFilters).filter(memory => { const tags = extractTags(memory) // Project filter let matchesProject = true if (currentProject !== "all") { if (currentProject === "default") { matchesProject = !memory.project || memory.project === "default" } else { matchesProject = memory.project === currentProject } } // Legacy category filter (until we fully migrate to new categories) let matchesCategory = true if (selectedCategory !== "all") { if (selectedCategory === "personal") { matchesCategory = tags.some(tag => ["personal", "me", "my", "self", "private"].includes(tag.toLowerCase())) || memory.content.toLowerCase().includes("my ") } else if (selectedCategory === "work") { matchesCategory = tags.some(tag => ["work", "business", "meeting", "client", "job"].includes(tag.toLowerCase())) || memory.content.toLowerCase().includes("work") || memory.content.toLowerCase().includes("meeting") } else if (selectedCategory === "code") { matchesCategory = tags.some(tag => ["code", "programming", "dev", "tech", "javascript", "typescript", "python", "react", "node"].includes(tag.toLowerCase())) || memory.content.includes("```") || memory.content.includes("npm ") || memory.content.includes("function") } else if (selectedCategory === "ideas") { matchesCategory = tags.some(tag => ["idea", "brainstorm", "concept", "inspiration", "plan", "roadmap", "todo"].includes(tag.toLowerCase())) || memory.content.toLowerCase().includes("idea") || memory.content.toLowerCase().includes("plan") } else if (selectedCategory === "connections") { matchesCategory = tags.length > 0 && memories.some(other => other.id !== memory.id && extractTags(other).some(tag => tags.includes(tag)) ) } else if (selectedCategory === "untagged") { matchesCategory = tags.length === 0 } } const matchesTag = tagFilter === "all" || tags.includes(tagFilter) return matchesTag && matchesCategory && matchesProject }) // Apply sorting to filtered results const sortedAndFiltered = sortMemories(filtered, sortOptions) // Extract available tags and projects for search filters const availableTags = Array.from(new Set(memories.flatMap(memory => extractTags(memory)))) const availableProjects = Array.from(new Set(memories.map(memory => memory.project).filter(project => project && project.trim() !== ""))) // Stats const total = memories.length const yesterday = new Date() yesterday.setDate(yesterday.getDate() - 1) const recent = memories.filter(memory => new Date(memory.timestamp || Date.now()) > yesterday ).length const avgSize = total > 0 ? Math.round(memories.reduce((sum, memory) => sum + memory.content.length, 0) / total) : 0 // === RENDER === return (
{/* Navigation */}