From 3bec95aaf8151eac6e55f0f0be2cd37799441bc7 Mon Sep 17 00:00:00 2001 From: weeihan Date: Sat, 11 Jul 2026 16:18:39 +0800 Subject: [PATCH] feat: AI report quality check before submission Adds POST /api/incidents/ai/quality-check using Claude tool_use to score incident descriptions 1-10. Report form blocks submission when score < 6, shows amber feedback with suggestions, and offers a 'Submit anyway' override. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr --- app/api/incidents/ai/quality-check/route.ts | 57 ++++++++++++++++++++ components/incidents/report-form.tsx | 60 +++++++++++++++++++++ tests/api/incidents/quality-check.test.ts | 55 +++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 app/api/incidents/ai/quality-check/route.ts create mode 100644 tests/api/incidents/quality-check.test.ts diff --git a/app/api/incidents/ai/quality-check/route.ts b/app/api/incidents/ai/quality-check/route.ts new file mode 100644 index 0000000..a11cc75 --- /dev/null +++ b/app/api/incidents/ai/quality-check/route.ts @@ -0,0 +1,57 @@ +export const dynamic = 'force-dynamic' + +import { NextRequest, NextResponse } from 'next/server' +import { createClient } from '@/lib/supabase/server' +import { anthropic } from '@/lib/claude/client' + +export async function POST(request: NextRequest) { + const supabase = await createClient() + const { data: { user }, error: authError } = await supabase.auth.getUser() + if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const body = await request.json() as { description?: string; incident_type?: string } + if (!body.description || !body.incident_type) { + return NextResponse.json({ error: 'description and incident_type required' }, { status: 422 }) + } + + const message = await anthropic.messages.create({ + model: 'claude-opus-4-8', + thinking: { type: 'adaptive' }, + max_tokens: 1024, + tools: [{ + name: 'assess_quality', + description: 'Assess HSE incident report description quality', + input_schema: { + type: 'object' as const, + properties: { + score: { type: 'number', description: '1-10 quality score' }, + passes: { type: 'boolean', description: 'True when score is 6 or above' }, + feedback: { type: 'string', description: 'One-sentence quality summary' }, + suggestions: { + type: 'array', + items: { type: 'string' }, + description: 'Up to 3 concrete suggestions to improve the description', + }, + }, + required: ['score', 'passes', 'feedback', 'suggestions'], + }, + }], + tool_choice: { type: 'tool', name: 'assess_quality' }, + messages: [{ + role: 'user', + content: `You are an HSE reporting assistant for a Malaysian 3PL warehouse. Assess this incident report description. + +Incident type: ${body.incident_type} +Description: ${body.description} + +Score 1–10 based on: specificity (location, time, persons involved), completeness (what happened + immediate actions), and clarity. Score 6 or above passes. If score is below 6, give up to 3 actionable suggestions.`, + }], + }) + + const toolBlock = message.content.find(b => b.type === 'tool_use') + if (!toolBlock || toolBlock.type !== 'tool_use') { + return NextResponse.json({ error: 'AI assessment failed' }, { status: 500 }) + } + + return NextResponse.json(toolBlock.input) +} diff --git a/components/incidents/report-form.tsx b/components/incidents/report-form.tsx index 62e4d66..53b5a77 100644 --- a/components/incidents/report-form.tsx +++ b/components/incidents/report-form.tsx @@ -33,6 +33,13 @@ export function ReportForm({ zoneToken }: Props) { const [submitting, setSubmitting] = useState(false) const [error, setError] = useState(null) const [files, setFiles] = useState([]) + const [qualityCheck, setQualityCheck] = useState<{ + score: number + passes: boolean + feedback: string + suggestions: string[] + } | null>(null) + const [overrideQuality, setOverrideQuality] = useState(false) const [form, setForm] = useState({ incident_type: '' as IncidentType | '', @@ -47,6 +54,34 @@ export function ReportForm({ zoneToken }: Props) { setError(null) setSubmitting(true) + if (!overrideQuality) { + try { + const qcRes = await fetch('/api/incidents/ai/quality-check', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + description: form.description, + incident_type: form.incident_type, + }), + }) + if (qcRes.ok) { + const qc = await qcRes.json() as { + score: number + passes: boolean + feedback: string + suggestions: string[] + } + setQualityCheck(qc) + if (!qc.passes) { + setSubmitting(false) + return + } + } + } catch { + // Quality check failure is non-blocking — proceed with submission + } + } + try { const fd = new FormData() if (zoneToken) fd.append('zone_token', zoneToken) @@ -115,6 +150,31 @@ export function ReportForm({ zoneToken }: Props) { /> + {qualityCheck && !qualityCheck.passes && ( +
+

+ Report quality — {qualityCheck.score}/10 +

+

{qualityCheck.feedback}

+ {qualityCheck.suggestions.length > 0 && ( +
    + {qualityCheck.suggestions.map((s, i) => ( +
  • {s}
  • + ))} +
+ )} + +
+ )} +