Refactor voice page and hooks to use React Query for data fetching
Deploy to VPS / deploy (push) Successful in 2m57s
Deploy to VPS / deploy (push) Successful in 2m57s
- Updated VoicePage component to utilize useVoiceConnect, useVoiceDisconnect, and useMicTransmit mutations. - Replaced local state management with React Query's useQuery for voice status, guilds, and channels. - Removed custom useAsync hook and replaced it with useQuery in useConfig, useStats, useUsers, useChannels, and useMessages hooks. - Simplified useRecordings and useSpeakers hooks to leverage React Query for data fetching and mutations. - Removed deprecated use-async hook and related code. - Enhanced error handling and loading states across various hooks.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Loader2, RefreshCw, Search, Sparkles } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { EmptyState, LoadingSkeleton } from "@/components/shared";
|
||||
@@ -9,30 +10,30 @@ import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { useSearch } from "@/hooks";
|
||||
import { useReanalyze } from "@/hooks";
|
||||
import { messagesApi } from "@/lib/api";
|
||||
import { safeParseJsonArray } from "@/lib/format";
|
||||
import type { MessageRecord } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function AnalysisPage() {
|
||||
const { results, searching, search } = useSearch();
|
||||
const [query, setQuery] = useState("");
|
||||
const [searched, setSearched] = useState(false);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const reanalyzeMut = useReanalyze();
|
||||
|
||||
const { data: results, isFetching } = useQuery<MessageRecord[]>({
|
||||
queryKey: ["analysis-search", query],
|
||||
queryFn: async () => {
|
||||
const result = await messagesApi.search(query, 50);
|
||||
return result.results;
|
||||
},
|
||||
enabled,
|
||||
});
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
if (!query.trim()) return;
|
||||
setSearched(true);
|
||||
search(query);
|
||||
}, [query, search]);
|
||||
|
||||
const handleReanalyze = useCallback(async (id: string) => {
|
||||
const { messagesApi } = await import("@/lib/api");
|
||||
try {
|
||||
await messagesApi.reanalyze(id);
|
||||
} catch (err) {
|
||||
console.error("analysis/reanalyze:", err);
|
||||
}
|
||||
}, []);
|
||||
setEnabled(true);
|
||||
}, [query]);
|
||||
|
||||
return (
|
||||
<div className="space-y-5 animate-fade-in-up">
|
||||
@@ -47,20 +48,19 @@ export default function AnalysisPage() {
|
||||
className="pl-9 h-9"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleSearch} disabled={!query.trim() || searching}>
|
||||
{searching && <Loader2 className="size-4 animate-spin mr-1.5" />}
|
||||
<Button onClick={handleSearch} disabled={!query.trim() || isFetching}>
|
||||
{isFetching && <Loader2 className="size-4 animate-spin mr-1.5" />}
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{searching ? (
|
||||
{isFetching ? (
|
||||
<LoadingSkeleton count={5} height="h-28" />
|
||||
) : results !== null ? (
|
||||
) : results !== undefined ? (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Found {results.length} result{results.length !== 1 ? "s" : ""}
|
||||
</p>
|
||||
|
||||
{results.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={Search}
|
||||
@@ -69,16 +69,87 @@ export default function AnalysisPage() {
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{results.map((msg) => (
|
||||
<SearchResultCard
|
||||
key={msg.id}
|
||||
message={msg}
|
||||
onReanalyze={handleReanalyze}
|
||||
/>
|
||||
<Card key={msg.id}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Avatar className="size-8 shrink-0 mt-0.5">
|
||||
<AvatarImage src={msg.avatar_url ?? undefined} />
|
||||
<AvatarFallback className="text-xs">
|
||||
{msg.username.charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0 space-y-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-medium">
|
||||
{msg.username}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(msg.created_at).toLocaleString()}
|
||||
</span>
|
||||
{msg.ai_status && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"text-[10px] px-1.5 py-0 h-4",
|
||||
msg.ai_status === "clean" && "text-green-500",
|
||||
msg.ai_status === "flagged" && "text-red-500",
|
||||
)}
|
||||
>
|
||||
{msg.ai_status}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed">{msg.content}</p>
|
||||
{msg.ai_moderation_flags &&
|
||||
msg.ai_moderation_flags !== "[]" && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{safeParseJsonArray(msg.ai_moderation_flags).map(
|
||||
(flag) => (
|
||||
<Badge
|
||||
key={flag}
|
||||
variant="destructive"
|
||||
className="text-[10px] px-1.5 py-0 h-4"
|
||||
>
|
||||
{flag}
|
||||
</Badge>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{msg.ai_analysis && (
|
||||
<p className="text-xs text-muted-foreground italic line-clamp-2 leading-relaxed">
|
||||
<Sparkles className="size-3 inline mr-1" />
|
||||
{msg.ai_analysis}
|
||||
</p>
|
||||
)}
|
||||
{msg.ai_confidence != null && (
|
||||
<div className="flex items-center gap-2 max-w-40">
|
||||
<Progress
|
||||
value={msg.ai_confidence * 100}
|
||||
className="h-1.5"
|
||||
/>
|
||||
<span className="text-[11px] text-muted-foreground tabular-nums shrink-0">
|
||||
{(msg.ai_confidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => reanalyzeMut.mutate(msg.id)}
|
||||
>
|
||||
<RefreshCw className="size-3 mr-1" />
|
||||
Reanalyze
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : !searched ? (
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-center">
|
||||
<Search className="size-12 text-muted-foreground/30 mb-4" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -88,92 +159,7 @@ export default function AnalysisPage() {
|
||||
Searches message content, AI flags, and analysis text.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchResultCard({
|
||||
message: msg,
|
||||
onReanalyze,
|
||||
}: {
|
||||
message: MessageRecord;
|
||||
onReanalyze: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Avatar className="size-8 shrink-0 mt-0.5">
|
||||
<AvatarImage src={msg.avatar_url ?? undefined} />
|
||||
<AvatarFallback className="text-xs">
|
||||
{msg.username.charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="flex-1 min-w-0 space-y-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-medium">{msg.username}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(msg.created_at).toLocaleString()}
|
||||
</span>
|
||||
{msg.ai_status && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"text-[10px] px-1.5 py-0 h-4",
|
||||
msg.ai_status === "clean" && "text-green-500",
|
||||
msg.ai_status === "flagged" && "text-red-500",
|
||||
)}
|
||||
>
|
||||
{msg.ai_status}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm leading-relaxed">{msg.content}</p>
|
||||
|
||||
{msg.ai_moderation_flags && msg.ai_moderation_flags !== "[]" && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{safeParseJsonArray(msg.ai_moderation_flags).map((flag) => (
|
||||
<Badge
|
||||
key={flag}
|
||||
variant="destructive"
|
||||
className="text-[10px] px-1.5 py-0 h-4"
|
||||
>
|
||||
{flag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{msg.ai_analysis && (
|
||||
<p className="text-xs text-muted-foreground italic line-clamp-2 leading-relaxed">
|
||||
<Sparkles className="size-3 inline mr-1" />
|
||||
{msg.ai_analysis}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{msg.ai_confidence != null && (
|
||||
<div className="flex items-center gap-2 max-w-40">
|
||||
<Progress value={msg.ai_confidence * 100} className="h-1.5" />
|
||||
<span className="text-[11px] text-muted-foreground tabular-nums shrink-0">
|
||||
{(msg.ai_confidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => onReanalyze(msg.id)}
|
||||
>
|
||||
<RefreshCw className="size-3 mr-1" />
|
||||
Reanalyze
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user