22
33import { useEffect , useMemo , useState } from "react" ;
44import styles from "./page.module.css" ;
5- import { AugmentedThread , ChatMessage , User } from "@/lib/general-chat/models" ;
5+ import {
6+ AugmentedThread ,
7+ ChatMessage ,
8+ ModerationFlag ,
9+ User ,
10+ } from "@/lib/general-chat/models" ;
611
712const demoUser : User = {
813 userId : "student-001" ,
@@ -29,6 +34,12 @@ function verificationBadge(status: ChatMessage["verifiedStatus"]) {
2934 return "Unverified" ;
3035}
3136
37+ function moderationBadge ( severity : ModerationFlag [ "severity" ] ) {
38+ if ( severity === "high" ) return "High risk" ;
39+ if ( severity === "medium" ) return "Needs review" ;
40+ return "Low risk" ;
41+ }
42+
3243function messageTone ( message : ChatMessage ) {
3344 if ( message . messageType === "system_warning" ) return styles . warning ;
3445 if ( message . messageType === "ai_verification" ) return styles . ai ;
@@ -86,6 +97,40 @@ export default function GeneralChatPage() {
8697
8798 const officialRules = useMemo ( ( ) => thread ?. officialRules ?? [ ] , [ thread ] ) ;
8899
100+ const moderationFlags = useMemo ( ( ) => thread ?. moderationFlags ?? [ ] , [ thread ] ) ;
101+
102+ const studentMessages = useMemo (
103+ ( ) => thread ?. messages . filter ( ( m ) => m . messageType === "student_answer" ) ?? [ ] ,
104+ [ thread ]
105+ ) ;
106+
107+ const verifiedMessages = useMemo (
108+ ( ) => studentMessages . filter ( ( m ) => m . verifiedStatus !== "unverified" ) ,
109+ [ studentMessages ]
110+ ) ;
111+
112+ const verificationCoverage = useMemo ( ( ) => {
113+ if ( studentMessages . length === 0 ) return 0 ;
114+ return Math . round ( ( verifiedMessages . length / studentMessages . length ) * 100 ) ;
115+ } , [ studentMessages , verifiedMessages ] ) ;
116+
117+ const messageLookup = useMemo ( ( ) => {
118+ const lookup : Record < string , ChatMessage > = { } ;
119+ thread ?. messages . forEach ( ( m ) => {
120+ lookup [ m . messageId ] = m ;
121+ } ) ;
122+ return lookup ;
123+ } , [ thread ] ) ;
124+
125+ const moderationByMessage = useMemo ( ( ) => {
126+ const lookup : Record < string , ModerationFlag [ ] > = { } ;
127+ moderationFlags . forEach ( ( flag ) => {
128+ lookup [ flag . messageId ] = lookup [ flag . messageId ] || [ ] ;
129+ lookup [ flag . messageId ] . push ( flag ) ;
130+ } ) ;
131+ return lookup ;
132+ } , [ moderationFlags ] ) ;
133+
89134 async function refreshThread ( threadId : string ) {
90135 const res = await fetch ( `/api/chat/thread?threadId=${ threadId } ` ) ;
91136 if ( res . ok ) {
@@ -197,6 +242,86 @@ export default function GeneralChatPage() {
197242 </ div >
198243 </ header >
199244
245+ < section className = { styles . statusRow } >
246+ < div className = { styles . statusCard } >
247+ < div className = { styles . cardHeader } >
248+ < span className = { styles . cardTitle } > Verification coverage</ span >
249+ < span className = { styles . pill } > { verificationCoverage } %</ span >
250+ </ div >
251+ < p className = { styles . small } > AI reviews every student answer as a separate immutable post.</ p >
252+ < div className = { styles . progressTrack } >
253+ < div
254+ className = { styles . progressFill }
255+ style = { { width : `${ verificationCoverage } %` } }
256+ />
257+ </ div >
258+ < div className = { styles . cardMeta } >
259+ < span > { verifiedMessages . length } verified</ span >
260+ < span >
261+ { studentMessages . length - verifiedMessages . length } awaiting verification
262+ </ span >
263+ </ div >
264+ </ div >
265+ < div className = { styles . statusCard } >
266+ < div className = { styles . cardHeader } >
267+ < span className = { styles . cardTitle } > Moderation</ span >
268+ < span className = { `${ styles . pill } ${ styles . pillMuted } ` } >
269+ { moderationFlags . length } flag{ moderationFlags . length === 1 ? "" : "s" }
270+ </ span >
271+ </ div >
272+ < p className = { styles . small } >
273+ Content Safety scans run automatically; high-risk posts trigger system warnings.
274+ </ p >
275+ < ul className = { styles . flagList } >
276+ { moderationFlags . slice ( - 3 ) . reverse ( ) . map ( ( flag ) => {
277+ const message = messageLookup [ flag . messageId ] ;
278+ return (
279+ < li key = { flag . flagId } className = { styles . flagItem } >
280+ < span className = { `${ styles . flagPill } ${ styles [ flag . severity ] } ` } >
281+ { moderationBadge ( flag . severity ) }
282+ </ span >
283+ < div >
284+ < p className = { styles . flagReason } > { flag . reason } </ p >
285+ { message && (
286+ < p className = { styles . flagMessage } >
287+ “{ message . content . slice ( 0 , 110 ) } { message . content . length > 110 ? "…" : "" } ”
288+ </ p >
289+ ) }
290+ </ div >
291+ </ li >
292+ ) ;
293+ } ) }
294+ { moderationFlags . length === 0 && (
295+ < li className = { styles . flagItemMuted } > No moderation events yet.</ li >
296+ ) }
297+ </ ul >
298+ </ div >
299+ < div className = { styles . statusCard } >
300+ < div className = { styles . cardHeader } >
301+ < span className = { styles . cardTitle } > Auditable trail</ span >
302+ < span className = { `${ styles . pill } ${ styles . pillMuted } ` } >
303+ { thread ?. verifications . length ?? 0 } AI posts
304+ </ span >
305+ </ div >
306+ < p className = { styles . small } >
307+ AI verification messages are stored separately so student edits never override safety and reference history.
308+ </ p >
309+ < div className = { styles . auditList } >
310+ { ( thread ?. verifications . slice ( - 2 ) ?? [ ] ) . map ( ( verification ) => (
311+ < div key = { verification . verificationId } className = { styles . auditRow } >
312+ < span className = { `${ styles . flagPill } ${ styles . audit } ` } > { verification . verificationResult } </ span >
313+ < span className = { styles . auditText } >
314+ Sources: { verification . officialSourceIds . join ( ", " ) || "n/a" }
315+ </ span >
316+ </ div >
317+ ) ) }
318+ { ( thread ?. verifications . length ?? 0 ) === 0 && (
319+ < p className = { styles . flagItemMuted } > Verification feed will appear here.</ p >
320+ ) }
321+ </ div >
322+ </ div >
323+ </ section >
324+
200325 < section className = { styles . grid } >
201326 < div className = { styles . chatPanel } >
202327 < div className = { styles . banner } >
@@ -222,6 +347,14 @@ export default function GeneralChatPage() {
222347 < span className = { styles . status } >
223348 { verificationBadge ( message . verifiedStatus ) }
224349 </ span >
350+ { moderationByMessage [ message . messageId ] ?. map ( ( flag ) => (
351+ < span
352+ key = { flag . flagId }
353+ className = { `${ styles . status } ${ styles [ flag . severity ] } ` }
354+ >
355+ { moderationBadge ( flag . severity ) }
356+ </ span >
357+ ) ) }
225358 </ div >
226359 < p className = { styles . content } > { message . content } </ p >
227360 < div className = { styles . footer } >
@@ -231,6 +364,11 @@ export default function GeneralChatPage() {
231364 AI result: { verificationsByMessage [ message . messageId ] }
232365 </ span >
233366 ) }
367+ { moderationByMessage [ message . messageId ] ?. [ 0 ] && (
368+ < span className = { styles . reference } >
369+ Moderation: { moderationByMessage [ message . messageId ] ?. [ 0 ] ?. reason }
370+ </ span >
371+ ) }
234372 </ div >
235373 { message . messageType === "student_answer" && (
236374 < div className = { styles . actions } >
0 commit comments