import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ArrowLeft, Bot, CheckCircle2, Database, Download, FilePenLine, FileText, ListTodo, Map as MapIcon, PlayCircle, PlusCircle, RefreshCw, Trash2, X } from 'lucide-react'; import { motion } from 'framer-motion'; import { supabase } from '../services/supabase'; import { useAuth } from '../context/useAuth'; import { getDefaultModel, getDefaultProvider } from '../services/llmConfig'; import { getApiUrl } from '../services/runtimeConfig'; import type { UiMode } from '../services/uiMode'; import EvidenceView from './EvidenceView'; interface Project { id: string; name: string; description: string | null; context: string | null; status: string; } interface Agent { id: string; name: string; role?: string | null; model: string; } interface Task { id: string; title: string; description: string | null; status: string; priority: number; assigned_agent_id: string | null; output_data: unknown | null; } interface TaskDependency { task_id: string; depends_on_task_id: string; } interface ProjectDetailProps { projectId: string; uiMode: UiMode; initialTaskId?: string | null; onBack: () => void; } const getBackendErrorDetail = async (response: Response) => { let detail = `Backend returned ${response.status}`; try { const body = await response.json(); detail = body.detail || body.message || detail; } catch { // Keep the HTTP status fallback. } return detail; }; const ensureBackendOk = async (response: Response, fallback?: string) => { if (!response.ok) { const detail = fallback ?? (await getBackendErrorDetail(response)); throw new Error(detail); } }; const hasTaskErrorOutput = (task: Task) => Boolean(task.output_data && typeof task.output_data === 'object' && 'error' in task.output_data); const ProjectDetail: React.FC = ({ projectId, uiMode, initialTaskId = null, onBack }) => { const { user } = useAuth(); const [project, setProject] = useState(null); const [tasks, setTasks] = useState([]); const [dependencies, setDependencies] = useState([]); const [dependencyTableAvailable, setDependencyTableAvailable] = useState(true); const [agents, setAgents] = useState([]); const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [agentId, setAgentId] = useState(''); const [dependencyIds, setDependencyIds] = useState([]); const [editingTaskId, setEditingTaskId] = useState(null); const [showAdvancedTaskControls, setShowAdvancedTaskControls] = useState(uiMode === 'expert'); const [saving, setSaving] = useState(false); const [orchestrating, setOrchestrating] = useState(false); const [approvingAll, setApprovingAll] = useState(false); const [error, setError] = useState(null); const [message, setMessage] = useState(null); const [filter, setFilter] = useState('all'); const [taskActionError, setTaskActionError] = useState(null); const [taskActionPending, setTaskActionPending] = useState(false); const [selectedTask, setSelectedTask] = useState(null); const [finalReport, setFinalReport] = useState(null); const [finalReportVariant, setFinalReportVariant] = useState<'full' | 'brief' | 'pessimistic'>('full'); const [showRoadmap, setShowRoadmap] = useState(false); const [reportLoading, setReportLoading] = useState(false); const [pdfLoading, setPdfLoading] = useState(false); const [activeTab, setActiveTab] = useState<'tasks' | 'evidence'>('tasks'); const [isEditingOutput, setIsEditingOutput] = useState(false); const [editedOutput, setEditedOutput] = useState(''); const defaultProvider = getDefaultProvider(); const defaultModel = getDefaultModel(defaultProvider); const defaultAgents = [ { name: 'Planner', role: 'Project Planner', api_provider: defaultProvider, model: defaultModel, system_prompt: 'You decompose goals into clear, ordered implementation tasks.' }, { name: 'Builder', role: 'Implementation Agent', api_provider: defaultProvider, model: defaultModel, system_prompt: 'You implement practical, production-oriented solutions with concise output.' }, { name: 'Reviewer', role: 'Quality Reviewer', api_provider: defaultProvider, model: defaultModel, system_prompt: 'You review outputs for correctness, security, completeness, and missing tests.' }, { name: 'Brief Writer', role: 'Executive Briefing Agent', api_provider: defaultProvider, model: defaultModel, system_prompt: 'You turn approved project work into concise executive briefs. Write plain English, no JSON, no code blocks.' }, { name: 'Pessimistic Analyst', role: 'Risk and Downside Analysis Agent', api_provider: defaultProvider, model: defaultModel, system_prompt: 'You produce skeptical downside-focused analysis. Identify weak assumptions, failure modes, risks, and mitigation priorities. Write plain English, no JSON.' } ]; const dependencyMap = useCallback( (taskId: string) => dependencies.filter((dependency) => dependency.task_id === taskId).map((dependency) => dependency.depends_on_task_id), [dependencies] ); const dependentMap = useCallback( (taskId: string) => dependencies.filter((dependency) => dependency.depends_on_task_id === taskId).map((dependency) => dependency.task_id), [dependencies] ); const loadProject = useCallback(async () => { setError(null); setMessage(null); const [ { data: projectData, error: projectError }, { data: taskData, error: taskError }, { data: agentData }, dependencyResponse ] = await Promise.all([ supabase.from('projects').select('id,name,description,context,status').eq('id', projectId).single(), supabase.from('tasks').select('id,title,description,status,priority,assigned_agent_id,output_data').eq('project_id', projectId).order('created_at', { ascending: false }), supabase.from('agents').select('id,name,role,model').order('created_at', { ascending: false }), supabase.from('task_dependencies').select('task_id,depends_on_task_id').eq('project_id', projectId) ]); if (projectError) setError(projectError.message); if (taskError) setError(taskError.message); if (dependencyResponse.error) { setDependencyTableAvailable(false); setDependencies([]); if (dependencyResponse.error.code !== '42P01') { setError(dependencyResponse.error.message); } } else { setDependencyTableAvailable(true); setDependencies(dependencyResponse.data ?? []); } setProject(projectData ?? null); setTasks(taskData ?? []); setAgents(agentData ?? []); }, [projectId]); useEffect(() => { loadProject(); }, [loadProject]); const resetTaskForm = () => { setEditingTaskId(null); setTitle(''); setDescription(''); setAgentId(''); setDependencyIds([]); }; const startEditingTask = useCallback((task: Task) => { if (project?.status === 'completed') { setError('Completed projects are locked. Tasks cannot be edited.'); return; } setEditingTaskId(task.id); setTitle(task.title); setDescription(task.description ?? ''); setAgentId(task.assigned_agent_id ?? ''); setDependencyIds(dependencyMap(task.id)); setError(null); setMessage(null); }, [dependencyMap, project?.status]); useEffect(() => { if (!initialTaskId || tasks.length === 0) return; const task = tasks.find((item) => item.id === initialTaskId); if (task) { startEditingTask(task); if (task.output_data) { setSelectedTask(task); } } }, [initialTaskId, startEditingTask, tasks]); const saveTaskDependencies = async (taskId: string, selectedDependencyIds: string[]) => { if (!dependencyTableAvailable) return null; const { error: deleteError } = await supabase .from('task_dependencies') .delete() .eq('project_id', projectId) .eq('task_id', taskId); if (deleteError) { return deleteError; } const uniqueIds = Array.from(new Set(selectedDependencyIds.filter((id) => id && id !== taskId))); if (uniqueIds.length === 0) { return null; } const { error: insertError } = await supabase.from('task_dependencies').insert( uniqueIds.map((dependsOnTaskId) => ({ project_id: projectId, task_id: taskId, depends_on_task_id: dependsOnTaskId })) ); return insertError; }; const createTask = async (event: React.FormEvent) => { event.preventDefault(); if (!canModifyProject) { setError('Completed projects are locked. Create a new project or reopen this one before adding tasks.'); return; } setSaving(true); setError(null); setMessage(null); const payload = { title, description, assigned_agent_id: agentId || null, }; const response = editingTaskId ? await supabase.from('tasks').update(payload).eq('id', editingTaskId).select('id').single() : await supabase.from('tasks').insert({ project_id: projectId, ...payload, status: 'todo', priority: 0 }).select('id').single(); if (response.error) { setError(response.error.message); } else { const savedTaskId = editingTaskId ?? response.data?.id; if (savedTaskId) { const dependencyError = await saveTaskDependencies(savedTaskId, dependencyIds); if (dependencyError) { setError(dependencyError.message); setSaving(false); return; } } resetTaskForm(); await loadProject(); setMessage(editingTaskId ? 'Task updated.' : 'Task added.'); } setSaving(false); }; const handleDeleteTask = async (task: Task) => { if (!canModifyProject) { setError('Completed projects are locked. Tasks cannot be deleted.'); return; } const confirmed = window.confirm(`Delete task "${task.title}"? This cannot be undone.`); if (!confirmed) return; setError(null); setMessage(null); const { error: deleteError } = await supabase.from('tasks').delete().eq('id', task.id); if (deleteError) { setError(deleteError.message); return; } if (editingTaskId === task.id) { resetTaskForm(); } if (selectedTask?.id === task.id) { setSelectedTask(null); } await loadProject(); setMessage('Task deleted.'); }; const assignTaskAgent = async (taskId: string, assignedAgentId: string) => { if (!canModifyProject) { setError('Completed projects are locked. Task assignments cannot be changed.'); return; } setError(null); setMessage(null); const { error: updateError } = await supabase .from('tasks') .update({ assigned_agent_id: assignedAgentId || null }) .eq('id', taskId); if (updateError) { setError(updateError.message); return; } if (editingTaskId === taskId) { setAgentId(assignedAgentId); } await loadProject(); setMessage('Task assignment updated.'); }; const createDefaultAgents = async () => { if (!canModifyProject) { setError('Completed projects are locked. Agents cannot be generated from this project.'); return; } if (!user) { setError('You must be signed in to create default agents.'); return; } setError(null); setMessage(null); const existingNames = new Set(agents.map((agent) => agent.name)); const missingAgents = defaultAgents .filter((agent) => !existingNames.has(agent.name)) .map((agent) => ({ ...agent, user_id: user.id })); if (missingAgents.length === 0) { setMessage('Default agents already exist.'); return; } const { error: insertError } = await supabase.from('agents').insert(missingAgents); if (insertError) { setError(insertError.message); return; } setMessage(`Created ${missingAgents.length} default agents.`); await loadProject(); }; const runOrchestrator = async () => { if (!canModifyProject) { setError('Completed projects are locked. The orchestrator cannot add or rerun tasks.'); return; } setOrchestrating(true); setError(null); setMessage(null); try { const errorOutputTaskIds = tasks .filter((task) => hasTaskErrorOutput(task)) .map((task) => task.id); if (errorOutputTaskIds.length > 0) { const { error: resetError } = await supabase .from('tasks') .update({ status: 'todo', output_data: null }) .in('id', errorOutputTaskIds); if (resetError) throw resetError; } const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/orchestrator/projects/${projectId}/run`, { method: 'POST' }); await ensureBackendOk( response, `Backend returned ${response.status} for POST /orchestrator/projects/${projectId}/run. Stop the stale process on port 8000 and restart backend from D:\\sistemas\\Aubm\\backend.` ); const body = await response.json().catch(() => null); setMessage(body?.mode === 'queue' ? 'Project tasks queued for worker execution.' : 'Project orchestrator started for queued and failed tasks.'); // Refresh after a delay to show the new tasks window.setTimeout(loadProject, 2000); } catch (exc) { setError(exc instanceof Error ? exc.message : 'Failed to start orchestrator.'); } finally { // We keep orchestrating=true for a bit longer to allow the backend to finish decomposition window.setTimeout(() => setOrchestrating(false), 2000); } }; const retryTask = async (task: Task) => { if (!canModifyProject) { setTaskActionError('Completed projects are locked. This task cannot be retried.'); return; } setTaskActionPending(true); setTaskActionError(null); setError(null); setMessage(null); try { const { error: resetError } = await supabase .from('tasks') .update({ status: 'todo', output_data: null }) .eq('id', task.id); if (resetError) throw resetError; setSelectedTask(null); await loadProject(); await runOrchestrator(); setMessage('Task reset and queued for retry.'); } catch (exc) { setTaskActionError(`Could not retry task: ${exc instanceof Error ? exc.message : 'Unknown error'}`); } finally { setTaskActionPending(false); } }; const handleApproveAll = async () => { if (!projectId) return; if (!canModifyProject) { setError('Completed projects are locked. Pending approvals cannot be changed.'); return; } setApprovingAll(true); setError(null); setMessage(null); try { const response = await fetch(`${getApiUrl()}/tasks/project/${projectId}/approve-all`, { method: 'POST' }); if (response.ok) { setMessage('All pending tasks approved!'); loadProject(); } else { setError('Failed to approve all tasks.'); } } catch { setError('Error connecting to backend.'); } setApprovingAll(false); }; const allTasksApproved = tasks.length > 0 && tasks.every((task) => task.status === 'done'); const taskLookup = new Map(tasks.map((task) => [task.id, task])); const tasksAwaitingApproval = tasks.filter((task) => task.status === 'awaiting_approval').length; const completedTasks = tasks.filter((task) => task.status === 'done').length; const retryableTasks = tasks.filter((task) => task.status === 'failed' || hasTaskErrorOutput(task)).length; const isProjectCompleted = project?.status === 'completed'; const canModifyProject = !isProjectCompleted; const roadmapPhases = useMemo(() => { const orderedPhases = [ 'Foundation', 'Build', 'Execution', 'Review', 'Recovery', 'Finalize', 'Completed' ]; const phaseMap = new Map(); const dependencyCounts = dependencies.reduce>((acc, dependency) => { acc[dependency.task_id] = (acc[dependency.task_id] ?? 0) + 1; return acc; }, {}); const blockerCounts = dependencies.reduce>((acc, dependency) => { acc[dependency.depends_on_task_id] = (acc[dependency.depends_on_task_id] ?? 0) + 1; return acc; }, {}); for (const task of tasks) { let phase = 'Build'; if (task.status === 'done') phase = 'Completed'; else if (task.status === 'awaiting_approval') phase = 'Review'; else if (task.status === 'queued' || task.status === 'in_progress') phase = 'Execution'; else if (task.status === 'failed') phase = 'Recovery'; else if ((dependencyCounts[task.id] ?? 0) === 0) phase = 'Foundation'; else if ((blockerCounts[task.id] ?? 0) === 0) phase = 'Finalize'; phaseMap.set(phase, [...(phaseMap.get(phase) ?? []), task]); } return orderedPhases .map((phase) => ({ phase, tasks: (phaseMap.get(phase) ?? []).sort((a, b) => b.priority - a.priority || a.title.localeCompare(b.title)) })) .filter((item) => item.tasks.length > 0); }, [dependencies, tasks]); const humanizeKey = (key: string) => key.replace(/[_-]/g, ' ').trim().replace(/\b\w/g, (char) => char.toUpperCase()); const formatHumanReadable = (value: unknown): string[] => { if (value === null || value === undefined) return ['Not specified.']; if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return [String(value)]; } if (Array.isArray(value)) { if (value.length === 0) return ['No items.']; return value.flatMap((item) => { if (item && typeof item === 'object') { const lines = formatHumanReadable(item); return lines.length ? [`- ${lines[0]}`, ...lines.slice(1).map((line) => ` ${line}`)] : []; } return [`- ${String(item)}`]; }); } if (typeof value === 'object') { return Object.entries(value as Record).flatMap(([key, item]) => { const label = humanizeKey(key); if (item && typeof item === 'object') { return [`${label}:`, ...formatHumanReadable(item).map((line) => ` ${line}`)]; } return [`${label}: ${item ?? 'Not specified.'}`]; }); } return [String(value)]; }; const formatTaskOutput = (output: unknown) => { if (!output) return 'No output was saved for this task.'; if (typeof output === 'string') return output; if (typeof output === 'object') { const outputRecord = output as Record; // Handle unified debate structure or standard agent result const primaryOutput = outputRecord.data ?? outputRecord.raw_output ?? outputRecord.final ?? output; if (outputRecord.is_debate && outputRecord.debate_history) { // We could also show a "Debate Consensus" prefix here return typeof primaryOutput === 'string' ? primaryOutput : formatHumanReadable(primaryOutput).join('\n'); } return typeof primaryOutput === 'string' ? primaryOutput : formatHumanReadable(primaryOutput).join('\n'); } return String(output); }; const updateTaskReviewStatus = async (taskId: string, action: 'approve' | 'reject') => { const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/tasks/${taskId}/${action}`, { method: 'POST' }); await ensureBackendOk(response); }; const approveTask = async (taskId: string) => { if (!canModifyProject) { setTaskActionError('Completed projects are locked. Task approval cannot be changed.'); return; } setTaskActionPending(true); setTaskActionError(null); setError(null); setMessage(null); try { await updateTaskReviewStatus(taskId, 'approve'); setSelectedTask(null); await loadProject(); setMessage('Task approved!'); } catch (exc) { setTaskActionError(`Could not approve task: ${exc instanceof Error ? exc.message : 'Unknown error'}`); } finally { setTaskActionPending(false); } }; const rejectTask = async (taskId: string) => { if (!canModifyProject) { setTaskActionError('Completed projects are locked. Task approval cannot be changed.'); return; } setTaskActionPending(true); setTaskActionError(null); setError(null); setMessage(null); try { await updateTaskReviewStatus(taskId, 'reject'); setSelectedTask(null); await loadProject(); setMessage('Task rejected. Agent will try again.'); } catch (exc) { setTaskActionError(`Could not reject task: ${exc instanceof Error ? exc.message : 'Unknown error'}`); } finally { setTaskActionPending(false); } }; const openFinalReport = async (variant: 'full' | 'brief' | 'pessimistic' = 'full') => { setReportLoading(true); setError(null); setMessage(null); try { const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/orchestrator/projects/${projectId}/final-report?variant=${variant}`); await ensureBackendOk(response); const body = await response.json(); setFinalReport(body.report); setFinalReportVariant(variant); await loadProject(); } catch (exc) { setError(exc instanceof Error ? exc.message : 'Failed to build final report.'); } finally { setReportLoading(false); } }; const saveEditedOutput = async () => { if (!selectedTask || !canModifyProject) return; setTaskActionPending(true); setTaskActionError(null); try { const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/tasks/${selectedTask.id}/output`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ output_data: editedOutput }) }); await ensureBackendOk(response); const updatedTask = { ...selectedTask, output_data: editedOutput }; setSelectedTask(updatedTask); setTasks(prev => prev.map(t => t.id === updatedTask.id ? updatedTask : t)); setIsEditingOutput(false); setMessage('Task output updated manually.'); } catch (exc) { setTaskActionError(exc instanceof Error ? exc.message : 'Failed to update output.'); } finally { setTaskActionPending(false); } }; const downloadFinalReportPdf = async () => { setPdfLoading(true); setError(null); try { const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/orchestrator/projects/${projectId}/final-report.pdf?variant=${finalReportVariant}`); await ensureBackendOk(response); const blob = await response.blob(); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${project?.name ?? 'project'}-${finalReportVariant}.pdf`.replace(/[^a-z0-9_.-]+/gi, '_'); document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); } catch (exc) { setError(exc instanceof Error ? exc.message : 'Failed to export PDF.'); } finally { setPdfLoading(false); } }; return (

{project?.name ?? 'Project'}

{project?.description || 'No description provided.'}

{tasks.length > 0 && ( )}
{/* Separator */}
{allTasksApproved && ( <> )} {canModifyProject && tasks.some(t => t.status === 'awaiting_approval') && ( )} {canModifyProject && ( )}
{error &&
{error}
} {message &&
{message}
} {isProjectCompleted && (
This project is completed and locked. Reports remain available, but tasks, agents, approvals, retries, and assignments are read-only.
)} {uiMode === 'guided' && (

Guided Workflow

1. Prepare agents

{agents.length > 0 ? `${agents.length} agents available.` : 'Create the default agents for this workspace.'}

2. Build the plan

{retryableTasks > 0 ? `${retryableTasks} failed tasks can be retried.` : tasks.length > 0 ? `${tasks.length} tasks in the current plan.` : 'Run the orchestrator to generate the task plan from the project context.'}

3. Review outputs

{tasksAwaitingApproval > 0 ? `${tasksAwaitingApproval} tasks are waiting for approval.` : 'No tasks are waiting for approval right now.'}

{canModifyProject && tasksAwaitingApproval > 0 && ( )}
4. Finalize

{allTasksApproved ? 'The project is ready for final reporting.' : `${completedTasks}/${tasks.length} tasks approved.`}

)}
{activeTab === 'tasks' ? ( <>
{(uiMode === 'expert' || showAdvancedTaskControls) && (

Default Agents

Create Planner, Builder, and Reviewer agents for this workspace.

)}

{editingTaskId ? 'Edit Task' : uiMode === 'guided' ? 'Add Manual Task' : 'Add Task'}

{editingTaskId && canModifyProject && ( )}
{!canModifyProject && (
This project is complete. Adding more tasks would change the approved scope, so task planning is disabled.
)}