import { Filter, Search, X } from "lucide-react"; import { useMemo, useState } from "react"; import type { Channel, Guild, MessageRecord } from "../../shared/api/client"; import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Select, Tabs, TabsContent, TabsList, TabsTrigger, } from "../../shared/ui"; import { ImageGrid } from "./components/ImageGrid"; import { MessageFeed } from "./components/MessageFeed"; interface MessagesPanelProps { guilds: Guild[]; channels: Channel[]; selectedGuild: string; selectedChannel: string; messages: MessageRecord[]; onGuildChange: (guildId: string) => void; onChannelChange: (channelId: string) => void; onReanalyze: (id: string) => Promise; onLoadMore?: () => void; hasMore?: boolean; loadingMore?: boolean; } type AiFilter = "all" | "clean" | "warn" | "flagged" | "error" | "pending"; export function MessagesPanel({ guilds, channels, selectedGuild, selectedChannel, messages, onGuildChange, onChannelChange, onReanalyze, 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 handleSearch = async () => { if (!searchQuery.trim()) { setSearchResults([]); setShowSearch(false); return; } setIsSearching(true); try { const params = new URLSearchParams({ q: searchQuery, ...(selectedChannel && { channelId: selectedChannel }), 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, warn: base.filter((m) => m.ai_status === "warn").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 (
Message Source Pick a guild and channel/thread to inspect captures. onChannelChange(e.target.value)} placeholder="Select channel or thread" options={channels.map((c) => ({ value: c.id, label: c.name }))} /> {stats.total > 0 && (
{stats.total} total{hasMore && !showSearch ? "+" : ""} {stats.clean} clean {stats.warn} warn {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 && ( )}
{( [ "all", "clean", "warn", "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
); }