Files
Curio/src/app/admin/reports/reports-client.tsx
T
arnaudne 5bf6013460 feat: full feature buildout — streaming, i18n, mastery map, admin, jobs
Progressive lesson streaming via onSegment callback (fixes SSE for non-English
users — locale was shadowed in lesson-reader useEffect). Adds: BullMQ workers,
Redis stream buffer, token budget enforcement, Langfuse tracing, golden-eval
runner, Playwright e2e scaffolding, lesson depth/locale/preferences schema,
mastery map UI, admin panel (blueprints/users/reports/quality/misconceptions),
image queries, source citations, view transitions, reading animations, i18n
(next-intl), PDF export, surprise endpoint, and 402 passing unit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-08 22:08:14 +02:00

82 lines
3.5 KiB
TypeScript

'use client';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
type Report = {
id: string;
checkpointId: string;
gradeId: string | null;
reason: string;
notes: string | null;
createdAt: Date;
intentKey: string;
blueprintTitle: string;
segmentOrd: number;
depth: string;
locale: string;
};
export default function AdminReportsClient({ reports }: { reports: Report[] }) {
const router = useRouter();
const [busy, setBusy] = useState<string | null>(null);
const act = async (id: string, action: 'dismiss' | 're-verify') => {
setBusy(id);
await fetch(`/api/admin/reports/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action }),
});
setBusy(null);
router.refresh();
};
return (
<div>
<h1 style={s.heading}>Content reports</h1>
{reports.length === 0 ? (
<p style={{ color: 'var(--color-ink-muted)' }}>No pending reports.</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
{reports.map((r) => (
<div key={r.id} style={s.card}>
<div style={s.cardMeta}>
<span style={s.tag}>{r.reason}</span>
<span style={s.date}>{new Date(r.createdAt).toLocaleDateString()}</span>
</div>
{r.notes && <p style={s.notes}>{r.notes}</p>}
<Link
href={`/learn/${r.intentKey}?depth=${r.depth}`}
target="_blank"
rel="noopener noreferrer"
style={s.lessonLink}
>
{r.blueprintTitle} · segment {r.segmentOrd + 1}
</Link>
<p style={s.checkpointMeta}>checkpoint {r.checkpointId.slice(0, 8)}</p>
<div style={{ display: 'flex', gap: 'var(--space-2)', marginTop: 'var(--space-3)' }}>
<button disabled={busy === r.id} onClick={() => act(r.id, 'dismiss')} style={s.btn}>Dismiss</button>
<button disabled={busy === r.id} onClick={() => act(r.id, 're-verify')} style={s.btn}>Flag for re-verify</button>
</div>
</div>
))}
</div>
)}
</div>
);
}
const s = {
heading: { fontFamily: 'var(--font-display)', fontSize: '1.5rem', fontWeight: 500, color: 'var(--color-ink)', marginBottom: 'var(--space-6)' },
card: { padding: 'var(--space-5)', border: '1px solid var(--color-border)', borderRadius: '4px', display: 'flex', flexDirection: 'column' as const, gap: 'var(--space-2)' },
cardMeta: { display: 'flex', gap: 'var(--space-3)', alignItems: 'center' },
tag: { fontFamily: 'var(--font-display)', fontSize: '0.75rem', fontWeight: 600, padding: '2px 8px', background: 'var(--color-accent-subtle)', color: 'var(--color-accent)', borderRadius: '3px' },
date: { fontSize: '0.8125rem', color: 'var(--color-ink-faint)', fontFamily: 'var(--font-display)' },
notes: { fontSize: '0.9375rem', color: 'var(--color-ink-muted)' },
lessonLink: { fontFamily: 'var(--font-display)', fontSize: '0.9375rem', fontWeight: 500, color: 'var(--color-accent)', textDecoration: 'none' },
checkpointMeta: { fontSize: '0.75rem', color: 'var(--color-ink-faint)', fontFamily: 'var(--font-display)' },
btn: { padding: 'var(--space-1) var(--space-4)', background: 'var(--color-surface)', border: '1px solid var(--color-border)', borderRadius: '4px', cursor: 'pointer', fontFamily: 'var(--font-display)', fontSize: '0.875rem', color: 'var(--color-ink)' },
} as const;