feat: visualize governed goal progress live
This commit is contained in:
+97
-28
@@ -1,54 +1,123 @@
|
|||||||
import type { GoalJob, GoalStage } from '../../lib/api';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import type { GoalJob, GoalStage, HarnessGateNode, HarnessTraceGraph } from '../../lib/api';
|
||||||
import { StatusBadge } from '../ui/Card';
|
import { StatusBadge } from '../ui/Card';
|
||||||
|
|
||||||
interface ModelInteractionDiagramProps {
|
interface ModelInteractionDiagramProps {
|
||||||
goal: GoalJob;
|
goal: GoalJob;
|
||||||
localStage?: GoalStage;
|
localStage?: GoalStage;
|
||||||
cloudStage?: GoalStage;
|
cloudStage?: GoalStage;
|
||||||
|
trace?: HarnessTraceGraph;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tone = (status: string) => status === 'pass' || status === 'completed' ? 'bg-emerald-500' : status === 'error' || status === 'failed' ? 'bg-rose-500' : status === 'warning' || status === 'degraded' ? 'bg-amber-500' : status === 'running' ? 'animate-pulse bg-indigo-500 motion-reduce:animate-none' : 'bg-slate-300';
|
const GATE_ORDER = ['H1-context', 'H2-tool', 'H3-eval', 'H4-security', 'H5-governance', 'H6-agentops', 'H7-orchestration'];
|
||||||
|
|
||||||
function Exchange({ from, to, label, status, reverse = false }: { from: string; to: string; label: string; status: string; reverse?: boolean }) {
|
function statusTone(status: string): { dot: string; surface: string; border: string; text: string } {
|
||||||
|
if (status === 'pass' || status === 'completed') return { dot: 'bg-emerald-400', surface: 'bg-emerald-400/10', border: 'border-emerald-300/60', text: 'text-emerald-100' };
|
||||||
|
if (status === 'error' || status === 'failed' || status === 'blocked') return { dot: 'bg-rose-400', surface: 'bg-rose-400/10', border: 'border-rose-300/60', text: 'text-rose-100' };
|
||||||
|
if (status === 'warning' || status === 'degraded' || status === 'requires_approval') return { dot: 'bg-amber-300', surface: 'bg-amber-300/10', border: 'border-amber-200/60', text: 'text-amber-100' };
|
||||||
|
if (status === 'running') return { dot: 'bg-cyan-300', surface: 'bg-cyan-300/10', border: 'border-cyan-200/60', text: 'text-cyan-50' };
|
||||||
|
return { dot: 'bg-slate-500', surface: 'bg-slate-700/40', border: 'border-slate-600', text: 'text-slate-300' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatReason(reason: string): string {
|
||||||
|
return reason.replaceAll('_', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function AgentAvatar({ label, role, model, status, selected, onClick }: {
|
||||||
|
label: string;
|
||||||
|
role: string;
|
||||||
|
model: string;
|
||||||
|
status: string;
|
||||||
|
selected: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
const tone = statusTone(status);
|
||||||
|
const initials = label.split(' ').map((word) => word[0]).join('').slice(0, 2);
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-[minmax(88px,1fr)_minmax(120px,2fr)_minmax(88px,1fr)] items-center gap-2">
|
<button type="button" onClick={onClick} aria-pressed={selected} className={`group relative min-w-0 rounded-2xl border p-4 text-left transition duration-300 hover:-translate-y-1 hover:border-white/60 hover:shadow-[0_14px_34px_rgba(15,23,42,0.36)] focus:outline-none focus:ring-2 focus:ring-cyan-300 ${selected ? 'border-cyan-200 bg-white/10 shadow-[0_0_0_1px_rgba(103,232,249,0.25)]' : 'border-white/10 bg-slate-950/30'}`}>
|
||||||
<div className={`truncate text-xs font-semibold ${reverse ? 'text-right text-slate-500' : 'text-slate-800'}`}>{reverse ? to : from}</div>
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`goal-agent-orb goal-agent-${status} relative grid h-12 w-12 shrink-0 place-items-center rounded-2xl border ${tone.border} ${tone.surface}`}>
|
||||||
|
<span className={`goal-agent-core h-5 w-5 rounded-full ${tone.dot}`} />
|
||||||
|
<span className="relative z-10 text-[10px] font-black tracking-tight text-slate-950">{initials}</span>
|
||||||
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className={`flex items-center ${reverse ? 'flex-row-reverse' : ''}`}>
|
<div className="text-[10px] font-bold uppercase tracking-[0.15em] text-indigo-200">{role}</div>
|
||||||
<span className={`h-2.5 w-2.5 shrink-0 rounded-full ${tone(status)}`} />
|
<div className="mt-1 truncate text-sm font-semibold text-white">{label}</div>
|
||||||
<span className={`h-px flex-1 ${status === 'error' || status === 'failed' ? 'bg-rose-300' : status === 'warning' ? 'bg-amber-300' : 'bg-indigo-300'}`} />
|
<div className="mt-1 truncate font-mono text-[10px] text-slate-400">{model || 'not selected'}</div>
|
||||||
<span className={`h-0 w-0 border-y-4 border-y-transparent ${reverse ? 'border-r-[7px] border-r-indigo-500' : 'border-l-[7px] border-l-indigo-500'}`} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 truncate text-center text-[10px] font-medium text-slate-500">{label}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={`truncate text-xs font-semibold ${reverse ? 'text-slate-800' : 'text-right text-slate-500'}`}>{reverse ? from : to}</div>
|
<div className="mt-3 flex items-center gap-2 text-[11px] font-semibold text-slate-300"><span className={`h-2 w-2 rounded-full ${tone.dot} ${status === 'running' ? 'animate-pulse motion-reduce:animate-none' : ''}`} />{status === 'queued' ? 'Awaiting controlled hand-off' : formatReason(status)}</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ControlGate({ node, selected, onClick }: { node: HarnessGateNode; selected: boolean; onClick: () => void }) {
|
||||||
|
const tone = statusTone(node.status);
|
||||||
|
const gateNumber = node.id.slice(0, 2);
|
||||||
|
return (
|
||||||
|
<button type="button" onClick={onClick} aria-pressed={selected} className={`group relative z-10 flex min-w-[112px] flex-1 items-center gap-2 rounded-xl border px-3 py-2 text-left transition hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-cyan-300 ${selected ? `${tone.surface} ${tone.border} shadow-lg` : 'border-slate-700 bg-slate-900/80 hover:border-slate-500'}`}>
|
||||||
|
<span className={`grid h-7 w-7 shrink-0 place-items-center rounded-lg text-[10px] font-black ${tone.surface} ${tone.text}`}>{gateNumber}</span>
|
||||||
|
<span className="min-w-0"><span className="block truncate text-[11px] font-bold text-slate-100">{node.title.replace(`${gateNumber} · `, '')}</span><span className={`mt-0.5 flex items-center gap-1 text-[10px] ${tone.text}`}><i className={`h-1.5 w-1.5 rounded-full ${tone.dot} ${node.status === 'running' ? 'animate-pulse motion-reduce:animate-none' : ''}`} />{node.status}</span></span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GateDetail({ node }: { node: HarnessGateNode }) {
|
||||||
|
const tone = statusTone(node.status);
|
||||||
|
const evidence = Object.entries(node.evidence).filter(([, value]) => value !== null && value !== undefined).slice(0, 3);
|
||||||
|
return (
|
||||||
|
<div className={`rounded-2xl border p-4 ${tone.border} ${tone.surface}`} aria-live="polite">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2"><div><div className="text-[10px] font-bold uppercase tracking-[0.16em] text-slate-400">Focused control</div><div className="mt-1 text-sm font-semibold text-white">{node.title}</div></div><StatusBadge value={node.status} /></div>
|
||||||
|
<p className="mt-3 text-sm leading-6 text-slate-200">{node.description}</p>
|
||||||
|
<div className="mt-3 rounded-xl bg-slate-950/40 p-3"><div className="text-[10px] font-bold uppercase tracking-[0.12em] text-slate-500">Observed impact</div><div className="mt-1 text-sm font-medium text-white">{formatReason(node.reason)}</div></div>
|
||||||
|
{evidence.length > 0 && <div className="mt-3 flex flex-wrap gap-2">{evidence.map(([key, value]) => <span key={key} className="max-w-full truncate rounded-lg border border-white/10 bg-slate-950/40 px-2 py-1 font-mono text-[10px] text-slate-300">{key}: {typeof value === 'string' ? value : JSON.stringify(value)}</span>)}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModelInteractionDiagram({ goal, localStage, cloudStage }: ModelInteractionDiagramProps) {
|
export function GovernedOutcomePulse({ goal, trace }: { goal: GoalJob; trace?: HarnessTraceGraph }) {
|
||||||
|
const nodes = trace?.nodes ?? [];
|
||||||
|
const progressed = trace?.progress ?? nodes.filter((node) => node.status !== 'queued').length;
|
||||||
|
const current = [...nodes].reverse().find((node) => node.status !== 'queued') ?? null;
|
||||||
|
const outcome = nodes.find((node) => node.id === 'H7-orchestration');
|
||||||
|
const isFailure = goal.status === 'failed' || outcome?.status === 'blocked' || outcome?.status === 'error';
|
||||||
|
const label = isFailure ? 'Stopped with evidence retained' : goal.status === 'requires_approval' ? 'Awaiting human approval' : goal.status === 'completed' ? 'Certified governed outcome' : 'Governed outcome in progress';
|
||||||
|
const percentage = Math.min(100, Math.round((progressed / GATE_ORDER.length) * 100));
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-slate-700 bg-slate-950 p-4 text-white" aria-live="polite">
|
||||||
|
<div className="flex flex-wrap items-start justify-between gap-3"><div><div className="text-[10px] font-bold uppercase tracking-[0.16em] text-cyan-300">Governed outcome · live</div><div className="mt-1 text-base font-semibold">{label}</div></div><StatusBadge value={goal.status} /></div>
|
||||||
|
<div className="mt-4 h-2 overflow-hidden rounded-full bg-slate-800"><div className={`h-full rounded-full transition-all duration-700 ${isFailure ? 'bg-rose-400' : 'bg-gradient-to-r from-indigo-500 via-cyan-400 to-emerald-400'}`} style={{ width: `${percentage}%` }} /></div>
|
||||||
|
<div className="mt-2 flex items-center justify-between text-[11px] text-slate-400"><span>{progressed}/7 controls have emitted evidence</span><span>{percentage}%</span></div>
|
||||||
|
<div className="mt-4 grid gap-2 sm:grid-cols-3"><div className="rounded-xl bg-white/5 p-3"><div className="text-[10px] font-bold uppercase tracking-wide text-slate-500">Now at</div><div className="mt-1 text-sm font-semibold text-white">{current?.title ?? 'Waiting for H1'}</div></div><div className="rounded-xl bg-white/5 p-3"><div className="text-[10px] font-bold uppercase tracking-wide text-slate-500">Decision</div><div className="mt-1 text-sm font-semibold text-white">{outcome?.status === 'pass' ? 'Certified' : isFailure ? 'Safely stopped' : 'Not final yet'}</div></div><div className="rounded-xl bg-white/5 p-3"><div className="text-[10px] font-bold uppercase tracking-wide text-slate-500">Latest evidence</div><div className="mt-1 line-clamp-2 text-sm font-semibold text-white">{current ? formatReason(current.reason) : 'Trace stream connecting'}</div></div></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModelInteractionDiagram({ goal, localStage, cloudStage, trace }: ModelInteractionDiagramProps) {
|
||||||
|
const nodes = useMemo(() => trace?.nodes ?? GATE_ORDER.map((id) => ({ id, title: id.replace('-', ' · '), description: 'Waiting for trace evidence', status: 'queued' as const, reason: 'Waiting for evidence', updated_at: null, evidence: {}, events: [] })), [trace]);
|
||||||
|
const preferred = [...nodes].reverse().find((node) => node.status === 'running' || node.status === 'error' || node.status === 'blocked' || node.status === 'warning')?.id ?? [...nodes].reverse().find((node) => node.status !== 'queued')?.id ?? 'H1-context';
|
||||||
|
const [focusedGate, setFocusedGate] = useState(preferred);
|
||||||
|
const [focusedAgent, setFocusedAgent] = useState<'local' | 'cloud'>('local');
|
||||||
|
useEffect(() => setFocusedGate(preferred), [preferred, trace?.updated_at]);
|
||||||
|
const focusedNode = nodes.find((node) => node.id === focusedGate) ?? nodes[0];
|
||||||
const localStatus = localStage?.status ?? 'queued';
|
const localStatus = localStage?.status ?? 'queued';
|
||||||
const cloudStatus = cloudStage?.status ?? 'queued';
|
const cloudStatus = cloudStage?.status ?? 'queued';
|
||||||
|
const direction = focusedAgent === 'local' ? 'Local worker → security and review controls' : 'Cloud reviewer → governance and final certification';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm" aria-label="Model interaction diagram">
|
<section className="overflow-hidden rounded-3xl border border-slate-800 bg-slate-950 shadow-[0_24px_70px_rgba(15,23,42,0.35)]" aria-label="Interactive governed model exchange">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-100 px-5 py-4">
|
<div className="relative overflow-hidden border-b border-white/10 bg-[radial-gradient(circle_at_18%_0%,rgba(99,102,241,0.42),transparent_28rem),radial-gradient(circle_at_84%_14%,rgba(34,211,238,0.2),transparent_22rem)] px-5 py-5">
|
||||||
<div><div className="text-[10px] font-bold uppercase tracking-[0.16em] text-indigo-600">Live model exchange</div><h2 className="mt-1 font-semibold text-slate-900">Models trao đổi và kiểm chứng như thế nào</h2></div>
|
<div className="relative flex flex-wrap items-start justify-between gap-3"><div><div className="text-[10px] font-bold uppercase tracking-[0.18em] text-cyan-200">Interactive model exchange</div><h2 className="mt-1 text-lg font-semibold text-white">Ai đang tác động vào outcome — và control nào đang quyết định</h2><p className="mt-1 text-xs text-slate-300">Chọn avatar hoặc từng H-gate để khám phá tác động và evidence thực tế.</p></div><StatusBadge value={goal.status} /></div>
|
||||||
<StatusBadge value={goal.status} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 p-5 lg:grid-cols-[220px_minmax(0,1fr)]">
|
<div className="grid gap-5 p-5 xl:grid-cols-[minmax(300px,0.78fr)_minmax(0,1.7fr)]">
|
||||||
<div className="grid grid-cols-2 gap-2 lg:grid-cols-1">
|
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-1"><AgentAvatar label="Local worker" role="Private first pass" model={localStage?.model || goal.local_model} status={localStatus} selected={focusedAgent === 'local'} onClick={() => setFocusedAgent('local')} /><AgentAvatar label="Cloud reviewer" role="Independent critique" model={cloudStage?.model || goal.cloud_model} status={cloudStatus} selected={focusedAgent === 'cloud'} onClick={() => setFocusedAgent('cloud')} /><div className="rounded-xl border border-indigo-400/20 bg-indigo-400/10 p-3 text-xs leading-5 text-indigo-100"><span className="font-semibold text-cyan-200">Selected influence:</span> {direction}</div></div>
|
||||||
<div className="rounded-xl border border-indigo-100 bg-indigo-50 p-3"><div className="text-[10px] font-bold uppercase tracking-wide text-indigo-500">Local worker</div><div className="mt-1 truncate text-xs font-semibold text-slate-800">{localStage?.model || goal.local_model}</div></div>
|
<div className="min-w-0 rounded-2xl border border-white/10 bg-slate-900/60 p-4">
|
||||||
<div className="rounded-xl border border-violet-100 bg-violet-50 p-3"><div className="text-[10px] font-bold uppercase tracking-wide text-violet-500">Cloud reviewer</div><div className="mt-1 truncate text-xs font-semibold text-slate-800">{cloudStage?.model || goal.cloud_model}</div></div>
|
<div className="mb-3 flex items-center gap-2 text-[11px] font-bold uppercase tracking-[0.14em] text-slate-400"><span className="goal-control-signal h-2 w-2 rounded-full bg-cyan-300" /> Control spine · H1 → H7</div>
|
||||||
</div>
|
<div className="relative flex flex-wrap gap-2 before:absolute before:left-4 before:right-4 before:top-5 before:h-px before:bg-gradient-to-r before:from-indigo-500/30 before:via-cyan-300/50 before:to-emerald-400/30">{nodes.map((node) => <ControlGate key={node.id} node={node} selected={node.id === focusedGate} onClick={() => setFocusedGate(node.id)} />)}</div>
|
||||||
<div className="space-y-4 rounded-xl border border-slate-200 bg-[radial-gradient(circle_at_top,#eef2ff_0,transparent_55%)] p-4">
|
{focusedNode && <div className="mt-4"><GateDetail node={focusedNode} /></div>}
|
||||||
<Exchange from="Orchestrator" to="Local model" label="Objective + governed context" status={localStatus} />
|
|
||||||
<Exchange from="Local model" to="H4 Security" label="Primary proposal" status={localStatus} />
|
|
||||||
<Exchange from="H4 Security" to="Cloud model" label="Safe draft for independent review" status={cloudStatus} />
|
|
||||||
<Exchange from="Cloud model" to="Orchestrator" label="Critique + refined outcome" status={cloudStatus} reverse />
|
|
||||||
<Exchange from="Orchestrator" to="H5 Audit" label="Certified final decision" status={goal.status} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-slate-100 bg-slate-50 px-5 py-3 text-[11px] leading-5 text-slate-500">Mỗi mũi tên thể hiện một boundary có kiểm soát; màu sắc lấy từ trạng thái thực tế của goal và từng worker.</div>
|
<div className="border-t border-white/10 bg-slate-900/80 px-5 py-3 text-xs text-slate-400">Màu, chuyển động và tiến độ lấy từ trace H1–H7. Nếu một gate dừng, governed outcome dừng đúng tại evidence đó — không diễn giải nó là lỗi mới ở các gate sau.</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,26 @@ button, input, select, textarea { font: inherit; }
|
|||||||
animation: trace-flow 1.2s linear infinite;
|
animation: trace-flow 1.2s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes goal-orb-breathe {
|
||||||
|
0%, 100% { transform: scale(1); box-shadow: 0 0 0 rgba(103, 232, 249, 0); }
|
||||||
|
50% { transform: scale(1.055); box-shadow: 0 0 28px rgba(103, 232, 249, 0.28); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes goal-core-pulse {
|
||||||
|
0%, 100% { opacity: 0.45; transform: scale(0.7); }
|
||||||
|
50% { opacity: 0.1; transform: scale(2.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes goal-signal-ping {
|
||||||
|
0% { box-shadow: 0 0 0 0 rgba(103, 232, 249, 0.68); }
|
||||||
|
75%, 100% { box-shadow: 0 0 0 9px rgba(103, 232, 249, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-agent-running { animation: goal-orb-breathe 1.8s ease-in-out infinite; }
|
||||||
|
.goal-agent-running .goal-agent-core { position: absolute; animation: goal-core-pulse 1.8s ease-out infinite; }
|
||||||
|
.goal-control-signal { animation: goal-signal-ping 1.4s ease-out infinite; }
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.trace-edge-active::after { animation: none; }
|
.trace-edge-active::after { animation: none; }
|
||||||
|
.goal-agent-running, .goal-agent-running .goal-agent-core, .goal-control-signal { animation: none; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { api, type GoalJob, type SettingsActor } from '../lib/api';
|
|||||||
import { Card, StatusBadge } from '../components/ui/Card';
|
import { Card, StatusBadge } from '../components/ui/Card';
|
||||||
import { TraceExplorer } from '../components/trace/TraceExplorer';
|
import { TraceExplorer } from '../components/trace/TraceExplorer';
|
||||||
import { MarkdownText } from '../components/ui/MarkdownText';
|
import { MarkdownText } from '../components/ui/MarkdownText';
|
||||||
import { ModelInteractionDiagram } from '../components/goals/ModelInteractionDiagram';
|
import { GovernedOutcomePulse, ModelInteractionDiagram } from '../components/goals/ModelInteractionDiagram';
|
||||||
|
|
||||||
const DEFAULT_ACTOR: SettingsActor = { actor: 'local-operator', role: 'org-admin', project: 'default', tenant: 'default' };
|
const DEFAULT_ACTOR: SettingsActor = { actor: 'local-operator', role: 'org-admin', project: 'default', tenant: 'default' };
|
||||||
const TERMINAL = new Set<GoalJob['status']>(['completed', 'degraded', 'failed', 'requires_approval']);
|
const TERMINAL = new Set<GoalJob['status']>(['completed', 'degraded', 'failed', 'requires_approval']);
|
||||||
@@ -69,6 +69,12 @@ export function Goals() {
|
|||||||
return current && TERMINAL.has(current.status) ? false : 1500;
|
return current && TERMINAL.has(current.status) ? false : 1500;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const traceQuery = useQuery({
|
||||||
|
queryKey: ['goal-trace', selectedId],
|
||||||
|
queryFn: () => api.traceGraph(selectedQuery.data!.trace_id),
|
||||||
|
enabled: Boolean(selectedQuery.data?.trace_id),
|
||||||
|
refetchInterval: (query) => query.state.data?.terminal ? false : 1200,
|
||||||
|
});
|
||||||
const start = useMutation({
|
const start = useMutation({
|
||||||
mutationFn: () => api.startGoal({ ...actor, project: effectiveProject }, goal.trim(), effectiveProject),
|
mutationFn: () => api.startGoal({ ...actor, project: effectiveProject }, goal.trim(), effectiveProject),
|
||||||
onSuccess: (job) => {
|
onSuccess: (job) => {
|
||||||
@@ -178,9 +184,10 @@ export function Goals() {
|
|||||||
<WorkerCard title="Cloud reviewer" subtitle="Independent critique and refinement" status={cloudStage?.status ?? 'queued'} detail={cloudStage?.detail ?? 'Waiting'} provider={cloudStage?.provider ?? selected.cloud_provider} model={cloudStage?.model ?? selected.cloud_model} />
|
<WorkerCard title="Cloud reviewer" subtitle="Independent critique and refinement" status={cloudStage?.status ?? 'queued'} detail={cloudStage?.detail ?? 'Waiting'} provider={cloudStage?.provider ?? selected.cloud_provider} model={cloudStage?.model ?? selected.cloud_model} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ModelInteractionDiagram goal={selected} localStage={localStage} cloudStage={cloudStage} />
|
<ModelInteractionDiagram goal={selected} localStage={localStage} cloudStage={cloudStage} trace={traceQuery.data} />
|
||||||
|
|
||||||
<Card title="Governed outcome" right={<StatusBadge value={selected.status} />}>
|
<Card title="Governed outcome" right={<StatusBadge value={selected.status} />}>
|
||||||
|
<GovernedOutcomePulse goal={selected} trace={traceQuery.data} />
|
||||||
{selected.workspace && <div className="mb-4 flex flex-wrap items-center gap-2 rounded-xl border border-indigo-200 bg-indigo-50 p-3 text-xs text-indigo-900"><strong>{selected.workspace.domain}</strong><span>·</span><span className="font-mono">{selected.workspace.project_id}</span>{selected.context_manifest && <><span>·</span><span>{selected.context_manifest.files} files / {selected.context_manifest.characters} chars</span>{selected.context_manifest.truncated && <StatusBadge value="bounded snapshot" />}</>}</div>}
|
{selected.workspace && <div className="mb-4 flex flex-wrap items-center gap-2 rounded-xl border border-indigo-200 bg-indigo-50 p-3 text-xs text-indigo-900"><strong>{selected.workspace.domain}</strong><span>·</span><span className="font-mono">{selected.workspace.project_id}</span>{selected.context_manifest && <><span>·</span><span>{selected.context_manifest.files} files / {selected.context_manifest.characters} chars</span>{selected.context_manifest.truncated && <StatusBadge value="bounded snapshot" />}</>}</div>}
|
||||||
<div className="rounded-xl border border-slate-200 bg-slate-50 p-4">
|
<div className="rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.12em] text-slate-400">Objective</div>
|
<div className="text-xs font-semibold uppercase tracking-[0.12em] text-slate-400">Objective</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user