"use client" import { useState, useEffect, useCallback } from "react" import { DndContext, DragOverlay, closestCorners, KeyboardSensor, PointerSensor, useSensor, useSensors, DragStartEvent, DragOverEvent, DragEndEvent, defaultDropAnimationSideEffects, DropAnimation } from "@dnd-kit/core" import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, } from "@dnd-kit/sortable" import { TaskCard } from "./task-card" import { TaskDialog } from "./task-dialog" import { Button } from "@/components/ui/button" import { Plus, Filter, Search, Loader2 } from "lucide-react" import { Input } from "@/components/ui/input" import { DropdownMenu, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { useBridgeRequest } from "@/hooks/use-bridge" type TaskStatus = "backlog" | "ready" | "in-progress" | "review" | "done" interface Task { id: string project_id: string | null title: string description: string status: TaskStatus priority: string assigned_agent: string | null progress: number tags: string[] created_at: string updated_at: string } const COLUMNS: TaskStatus[] = ["backlog", "ready", "in-progress", "review", "done"] const STATUS_LABELS: Record = { "backlog": "Backlog", "ready": "Ready", "in-progress": "In Progress", "review": "Review", "done": "Done", } export function KanbanBoard() { const { request } = useBridgeRequest() const [tasks, setTasks] = useState([]) const [activeId, setActiveId] = useState(null) const [isDialogOpen, setIsDialogOpen] = useState(false) const [editingTask, setEditingTask] = useState(null) const [searchQuery, setSearchQuery] = useState("") const [filterPriority, setFilterPriority] = useState>(new Set()) const [filterAgent, setFilterAgent] = useState>(new Set()) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) // Load tasks from API const loadTasks = useCallback(async () => { setLoading(true) try { const data = await request("/api/tiger/tasks") as { ok: boolean; tasks?: Task[] } if (data.ok && data.tasks) { setTasks(data.tasks.map((t: Task) => ({ ...t, tags: typeof t.tags === "string" ? JSON.parse(t.tags || "[]") : t.tags || [], }))) } } catch (e) { console.error("Failed to load tasks:", e) } finally { setLoading(false) } }, [request]) useEffect(() => { loadTasks() }, [loadTasks]) // Update task status when dragged const updateTaskStatus = useCallback(async (taskId: string, newStatus: TaskStatus) => { setSaving(true) try { await request(`/api/tiger/tasks/${taskId}`, "POST", { status: newStatus }) setTasks(prev => prev.map(t => t.id === taskId ? { ...t, status: newStatus, updated_at: new Date().toISOString() } : t )) } catch (e) { console.error("Failed to update task:", e) } finally { setSaving(false) } }, [request]) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ) const handleDragStart = useCallback((event: DragStartEvent) => { setActiveId(event.active.id as string) }, []) const handleDragOver = useCallback((event: DragOverEvent) => { const { active, over } = event if (!over) return const activeId = active.id as string const overId = over.id as string const activeTask = tasks.find(t => t.id === activeId) if (!activeTask) return // If dragging over a column if (COLUMNS.includes(overId as TaskStatus)) { if (activeTask.status !== overId) { setTasks(prev => prev.map(t => t.id === activeId ? { ...t, status: overId as TaskStatus } : t )) } return } // If dragging over another task const overTask = tasks.find(t => t.id === overId) if (overTask && activeTask.status !== overTask.status) { setTasks(prev => prev.map(t => t.id === activeId ? { ...t, status: overTask.status } : t )) } }, [tasks]) const handleDragEnd = useCallback((event: DragEndEvent) => { const { active, over } = event setActiveId(null) if (!over) return const activeId = active.id as string const overId = over.id as string if (activeId === overId) return // Find the task and its new status const activeTask = tasks.find(t => t.id === activeId) if (!activeTask) return let newStatus: TaskStatus = activeTask.status if (COLUMNS.includes(overId as TaskStatus)) { newStatus = overId as TaskStatus } else { const overTask = tasks.find(t => t.id === overId) if (overTask) { newStatus = overTask.status } } // Persist to API if (newStatus !== activeTask.status) { updateTaskStatus(activeId, newStatus) } // Reorder within column setTasks(prev => { const oldIndex = prev.findIndex(t => t.id === activeId) const newIndex = prev.findIndex(t => t.id === overId) if (oldIndex !== -1 && newIndex !== -1) { return arrayMove(prev, oldIndex, newIndex) } return prev }) }, [tasks, updateTaskStatus]) const handleAddTask = useCallback(async (taskData: Partial) => { try { const result = await request("/api/tiger/tasks", "POST", { title: taskData.title, description: taskData.description, priority: taskData.priority || "medium", status: taskData.status || "backlog", assigned_agent: taskData.assigned_agent, }) as { ok: boolean; task?: Task } if (result.ok && result.task) { setTasks(prev => [result.task!, ...prev]) } setIsDialogOpen(false) } catch (e) { console.error("Failed to create task:", e) } }, [request]) const handleUpdateTask = useCallback(async (taskData: Partial) => { if (!editingTask) return try { const result = await request(`/api/tiger/tasks/${editingTask.id}`, "POST", taskData) as { ok: boolean; task?: Task } if (result.ok && result.task) { setTasks(prev => prev.map(t => t.id === editingTask.id ? { ...result.task!, tags: typeof result.task!.tags === "string" ? JSON.parse(result.task!.tags || "[]") : result.task!.tags || [] } : t )) } setIsDialogOpen(false) setEditingTask(null) } catch (e) { console.error("Failed to update task:", e) } }, [editingTask, request]) const handleDeleteTask = useCallback(async (taskId: string) => { try { await request(`/api/tiger/tasks/${taskId}`, "POST", { _method: "DELETE" }) setTasks(prev => prev.filter(t => t.id !== taskId)) setIsDialogOpen(false) setEditingTask(null) } catch (e) { console.error("Failed to delete task:", e) } }, [request]) const handleEditTask = useCallback((task: Task) => { setEditingTask(task) setIsDialogOpen(true) }, []) const handleRunTask = useCallback(async (taskId: string) => { try { const result = await request(`/api/tiger/dispatch`, "POST", { taskId }) as { ok: boolean; message?: string } if (result.ok) { console.log("Task dispatched:", result.message) } } catch (e) { console.error("Failed to run task:", e) } }, [request]) // Filter tasks const filteredTasks = tasks.filter(task => { if (searchQuery && !task.title.toLowerCase().includes(searchQuery.toLowerCase()) && !task.description.toLowerCase().includes(searchQuery.toLowerCase())) { return false } if (filterPriority.size > 0 && !filterPriority.has(task.priority)) { return false } if (filterAgent.size > 0 && (!task.assigned_agent || !filterAgent.has(task.assigned_agent))) { return false } return true }) const activeTask = activeId ? tasks.find(t => t.id === activeId) : null const dropAnimation: DropAnimation = { sideEffects: defaultDropAnimationSideEffects({ styles: { active: { opacity: "0.5", }, }, }), } if (loading) { return (
) } return (
{/* Toolbar */}
setSearchQuery(e.target.value)} className="pl-9" />
{["low", "medium", "high", "urgent"].map(priority => ( { const newSet = new Set(filterPriority) if (checked) newSet.add(priority) else newSet.delete(priority) setFilterPriority(newSet) }} > {priority.charAt(0).toUpperCase() + priority.slice(1)} ))} {["manual", "coder", "researcher", "writer", "pm"].map(agent => ( { const newSet = new Set(filterAgent) if (checked) newSet.add(agent) else newSet.delete(agent) setFilterAgent(newSet) }} > {agent.charAt(0).toUpperCase() + agent.slice(1)} ))}
{/* Saving indicator */} {saving && (
Saving changes...
)} {/* Kanban Board */}
{COLUMNS.map(status => (
{STATUS_LABELS[status]} ({filteredTasks.filter(t => t.status === status).length})
t.status === status).map(t => t.id)} strategy={verticalListSortingStrategy}>
{filteredTasks.filter(t => t.status === status).map(task => ( handleEditTask(task)} /> ))}
))}
{activeTask ? ( {}} /> ) : null}
{/* Task Dialog */} handleDeleteTask(editingTask.id) : undefined} onRun={editingTask ? () => handleRunTask(editingTask.id) : undefined} />
) }