Refactor voice page and hooks to use React Query for data fetching
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:
asepharyana
2026-07-26 16:55:20 +07:00
parent 323fa207af
commit 05bbae66da
20 changed files with 980 additions and 1582 deletions
@@ -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>
);
}