feat: plan 18
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { api, ChatAction, ChatAgent, ChatAnswer, SettingsActor } from '../lib/api';
|
||||
import { api, ChatAction, ChatAgent, ChatAnswer, ChatStreamPhase, SettingsActor } from '../lib/api';
|
||||
import { Card, StatusBadge } from '../components/ui/Card';
|
||||
|
||||
const ROLES = ['viewer', 'auditor', 'operator', 'project-admin', 'org-admin'];
|
||||
@@ -10,6 +10,23 @@ function badgeValue(res: ChatAnswer | undefined, fallback = 'idle') {
|
||||
return `${res.mode} / ${res.risk}`;
|
||||
}
|
||||
|
||||
function finalToAnswer(p: ChatStreamPhase, actor: SettingsActor): ChatAnswer {
|
||||
return {
|
||||
...(p as Partial<ChatAnswer>),
|
||||
success: p.decision === 'ANSWERED',
|
||||
mode: p.mode,
|
||||
risk: (p.risk as string) ?? 'low',
|
||||
decision: p.decision,
|
||||
answer: p.answer,
|
||||
sources: p.sources ?? [],
|
||||
certified: p.certified,
|
||||
audit: p.audit ?? {},
|
||||
audit_verify: p.audit_verify ?? { ok: true, output: '' },
|
||||
router: p.router ?? {},
|
||||
actor,
|
||||
} as ChatAnswer;
|
||||
}
|
||||
|
||||
function auditHash(res: ChatAnswer) {
|
||||
return res.audit?.hash || res.audit?.record_hash || res.audit?.head || 'n/a';
|
||||
}
|
||||
@@ -32,6 +49,9 @@ export function Chat() {
|
||||
const [message, setMessage] = useState('Summarize Plan 18 MVP-0 status');
|
||||
const [last, setLast] = useState<ChatAnswer | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [streaming, setStreaming] = useState(false);
|
||||
const [draftText, setDraftText] = useState<string | null>(null);
|
||||
const [streamBusy, setStreamBusy] = useState(false);
|
||||
|
||||
const auditQuery = useQuery({
|
||||
queryKey: ['chat-audit'],
|
||||
@@ -72,6 +92,37 @@ export function Chat() {
|
||||
},
|
||||
});
|
||||
|
||||
const runStream = async () => {
|
||||
setStreamBusy(true);
|
||||
setError(null);
|
||||
setDraftText(null);
|
||||
try {
|
||||
await api.askChatStream(
|
||||
actor,
|
||||
{ message, chatId, agentId: selectedAgent?.id ?? agentId, skillId: selectedSkill, delegationLevel },
|
||||
(phase: ChatStreamPhase) => {
|
||||
if (phase.phase === 'draft') {
|
||||
setDraftText(phase.answer);
|
||||
} else {
|
||||
setDraftText(null);
|
||||
setLast(finalToAnswer(phase, actor));
|
||||
}
|
||||
},
|
||||
);
|
||||
void auditQuery.refetch();
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Stream failed');
|
||||
} finally {
|
||||
setStreamBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onAsk = () => {
|
||||
if (streaming) void runStream();
|
||||
else ask.mutate({});
|
||||
};
|
||||
const busy = ask.isPending || streamBusy;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
@@ -92,13 +143,26 @@ export function Chat() {
|
||||
<button
|
||||
type="button"
|
||||
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white disabled:bg-gray-300"
|
||||
disabled={!message.trim() || ask.isPending}
|
||||
onClick={() => ask.mutate({})}
|
||||
disabled={!message.trim() || busy}
|
||||
onClick={onAsk}
|
||||
>
|
||||
{ask.isPending ? 'Asking...' : 'Ask'}
|
||||
{busy ? 'Asking...' : 'Ask'}
|
||||
</button>
|
||||
<label className="flex items-center gap-1 text-xs text-gray-600">
|
||||
<input type="checkbox" checked={streaming} onChange={(e) => setStreaming(e.target.checked)} />
|
||||
Stream
|
||||
</label>
|
||||
<StatusBadge value="read-only" />
|
||||
</div>
|
||||
{draftText && (
|
||||
<div className="rounded border border-orange-200 bg-orange-50 p-3 text-sm text-gray-700">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<StatusBadge value="draft" />
|
||||
<span className="text-xs text-orange-700">UNCERTIFIED — awaiting H4/certify</span>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap leading-6">{draftText}</div>
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700">{error}</div>}
|
||||
</div>
|
||||
|
||||
@@ -171,6 +235,11 @@ export function Chat() {
|
||||
<StatusBadge value={last.mode} />
|
||||
<StatusBadge value={last.risk} />
|
||||
<StatusBadge value={last.decision} />
|
||||
{last.synthesis && (
|
||||
<StatusBadge value={last.synthesis.mode === 'model'
|
||||
? `model: ${last.synthesis.provider ?? 'provider'}`
|
||||
: 'deterministic'} />
|
||||
)}
|
||||
{last.agent_binding && <StatusBadge value={last.agent_binding.agent_selected} />}
|
||||
{last.agent_binding && <StatusBadge value={`L${last.agent_binding.delegation_level}`} />}
|
||||
{last.loop_run && <StatusBadge value={last.loop_run.draft_certified ? 'loop certified' : 'loop held'} />}
|
||||
|
||||
Reference in New Issue
Block a user