import { Filter, RotateCw, Search, X } from "lucide-react"; import { useMemo, useState } from "react"; import type { MessageRecord } from "../../shared/api/client"; import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Input, Tabs, TabsContent, TabsList, TabsTrigger, } from "../../shared/ui"; import { ImageGrid } from "./components/ImageGrid"; import { MessageFeed } from "./components/MessageFeed"; interface MessagesPanelProps { guildName: string | null; messages: MessageRecord[]; onReanalyze: (id: string) => Promise; onReanalyzeAllErrors?: () => Promise; onLoadMore?: () => void; hasMore?: boolean; loadingMore?: boolean; } type AiFilter = "all" | "clean" | "flagged" | "error" | "pending"; export function MessagesPanel({ guildName, messages, onReanalyze, onReanalyzeAllErrors, onLoadMore, hasMore, loadingMore, }: MessagesPanelProps) { const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [showSearch, setShowSearch] = useState(false); const [aiFilter, setAiFilter] = useState("all"); const [viewTab, setViewTab] = useState<"all" | "images">("all"); const [retryingAll, setRetryingAll] = useState(false); const [retriedCount, setRetriedCount] = useState(null); const handleSearch = async () => { if (!searchQuery.trim()) { setSearchResults([]); setShowSearch(false); return; } setIsSearching(true); try { const params = new URLSearchParams({ q: searchQuery, limit: "50" }); const response = await fetch(`/api/analysis/search?${params}`); if (!response.ok) throw new Error("Search failed"); const data = await response.json(); setSearchResults(data.results || []); setShowSearch(true); } catch { setSearchResults([]); } finally { setIsSearching(false); } }; const stats = useMemo(() => { const base = showSearch ? searchResults : messages; return { total: base.length, clean: base.filter((m) => m.ai_status === "clean").length, flagged: base.filter((m) => m.ai_status === "flagged").length, error: base.filter((m) => m.ai_status === "error").length, pending: base.filter((m) => m.ai_status === "pending" || !m.ai_status) .length, deleted: base.filter((m) => m.deleted_at).length, edited: base.filter((m) => m.edited_at).length, }; }, [messages, searchResults, showSearch]); const filteredMessages = useMemo(() => { const base = showSearch ? searchResults : messages; if (aiFilter === "all") return base; return base.filter((m) => { const status = m.ai_status ?? "pending"; if (aiFilter === "pending") return status === "pending" || status === null || status === undefined; return status === aiFilter; }); }, [messages, searchResults, showSearch, aiFilter]); return (
Messages {guildName && (

Monitoring all text channels in{" "} {guildName}

)}

Messages are automatically captured from all text channels in the monitored guild. Real-time updates arrive via WebSocket.

{stats.total > 0 && (
{stats.total} total{hasMore && !showSearch ? "+" : ""} {stats.clean} clean {stats.flagged} flagged {stats.error} error {stats.pending} pending {stats.deleted > 0 && ( {stats.deleted} deleted )} {stats.edited > 0 && ( {stats.edited} edited )}
)}
setSearchQuery(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} disabled={isSearching} />
{showSearch && ( )} {stats.error > 0 && onReanalyzeAllErrors && ( )} {retriedCount !== null && ( {retriedCount} message{retriedCount !== 1 ? "s" : ""} queued for re-analysis )}
{( [ "all", "clean", "flagged", "error", "pending", ] as AiFilter[] ).map((f) => ( ))}
{showSearch && searchResults.length > 0 && (
Found {searchResults.length} result {searchResults.length !== 1 ? "s" : ""}
)} setViewTab(v as "all" | "images")} > {showSearch ? `Search (${filteredMessages.length})` : `All (${filteredMessages.length})`} Images
); }