Implements phase 1 of AI_PIPELINE_HARDENING_PLAN.md. Every Anthropic call now goes through one module that owns retry, timeout, abort, structured- output parsing, schema validation, and best-effort call telemetry. * src/lib/llm.js — single callLLM entry point. Resolves model per tier (fast / standard / reasoning) with admin:model legacy fallback for the standard tier; 60s default timeout via AbortController; balanced-brace JSON extraction; LLMHttpError, LLMTruncatedError, LLMOutputError, and LLMValidationError surface clearly distinct failure modes. * src/lib/llmRetry.js — exponential backoff with full jitter, retries only on transient HTTP statuses, honours Retry-After up to 60s, never retries on AbortError. * src/lib/llmSchemas.js — Zod schemas for every structured task plus normalizeHandbookResult (collapses legacy "executes" relations into the canonical "executed_by" vocabulary). * src/lib/api.js — thin shim over callLLM so existing callers (extraction pipeline, learning, quiz, R42, knowledge graph) keep working unchanged. * src/lib/__tests__/ — 32 Vitest cases covering parse paths, error surfaces, simulation mode, model resolution, and schema validation. * src/pages/Admin/index.jsx — three model inputs (fast / standard / reasoning) replacing the single legacy field; legacy value falls back for the standard tier so existing overrides survive. Adds Zod and Vitest, plus an "npm run test" script. Also cleans up the pre-existing repo-wide ESLint failures so phase 1's "npm run lint passes" acceptance criterion can be checked: drops unused React imports across the JSX tree (React 19 JSX runtime auto-imports), attaches cause to rethrown errors in the service modules, ignores pb_migrations in the ESLint config (PocketBase JSVM globals), and removes one dead handleCreateCustom function in Leren.jsx. A real behaviour bug surfaced in Testen.jsx — the quiz timer captured a stale finishQuiz via setInterval closure; now updated via finishQuizRef so the timer always invokes the latest callback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
90 lines
3.1 KiB
JavaScript
90 lines
3.1 KiB
JavaScript
import { useEffect, useState } from 'react';
|
|
import { Check, X, Clock, Sparkles } from 'lucide-react';
|
|
import { kbStore } from '../../lib/kbStore';
|
|
import Button from '../ui/Button';
|
|
|
|
/**
|
|
* Admin sub-panel inside the Knowledge Graph view. Shows pending R42 chatbot
|
|
* suggestions with approve / reject controls.
|
|
*/
|
|
export default function SuggestionsQueue({ onApplied }) {
|
|
const [pending, setPending] = useState([]);
|
|
|
|
const refresh = () => setPending(kbStore.listSuggestions('pending'));
|
|
|
|
useEffect(() => {
|
|
refresh();
|
|
const onChange = () => refresh();
|
|
window.addEventListener('respellion:kb-updated', onChange);
|
|
return () => window.removeEventListener('respellion:kb-updated', onChange);
|
|
}, []);
|
|
|
|
if (pending.length === 0) {
|
|
return (
|
|
<div className="text-xs text-fg-muted flex items-center gap-2">
|
|
<Sparkles size={14} /> Geen openstaande voorstellen van R42.
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="text-xs text-fg-muted uppercase tracking-wider font-mono flex items-center gap-2">
|
|
<Sparkles size={14} /> R42-voorstellen ({pending.length})
|
|
</div>
|
|
{pending.map(s => (
|
|
<div key={s.id} className="bg-bg rounded-[var(--r-sm)] border border-bg-warm p-3 text-sm">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="text-xs text-fg-muted flex items-center gap-2">
|
|
<Clock size={12} />
|
|
{new Date(s.ts).toLocaleString()}
|
|
{s.proposedByName && <> · door {s.proposedByName}</>}
|
|
</div>
|
|
</div>
|
|
{s.reason && <p className="mb-2 text-fg">{s.reason}</p>}
|
|
{(s.topics?.length > 0 || s.relations?.length > 0) && (
|
|
<ul className="text-xs space-y-1 mb-3">
|
|
{s.topics?.map(t => (
|
|
<li key={`t-${t.id}`}>
|
|
<strong>{t.label}</strong>{' '}
|
|
<span className="text-fg-muted">({t.type})</span>
|
|
{t.description && <> — {t.description}</>}
|
|
</li>
|
|
))}
|
|
{s.relations?.map((r, i) => (
|
|
<li key={`r-${i}`}>
|
|
<code className="text-teal">{r.source}</code>{' → '}
|
|
<span className="text-fg-muted">{r.type}</span>{' → '}
|
|
<code className="text-teal">{r.target}</code>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
<div className="flex gap-2">
|
|
<Button
|
|
onClick={async () => {
|
|
await kbStore.approveSuggestion(s.id);
|
|
refresh();
|
|
onApplied?.();
|
|
}}
|
|
className="text-xs py-1 px-3 flex items-center gap-1"
|
|
>
|
|
<Check size={14} /> Goedkeuren
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
kbStore.rejectSuggestion(s.id);
|
|
refresh();
|
|
}}
|
|
className="text-xs py-1 px-3 flex items-center gap-1"
|
|
>
|
|
<X size={14} /> Afwijzen
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|