"use client"; import { useEffect, useState } from "react"; import { MessageSquare, Search, Paperclip, Image as ImageIcon, ShieldAlert, AlertTriangle, CheckCircle2, Loader2, } from "lucide-react"; import { useWebSocket } from "@/lib/ws/context"; import { useGuilds, useMessages, useMessagesWsSync, useMessageSearch, useMessageDetail, } from "@/hooks"; import { useAmbient } from "@/components/ambient/ambient-context"; import { GlassPanel, GlassCard, Avatar, Badge, Input, Skeleton } from "@/components/primitives"; import { SectionHeader, EmptyState, ErrorState, LoadingState } from "@/components/shared"; import { GuildChannelPicker } from "@/components/shared/guild-picker"; import { renderMessageContent, getMessageChannelLabel, safeParseJsonArray, formatBytes } from "@/lib/format"; import type { AiStatus, Guild, MessageRecord } from "@/lib/types"; function relTime(ts?: number | null) { if (!ts) return ""; const d = Date.now() - ts; const m = Math.floor(d / 60000); if (m < 1) return "just now"; if (m < 60) return `${m}m`; const h = Math.floor(m / 60); if (h < 24) return `${h}h`; return `${Math.floor(h / 24)}d`; } function aiTone(s?: AiStatus | null): "signal" | "amber" | "vermilion" | "neutral" { if (s === "clean") return "signal"; if (s === "warn") return "amber"; if (s === "flagged" || s === "error") return "vermilion"; if (s === "processing" || s === "pending") return "neutral"; return "neutral"; } export function MessagesView({ initialGuilds, initialGuildId, }: { initialGuilds?: Guild[]; initialGuildId?: string | null; }) { const ws = useWebSocket(); const { data: guilds } = useGuilds(initialGuilds); const [guildId, setGuildId] = useState( initialGuildId ?? initialGuilds?.[0]?.id ?? null, ); const [channelId, setChannelId] = useState(null); const [selected, setSelected] = useState(null); const [query, setQuery] = useState(""); const { data: messages, isLoading, error } = useMessages(guildId ?? "", channelId ?? undefined); useMessagesWsSync(ws, guildId ?? ""); const search = useMessageSearch(query, query.trim().length >= 2); const detail = useMessageDetail(selected); const ambient = useAmbient(); useEffect(() => { ambient.set(query ? "amber" : "signal", 0.3, query ? "search" : "messages"); }, [query, ambient]); const searching = query.trim().length >= 2; const list = searching ? search.data ?? [] : (messages ?? []); return (
{ setGuildId(g); setChannelId(c); setSelected(null); }} />
setQuery(e.target.value)} />
{list.length} shown } /> {error && !messages ? ( ) : isLoading && !messages ? ( ) : list.length === 0 ? ( } title="No messages" description="Pick a guild to begin, or run a search." /> ) : (
{list.map((m) => ( ))}
)}
{!selected ? ( ) : detail.loading ? (
) : detail.message ? ( ) : ( )}
); } function AiBadge({ status }: { status?: AiStatus | null }) { if (!status) return null; const tone = aiTone(status); const icon = status === "clean" ? : status === "flagged" ? : status === "warn" ? : status === "processing" || status === "pending" ? : ; return {icon}{status}; } function MessageDetail({ m, attachments }: { m: MessageRecord; attachments: import("@/lib/types").AttachmentRecord[] }) { const flags = safeParseJsonArray(m.ai_moderation_flags); const cats = safeParseJsonArray(m.ai_categories); return (
{m.username}
{getMessageChannelLabel(m)} · {relTime(m.created_at)}
{renderMessageContent(m.edited_content ?? m.content, m.metadata) || "(no text)"}
{m.ai_analysis && (
AI analysis
{m.ai_analysis}
)} {(flags.length > 0 || cats.length > 0) && (
{flags.map((f) => {f})} {cats.map((c) => {c})}
)} {attachments.length > 0 && (
Attachments ({attachments.length})
{attachments.map((a) => ( {a.filename} {formatBytes(a.size)} ))}
)}
); }