feat: add searchMessages function and corresponding API endpoint for message queries

This commit is contained in:
MythEclipse
2026-05-18 06:39:12 +07:00
parent 69ba7b497f
commit f3c915eacd
11 changed files with 299 additions and 38 deletions
@@ -1,7 +1,8 @@
import { RotateCw } from "lucide-react";
import { RotateCw, AlertCircle, CheckCircle2, AlertTriangle } from "lucide-react";
import type { MessageRecord } from "../../types/messages";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { useState } from "react";
export interface MessageCardProps {
message: MessageRecord;
@@ -15,9 +16,27 @@ function aiVariant(status: string) {
return "secondary";
}
function getAiIcon(status: string) {
if (status === "clean") return <CheckCircle2 className="h-4 w-4" />;
if (status === "warn") return <AlertTriangle className="h-4 w-4" />;
if (status === "flagged") return <AlertCircle className="h-4 w-4" />;
if (status === "error") return <AlertCircle className="h-4 w-4" />;
return null;
}
export function MessageCard({ message, onReanalyze }: MessageCardProps) {
const displayContent = message.edited_content ?? message.content;
const aiStatus = message.ai_status ?? "pending";
const [isReanalyzing, setIsReanalyzing] = useState(false);
const handleReanalyze = async () => {
setIsReanalyzing(true);
try {
onReanalyze(message.id);
} finally {
setIsReanalyzing(false);
}
};
return (
<article className="rounded-2xl border border-border bg-card p-4 shadow-sm">
@@ -30,20 +49,45 @@ export function MessageCard({ message, onReanalyze }: MessageCardProps) {
<div className="min-w-0 flex-1 space-y-3">
<div className="flex flex-wrap items-center gap-2">
<span className="font-medium">{message.username || message.user_id}</span>
<span className="text-xs text-muted-foreground">{new Date(message.created_at).toLocaleString()}</span>
<span className="text-xs text-muted-foreground">
{new Date(message.created_at).toLocaleString()}
</span>
{message.edited_at ? <Badge variant="outline">edited</Badge> : null}
{message.deleted_at ? <Badge variant="destructive">deleted</Badge> : null}
<Badge variant={aiVariant(aiStatus)}>{aiStatus}</Badge>
<Badge variant={aiVariant(aiStatus)} className="flex items-center gap-1">
{getAiIcon(aiStatus)}
{aiStatus}
</Badge>
</div>
<p className="whitespace-pre-wrap break-words text-sm leading-6 text-foreground/90">
{displayContent || "(empty message)"}
</p>
{message.ai_analysis ? <div className="rounded-xl bg-muted p-3 text-sm text-muted-foreground">{message.ai_analysis}</div> : null}
{message.ai_error ? <div className="rounded-xl bg-destructive/10 p-3 text-sm text-destructive">AI error: {message.ai_error}</div> : null}
<Button size="sm" variant="outline" onClick={() => onReanalyze(message.id)} disabled={aiStatus === "pending"}>
<RotateCw className="h-3.5 w-3.5" />
Re-analyze
</Button>
{message.ai_analysis ? (
<div className="rounded-xl bg-muted p-3 text-sm text-muted-foreground">
{message.ai_analysis}
</div>
) : null}
{message.ai_error ? (
<div className="rounded-xl bg-destructive/10 p-3 text-sm text-destructive">
AI error: {message.ai_error}
</div>
) : null}
<div className="flex gap-2">
<Button
size="sm"
variant={aiStatus === "error" ? "destructive" : "outline"}
onClick={handleReanalyze}
disabled={aiStatus === "pending" || isReanalyzing}
>
<RotateCw className={`h-3.5 w-3.5 ${isReanalyzing ? "animate-spin" : ""}`} />
{isReanalyzing ? "Reanalyzing..." : "Re-analyze"}
</Button>
{aiStatus === "error" && (
<span className="text-xs text-destructive self-center">
Click to retry analysis
</span>
)}
</div>
</div>
</div>
</article>
@@ -1,3 +1,4 @@
import { useState } from "react";
import type { Channel, Guild } from "../../types/voice";
import type { MessageRecord } from "../../types/messages";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
@@ -5,6 +6,8 @@ import { Select } from "../ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import { ImageGrid } from "./ImageGrid";
import { MessageFeed } from "./MessageFeed";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
interface MessagesPanelProps {
guilds: Guild[];
@@ -27,6 +30,40 @@ export function MessagesPanel({
onChannelChange,
onReanalyze,
}: MessagesPanelProps) {
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<MessageRecord[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [showSearch, setShowSearch] = useState(false);
const handleSearch = async () => {
if (!searchQuery.trim()) {
setSearchResults([]);
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 || []);
} catch (error) {
console.error("Search error:", error);
setSearchResults([]);
} finally {
setIsSearching(false);
}
};
const displayMessages = showSearch ? searchResults : messages;
return (
<div className="grid gap-6">
<Card>
@@ -49,16 +86,67 @@ export function MessagesPanel({
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Search Messages</CardTitle>
<CardDescription>Search for messages by content</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="flex gap-2">
<Input
placeholder="Search message content..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
disabled={isSearching}
/>
<Button onClick={handleSearch} disabled={isSearching || !searchQuery.trim()}>
{isSearching ? "Searching..." : "Search"}
</Button>
{showSearch && (
<Button
variant="outline"
onClick={() => {
setShowSearch(false);
setSearchResults([]);
setSearchQuery("");
}}
>
Clear
</Button>
)}
</div>
{showSearch && searchResults.length > 0 && (
<div className="text-sm text-muted-foreground">
Found {searchResults.length} result{searchResults.length !== 1 ? "s" : ""}
</div>
)}
</CardContent>
</Card>
<Tabs defaultValue="all">
<TabsList>
<TabsTrigger value="all">All Messages</TabsTrigger>
<TabsTrigger value="all">
{showSearch ? "Search Results" : "All Messages"}
</TabsTrigger>
<TabsTrigger value="images">Images</TabsTrigger>
</TabsList>
<TabsContent value="all">
<MessageFeed messages={messages} onReanalyze={onReanalyze} emptyText={selectedChannel ? "No captures yet." : "Select a channel to view captures."} />
<MessageFeed
messages={displayMessages}
onReanalyze={onReanalyze}
emptyText={
showSearch
? "No messages found matching your search."
: selectedChannel
? "No captures yet."
: "Select a channel to view captures."
}
/>
</TabsContent>
<TabsContent value="images">
<ImageGrid messages={messages} />
<ImageGrid messages={displayMessages} />
</TabsContent>
</Tabs>
</div>