feat(analytics): add AI stats, attachment stats, and moderation actions queries to useAnalytics hook

This commit is contained in:
MythEclipse
2026-06-03 18:17:25 +07:00
parent f1bc36d6d8
commit caa9ecca63
2 changed files with 61 additions and 20 deletions
@@ -27,7 +27,7 @@ export function ActivityChart({ hourly, loading }: ActivityChartProps) {
return { return {
hour: `${String(jakartaHour).padStart(2, "0")}:00`, hour: `${String(jakartaHour).padStart(2, "0")}:00`,
clean: b.clean, clean: b.clean,
warned: 0, warned: b.warned,
flagged: b.flagged, flagged: b.flagged,
error: b.error, error: b.error,
total: b.count, total: b.count,
@@ -46,8 +46,9 @@ export function ActivityChart({ hourly, loading }: ActivityChartProps) {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-3"> <div className="space-y-3">
<div className="grid grid-cols-3 gap-2 text-[10px] uppercase tracking-wider text-muted-foreground"> <div className="grid grid-cols-4 gap-2 text-[10px] uppercase tracking-wider text-muted-foreground">
<span>Clean</span> <span>Clean</span>
<span>Warned</span>
<span>Flagged</span> <span>Flagged</span>
<span>Error</span> <span>Error</span>
</div> </div>
@@ -55,6 +56,7 @@ export function ActivityChart({ hourly, loading }: ActivityChartProps) {
{data.map((bucket) => { {data.map((bucket) => {
const total = Math.max(bucket.total, 1); const total = Math.max(bucket.total, 1);
const clean = bucket.clean / total; const clean = bucket.clean / total;
const warned = bucket.warned / total;
const flagged = bucket.flagged / total; const flagged = bucket.flagged / total;
const error = bucket.error / total; const error = bucket.error / total;
return ( return (
@@ -70,9 +72,13 @@ export function ActivityChart({ hourly, loading }: ActivityChartProps) {
</div> </div>
<div className="flex h-3 overflow-hidden rounded-full bg-sky-50"> <div className="flex h-3 overflow-hidden rounded-full bg-sky-50">
<div <div
className="bg-gradient-to-r from-primary to-pink-300" className="bg-gradient-to-r from-primary to-teal-300"
style={{ width: `${clean * 100}%` }} style={{ width: `${clean * 100}%` }}
/> />
<div
className="bg-yellow-400/60"
style={{ width: `${warned * 100}%` }}
/>
<div <div
className="bg-accent/70" className="bg-accent/70"
style={{ width: `${flagged * 100}%` }} style={{ width: `${flagged * 100}%` }}
@@ -1,17 +1,23 @@
import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { useCallback, useEffect } from "react"; import { useCallback, useEffect } from "react";
import type { import type {
AIStats,
AnalyticsOverview, AnalyticsOverview,
AttachmentStats,
HeatmapCell, HeatmapCell,
HourlyBucket, HourlyBucket,
ModerationActionRecord,
TopicTrend, TopicTrend,
TrendBucket, TrendBucket,
UserStat, UserStat,
ViolatorStat, ViolatorStat,
} from "../../../shared/api/client"; } from "../../../shared/api/client";
import { import {
fetchAIStats,
fetchAnalyticsOverview, fetchAnalyticsOverview,
fetchAttachmentStats,
fetchHeatmap, fetchHeatmap,
fetchModerationActions,
fetchTrend, fetchTrend,
fetchViolators, fetchViolators,
} from "../../../shared/api/client"; } from "../../../shared/api/client";
@@ -21,23 +27,15 @@ function analyticsKeys(
channelId: string | undefined, channelId: string | undefined,
hours: number, hours: number,
) { ) {
const base = [guildId, channelId ?? "", hours] as const;
return { return {
overview: [ overview: ["analytics", "overview", ...base] as const,
"analytics", violators: ["analytics", "violators", ...base] as const,
"overview", trend: ["analytics", "trend", ...base] as const,
guildId, heatmap: ["analytics", "heatmap", ...base] as const,
channelId ?? "", aiStats: ["analytics", "ai-stats", ...base] as const,
hours, attachmentStats: ["analytics", "attachment-stats", ...base] as const,
] as const, moderationActions: ["analytics", "moderation-actions", ...base] as const,
violators: [
"analytics",
"violators",
guildId,
channelId ?? "",
hours,
] as const,
trend: ["analytics", "trend", guildId, channelId ?? "", hours] as const,
heatmap: ["analytics", "heatmap", guildId, channelId ?? "", hours] as const,
}; };
} }
@@ -86,6 +84,30 @@ export function useAnalytics({
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
}); });
const aiStatsQuery = useQuery({
queryKey: keys.aiStats,
queryFn: () => fetchAIStats({ guildId, channelId, hours }),
enabled: !!guildId,
staleTime: 30_000,
placeholderData: keepPreviousData,
});
const attachmentStatsQuery = useQuery({
queryKey: keys.attachmentStats,
queryFn: () => fetchAttachmentStats({ guildId, channelId, hours }),
enabled: !!guildId,
staleTime: 30_000,
placeholderData: keepPreviousData,
});
const moderationActionsQuery = useQuery({
queryKey: keys.moderationActions,
queryFn: () => fetchModerationActions({ guildId, channelId, hours, limit: 50 }),
enabled: !!guildId,
staleTime: 30_000,
placeholderData: keepPreviousData,
});
const refresh = useCallback(() => { const refresh = useCallback(() => {
if (!guildId) return; if (!guildId) return;
window.dispatchEvent(new CustomEvent("analytics_refresh")); window.dispatchEvent(new CustomEvent("analytics_refresh"));
@@ -94,7 +116,6 @@ export function useAnalytics({
useEffect(() => { useEffect(() => {
const handler = () => { const handler = () => {
if (!guildId) return; if (!guildId) return;
// Use queryClient.invalidateQueries from the React Query internals
window.dispatchEvent(new CustomEvent("analytics_force_refresh")); window.dispatchEvent(new CustomEvent("analytics_force_refresh"));
}; };
window.addEventListener("analytics_refresh", handler); window.addEventListener("analytics_refresh", handler);
@@ -128,6 +149,17 @@ export function useAnalytics({
heatmapLoading: heatmapQuery.isLoading && !heatmapQuery.data, heatmapLoading: heatmapQuery.isLoading && !heatmapQuery.data,
heatmapFetching: heatmapQuery.isFetching && !heatmapQuery.isLoading, heatmapFetching: heatmapQuery.isFetching && !heatmapQuery.isLoading,
aiStats: aiStatsQuery.data ?? null,
aiStatsLoading: aiStatsQuery.isLoading && !aiStatsQuery.data,
attachmentStats: attachmentStatsQuery.data ?? null,
attachmentStatsLoading:
attachmentStatsQuery.isLoading && !attachmentStatsQuery.data,
moderationActions: moderationActionsQuery.data ?? [],
moderationActionsLoading:
moderationActionsQuery.isLoading && !moderationActionsQuery.data,
hourly: overview?.hourly ?? ([] as HourlyBucket[]), hourly: overview?.hourly ?? ([] as HourlyBucket[]),
topics: overview?.topics ?? ([] as TopicTrend[]), topics: overview?.topics ?? ([] as TopicTrend[]),
topUsers: overview?.top_users ?? ([] as UserStat[]), topUsers: overview?.top_users ?? ([] as UserStat[]),
@@ -139,9 +171,12 @@ export function useAnalytics({
} }
export type { export type {
AIStats,
AnalyticsOverview, AnalyticsOverview,
AttachmentStats,
HeatmapCell, HeatmapCell,
HourlyBucket, HourlyBucket,
ModerationActionRecord,
TopicTrend, TopicTrend,
TrendBucket, TrendBucket,
UserStat, UserStat,