cesjavi commited on
Commit
ee2e59e
·
1 Parent(s): 7ebd2cf

Phase 7: Collaborative Editing (Manual output editing in review modal)

Browse files
backend/routers/agent_runner.py CHANGED
@@ -341,6 +341,36 @@ async def run_task(task_id: str, background_tasks: BackgroundTasks, use_queue: b
341
 
342
  return {"message": "Task execution started", "task_id": task_id}
343
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  @router.post("/{task_id}/approve")
345
  async def approve_task(task_id: str, background_tasks: BackgroundTasks):
346
  task_res = supabase.table("tasks").select("*").eq("id", task_id).single().execute()
 
341
 
342
  return {"message": "Task execution started", "task_id": task_id}
343
 
344
+ @router.patch("/{task_id}/output")
345
+ async def update_task_output(task_id: str, payload: dict):
346
+ """
347
+ Updates the output_data of a task. Allows for manual human corrections.
348
+ """
349
+ if "output_data" not in payload:
350
+ raise HTTPException(status_code=400, detail="Missing output_data in payload")
351
+
352
+ # Verify task existence and project state
353
+ task_res = supabase.table("tasks").select("id, project_id").eq("id", task_id).single().execute()
354
+ if not task_res.data:
355
+ raise HTTPException(status_code=404, detail="Task not found")
356
+ _assert_task_project_is_mutable(task_res.data)
357
+
358
+ result = supabase.table("tasks").update({
359
+ "output_data": payload["output_data"]
360
+ }).eq("id", task_id).execute()
361
+
362
+ if not result.data:
363
+ raise HTTPException(status_code=500, detail="Failed to update task output")
364
+
365
+ await audit_service.log_action(
366
+ user_id=None,
367
+ action="task_output_manually_edited",
368
+ task_id=task_id,
369
+ metadata={"project_id": task_res.data["project_id"]}
370
+ )
371
+
372
+ return {"message": "Task output updated", "task": result.data[0]}
373
+
374
  @router.post("/{task_id}/approve")
375
  async def approve_task(task_id: str, background_tasks: BackgroundTasks):
376
  task_res = supabase.table("tasks").select("*").eq("id", task_id).single().execute()
frontend/src/components/ProjectDetail.tsx CHANGED
@@ -94,6 +94,8 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
94
  const [reportLoading, setReportLoading] = useState(false);
95
  const [pdfLoading, setPdfLoading] = useState(false);
96
  const [activeTab, setActiveTab] = useState<'tasks' | 'evidence'>('tasks');
 
 
97
  const defaultProvider = getDefaultProvider();
98
  const defaultModel = getDefaultModel(defaultProvider);
99
 
@@ -653,6 +655,31 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
653
  }
654
  };
655
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
  const downloadFinalReportPdf = async () => {
657
  setPdfLoading(true);
658
  setError(null);
@@ -1083,8 +1110,41 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
1083
  <div className="modal-overlay" onClick={() => setSelectedTask(null)}>
1084
  <div className="glass-panel modal-content task-review-modal" onClick={(e) => e.stopPropagation()}>
1085
  <h3>Review: {selectedTask.title}</h3>
1086
- <div className="task-output-preview">
1087
- <pre>{formatTaskOutput(selectedTask.output_data)}</pre>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1088
  </div>
1089
  {taskActionError && <div className="inline-status modal-error">{taskActionError}</div>}
1090
  <div className="button-row modal-actions">
@@ -1101,6 +1161,10 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
1101
  This task has a saved execution error and needs to be retried.
1102
  </div>
1103
  </>
 
 
 
 
1104
  ) : selectedTask.status === 'awaiting_approval' ? (
1105
  <>
1106
  <button className="btn btn-primary" onClick={() => approveTask(selectedTask.id)} disabled={taskActionPending}>
@@ -1115,7 +1179,10 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
1115
  This task is completed and approved.
1116
  </div>
1117
  )}
1118
- <button className="btn btn-glass" onClick={() => setSelectedTask(null)} disabled={taskActionPending}>
 
 
 
1119
  Close
1120
  </button>
1121
  </div>
 
94
  const [reportLoading, setReportLoading] = useState(false);
95
  const [pdfLoading, setPdfLoading] = useState(false);
96
  const [activeTab, setActiveTab] = useState<'tasks' | 'evidence'>('tasks');
97
+ const [isEditingOutput, setIsEditingOutput] = useState(false);
98
+ const [editedOutput, setEditedOutput] = useState('');
99
  const defaultProvider = getDefaultProvider();
100
  const defaultModel = getDefaultModel(defaultProvider);
101
 
 
655
  }
656
  };
657
 
658
+ const saveEditedOutput = async () => {
659
+ if (!selectedTask || !canModifyProject) return;
660
+ setTaskActionPending(true);
661
+ setTaskActionError(null);
662
+ try {
663
+ const apiUrl = getApiUrl();
664
+ const response = await fetch(`${apiUrl}/tasks/${selectedTask.id}/output`, {
665
+ method: 'PATCH',
666
+ headers: { 'Content-Type': 'application/json' },
667
+ body: JSON.stringify({ output_data: editedOutput })
668
+ });
669
+ await ensureBackendOk(response);
670
+
671
+ const updatedTask = { ...selectedTask, output_data: editedOutput };
672
+ setSelectedTask(updatedTask);
673
+ setTasks(prev => prev.map(t => t.id === updatedTask.id ? updatedTask : t));
674
+ setIsEditingOutput(false);
675
+ setMessage('Task output updated manually.');
676
+ } catch (exc) {
677
+ setTaskActionError(exc instanceof Error ? exc.message : 'Failed to update output.');
678
+ } finally {
679
+ setTaskActionPending(false);
680
+ }
681
+ };
682
+
683
  const downloadFinalReportPdf = async () => {
684
  setPdfLoading(true);
685
  setError(null);
 
1110
  <div className="modal-overlay" onClick={() => setSelectedTask(null)}>
1111
  <div className="glass-panel modal-content task-review-modal" onClick={(e) => e.stopPropagation()}>
1112
  <h3>Review: {selectedTask.title}</h3>
1113
+ <div className="task-output-preview" style={{ position: 'relative' }}>
1114
+ {isEditingOutput ? (
1115
+ <textarea
1116
+ className="edit-output-textarea"
1117
+ value={editedOutput}
1118
+ onChange={(e) => setEditedOutput(e.target.value)}
1119
+ style={{
1120
+ width: '100%',
1121
+ height: '400px',
1122
+ background: 'rgba(0,0,0,0.2)',
1123
+ color: 'var(--text-main)',
1124
+ border: '1px solid var(--accent)',
1125
+ borderRadius: '8px',
1126
+ padding: 'var(--space-md)',
1127
+ fontFamily: 'monospace',
1128
+ fontSize: '0.9rem'
1129
+ }}
1130
+ />
1131
+ ) : (
1132
+ <pre>{formatTaskOutput(selectedTask.output_data)}</pre>
1133
+ )}
1134
+
1135
+ {canModifyProject && !hasTaskErrorOutput(selectedTask) && selectedTask.status !== 'done' && (
1136
+ <button
1137
+ className="btn btn-glass btn-sm"
1138
+ onClick={() => {
1139
+ if (!isEditingOutput) setEditedOutput(formatTaskOutput(selectedTask.output_data));
1140
+ setIsEditingOutput(!isEditingOutput);
1141
+ }}
1142
+ style={{ position: 'absolute', top: '-40px', right: '0' }}
1143
+ >
1144
+ <FilePenLine size={14} />
1145
+ {isEditingOutput ? 'Cancel Editing' : 'Edit Output Manually'}
1146
+ </button>
1147
+ )}
1148
  </div>
1149
  {taskActionError && <div className="inline-status modal-error">{taskActionError}</div>}
1150
  <div className="button-row modal-actions">
 
1161
  This task has a saved execution error and needs to be retried.
1162
  </div>
1163
  </>
1164
+ ) : isEditingOutput ? (
1165
+ <button className="btn btn-primary" onClick={saveEditedOutput} disabled={taskActionPending}>
1166
+ {taskActionPending ? 'Saving...' : 'Save Changes'}
1167
+ </button>
1168
  ) : selectedTask.status === 'awaiting_approval' ? (
1169
  <>
1170
  <button className="btn btn-primary" onClick={() => approveTask(selectedTask.id)} disabled={taskActionPending}>
 
1179
  This task is completed and approved.
1180
  </div>
1181
  )}
1182
+ <button className="btn btn-glass" onClick={() => {
1183
+ setSelectedTask(null);
1184
+ setIsEditingOutput(false);
1185
+ }} disabled={taskActionPending}>
1186
  Close
1187
  </button>
1188
  </div>