cesjavi commited on
Commit
5c2b07c
·
1 Parent(s): f0a341a

UI: Improved button hierarchy and highlighted Final Report

Browse files
frontend/src/components/ProjectDetail.tsx CHANGED
@@ -1,5 +1,5 @@
1
- import React, { useCallback, useEffect, useState } from 'react';
2
- import { ArrowLeft, Bot, CheckCircle2, Download, FilePenLine, FileText, ListTodo, PlayCircle, PlusCircle, RefreshCw, Trash2, X } from 'lucide-react';
3
  import { motion } from 'framer-motion';
4
  import { supabase } from '../services/supabase';
5
  import { useAuth } from '../context/useAuth';
@@ -37,18 +37,6 @@ interface TaskDependency {
37
  depends_on_task_id: string;
38
  }
39
 
40
- interface ChartDatum {
41
- label: string;
42
- value: number;
43
- }
44
-
45
- interface ReportCharts {
46
- status: ChartDatum[];
47
- priorities: ChartDatum[];
48
- categories: ChartDatum[];
49
- scores: ChartDatum[];
50
- }
51
-
52
  interface ProjectDetailProps {
53
  projectId: string;
54
  uiMode: UiMode;
@@ -74,6 +62,9 @@ const ensureBackendOk = async (response: Response, fallback?: string) => {
74
  }
75
  };
76
 
 
 
 
77
  const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initialTaskId = null, onBack }) => {
78
  const { user } = useAuth();
79
  const [project, setProject] = useState<Project | null>(null);
@@ -95,9 +86,10 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
95
  const [filter, setFilter] = useState<string>('all');
96
  const [taskActionError, setTaskActionError] = useState<string | null>(null);
97
  const [taskActionPending, setTaskActionPending] = useState(false);
 
98
  const [finalReport, setFinalReport] = useState<string | null>(null);
99
  const [finalReportVariant, setFinalReportVariant] = useState<'full' | 'brief' | 'pessimistic'>('full');
100
- const [reportCharts, setReportCharts] = useState<ReportCharts | null>(null);
101
  const [reportLoading, setReportLoading] = useState(false);
102
  const [pdfLoading, setPdfLoading] = useState(false);
103
  const defaultProvider = getDefaultProvider();
@@ -197,7 +189,11 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
197
  setDependencyIds([]);
198
  };
199
 
200
- const startEditingTask = (task: Task) => {
 
 
 
 
201
  setEditingTaskId(task.id);
202
  setTitle(task.title);
203
  setDescription(task.description ?? '');
@@ -205,7 +201,7 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
205
  setDependencyIds(dependencyMap(task.id));
206
  setError(null);
207
  setMessage(null);
208
- };
209
 
210
  useEffect(() => {
211
  if (!initialTaskId || tasks.length === 0) return;
@@ -216,7 +212,7 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
216
  setSelectedTask(task);
217
  }
218
  }
219
- }, [initialTaskId, tasks]);
220
 
221
  const saveTaskDependencies = async (taskId: string, selectedDependencyIds: string[]) => {
222
  if (!dependencyTableAvailable) return null;
@@ -249,6 +245,10 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
249
 
250
  const createTask = async (event: React.FormEvent) => {
251
  event.preventDefault();
 
 
 
 
252
  setSaving(true);
253
  setError(null);
254
  setMessage(null);
@@ -290,6 +290,10 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
290
  };
291
 
292
  const handleDeleteTask = async (task: Task) => {
 
 
 
 
293
  const confirmed = window.confirm(`Delete task "${task.title}"? This cannot be undone.`);
294
  if (!confirmed) return;
295
 
@@ -314,6 +318,10 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
314
  };
315
 
316
  const assignTaskAgent = async (taskId: string, assignedAgentId: string) => {
 
 
 
 
317
  setError(null);
318
  setMessage(null);
319
 
@@ -336,6 +344,10 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
336
  };
337
 
338
  const createDefaultAgents = async () => {
 
 
 
 
339
  if (!user) {
340
  setError('You must be signed in to create default agents.');
341
  return;
@@ -365,31 +377,28 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
365
  };
366
 
367
  const runOrchestrator = async () => {
368
- if (tasks.length > 0) {
369
- const confirmReset = window.confirm(
370
- "This project already has tasks. Re-orchestrating will delete all existing tasks and progress to generate a fresh plan. Do you want to continue?"
371
- );
372
- if (!confirmReset) return;
373
-
374
- // Clear existing tasks for a fresh start
375
- setOrchestrating(true);
376
- setError(null);
377
- setMessage(null);
378
- try {
379
- const { error: deleteError } = await supabase.from('tasks').delete().eq('project_id', projectId);
380
- if (deleteError) throw deleteError;
381
- } catch (err: any) {
382
- setError(`Failed to clear existing tasks: ${err.message}`);
383
- setOrchestrating(false);
384
- return;
385
- }
386
  }
387
-
388
  setOrchestrating(true);
389
  setError(null);
390
  setMessage(null);
391
 
392
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  const apiUrl = getApiUrl();
394
  const response = await fetch(`${apiUrl}/orchestrator/projects/${projectId}/run`, {
395
  method: 'POST'
@@ -399,7 +408,8 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
399
  response,
400
  `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.`
401
  );
402
- setMessage('Project orchestrator started.');
 
403
  // Refresh after a delay to show the new tasks
404
  window.setTimeout(loadProject, 2000);
405
  } catch (exc) {
@@ -409,8 +419,42 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
409
  window.setTimeout(() => setOrchestrating(false), 2000);
410
  }
411
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  const handleApproveAll = async () => {
413
  if (!projectId) return;
 
 
 
 
414
  setApprovingAll(true);
415
  setError(null);
416
  setMessage(null);
@@ -429,12 +473,52 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
429
  }
430
  setApprovingAll(false);
431
  };
432
-
433
- const [selectedTask, setSelectedTask] = useState<Task | null>(null);
434
  const allTasksApproved = tasks.length > 0 && tasks.every((task) => task.status === 'done');
435
  const taskLookup = new Map(tasks.map((task) => [task.id, task]));
436
  const tasksAwaitingApproval = tasks.filter((task) => task.status === 'awaiting_approval').length;
437
  const completedTasks = tasks.filter((task) => task.status === 'done').length;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
 
439
  const humanizeKey = (key: string) => key.replace(/[_-]/g, ' ').trim().replace(/\b\w/g, (char) => char.toUpperCase());
440
 
@@ -502,6 +586,10 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
502
  };
503
 
504
  const approveTask = async (taskId: string) => {
 
 
 
 
505
  setTaskActionPending(true);
506
  setTaskActionError(null);
507
  setError(null);
@@ -520,6 +608,10 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
520
  };
521
 
522
  const rejectTask = async (taskId: string) => {
 
 
 
 
523
  setTaskActionPending(true);
524
  setTaskActionError(null);
525
  setError(null);
@@ -550,7 +642,6 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
550
 
551
  const body = await response.json();
552
  setFinalReport(body.report);
553
- setReportCharts(body.charts ?? null);
554
  setFinalReportVariant(variant);
555
  await loadProject();
556
  } catch (exc) {
@@ -560,41 +651,6 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
560
  }
561
  };
562
 
563
- const renderBarChart = (title: string, data: ChartDatum[]) => {
564
- const maxValue = Math.max(...data.map((item) => item.value), 1);
565
- return (
566
- <div className="report-chart">
567
- <h4>{title}</h4>
568
- {data.map((item) => (
569
- <div key={item.label} className="report-chart-row">
570
- <span>{item.label}</span>
571
- <div className="report-chart-track">
572
- <div className="report-chart-fill" style={{ width: `${(item.value / maxValue) * 100}%` }} />
573
- </div>
574
- <strong>{item.value}</strong>
575
- </div>
576
- ))}
577
- </div>
578
- );
579
- };
580
-
581
- const renderScoreChart = (data: ChartDatum[]) => (
582
- <div className="report-chart report-score-chart">
583
- <h4>Scores</h4>
584
- <div className="report-score-grid">
585
- {data.map((item) => (
586
- <div key={item.label} className="report-score">
587
- <span>{item.label}</span>
588
- <strong>{item.value}</strong>
589
- <div className="report-chart-track">
590
- <div className="report-chart-fill" style={{ width: `${Math.min(item.value, 100)}%` }} />
591
- </div>
592
- </div>
593
- ))}
594
- </div>
595
- </div>
596
- );
597
-
598
  const downloadFinalReportPdf = async () => {
599
  setPdfLoading(true);
600
  setError(null);
@@ -632,44 +688,76 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
632
  <h2>{project?.name ?? 'Project'}</h2>
633
  <p style={{ color: 'var(--text-dim)' }}>{project?.description || 'No description provided.'}</p>
634
  </div>
635
- <div className="button-row">
636
- {allTasksApproved && (
637
- <button className="btn btn-primary" onClick={() => openFinalReport('full')} disabled={reportLoading}>
638
- <FileText size={18} />
639
- {reportLoading ? 'Building...' : 'Final Report'}
640
  </button>
641
- )}
642
- {allTasksApproved && (
643
- <button className="btn btn-glass" onClick={() => openFinalReport('brief')} disabled={reportLoading}>
644
- <FileText size={18} />
645
- Short Brief
646
- </button>
647
- )}
648
- {allTasksApproved && (
649
- <button className="btn btn-glass" onClick={() => openFinalReport('pessimistic')} disabled={reportLoading}>
650
- <FileText size={18} />
651
- Pessimistic Analysis
652
- </button>
653
- )}
654
- {tasks.some(t => t.status === 'awaiting_approval') && (
655
- <button className="btn btn-glass" onClick={handleApproveAll} disabled={approvingAll} style={{ borderColor: 'var(--success)', color: 'var(--success)' }}>
656
- <CheckCircle2 size={18} />
657
- {approvingAll ? 'Approving...' : 'Approve All'}
658
- </button>
659
- )}
660
- <button className="btn btn-primary" onClick={runOrchestrator} disabled={orchestrating}>
661
- <PlayCircle size={18} />
662
- {orchestrating ? 'Starting...' : 'Run Orchestrator'}
663
- </button>
664
- <button className="btn btn-glass" onClick={loadProject}>
665
- <RefreshCw size={18} />
666
- Refresh
667
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
668
  </div>
 
669
  </div>
670
 
671
  {error && <div className="inline-status">{error}</div>}
672
  {message && <div className="inline-status"><CheckCircle2 size={16} color="var(--success)" />{message}</div>}
 
 
 
 
 
 
673
 
674
  {uiMode === 'guided' && (
675
  <section className="glass-panel project-form">
@@ -683,17 +771,17 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
683
  <strong>1. Prepare agents</strong>
684
  <p>{agents.length > 0 ? `${agents.length} agents available.` : 'Create the default agents for this workspace.'}</p>
685
  </div>
686
- <button className="btn btn-glass btn-sm" type="button" onClick={createDefaultAgents}>
687
  Generate Defaults
688
  </button>
689
  </div>
690
  <div className="task-row">
691
  <div>
692
  <strong>2. Build the plan</strong>
693
- <p>{tasks.length > 0 ? `${tasks.length} tasks in the current plan.` : 'Run the orchestrator to generate the task plan from the project context.'}</p>
694
  </div>
695
- <button className="btn btn-primary btn-sm" type="button" onClick={runOrchestrator} disabled={orchestrating}>
696
- {orchestrating ? 'Starting...' : 'Generate Plan'}
697
  </button>
698
  </div>
699
  <div className="task-row">
@@ -701,7 +789,7 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
701
  <strong>3. Review outputs</strong>
702
  <p>{tasksAwaitingApproval > 0 ? `${tasksAwaitingApproval} tasks are waiting for approval.` : 'No tasks are waiting for approval right now.'}</p>
703
  </div>
704
- {tasksAwaitingApproval > 0 && (
705
  <button className="btn btn-glass btn-sm" type="button" onClick={handleApproveAll} disabled={approvingAll}>
706
  {approvingAll ? 'Approving...' : 'Approve Pending'}
707
  </button>
@@ -736,7 +824,7 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
736
  Create Planner, Builder, and Reviewer agents for this workspace.
737
  </p>
738
  </div>
739
- <button className="btn btn-glass" onClick={createDefaultAgents}>
740
  <PlusCircle size={18} />
741
  Generate Defaults
742
  </button>
@@ -748,25 +836,30 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
748
  <PlusCircle size={22} color="var(--accent)" />
749
  <h3>{editingTaskId ? 'Edit Task' : uiMode === 'guided' ? 'Add Manual Task' : 'Add Task'}</h3>
750
  </div>
751
- {editingTaskId && (
752
  <button className="btn btn-icon" type="button" onClick={resetTaskForm} title="Cancel edit">
753
  <X size={18} />
754
  </button>
755
  )}
756
  </div>
 
 
 
 
 
757
  <form onSubmit={createTask} style={{ display: 'grid', gap: 'var(--space-md)' }}>
758
  <label>
759
  <span>Task Title</span>
760
- <input value={title} onChange={(event) => setTitle(event.target.value)} required placeholder="Draft implementation plan" />
761
  </label>
762
  <label>
763
  <span>Description</span>
764
- <textarea value={description} onChange={(event) => setDescription(event.target.value)} rows={4} placeholder="Instructions for the assigned agent..." />
765
  </label>
766
  {(uiMode === 'expert' || showAdvancedTaskControls) && (
767
  <label>
768
  <span>Assigned Agent</span>
769
- <select value={agentId} onChange={(event) => setAgentId(event.target.value)}>
770
  <option value="">Unassigned</option>
771
  {agents.map((agent) => (
772
  <option key={agent.id} value={agent.id}>{agent.name} ({agent.model})</option>
@@ -798,6 +891,7 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
798
  <input
799
  type="checkbox"
800
  checked={dependencyIds.includes(task.id)}
 
801
  onChange={(event) => {
802
  setDependencyIds((current) =>
803
  event.target.checked
@@ -815,7 +909,7 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
815
  </div>
816
  </div>
817
  )}
818
- <button className="btn btn-primary" type="submit" disabled={saving}>
819
  <CheckCircle2 size={18} />
820
  {saving ? (editingTaskId ? 'Saving...' : 'Adding...') : (editingTaskId ? 'Save Task' : 'Add Task')}
821
  </button>
@@ -828,7 +922,7 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
828
  <h3>Tasks</h3>
829
  </div>
830
  <div className="filter-bar" style={{ display: 'flex', gap: '8px', marginBottom: '16px', overflowX: 'auto', paddingBottom: '4px' }}>
831
- {['all', 'todo', 'in_progress', 'awaiting_approval', 'done', 'failed'].map((f) => (
832
  <button
833
  key={f}
834
  className={`btn ${filter === f ? 'btn-primary' : 'btn-glass'}`}
@@ -875,6 +969,7 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
875
  {(uiMode === 'expert' || showAdvancedTaskControls) && (
876
  <select
877
  value={task.assigned_agent_id ?? ''}
 
878
  onClick={(e) => e.stopPropagation()}
879
  onChange={(e) => {
880
  e.stopPropagation();
@@ -888,29 +983,33 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
888
  ))}
889
  </select>
890
  )}
891
- <button
892
- className="btn btn-glass btn-sm"
893
- type="button"
894
- onClick={(e) => {
895
- e.stopPropagation();
896
- startEditingTask(task);
897
- }}
898
- >
899
- <FilePenLine size={14} />
900
- Edit
901
- </button>
902
- <button
903
- className="btn btn-glass btn-sm"
904
- type="button"
905
- onClick={(e) => {
906
- e.stopPropagation();
907
- handleDeleteTask(task);
908
- }}
909
- style={{ color: 'var(--danger)', borderColor: 'rgba(231, 76, 60, 0.25)' }}
910
- >
911
- <Trash2 size={14} />
912
- Delete
913
- </button>
 
 
 
 
914
  </div>
915
  {(uiMode === 'expert' || showAdvancedTaskControls) && dependencyMap(task.id).length > 0 && (
916
  <div style={{ marginTop: 'var(--space-sm)', display: 'grid', gap: '4px' }}>
@@ -954,7 +1053,20 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
954
  </div>
955
  {taskActionError && <div className="inline-status modal-error">{taskActionError}</div>}
956
  <div className="button-row modal-actions">
957
- {selectedTask.status === 'awaiting_approval' ? (
 
 
 
 
 
 
 
 
 
 
 
 
 
958
  <>
959
  <button className="btn btn-primary" onClick={() => approveTask(selectedTask.id)} disabled={taskActionPending}>
960
  {taskActionPending ? 'Saving...' : 'Approve Task'}
@@ -976,17 +1088,65 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
976
  </div>
977
  )}
978
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
979
  {finalReport && (
980
  <div className="modal-overlay" onClick={() => setFinalReport(null)}>
981
  <div className="glass-panel modal-content task-review-modal" onClick={(e) => e.stopPropagation()}>
982
  <h3>Final Report</h3>
983
- {reportCharts && (
984
- <div className="report-charts">
985
- {renderScoreChart(reportCharts.scores)}
986
- {renderBarChart('Task Categories', reportCharts.categories)}
987
- {renderBarChart('Priorities', reportCharts.priorities)}
988
- </div>
989
- )}
990
  <div className="task-output-preview final-report-preview">
991
  <pre>{finalReport}</pre>
992
  </div>
 
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { ArrowLeft, Bot, CheckCircle2, Download, FilePenLine, FileText, ListTodo, Map as MapIcon, PlayCircle, PlusCircle, RefreshCw, Trash2, X } from 'lucide-react';
3
  import { motion } from 'framer-motion';
4
  import { supabase } from '../services/supabase';
5
  import { useAuth } from '../context/useAuth';
 
37
  depends_on_task_id: string;
38
  }
39
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  interface ProjectDetailProps {
41
  projectId: string;
42
  uiMode: UiMode;
 
62
  }
63
  };
64
 
65
+ const hasTaskErrorOutput = (task: Task) =>
66
+ Boolean(task.output_data && typeof task.output_data === 'object' && 'error' in task.output_data);
67
+
68
  const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initialTaskId = null, onBack }) => {
69
  const { user } = useAuth();
70
  const [project, setProject] = useState<Project | null>(null);
 
86
  const [filter, setFilter] = useState<string>('all');
87
  const [taskActionError, setTaskActionError] = useState<string | null>(null);
88
  const [taskActionPending, setTaskActionPending] = useState(false);
89
+ const [selectedTask, setSelectedTask] = useState<Task | null>(null);
90
  const [finalReport, setFinalReport] = useState<string | null>(null);
91
  const [finalReportVariant, setFinalReportVariant] = useState<'full' | 'brief' | 'pessimistic'>('full');
92
+ const [showRoadmap, setShowRoadmap] = useState(false);
93
  const [reportLoading, setReportLoading] = useState(false);
94
  const [pdfLoading, setPdfLoading] = useState(false);
95
  const defaultProvider = getDefaultProvider();
 
189
  setDependencyIds([]);
190
  };
191
 
192
+ const startEditingTask = useCallback((task: Task) => {
193
+ if (project?.status === 'completed') {
194
+ setError('Completed projects are locked. Tasks cannot be edited.');
195
+ return;
196
+ }
197
  setEditingTaskId(task.id);
198
  setTitle(task.title);
199
  setDescription(task.description ?? '');
 
201
  setDependencyIds(dependencyMap(task.id));
202
  setError(null);
203
  setMessage(null);
204
+ }, [dependencyMap, project?.status]);
205
 
206
  useEffect(() => {
207
  if (!initialTaskId || tasks.length === 0) return;
 
212
  setSelectedTask(task);
213
  }
214
  }
215
+ }, [initialTaskId, startEditingTask, tasks]);
216
 
217
  const saveTaskDependencies = async (taskId: string, selectedDependencyIds: string[]) => {
218
  if (!dependencyTableAvailable) return null;
 
245
 
246
  const createTask = async (event: React.FormEvent) => {
247
  event.preventDefault();
248
+ if (!canModifyProject) {
249
+ setError('Completed projects are locked. Create a new project or reopen this one before adding tasks.');
250
+ return;
251
+ }
252
  setSaving(true);
253
  setError(null);
254
  setMessage(null);
 
290
  };
291
 
292
  const handleDeleteTask = async (task: Task) => {
293
+ if (!canModifyProject) {
294
+ setError('Completed projects are locked. Tasks cannot be deleted.');
295
+ return;
296
+ }
297
  const confirmed = window.confirm(`Delete task "${task.title}"? This cannot be undone.`);
298
  if (!confirmed) return;
299
 
 
318
  };
319
 
320
  const assignTaskAgent = async (taskId: string, assignedAgentId: string) => {
321
+ if (!canModifyProject) {
322
+ setError('Completed projects are locked. Task assignments cannot be changed.');
323
+ return;
324
+ }
325
  setError(null);
326
  setMessage(null);
327
 
 
344
  };
345
 
346
  const createDefaultAgents = async () => {
347
+ if (!canModifyProject) {
348
+ setError('Completed projects are locked. Agents cannot be generated from this project.');
349
+ return;
350
+ }
351
  if (!user) {
352
  setError('You must be signed in to create default agents.');
353
  return;
 
377
  };
378
 
379
  const runOrchestrator = async () => {
380
+ if (!canModifyProject) {
381
+ setError('Completed projects are locked. The orchestrator cannot add or rerun tasks.');
382
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  }
 
384
  setOrchestrating(true);
385
  setError(null);
386
  setMessage(null);
387
 
388
  try {
389
+ const errorOutputTaskIds = tasks
390
+ .filter((task) => hasTaskErrorOutput(task))
391
+ .map((task) => task.id);
392
+
393
+ if (errorOutputTaskIds.length > 0) {
394
+ const { error: resetError } = await supabase
395
+ .from('tasks')
396
+ .update({ status: 'todo', output_data: null })
397
+ .in('id', errorOutputTaskIds);
398
+
399
+ if (resetError) throw resetError;
400
+ }
401
+
402
  const apiUrl = getApiUrl();
403
  const response = await fetch(`${apiUrl}/orchestrator/projects/${projectId}/run`, {
404
  method: 'POST'
 
408
  response,
409
  `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.`
410
  );
411
+ const body = await response.json().catch(() => null);
412
+ setMessage(body?.mode === 'queue' ? 'Project tasks queued for worker execution.' : 'Project orchestrator started for queued and failed tasks.');
413
  // Refresh after a delay to show the new tasks
414
  window.setTimeout(loadProject, 2000);
415
  } catch (exc) {
 
419
  window.setTimeout(() => setOrchestrating(false), 2000);
420
  }
421
  };
422
+
423
+ const retryTask = async (task: Task) => {
424
+ if (!canModifyProject) {
425
+ setTaskActionError('Completed projects are locked. This task cannot be retried.');
426
+ return;
427
+ }
428
+ setTaskActionPending(true);
429
+ setTaskActionError(null);
430
+ setError(null);
431
+ setMessage(null);
432
+
433
+ try {
434
+ const { error: resetError } = await supabase
435
+ .from('tasks')
436
+ .update({ status: 'todo', output_data: null })
437
+ .eq('id', task.id);
438
+
439
+ if (resetError) throw resetError;
440
+
441
+ setSelectedTask(null);
442
+ await loadProject();
443
+ await runOrchestrator();
444
+ setMessage('Task reset and queued for retry.');
445
+ } catch (exc) {
446
+ setTaskActionError(`Could not retry task: ${exc instanceof Error ? exc.message : 'Unknown error'}`);
447
+ } finally {
448
+ setTaskActionPending(false);
449
+ }
450
+ };
451
+
452
  const handleApproveAll = async () => {
453
  if (!projectId) return;
454
+ if (!canModifyProject) {
455
+ setError('Completed projects are locked. Pending approvals cannot be changed.');
456
+ return;
457
+ }
458
  setApprovingAll(true);
459
  setError(null);
460
  setMessage(null);
 
473
  }
474
  setApprovingAll(false);
475
  };
 
 
476
  const allTasksApproved = tasks.length > 0 && tasks.every((task) => task.status === 'done');
477
  const taskLookup = new Map(tasks.map((task) => [task.id, task]));
478
  const tasksAwaitingApproval = tasks.filter((task) => task.status === 'awaiting_approval').length;
479
  const completedTasks = tasks.filter((task) => task.status === 'done').length;
480
+ const retryableTasks = tasks.filter((task) => task.status === 'failed' || hasTaskErrorOutput(task)).length;
481
+ const isProjectCompleted = project?.status === 'completed';
482
+ const canModifyProject = !isProjectCompleted;
483
+ const roadmapPhases = useMemo(() => {
484
+ const orderedPhases = [
485
+ 'Foundation',
486
+ 'Build',
487
+ 'Execution',
488
+ 'Review',
489
+ 'Recovery',
490
+ 'Finalize',
491
+ 'Completed'
492
+ ];
493
+ const phaseMap = new Map<string, Task[]>();
494
+ const dependencyCounts = dependencies.reduce<Record<string, number>>((acc, dependency) => {
495
+ acc[dependency.task_id] = (acc[dependency.task_id] ?? 0) + 1;
496
+ return acc;
497
+ }, {});
498
+ const blockerCounts = dependencies.reduce<Record<string, number>>((acc, dependency) => {
499
+ acc[dependency.depends_on_task_id] = (acc[dependency.depends_on_task_id] ?? 0) + 1;
500
+ return acc;
501
+ }, {});
502
+
503
+ for (const task of tasks) {
504
+ let phase = 'Build';
505
+ if (task.status === 'done') phase = 'Completed';
506
+ else if (task.status === 'awaiting_approval') phase = 'Review';
507
+ else if (task.status === 'queued' || task.status === 'in_progress') phase = 'Execution';
508
+ else if (task.status === 'failed') phase = 'Recovery';
509
+ else if ((dependencyCounts[task.id] ?? 0) === 0) phase = 'Foundation';
510
+ else if ((blockerCounts[task.id] ?? 0) === 0) phase = 'Finalize';
511
+
512
+ phaseMap.set(phase, [...(phaseMap.get(phase) ?? []), task]);
513
+ }
514
+
515
+ return orderedPhases
516
+ .map((phase) => ({
517
+ phase,
518
+ tasks: (phaseMap.get(phase) ?? []).sort((a, b) => b.priority - a.priority || a.title.localeCompare(b.title))
519
+ }))
520
+ .filter((item) => item.tasks.length > 0);
521
+ }, [dependencies, tasks]);
522
 
523
  const humanizeKey = (key: string) => key.replace(/[_-]/g, ' ').trim().replace(/\b\w/g, (char) => char.toUpperCase());
524
 
 
586
  };
587
 
588
  const approveTask = async (taskId: string) => {
589
+ if (!canModifyProject) {
590
+ setTaskActionError('Completed projects are locked. Task approval cannot be changed.');
591
+ return;
592
+ }
593
  setTaskActionPending(true);
594
  setTaskActionError(null);
595
  setError(null);
 
608
  };
609
 
610
  const rejectTask = async (taskId: string) => {
611
+ if (!canModifyProject) {
612
+ setTaskActionError('Completed projects are locked. Task approval cannot be changed.');
613
+ return;
614
+ }
615
  setTaskActionPending(true);
616
  setTaskActionError(null);
617
  setError(null);
 
642
 
643
  const body = await response.json();
644
  setFinalReport(body.report);
 
645
  setFinalReportVariant(variant);
646
  await loadProject();
647
  } catch (exc) {
 
651
  }
652
  };
653
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  const downloadFinalReportPdf = async () => {
655
  setPdfLoading(true);
656
  setError(null);
 
688
  <h2>{project?.name ?? 'Project'}</h2>
689
  <p style={{ color: 'var(--text-dim)' }}>{project?.description || 'No description provided.'}</p>
690
  </div>
691
+ <div className="project-actions-container">
692
+ <div className="action-group">
693
+ <button className="btn btn-glass btn-sm" onClick={loadProject}>
694
+ <RefreshCw size={16} />
695
+ Refresh
696
  </button>
697
+ {tasks.length > 0 && (
698
+ <button className="btn btn-glass btn-sm" onClick={() => setShowRoadmap(true)}>
699
+ <MapIcon size={16} />
700
+ Roadmap
701
+ </button>
702
+ )}
703
+ </div>
704
+
705
+ <div className="action-group reports-group">
706
+ {allTasksApproved && (
707
+ <>
708
+ <button
709
+ className="btn btn-primary btn-final-report"
710
+ onClick={() => openFinalReport('full')}
711
+ disabled={reportLoading}
712
+ style={{
713
+ background: 'linear-gradient(135deg, var(--accent) 0%, #7c3aed 100%)',
714
+ boxShadow: '0 4px 15px rgba(124, 58, 237, 0.3)',
715
+ fontWeight: 700,
716
+ padding: '0.6rem 1.2rem'
717
+ }}
718
+ >
719
+ <FileText size={18} />
720
+ {reportLoading ? 'Building...' : 'Final Report'}
721
+ </button>
722
+ <div className="report-variants">
723
+ <button className="btn btn-glass btn-sm" onClick={() => openFinalReport('brief')} disabled={reportLoading} title="Short Brief">
724
+ <FileText size={16} />
725
+ Brief
726
+ </button>
727
+ <button className="btn btn-glass btn-sm" onClick={() => openFinalReport('pessimistic')} disabled={reportLoading} title="Pessimistic Analysis">
728
+ <FileText size={16} />
729
+ Risks
730
+ </button>
731
+ </div>
732
+ </>
733
+ )}
734
+
735
+ {canModifyProject && tasks.some(t => t.status === 'awaiting_approval') && (
736
+ <button className="btn btn-glass" onClick={handleApproveAll} disabled={approvingAll} style={{ borderColor: 'var(--success)', color: 'var(--success)' }}>
737
+ <CheckCircle2 size={18} />
738
+ Approve All
739
+ </button>
740
+ )}
741
+
742
+ {canModifyProject && (
743
+ <button className="btn btn-primary" onClick={runOrchestrator} disabled={orchestrating}>
744
+ <PlayCircle size={18} />
745
+ {orchestrating ? 'Starting...' : retryableTasks > 0 ? `Retry (${retryableTasks})` : 'Run'}
746
+ </button>
747
+ )}
748
+ </div>
749
  </div>
750
+
751
  </div>
752
 
753
  {error && <div className="inline-status">{error}</div>}
754
  {message && <div className="inline-status"><CheckCircle2 size={16} color="var(--success)" />{message}</div>}
755
+ {isProjectCompleted && (
756
+ <div className="inline-status project-locked-notice">
757
+ <CheckCircle2 size={16} color="var(--success)" />
758
+ <span>This project is completed and locked. Reports remain available, but tasks, agents, approvals, retries, and assignments are read-only.</span>
759
+ </div>
760
+ )}
761
 
762
  {uiMode === 'guided' && (
763
  <section className="glass-panel project-form">
 
771
  <strong>1. Prepare agents</strong>
772
  <p>{agents.length > 0 ? `${agents.length} agents available.` : 'Create the default agents for this workspace.'}</p>
773
  </div>
774
+ <button className="btn btn-glass btn-sm" type="button" onClick={createDefaultAgents} disabled={!canModifyProject}>
775
  Generate Defaults
776
  </button>
777
  </div>
778
  <div className="task-row">
779
  <div>
780
  <strong>2. Build the plan</strong>
781
+ <p>{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.'}</p>
782
  </div>
783
+ <button className="btn btn-primary btn-sm" type="button" onClick={runOrchestrator} disabled={orchestrating || !canModifyProject}>
784
+ {orchestrating ? 'Starting...' : retryableTasks > 0 ? 'Retry Failed' : tasks.length > 0 ? 'Run Queued' : 'Generate Plan'}
785
  </button>
786
  </div>
787
  <div className="task-row">
 
789
  <strong>3. Review outputs</strong>
790
  <p>{tasksAwaitingApproval > 0 ? `${tasksAwaitingApproval} tasks are waiting for approval.` : 'No tasks are waiting for approval right now.'}</p>
791
  </div>
792
+ {canModifyProject && tasksAwaitingApproval > 0 && (
793
  <button className="btn btn-glass btn-sm" type="button" onClick={handleApproveAll} disabled={approvingAll}>
794
  {approvingAll ? 'Approving...' : 'Approve Pending'}
795
  </button>
 
824
  Create Planner, Builder, and Reviewer agents for this workspace.
825
  </p>
826
  </div>
827
+ <button className="btn btn-glass" onClick={createDefaultAgents} disabled={!canModifyProject}>
828
  <PlusCircle size={18} />
829
  Generate Defaults
830
  </button>
 
836
  <PlusCircle size={22} color="var(--accent)" />
837
  <h3>{editingTaskId ? 'Edit Task' : uiMode === 'guided' ? 'Add Manual Task' : 'Add Task'}</h3>
838
  </div>
839
+ {editingTaskId && canModifyProject && (
840
  <button className="btn btn-icon" type="button" onClick={resetTaskForm} title="Cancel edit">
841
  <X size={18} />
842
  </button>
843
  )}
844
  </div>
845
+ {!canModifyProject && (
846
+ <div className="read-only-note">
847
+ This project is complete. Adding more tasks would change the approved scope, so task planning is disabled.
848
+ </div>
849
+ )}
850
  <form onSubmit={createTask} style={{ display: 'grid', gap: 'var(--space-md)' }}>
851
  <label>
852
  <span>Task Title</span>
853
+ <input value={title} onChange={(event) => setTitle(event.target.value)} required placeholder="Draft implementation plan" disabled={!canModifyProject} />
854
  </label>
855
  <label>
856
  <span>Description</span>
857
+ <textarea value={description} onChange={(event) => setDescription(event.target.value)} rows={4} placeholder="Instructions for the assigned agent..." disabled={!canModifyProject} />
858
  </label>
859
  {(uiMode === 'expert' || showAdvancedTaskControls) && (
860
  <label>
861
  <span>Assigned Agent</span>
862
+ <select value={agentId} onChange={(event) => setAgentId(event.target.value)} disabled={!canModifyProject}>
863
  <option value="">Unassigned</option>
864
  {agents.map((agent) => (
865
  <option key={agent.id} value={agent.id}>{agent.name} ({agent.model})</option>
 
891
  <input
892
  type="checkbox"
893
  checked={dependencyIds.includes(task.id)}
894
+ disabled={!canModifyProject}
895
  onChange={(event) => {
896
  setDependencyIds((current) =>
897
  event.target.checked
 
909
  </div>
910
  </div>
911
  )}
912
+ <button className="btn btn-primary" type="submit" disabled={saving || !canModifyProject}>
913
  <CheckCircle2 size={18} />
914
  {saving ? (editingTaskId ? 'Saving...' : 'Adding...') : (editingTaskId ? 'Save Task' : 'Add Task')}
915
  </button>
 
922
  <h3>Tasks</h3>
923
  </div>
924
  <div className="filter-bar" style={{ display: 'flex', gap: '8px', marginBottom: '16px', overflowX: 'auto', paddingBottom: '4px' }}>
925
+ {['all', 'todo', 'queued', 'in_progress', 'awaiting_approval', 'done', 'failed'].map((f) => (
926
  <button
927
  key={f}
928
  className={`btn ${filter === f ? 'btn-primary' : 'btn-glass'}`}
 
969
  {(uiMode === 'expert' || showAdvancedTaskControls) && (
970
  <select
971
  value={task.assigned_agent_id ?? ''}
972
+ disabled={!canModifyProject}
973
  onClick={(e) => e.stopPropagation()}
974
  onChange={(e) => {
975
  e.stopPropagation();
 
983
  ))}
984
  </select>
985
  )}
986
+ {canModifyProject && (
987
+ <>
988
+ <button
989
+ className="btn btn-glass btn-sm"
990
+ type="button"
991
+ onClick={(e) => {
992
+ e.stopPropagation();
993
+ startEditingTask(task);
994
+ }}
995
+ >
996
+ <FilePenLine size={14} />
997
+ Edit
998
+ </button>
999
+ <button
1000
+ className="btn btn-glass btn-sm"
1001
+ type="button"
1002
+ onClick={(e) => {
1003
+ e.stopPropagation();
1004
+ handleDeleteTask(task);
1005
+ }}
1006
+ style={{ color: 'var(--danger)', borderColor: 'rgba(231, 76, 60, 0.25)' }}
1007
+ >
1008
+ <Trash2 size={14} />
1009
+ Delete
1010
+ </button>
1011
+ </>
1012
+ )}
1013
  </div>
1014
  {(uiMode === 'expert' || showAdvancedTaskControls) && dependencyMap(task.id).length > 0 && (
1015
  <div style={{ marginTop: 'var(--space-sm)', display: 'grid', gap: '4px' }}>
 
1053
  </div>
1054
  {taskActionError && <div className="inline-status modal-error">{taskActionError}</div>}
1055
  <div className="button-row modal-actions">
1056
+ {!canModifyProject ? (
1057
+ <div style={{ flex: 1, textAlign: 'left', color: 'var(--text-dim)', fontSize: '0.9rem' }}>
1058
+ This project is completed and locked. Task output can be reviewed, but task status cannot be changed.
1059
+ </div>
1060
+ ) : hasTaskErrorOutput(selectedTask) ? (
1061
+ <>
1062
+ <button className="btn btn-primary" onClick={() => retryTask(selectedTask)} disabled={taskActionPending || orchestrating}>
1063
+ {taskActionPending || orchestrating ? 'Retrying...' : 'Retry Task'}
1064
+ </button>
1065
+ <div style={{ flex: 1, textAlign: 'left', color: 'var(--danger)', fontSize: '0.9rem' }}>
1066
+ This task has a saved execution error and needs to be retried.
1067
+ </div>
1068
+ </>
1069
+ ) : selectedTask.status === 'awaiting_approval' ? (
1070
  <>
1071
  <button className="btn btn-primary" onClick={() => approveTask(selectedTask.id)} disabled={taskActionPending}>
1072
  {taskActionPending ? 'Saving...' : 'Approve Task'}
 
1088
  </div>
1089
  )}
1090
 
1091
+ {showRoadmap && (
1092
+ <div className="modal-overlay" onClick={() => setShowRoadmap(false)}>
1093
+ <div className="glass-panel modal-content task-review-modal roadmap-modal" onClick={(e) => e.stopPropagation()}>
1094
+ <div className="modal-title-row">
1095
+ <div>
1096
+ <h3>Roadmap: {project?.name ?? 'Project'}</h3>
1097
+ <p>{completedTasks}/{tasks.length} tasks complete. Phases are inferred from task status, priority, and dependencies.</p>
1098
+ </div>
1099
+ <button className="btn btn-glass btn-sm" onClick={() => setShowRoadmap(false)}>
1100
+ Close
1101
+ </button>
1102
+ </div>
1103
+
1104
+ <div className="roadmap-timeline">
1105
+ {roadmapPhases.map((phase, phaseIndex) => (
1106
+ <section key={phase.phase} className="roadmap-phase">
1107
+ <div className="roadmap-phase-marker">
1108
+ <span>{phaseIndex + 1}</span>
1109
+ </div>
1110
+ <div className="roadmap-phase-content">
1111
+ <div className="roadmap-phase-header">
1112
+ <h4>{phase.phase}</h4>
1113
+ <span>{phase.tasks.length} task{phase.tasks.length === 1 ? '' : 's'}</span>
1114
+ </div>
1115
+ <div className="roadmap-task-list">
1116
+ {phase.tasks.map((task) => (
1117
+ <article key={task.id} className="roadmap-task">
1118
+ <div>
1119
+ <strong>{task.title}</strong>
1120
+ <p>{task.description || 'No description provided.'}</p>
1121
+ {(dependencyMap(task.id).length > 0 || dependentMap(task.id).length > 0) && (
1122
+ <small>
1123
+ {dependencyMap(task.id).length > 0 && `Depends on ${dependencyMap(task.id).length}`}
1124
+ {dependencyMap(task.id).length > 0 && dependentMap(task.id).length > 0 && ' · '}
1125
+ {dependentMap(task.id).length > 0 && `Blocks ${dependentMap(task.id).length}`}
1126
+ </small>
1127
+ )}
1128
+ </div>
1129
+ <div className="roadmap-task-meta">
1130
+ <span className={`status-badge status-${task.status}`}>
1131
+ {task.status.replace('_', ' ')}
1132
+ </span>
1133
+ <span>Priority {task.priority}</span>
1134
+ </div>
1135
+ </article>
1136
+ ))}
1137
+ </div>
1138
+ </div>
1139
+ </section>
1140
+ ))}
1141
+ </div>
1142
+ </div>
1143
+ </div>
1144
+ )}
1145
+
1146
  {finalReport && (
1147
  <div className="modal-overlay" onClick={() => setFinalReport(null)}>
1148
  <div className="glass-panel modal-content task-review-modal" onClick={(e) => e.stopPropagation()}>
1149
  <h3>Final Report</h3>
 
 
 
 
 
 
 
1150
  <div className="task-output-preview final-report-preview">
1151
  <pre>{finalReport}</pre>
1152
  </div>