diff --git a/visual_workflow_builder/frontend_v4/src/App.tsx b/visual_workflow_builder/frontend_v4/src/App.tsx index ee35ed69a..b4f2c56fe 100644 --- a/visual_workflow_builder/frontend_v4/src/App.tsx +++ b/visual_workflow_builder/frontend_v4/src/App.tsx @@ -25,6 +25,7 @@ import ExecutionOverlay from './components/ExecutionOverlay'; import type { Variable } from './components/VariableManager'; import RightPanel from './components/RightPanel'; import SelfHealingDialog from './components/SelfHealingDialog'; +import PauseDialog from './components/PauseDialog'; import ConfidenceDashboard from './components/ConfidenceDashboard'; import WorkflowValidation from './components/WorkflowValidation'; import ReviewModal from './components/ReviewModal'; @@ -569,6 +570,47 @@ function App() { }} /> + {/* QW4 — Pause supervisée (safety_checks). + Affiché si le serveur renvoie status == paused_need_help, ou + status == paused avec un payload de checks. Backward 100% : si + safety_checks vide, PauseDialog rend la bulle simple legacy. */} + {(appState?.execution?.status === 'paused_need_help' || + (appState?.execution?.status === 'paused' && + (appState?.execution?.safety_checks?.length ?? 0) > 0)) && ( +
+ { + const replayId = appState.execution?.replay_id || appState.execution?.id; + if (replayId) { + // Voie streaming server (Agent V1 / replay distant) + const resp = await fetch('/api/v3/replay/resume', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + replay_id: replayId, + acknowledged_check_ids: ackIds, + }), + }); + if (!resp.ok) { + const err = await resp.json().catch(() => ({})); + throw new Error(err?.detail?.error || resp.statusText); + } + } else { + // Voie locale (execute/resume) + await api.resumeExecution(); + } + await loadState(); + }} + onCancel={() => { + handleStopExecution(); + }} + /> +
+ )} + {/* ConfidenceDashboard déplacé dans le header */} ); diff --git a/visual_workflow_builder/frontend_v4/src/components/PauseDialog.tsx b/visual_workflow_builder/frontend_v4/src/components/PauseDialog.tsx new file mode 100644 index 000000000..73b34a91b --- /dev/null +++ b/visual_workflow_builder/frontend_v4/src/components/PauseDialog.tsx @@ -0,0 +1,126 @@ +// QW4 — PauseDialog : bulle de pause supervisée avec ChecklistPanel intégré. +// +// 2 modes de rendu : +// - safety_checks vide -> bulle simple legacy (Continuer / Annuler) +// - safety_checks fournis -> ChecklistPanel ; bouton Continuer désactivé +// tant qu'un check `required` n'est pas coché. +// +// Les checks `llm_contextual` portent un badge [Léa] avec evidence en tooltip. + +import { useState, useMemo } from 'react'; +import type { SafetyCheck } from '../types'; + +interface Props { + pauseMessage: string; + pauseReason?: string; + safetyChecks: SafetyCheck[]; + onResume: (acknowledgedIds: string[]) => Promise; + onCancel: () => void; +} + +export default function PauseDialog({ + pauseMessage, + pauseReason, + safetyChecks, + onResume, + onCancel, +}: Props) { + const [checked, setChecked] = useState>({}); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const allRequiredOK = useMemo(() => { + return safetyChecks + .filter((c) => c.required) + .every((c) => checked[c.id] === true); + }, [safetyChecks, checked]); + + const toggle = (id: string) => { + setChecked((prev) => ({ ...prev, [id]: !prev[id] })); + }; + + const handleResume = async () => { + setSubmitting(true); + setError(null); + try { + const acknowledgedIds = Object.entries(checked) + .filter(([, v]) => v) + .map(([k]) => k); + await onResume(acknowledgedIds); + } catch (e: any) { + setError(e?.message || 'Erreur lors de la reprise'); + } finally { + setSubmitting(false); + } + }; + + // Backward compat : pas de checks -> bulle simple legacy + if (safetyChecks.length === 0) { + return ( +
+

{pauseMessage}

+ {pauseReason && Raison : {pauseReason}} +
+ + +
+
+ ); + } + + return ( +
+

Pause supervisée

+

{pauseMessage}

+ {pauseReason && ( +
+ Raison : {pauseReason} +
+ )} + +
    + {safetyChecks.map((c) => ( +
  • + + {c.source === 'llm_contextual' && c.evidence && ( + -> {c.evidence} + )} +
  • + ))} +
+ + {error &&
{error}
} + +
+ + +
+
+ ); +} diff --git a/visual_workflow_builder/frontend_v4/src/components/PropertiesPanel.tsx b/visual_workflow_builder/frontend_v4/src/components/PropertiesPanel.tsx index 2711f1a94..76efa92c1 100644 --- a/visual_workflow_builder/frontend_v4/src/components/PropertiesPanel.tsx +++ b/visual_workflow_builder/frontend_v4/src/components/PropertiesPanel.tsx @@ -1353,6 +1353,136 @@ export default function PropertiesPanel({ step, onUpdateParams, onDelete }: Prop ); + case 'pause_for_human': { + // QW4 — éditeur safety_level + safety_checks (déclaratifs) + const safetyChecks = Array.isArray(params.safety_checks) + ? (params.safety_checks as Array<{ id?: string; label?: string; required?: boolean }>) + : []; + return ( + <> +
+ +