'use client'; import { useState } from 'react'; import { useRouter } from 'next/navigation'; import type { NovelQueueItem } from '@/lib/db/queries'; interface Props { queue: NovelQueueItem[]; } interface LabelFormState { tag: string; signature: string; description: string; correction: string; addGolden: boolean; } function EvidenceHighlight({ text, span }: { text: string; span: string }) { if (!span || !text.includes(span)) { return {text}; } const idx = text.indexOf(span); return ( {text.slice(0, idx)} {span} {text.slice(idx + span.length)} ); } function QueueCard({ item, onProcessed }: { item: NovelQueueItem; onProcessed: () => void }) { const [busy, setBusy] = useState(false); const [expanded, setExpanded] = useState(false); const [form, setForm] = useState({ tag: '', signature: item.evidenceSpan, description: '', correction: '', addGolden: false, }); const [msg, setMsg] = useState(null); const label = async () => { if (!form.tag.trim() || !form.signature.trim()) return; if (!item.conceptId) { setMsg('No concept linked — cannot label.'); return; } setBusy(true); setMsg(null); const res = await fetch('/api/admin/misconceptions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'label', queueId: item.id, conceptId: item.conceptId, tag: form.tag.trim(), signature: form.signature.trim(), description: form.description.trim() || undefined, correction: form.correction.trim() || undefined, addGolden: form.addGolden, goldenData: form.addGolden ? { responseText: item.responseText, expectedTag: form.tag.trim(), conceptId: item.conceptId, } : undefined, }), }); setBusy(false); if (res.ok) { onProcessed(); } else { setMsg('Label failed — check server logs.'); } }; const dismiss = async () => { setBusy(true); await fetch('/api/admin/misconceptions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'dismiss', queueId: item.id }), }); setBusy(false); onProcessed(); }; return (
{item.conceptName ?? 'Unknown concept'} {new Date(item.createdAt).toLocaleDateString()}

{item.checkpointPrompt}

{item.existingMisconceptions.length > 0 && (
Existing misconceptions for this concept ({item.existingMisconceptions.length})
    {item.existingMisconceptions.map((m) => (
  • {m.tag} — {m.signature}
  • ))}
)} {expanded && (
{msg &&

{msg}

}
)}
); } export default function MisconceptionsClient({ queue }: Props) { const router = useRouter(); const [items, setItems] = useState(queue); const markProcessed = (id: string) => { setItems((prev) => prev.filter((i) => i.id !== id)); router.refresh(); }; return (

Novel Misconceptions Queue

{items.length === 0 ? 'Queue is empty — all novel responses have been reviewed.' : `${items.length} unprocessed response${items.length !== 1 ? 's' : ''} — label or dismiss each.`}

{items.map((item) => ( markProcessed(item.id)} /> ))}
); } const s = { heading: { fontFamily: 'var(--font-display)', fontSize: '1.5rem', fontWeight: 500, color: 'var(--color-ink)', marginBottom: 'var(--space-2)' }, sub: { fontFamily: 'var(--font-display)', fontSize: '0.9375rem', color: 'var(--color-ink-muted)', marginBottom: 'var(--space-8)' }, card: { border: '1px solid var(--color-border)', borderRadius: '6px', padding: 'var(--space-5)', marginBottom: 'var(--space-4)' }, cardHead: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 'var(--space-3)', marginBottom: 'var(--space-3)', flexWrap: 'wrap' as const }, conceptTag: { fontFamily: 'var(--font-display)', fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-accent)', textTransform: 'uppercase' as const, letterSpacing: '0.06em' }, timestamp: { fontFamily: 'var(--font-display)', fontSize: '0.75rem', color: 'var(--color-ink-faint)', marginLeft: 'var(--space-3)' }, actionRow: { display: 'flex', gap: 'var(--space-2)' }, checkpointPrompt: { fontFamily: 'var(--font-body)', fontSize: '0.9375rem', color: 'var(--color-ink-muted)', fontStyle: 'italic', marginBottom: 'var(--space-3)' }, responsePre: { fontFamily: 'var(--font-body)', fontSize: '0.9375rem', lineHeight: 1.65, color: 'var(--color-ink)', background: 'var(--color-surface)', border: '1px solid var(--color-border-subtle)', borderRadius: '4px', padding: 'var(--space-3)', marginBottom: 'var(--space-3)' }, responseText: { whiteSpace: 'pre-wrap' as const }, evidenceMark: { background: 'var(--color-misconception-subtle)', color: 'var(--color-misconception)', borderBottom: '2px solid var(--color-misconception)', padding: '0 2px', borderRadius: '2px' }, details: { marginBottom: 'var(--space-3)' }, summary: { fontFamily: 'var(--font-display)', fontSize: '0.8125rem', color: 'var(--color-ink-muted)', cursor: 'pointer' }, miscList: { listStyle: 'none', padding: 0, margin: 'var(--space-2) 0 0' }, miscItem: { fontFamily: 'var(--font-display)', fontSize: '0.8125rem', color: 'var(--color-ink-muted)', marginBottom: 'var(--space-1)' }, labelForm: { borderTop: '1px solid var(--color-border-subtle)', paddingTop: 'var(--space-4)', marginTop: 'var(--space-3)', display: 'flex', flexDirection: 'column' as const, gap: 'var(--space-3)' }, fieldLabel: { display: 'flex', flexDirection: 'column' as const, gap: 'var(--space-1)', fontFamily: 'var(--font-display)', fontSize: '0.8125rem', color: 'var(--color-ink-muted)' }, input: { fontFamily: 'var(--font-display)', fontSize: '0.9375rem', color: 'var(--color-ink)', background: 'var(--color-surface)', border: '1px solid var(--color-border)', borderRadius: '4px', padding: 'var(--space-2) var(--space-3)', width: '100%' }, error: { color: 'var(--color-misconception)', fontSize: '0.875rem' }, labelBtn: { padding: 'var(--space-1) var(--space-3)', background: 'var(--color-accent-subtle)', color: 'var(--color-accent)', border: '1px solid var(--color-accent)', borderRadius: '4px', cursor: 'pointer', fontSize: '0.8125rem' }, dismissBtn: { padding: 'var(--space-1) var(--space-3)', background: 'var(--color-surface)', color: 'var(--color-ink-muted)', border: '1px solid var(--color-border)', borderRadius: '4px', cursor: 'pointer', fontSize: '0.8125rem' }, saveBtn: { alignSelf: 'flex-start', padding: 'var(--space-2) var(--space-5)', background: 'var(--color-accent)', color: 'var(--color-on-accent)', border: 'none', borderRadius: '4px', cursor: 'pointer', fontFamily: 'var(--font-display)', fontSize: '0.9375rem', fontWeight: 500 }, } as const;