"use client"; import { AlertTriangle, CheckCircle2, Image as ImageIcon, Loader2, MessageSquare, Paperclip, Search, ShieldAlert, } from "lucide-react"; import { useEffect, useState } from "react"; import { useAmbient } from "@/components/ambient/ambient-context"; import { Avatar, Badge, GlassPanel, Input, Skeleton, } from "@/components/primitives"; import { EmptyState, ErrorState, LoadingState, SectionHeader, } from "@/components/shared"; import { GuildChannelPicker } from "@/components/shared/guild-picker"; import { useLoadMore, useMessageDetail, useMessageSearch, useMessages, useMessagesHasMore, useMessagesWsSync, } from "@/hooks"; import { formatBytes, getMessageChannelLabel, renderMessageContent, safeParseJsonArray, } from "@/lib/format"; import type { AiStatus, Guild, MessageRecord } from "@/lib/types"; import { useWebSocket } from "@/lib/ws/context"; 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 [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); // Cursor to the next (older) page + whether more history exists. const { data: pageInfo } = useMessagesHasMore(guildId ?? "", channelId ?? undefined); const nextCursor = pageInfo?.cursor ?? null; const hasMore = pageInfo?.hasMore ?? false; const loadMore = useLoadMore(); 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." /> ) : (
{!searching && (
{hasMore ? ( ) : ( messages && messages.length > 0 && ( beginning of history ) )}
)}
{ // Auto-load older messages when the user scrolls to the top. if (searching || !hasMore || loadMore.isPending) return; if (e.currentTarget.scrollTop <= 8) { loadMore.mutate({ guildId: guildId ?? "", channelId: channelId ?? undefined, cursor: nextCursor ?? "", }); } }} > {list.map((m) => ( ))}
)}
{!selected ? ( ) : detail.loading ? (
) : detail.message ? ( ) : ( )}
); } function AiBadge({ status, durationMs, }: { status?: AiStatus | null; durationMs?: number | null; }) { if (!status) return null; const tone = aiTone(status); const icon = status === "clean" ? ( ) : status === "flagged" ? ( ) : status === "warn" ? ( ) : status === "processing" || status === "pending" ? ( ) : ( ); const label = durationMs && durationMs > 0 ? `${status} · ${formatDuration(durationMs)}` : status; return ( {icon} {label} ); } /** Human-readable analysis duration, e.g. 850ms / 1.2s / 3.4s. */ function formatDuration(ms: number): string { if (ms < 1000) return `${Math.round(ms)}ms`; return `${(ms / 1000).toFixed(1)}s`; } 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)} ))}
)}
); }