feat(vwb): log supervised competence verdicts
This commit is contained in:
@@ -106,6 +106,24 @@ function App() {
|
||||
setRuntimeVariables(status.variables as Record<string, unknown>);
|
||||
}
|
||||
|
||||
const localExecution = status.execution;
|
||||
if (status.human_pause && localExecution) {
|
||||
const rawPause = status.human_pause as any;
|
||||
const pause = rawPause.human_pause || rawPause;
|
||||
setAppState((prev) => prev ? ({
|
||||
...prev,
|
||||
execution: {
|
||||
...localExecution,
|
||||
pause_message: pause.message || 'Validation humaine requise',
|
||||
pause_reason: pause.pause_reason || 'supervised_pause',
|
||||
safety_checks: [],
|
||||
verdict_required: Boolean(pause.verdict_required),
|
||||
verdict_endpoint: pause.verdict_endpoint,
|
||||
competence_id: pause.competence_id,
|
||||
},
|
||||
}) : prev);
|
||||
}
|
||||
|
||||
// Self-healing interactif: detecter si on attend un choix utilisateur
|
||||
if (status.waiting_for_choice && status.candidates) {
|
||||
setHealingCandidates(status.candidates);
|
||||
@@ -640,12 +658,19 @@ function App() {
|
||||
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)) && (
|
||||
(
|
||||
(appState?.execution?.safety_checks?.length ?? 0) > 0 ||
|
||||
Boolean(appState?.execution?.pause_message)
|
||||
))) && (
|
||||
<div className="pause-dialog-overlay">
|
||||
<PauseDialog
|
||||
pauseMessage={appState.execution.pause_message || 'Validation requise'}
|
||||
pauseReason={appState.execution.pause_reason}
|
||||
safetyChecks={appState.execution.safety_checks || []}
|
||||
verdictRequired={appState.execution.verdict_required}
|
||||
verdictEndpoint={appState.execution.verdict_endpoint}
|
||||
competenceId={appState.execution.competence_id}
|
||||
executionId={appState.execution.id}
|
||||
onResume={async (ackIds) => {
|
||||
const replayId = appState.execution?.replay_id || appState.execution?.id;
|
||||
if (replayId) {
|
||||
|
||||
@@ -14,6 +14,10 @@ interface Props {
|
||||
pauseMessage: string;
|
||||
pauseReason?: string;
|
||||
safetyChecks: SafetyCheck[];
|
||||
verdictRequired?: boolean;
|
||||
verdictEndpoint?: string;
|
||||
competenceId?: string;
|
||||
executionId?: string;
|
||||
onResume: (acknowledgedIds: string[]) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
@@ -22,6 +26,10 @@ export default function PauseDialog({
|
||||
pauseMessage,
|
||||
pauseReason,
|
||||
safetyChecks,
|
||||
verdictRequired = false,
|
||||
verdictEndpoint,
|
||||
competenceId,
|
||||
executionId,
|
||||
onResume,
|
||||
onCancel,
|
||||
}: Props) {
|
||||
@@ -54,6 +62,57 @@ export default function PauseDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const newVerdictId = (): string => {
|
||||
if (window.crypto?.randomUUID) {
|
||||
return window.crypto.randomUUID();
|
||||
}
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
|
||||
const value = Math.floor(Math.random() * 16);
|
||||
const resolved = char === 'x' ? value : (value & 0x3) | 0x8;
|
||||
return resolved.toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
const submitVerdict = async (verdictKind: 'valid' | 'invalid' | 'inconclusive') => {
|
||||
if (!verdictEndpoint || !competenceId) return;
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(verdictEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
verdict_id: newVerdictId(),
|
||||
verdict_kind: verdictKind,
|
||||
verdict_by: 'human:dom',
|
||||
context_signature: {
|
||||
machine_id: `browser:${window.navigator.platform || 'unknown'}`,
|
||||
screen_state_initial: '',
|
||||
screen_state_after_action: '',
|
||||
},
|
||||
evidence: {
|
||||
execution_id: executionId || '',
|
||||
pause_reason: pauseReason || '',
|
||||
},
|
||||
source: {
|
||||
frontend: 'vwb_v4',
|
||||
execution_id: executionId || '',
|
||||
},
|
||||
comments: `Verdict humain VWB: ${verdictKind}`,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
throw new Error(data.error || response.statusText);
|
||||
}
|
||||
await onResume([]);
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Erreur lors du verdict');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Backward compat : pas de checks -> bulle simple legacy
|
||||
if (safetyChecks.length === 0) {
|
||||
return (
|
||||
@@ -61,6 +120,19 @@ export default function PauseDialog({
|
||||
<p>{pauseMessage}</p>
|
||||
{pauseReason && <small className="pause-reason">Raison : {pauseReason}</small>}
|
||||
<div className="pause-actions">
|
||||
{verdictRequired && verdictEndpoint && competenceId && (
|
||||
<>
|
||||
<button onClick={() => submitVerdict('valid')} disabled={submitting}>
|
||||
Valide
|
||||
</button>
|
||||
<button onClick={() => submitVerdict('invalid')} disabled={submitting}>
|
||||
Invalide
|
||||
</button>
|
||||
<button onClick={() => submitVerdict('inconclusive')} disabled={submitting}>
|
||||
Incertain
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button onClick={() => onResume([])} disabled={submitting}>
|
||||
Continuer
|
||||
</button>
|
||||
@@ -110,6 +182,19 @@ export default function PauseDialog({
|
||||
{error && <div className="pause-error">{error}</div>}
|
||||
|
||||
<div className="pause-actions">
|
||||
{verdictRequired && verdictEndpoint && competenceId && (
|
||||
<>
|
||||
<button onClick={() => submitVerdict('valid')} disabled={submitting}>
|
||||
Valide
|
||||
</button>
|
||||
<button onClick={() => submitVerdict('invalid')} disabled={submitting}>
|
||||
Invalide
|
||||
</button>
|
||||
<button onClick={() => submitVerdict('inconclusive')} disabled={submitting}>
|
||||
Incertain
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={handleResume}
|
||||
disabled={!allRequiredOK || submitting}
|
||||
|
||||
@@ -191,7 +191,9 @@ export async function getExecutionStatus(): Promise<{
|
||||
total: number;
|
||||
original_bbox?: { x: number; y: number; width: number; height: number };
|
||||
error?: string;
|
||||
human_pause?: Record<string, unknown>;
|
||||
};
|
||||
human_pause?: Record<string, unknown> | null;
|
||||
}> {
|
||||
return request('GET', '/execute/status');
|
||||
}
|
||||
|
||||
@@ -350,6 +350,9 @@ export interface Execution {
|
||||
pause_reason?: string;
|
||||
pause_message?: string;
|
||||
safety_checks?: SafetyCheck[];
|
||||
verdict_required?: boolean;
|
||||
verdict_endpoint?: string;
|
||||
competence_id?: string;
|
||||
// ID du replay (utile pour appeler /replay/resume avec acknowledged_check_ids)
|
||||
replay_id?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user