diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 764a522..047be5f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,21 +1,12 @@ import { useEffect, useRef, useState } from "react"; -import { listMessages } from "./api/client"; +import { listMessages, reanalyzeMessage } from "./api/client"; import { connectDashboardSocket } from "./ws/client"; -import type { DashboardEvent } from "./ws/client"; - -interface MessageItem { - id: string; - channel_id: string; - user_id: string; - username: string; - avatar_url: string | null; - content: string; - created_at: number; - type: "text" | "edited" | "deleted"; -} +import type { DashboardEvent, MessageRecord } from "./api/client"; +import { MessageFeed } from "./components/messages/MessageFeed"; +import { ReviewPanel } from "./components/review/ReviewPanel"; export default function App() { - const [messages, setMessages] = useState([]); + const [messages, setMessages] = useState([]); const [wsStatus, setWsStatus] = useState("connecting"); const wsRef = useRef(null); @@ -72,6 +63,29 @@ export default function App() { }; }, []); + const handleReanalyze = async (id: string) => { + // Optimistic update + setMessages((prev) => + prev.map((m) => + m.id === id + ? { ...m, ai_status: "pending" as const, ai_error: null, ai_analysis: null } + : m, + ), + ); + + try { + await reanalyzeMessage(id); + } catch (err) { + console.error("Reanalyze failed:", err); + // Revert optimistic update on failure + setMessages((prev) => + prev.map((m) => + m.id === id ? { ...m, ai_status: "error" as const, ai_error: "Reanalyze failed" } : m, + ), + ); + } + }; + return (
@@ -88,38 +102,10 @@ export default function App() {
-
- {messages.length === 0 ? ( -

No messages yet

- ) : ( - messages.map((msg) => ( -
- {msg.username} -
- {msg.username} - - {new Date(msg.created_at).toLocaleString()} - - {msg.type === "deleted" && ( - [deleted] - )} -

{msg.content}

-
-
- )) - )} -
+
-
-
Review placeholder
-
+
); diff --git a/frontend/src/components/messages/MessageCard.tsx b/frontend/src/components/messages/MessageCard.tsx new file mode 100644 index 0000000..400644b --- /dev/null +++ b/frontend/src/components/messages/MessageCard.tsx @@ -0,0 +1,73 @@ +import type { MessageRecord } from "../../api/client"; + +export interface MessageCardProps { + message: MessageRecord; + onReanalyze: (id: string) => void; +} + +const STATUS_COLORS: Record = { + pending: "#f9e2af", + clean: "#a6e3a1", + warn: "#fab387", + flagged: "#f38ba8", + error: "#f38ba8", +}; + +export function MessageCard({ message, onReanalyze }: MessageCardProps) { + const displayContent = message.edited_content ?? message.content; + const aiStatus = message.ai_status ?? "pending"; + const statusColor = STATUS_COLORS[aiStatus] ?? "#6c7086"; + + return ( +
+ {message.username} +
+
+ {message.username} + + {new Date(message.created_at).toLocaleString()} + + {message.type === "edited" && ( + edited + )} + {message.type === "deleted" && ( + deleted + )} + + {aiStatus} + +
+ +

{displayContent}

+ + {message.ai_analysis && ( +
{message.ai_analysis}
+ )} + + {message.ai_error && ( +
{message.ai_error}
+ )} + +
+ +
+
+
+ ); +} diff --git a/frontend/src/components/messages/MessageFeed.tsx b/frontend/src/components/messages/MessageFeed.tsx new file mode 100644 index 0000000..99df5e1 --- /dev/null +++ b/frontend/src/components/messages/MessageFeed.tsx @@ -0,0 +1,25 @@ +import type { MessageRecord } from "../../api/client"; +import { MessageCard } from "./MessageCard"; + +export interface MessageFeedProps { + messages: MessageRecord[]; + onReanalyze: (id: string) => void; +} + +export function MessageFeed({ messages, onReanalyze }: MessageFeedProps) { + if (messages.length === 0) { + return ( +
+

No messages yet

+
+ ); + } + + return ( +
+ {messages.map((msg) => ( + + ))} +
+ ); +} diff --git a/frontend/src/components/review/ReviewPanel.tsx b/frontend/src/components/review/ReviewPanel.tsx new file mode 100644 index 0000000..e69348a --- /dev/null +++ b/frontend/src/components/review/ReviewPanel.tsx @@ -0,0 +1,37 @@ +import type { MessageRecord } from "../../api/client"; +import { MessageCard } from "../messages/MessageCard"; + +export interface ReviewPanelProps { + messages: MessageRecord[]; + onReanalyze: (id: string) => void; +} + +export function ReviewPanel({ messages, onReanalyze }: ReviewPanelProps) { + const reviewItems = messages.filter( + (m) => + m.ai_status === "warn" || + m.ai_status === "flagged" || + m.ai_status === "error", + ); + + return ( +
+
+

Needs Review

+ {reviewItems.length} +
+ + {reviewItems.length === 0 ? ( +
+

No items to review

+
+ ) : ( +
+ {reviewItems.map((msg) => ( + + ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 01bd8b3..a1234dc 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -166,6 +166,174 @@ body { flex-shrink: 0; } +/* Message Card */ +.message-card { + display: flex; + gap: 12px; + padding: 12px; + background: #181825; + border-radius: 6px; + border: 1px solid #313244; +} + +.message-card.type-deleted { + opacity: 0.6; +} + +.message-card-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + flex-shrink: 0; +} + +.message-card-body { + flex: 1; + min-width: 0; +} + +.message-card-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.message-card-username { + font-weight: 600; + color: #89b4fa; +} + +.message-card-time { + font-size: 11px; + color: #6c7086; +} + +.badge { + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + font-weight: 500; + color: #1e1e2e; +} + +.badge-edited { + background: #fab387; + color: #1e1e2e; +} + +.badge-deleted { + background: #f38ba8; + color: #1e1e2e; +} + +.message-card-content { + margin-top: 6px; + font-size: 13px; + color: #bac2de; + white-space: pre-wrap; + word-break: break-word; +} + +.message-card-analysis { + margin-top: 8px; + padding: 8px; + background: #1e1e2e; + border-radius: 4px; + font-size: 12px; + color: #a6e3a1; + white-space: pre-wrap; +} + +.message-card-error { + margin-top: 8px; + padding: 8px; + background: #1e1e2e; + border-radius: 4px; + font-size: 12px; + color: #f38ba8; +} + +.message-card-actions { + margin-top: 8px; +} + +.btn-reanalyze { + padding: 4px 10px; + font-size: 11px; + border: none; + border-radius: 4px; + background: #313244; + color: #cdd6f4; + cursor: pointer; +} + +.btn-reanalyze:hover:not(:disabled) { + background: #45475a; +} + +.btn-reanalyze:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Message Feed */ +.message-feed { + display: flex; + flex-direction: column; + gap: 8px; +} + +/* Review Panel */ +.review-panel { + border-top: 1px solid #313244; + background: #181825; + flex-shrink: 0; + max-height: 250px; + overflow-y: auto; +} + +.review-header { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + border-bottom: 1px solid #313244; +} + +.review-header h2 { + font-size: 13px; + font-weight: 600; + color: #cdd6f4; +} + +.review-count { + font-size: 11px; + padding: 2px 6px; + border-radius: 10px; + background: #f38ba8; + color: #1e1e2e; + font-weight: 600; +} + +.review-list { + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px; +} + +.review-placeholder { + padding: 16px; + font-size: 12px; + color: #6c7086; +} + +.review-panel .empty-state { + padding: 16px; +} + +/* Keep existing styles */ .review-placeholder { padding: 16px; font-size: 12px;