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 [
|
| 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 (
|
| 369 |
-
|
| 370 |
-
|
| 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 |
-
|
|
|
|
| 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="
|
| 636 |
-
|
| 637 |
-
<button className="btn btn-
|
| 638 |
-
<
|
| 639 |
-
|
| 640 |
</button>
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|