Skip to content

Commit 22a5644

Browse files
committed
2 parents 9d6c314 + b1ca709 commit 22a5644

2 files changed

Lines changed: 287 additions & 1 deletion

File tree

app/general-chat/page.module.css

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,130 @@
4848
gap: 16px;
4949
}
5050

51+
.statusRow {
52+
display: grid;
53+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
54+
gap: 12px;
55+
}
56+
57+
.statusCard {
58+
background: var(--panel-3);
59+
border: 1px solid var(--border);
60+
border-radius: 14px;
61+
padding: 12px;
62+
box-shadow: var(--shadow);
63+
display: flex;
64+
flex-direction: column;
65+
gap: 10px;
66+
}
67+
68+
.cardHeader {
69+
display: flex;
70+
justify-content: space-between;
71+
align-items: center;
72+
gap: 8px;
73+
}
74+
75+
.cardTitle {
76+
font-weight: 700;
77+
}
78+
79+
.cardMeta {
80+
display: flex;
81+
justify-content: space-between;
82+
color: var(--muted);
83+
font-size: 12px;
84+
}
85+
86+
.pill {
87+
background: var(--accent-2);
88+
color: #0b1020;
89+
padding: 4px 10px;
90+
border-radius: 999px;
91+
font-weight: 700;
92+
font-size: 12px;
93+
border: 1px solid rgba(255, 255, 255, 0.12);
94+
}
95+
96+
.pillMuted {
97+
background: rgba(255, 255, 255, 0.05);
98+
color: var(--muted);
99+
}
100+
101+
.progressTrack {
102+
width: 100%;
103+
height: 10px;
104+
border-radius: 12px;
105+
background: var(--panel-2);
106+
border: 1px solid var(--border);
107+
overflow: hidden;
108+
}
109+
110+
.progressFill {
111+
height: 100%;
112+
background: linear-gradient(90deg, var(--accent), var(--accent-2));
113+
border-radius: 12px;
114+
}
115+
116+
.flagList {
117+
list-style: none;
118+
padding: 0;
119+
margin: 0;
120+
display: flex;
121+
flex-direction: column;
122+
gap: 8px;
123+
}
124+
125+
.flagItem,
126+
.flagItemMuted {
127+
display: flex;
128+
gap: 10px;
129+
align-items: flex-start;
130+
}
131+
132+
.flagItemMuted {
133+
color: var(--muted);
134+
font-size: 13px;
135+
}
136+
137+
.flagPill {
138+
padding: 4px 10px;
139+
border-radius: 10px;
140+
font-size: 12px;
141+
font-weight: 700;
142+
border: 1px solid var(--border);
143+
min-width: 90px;
144+
text-align: center;
145+
}
146+
147+
.flagReason {
148+
margin: 0;
149+
font-weight: 600;
150+
}
151+
152+
.flagMessage {
153+
margin: 2px 0 0;
154+
color: var(--muted);
155+
font-size: 13px;
156+
}
157+
158+
.auditList {
159+
display: flex;
160+
flex-direction: column;
161+
gap: 6px;
162+
}
163+
164+
.auditRow {
165+
display: flex;
166+
gap: 8px;
167+
align-items: center;
168+
}
169+
170+
.auditText {
171+
color: var(--muted);
172+
font-size: 13px;
173+
}
174+
51175
.chatPanel {
52176
background: var(--panel);
53177
border: 1px solid var(--border);
@@ -110,6 +234,30 @@
110234
border-color: rgba(125, 211, 252, 0.35);
111235
}
112236

237+
.low {
238+
color: #befae7;
239+
border-color: rgba(16, 185, 129, 0.6);
240+
background: rgba(16, 185, 129, 0.08);
241+
}
242+
243+
.medium {
244+
color: #fde68a;
245+
border-color: rgba(251, 191, 36, 0.7);
246+
background: rgba(251, 191, 36, 0.08);
247+
}
248+
249+
.high {
250+
color: #fecdd3;
251+
border-color: rgba(248, 113, 113, 0.8);
252+
background: rgba(248, 113, 113, 0.12);
253+
}
254+
255+
.audit {
256+
background: rgba(125, 211, 252, 0.12);
257+
border-color: rgba(125, 211, 252, 0.35);
258+
color: var(--accent);
259+
}
260+
113261
.content {
114262
margin: 4px 0 0;
115263
line-height: 1.6;

app/general-chat/page.tsx

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
import { useEffect, useMemo, useState } from "react";
44
import 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

712
const 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+
3243
function 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

Comments
 (0)