import React, { useState, useEffect, useMemo, useCallback } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Switch } from '@/components/ui/switch' import { Label } from '@/components/ui/label' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { TaskTreeView } from './TaskTreeView' import { TaskStatusButton, TaskStatusButtonGroup } from './TaskStatusButton' import { TemplateSelector } from './TemplateSelector' import { StatusIcon, getStatusIcon, getStatusColor } from './StatusIcon' import { MemoryViewModal } from './MemoryViewModal' import { Memory } from '@/types' import { Clock, Edit, FileText, Users, Eye } from 'lucide-react' import { formatDistanceToNow } from '@/utils/helpers' interface Task { id: string serial: string title: string description?: string status: 'todo' | 'in_progress' | 'done' | 'blocked' priority: 'low' | 'medium' | 'high' | 'urgent' project: string category?: string created: string updated: string completed?: string parent_task?: string subtasks?: string[] tags?: string[] memory_connections?: Array<{ memory_id: string memory_serial: string connection_type: string relevance: number matched_terms?: string[] }> } interface TaskContext { task: Task direct_memories: Array<{ id: string content: string connection: { type: string relevance: number } }> related_tasks: Task[] related_memories: Array<{ id: string content: string }> } interface TaskManagementProps { tasks: Task[] isLoading: boolean currentProject?: string onTasksChange?: () => void } export function TaskManagement({ tasks: propTasks, isLoading: propIsLoading, currentProject, onTasksChange }: TaskManagementProps) { const [tasks, setTasks] = useState([]) const [isLoading, setIsLoading] = useState(true) const [selectedTask, setSelectedTask] = useState(null) const [taskContext, setTaskContext] = useState(null) const [showCreateDialog, setShowCreateDialog] = useState(false) const [allProjects, setAllProjects] = useState([]) const [filter, setFilter] = useState<{ status?: string project?: string priority?: string }>({ project: currentProject || 'all' }) // Bulk operations state const [selectedTasks, setSelectedTasks] = useState>(new Set()) const [showBulkDialog, setShowBulkDialog] = useState(false) const [bulkAction, setBulkAction] = useState<'status' | 'priority' | 'project' | 'delete'>('status') const [bulkValue, setBulkValue] = useState('') // Archive view state const [hideCompleted, setHideCompleted] = useState(() => { const saved = localStorage.getItem('hideCompletedTasks') return saved ? saved === 'true' : false }) // Memory view modal state const [memoryViewModal, setMemoryViewModal] = useState<{ isOpen: boolean memory: Memory | null }>({ isOpen: false, memory: null }) // Handle memory save from view modal const handleSaveMemory = async (updatedMemory: Memory): Promise => { try { const response = await fetch(`/api/memories/${updatedMemory.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: updatedMemory.content, category: updatedMemory.category, priority: updatedMemory.priority, tags: updatedMemory.tags, project: updatedMemory.project }) }) if (!response.ok) { throw new Error('Failed to update memory') } // Update the memory in the modal state to reflect changes setMemoryViewModal(prev => ({ ...prev, memory: updatedMemory })) // Optionally refresh task context if needed if (selectedTask) { getTaskContext(selectedTask.id) } } catch (error) { console.error('Failed to save memory:', error) throw error } } // Create task form state const [newTask, setNewTask] = useState({ title: '', description: '', project: currentProject || '', category: 'code' as const, priority: 'medium' as const, tags: '', autoLinkMemories: true }) const [showTaskTemplateSelector, setShowTaskTemplateSelector] = useState(false) const [suggestedMemories, setSuggestedMemories] = useState([]) const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false) // Filter tasks based on current filter settings using useMemo const filteredTasks = useMemo(() => { return tasks.filter(task => { if (filter.status && task.status !== filter.status) return false if (filter.priority && task.priority !== filter.priority) return false return true }) }, [tasks, filter.status, filter.priority]) // Filter out completed tasks if hideCompleted is true const visibleTasks = useMemo(() => { if (hideCompleted) { return filteredTasks.filter(task => task.status !== 'done') } return filteredTasks }, [filteredTasks, hideCompleted]) // Get only completed tasks for archive view const archivedTasks = useMemo(() => { return filteredTasks.filter(task => task.status === 'done') }, [filteredTasks]) // Template handling const handleTaskTemplate = (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 setNewTask(prev => ({ ...prev, title: variables.title || variables.feature_name || variables.bug_title || variables.research_topic || variables.project_name || template.name, description: content, category: template.category, tags: [...template.tags].join(', ') })) // Close template selector and open create dialog setShowTaskTemplateSelector(false) setShowCreateDialog(true) } // Load tasks const loadTasks = async () => { try { setIsLoading(true) // Load all tasks by fetching all pages let allTasks = [] let page = 1 let hasMore = true while (hasMore) { // Use direct API endpoint instead of MCP bridge const params = new URLSearchParams() params.append('page', page.toString()) params.append('limit', '100') if (filter.project && filter.project !== 'all') { params.append('project', filter.project) } if (filter.status && filter.status !== 'all') { params.append('status', filter.status) } const response = await fetch(`/api/tasks?${params}`) 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) console.log('Loaded tasks:', allTasks.length, allTasks) } catch (error) { console.warn('Task system not fully integrated yet:', error) setTasks([]) } finally { setIsLoading(false) } } // Preview memory suggestions const previewMemorySuggestions = async () => { if (!newTask.title.trim() || !newTask.autoLinkMemories) { setSuggestedMemories([]) return } setIsLoadingSuggestions(true) try { const response = await fetch('/api/memories/suggest-for-task', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: newTask.title, description: newTask.description, project: newTask.project || currentProject || 'default', category: newTask.category, tags: newTask.tags.split(',').map(t => t.trim()).filter(Boolean) }) }) if (response.ok) { const suggestions = await response.json() setSuggestedMemories(suggestions.slice(0, 3)) // Show top 3 suggestions } else { setSuggestedMemories([]) } } catch (error) { console.error('Failed to get memory suggestions:', error) setSuggestedMemories([]) } finally { setIsLoadingSuggestions(false) } } // Create task const createTask = async () => { if (!newTask.title.trim()) return try { const response = await fetch('/api/mcp-tools/create_task', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: newTask.title, description: newTask.description, project: newTask.project || currentProject || 'default', category: newTask.category, priority: newTask.priority, tags: newTask.tags.split(',').map(t => t.trim()).filter(Boolean), auto_link: newTask.autoLinkMemories }) }) if (response.ok) { setNewTask({ title: '', description: '', project: currentProject || '', category: 'code', priority: 'medium', tags: '', autoLinkMemories: true }) setSuggestedMemories([]) setShowCreateDialog(false) if (onTasksChange) { await onTasksChange() } else { if (onTasksChange) { await onTasksChange() } else { await loadTasks() } } } else { console.warn('Task creation not available yet') } } catch (error) { console.warn('Task creation not available yet:', error) } } // Update task status const updateTaskStatus = async (taskId: string, status: Task['status']) => { try { const response = await fetch('/api/mcp-tools/update_task', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task_id: taskId, status }) }) if (response.ok) { if (onTasksChange) { await onTasksChange() } else { if (onTasksChange) { await onTasksChange() } else { await loadTasks() } } } } catch (error) { console.error('Failed to update task:', error) } } // Delete task const deleteTask = async (taskId: string) => { try { const response = await fetch('/api/mcp-tools/delete_task', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task_id: taskId }) }) if (response.ok) { if (onTasksChange) { await onTasksChange() } else { if (onTasksChange) { await onTasksChange() } else { await loadTasks() } } if (selectedTask?.id === taskId) { setSelectedTask(null) setTaskContext(null) } } } catch (error) { console.error('Failed to delete task:', error) } } // Bulk operations const handleTaskSelect = useCallback((taskId: string) => { setSelectedTasks(prev => { const newSet = new Set(prev) if (newSet.has(taskId)) { newSet.delete(taskId) } else { newSet.add(taskId) } return newSet }) }, []) const selectAllTasks = useCallback(() => { const visibleTaskIds = filteredTasks.map(t => t.id) setSelectedTasks(new Set(visibleTaskIds)) }, [filteredTasks]) const clearTaskSelection = useCallback(() => { setSelectedTasks(new Set()) }, []) const bulkDeleteTasks = async () => { if (confirm(`Delete ${selectedTasks.size} selected tasks?`)) { try { await Promise.all(Array.from(selectedTasks).map(id => deleteTask(id))) setSelectedTasks(new Set()) } catch (error) { console.error('Failed to bulk delete tasks:', error) } } } const bulkUpdateTasks = async () => { try { for (const taskId of selectedTasks) { if (bulkAction === 'status') { await updateTaskStatus(taskId, bulkValue as any) } else if (bulkAction === 'priority') { const response = await fetch('/api/mcp-tools/update_task', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task_id: taskId, priority: bulkValue }) }) if (!response.ok) throw new Error('Update failed') } else if (bulkAction === 'project') { const response = await fetch('/api/mcp-tools/update_task', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task_id: taskId, project: bulkValue }) }) if (!response.ok) throw new Error('Update failed') } } if (onTasksChange) { await onTasksChange() } else { await loadTasks() } setSelectedTasks(new Set()) setShowBulkDialog(false) setBulkValue('') } catch (error) { console.error('Failed to bulk update tasks:', error) } } // Get task context const getTaskContext = async (taskId: string) => { try { const response = await fetch('/api/mcp-tools/get_task_context', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task_id: taskId, depth: 'direct' }) }) if (response.ok) { const data = await response.json() setTaskContext(data.context) } } catch (error) { console.error('Failed to get task context:', error) } } const handleTaskClick = async (task: Task) => { setSelectedTask(task) await getTaskContext(task.id) } // Sync with prop tasks useEffect(() => { setTasks(propTasks || []) setIsLoading(propIsLoading) }, [propTasks, propIsLoading]) useEffect(() => { // Only load tasks if not using prop tasks if (!propTasks || propTasks.length === 0) { loadTasks() } }, [filter, currentProject]) // Auto-preview memory suggestions when task details change useEffect(() => { const timer = setTimeout(() => { previewMemorySuggestions() }, 500) // Debounce for 500ms return () => clearTimeout(timer) }, [newTask.title, newTask.description, newTask.project, newTask.category, newTask.tags, newTask.autoLinkMemories]) // Listen for global keyboard shortcuts useEffect(() => { const handleCreateNewTask = () => { setShowCreateDialog(true) } const handleSelectAllTasks = () => { selectAllTasks() } const handleClearTaskSelection = () => { clearTaskSelection() } const handleSelectTask = (event: any) => { const task = event.detail if (task) { setSelectedTask(task) getTaskContext(task.id) } } document.addEventListener('createNewTask', handleCreateNewTask) document.addEventListener('selectAllTasks', handleSelectAllTasks) document.addEventListener('clearTaskSelection', handleClearTaskSelection) document.addEventListener('selectTask', handleSelectTask) return () => { document.removeEventListener('createNewTask', handleCreateNewTask) document.removeEventListener('selectAllTasks', handleSelectAllTasks) document.removeEventListener('clearTaskSelection', handleClearTaskSelection) document.removeEventListener('selectTask', handleSelectTask) } }, [selectAllTasks, clearTaskSelection]) // Extract unique projects from tasks useEffect(() => { const projects = new Set() tasks.forEach(task => { if (task.project) { projects.add(task.project) } }) setAllProjects(Array.from(projects).sort()) }, [tasks]) // Removed getStatusIcon function - now using StatusIcon component const getPriorityClass = (priority: Task['priority']) => { switch (priority) { case 'urgent': return 'complexity-l4' // Red for urgent case 'high': return 'complexity-l3' // Amber for high case 'medium': return 'complexity-l2' // Blue for medium case 'low': return 'complexity-l1' // Emerald for low default: return 'complexity-l1' } } const getPriorityBadge = (priority: Task['priority']) => { const baseClasses = 'category-badge' switch (priority) { case 'urgent': return `${baseClasses} category-code text-xs` // Red styling case 'high': return `${baseClasses} category-research text-xs` // Amber styling case 'medium': return `${baseClasses} category-personal text-xs` // Blue styling case 'low': return `${baseClasses} category-work text-xs` // Emerald styling default: return `${baseClasses} category-personal text-xs` } } const getStatusColor = (status: Task['status']) => { switch (status) { case 'todo': return 'bg-blue-500/20 text-blue-300 border border-blue-500/30' case 'in_progress': return 'bg-purple-500/20 text-purple-300 border border-purple-500/30' case 'done': return 'bg-green-500/20 text-green-300 border border-green-500/30' case 'blocked': return 'bg-red-500/20 text-red-300 border border-red-500/30' default: return 'bg-gray-500/20 text-gray-300 border border-gray-500/30' } } const getPriorityColor = (priority: Task['priority']) => { switch (priority) { case 'urgent': return 'bg-red-500' case 'high': return 'bg-amber-500' case 'medium': return 'bg-blue-500' case 'low': return 'bg-emerald-500' default: return 'bg-gray-500' } } const formatRelativeTime = (dateString: string) => { const date = new Date(dateString) const now = new Date() const diffMs = now.getTime() - date.getTime() const diffMins = Math.floor(diffMs / 60000) const diffHours = Math.floor(diffMins / 60) const diffDays = Math.floor(diffHours / 24) if (diffMins < 60) return `${diffMins}m ago` if (diffHours < 24) return `${diffHours}h ago` if (diffDays < 7) return `${diffDays}d ago` return date.toLocaleDateString() } const getCategoryClass = (category: string) => { const baseClasses = 'category-badge' switch (category) { case 'personal': return `${baseClasses} category-personal` case 'work': return `${baseClasses} category-work` case 'code': return `${baseClasses} category-code` case 'research': return `${baseClasses} category-research` default: return `${baseClasses} category-personal` } } // Group tasks by project first, then by status within each project const tasksByProject = useMemo(() => { return visibleTasks.reduce((acc, task) => { const project = task.project || 'default' if (!acc[project]) { acc[project] = { todo: [], in_progress: [], done: [], blocked: [] } } // Ensure the status exists in our predefined statuses, default to 'todo' if not const status = task.status in acc[project] ? task.status : 'todo' acc[project][status].push(task) return acc }, {} as Record>) }, [visibleTasks]) // Legacy grouping for backward compatibility const tasksByStatus = useMemo(() => { return { todo: filteredTasks.filter(t => t.status === 'todo'), in_progress: filteredTasks.filter(t => t.status === 'in_progress'), done: filteredTasks.filter(t => t.status === 'done'), blocked: filteredTasks.filter(t => t.status === 'blocked') } }, [filteredTasks]) if (isLoading) { return (
Loading tasks...
) } return (
{/* Header */}

Task Management

{tasks.length} tasks {archivedTasks.length > 0 && hideCompleted && ( {archivedTasks.length} archived )}
{/* Hide Completed Toggle */}
{ setHideCompleted(checked) localStorage.setItem('hideCompletedTasks', checked.toString()) }} />
Create New Task Create a new task with optional memory linking and project organization.
setNewTask({ ...newTask, title: e.target.value })} placeholder="Task title..." className="bg-gray-700 border-gray-600 text-white" />