feat: update dependencies and improve dashboard functionality
Deploy to VPS / deploy (push) Failing after 1m43s
Deploy to VPS / deploy (push) Failing after 1m43s
- Added new dependencies for Next.js and lucide-react in pnpm-workspace.yaml. - Refactored DashboardPage component to improve readability and error handling. - Enhanced Header component to display error status with an alert icon. - Updated MobileTabBar and Sidebar components to use a centralized tabs definition. - Improved ChannelsView in dashboard-panel to handle channel fetching more cleanly. - Fixed ActiveSpeaker type to use camelCase for userId. - Updated MessagesPanel to handle guildId checks more gracefully. - Adjusted API calls in dashboard and messages to align with backend expectations. - Refined type definitions across various interfaces for consistency and clarity.
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2, RefreshCw } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { voiceApi } from "@/lib/api";
|
||||
import { useAppConfig } from "@/lib/hooks/use-config";
|
||||
import type { Guild } from "@/lib/types";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { DashboardPanel } from "@/features/dashboard/dashboard-panel";
|
||||
import { LivePanel } from "@/features/live/live-panel";
|
||||
import { MessagesPanel } from "@/features/messages/messages-panel";
|
||||
import { voiceApi } from "@/lib/api";
|
||||
import { useAppConfig } from "@/lib/hooks/use-config";
|
||||
import type { Guild } from "@/lib/types";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -45,12 +45,17 @@ export default function DashboardPage() {
|
||||
if (!cancelled) setGuilds(g);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) setGuildsError(err instanceof Error ? err.message : "Failed to load guilds");
|
||||
if (!cancelled)
|
||||
setGuildsError(
|
||||
err instanceof Error ? err.message : "Failed to load guilds",
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setGuildsLoading(false);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Resolve guild ID once config and guilds are loaded
|
||||
@@ -80,9 +85,15 @@ export default function DashboardPage() {
|
||||
onRetry={() => {
|
||||
setGuildsLoading(true);
|
||||
setGuildsError(null);
|
||||
voiceApi.getGuilds().then(setGuilds).catch(
|
||||
(err) => setGuildsError(err instanceof Error ? err.message : "Failed to load guilds"),
|
||||
).finally(() => setGuildsLoading(false));
|
||||
voiceApi
|
||||
.getGuilds()
|
||||
.then(setGuilds)
|
||||
.catch((err) =>
|
||||
setGuildsError(
|
||||
err instanceof Error ? err.message : "Failed to load guilds",
|
||||
),
|
||||
)
|
||||
.finally(() => setGuildsLoading(false));
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -90,12 +101,8 @@ export default function DashboardPage() {
|
||||
{isReady ? (
|
||||
<>
|
||||
{tab === "live" && <LivePanel />}
|
||||
{tab === "dashboard" && (
|
||||
<DashboardPanel guildId={selectedGuildId} />
|
||||
)}
|
||||
{tab === "messages" && (
|
||||
<MessagesPanel guildId={selectedGuildId} />
|
||||
)}
|
||||
{tab === "dashboard" && <DashboardPanel guildId={selectedGuildId} />}
|
||||
{tab === "messages" && <MessagesPanel guildId={selectedGuildId} />}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
@@ -165,7 +172,10 @@ function GuildBar({
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-lg border p-3">
|
||||
<label htmlFor="guild-select" className="text-sm font-medium text-muted-foreground whitespace-nowrap">
|
||||
<label
|
||||
htmlFor="guild-select"
|
||||
className="text-sm font-medium text-muted-foreground whitespace-nowrap"
|
||||
>
|
||||
Guild:
|
||||
</label>
|
||||
<select
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import Script from "next/script";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
@@ -29,16 +30,9 @@ export default function RootLayout({
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
try {
|
||||
const theme = localStorage.getItem('theme') || 'dark';
|
||||
document.documentElement.classList.add(theme);
|
||||
} catch(e) {}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<Script id="theme-script" strategy="beforeInteractive">
|
||||
{`try{const t=localStorage.getItem('theme')||'dark';document.documentElement.classList.add(t)}catch(e){}`}
|
||||
</Script>
|
||||
</head>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Moon, Sun, Wifi, WifiOff } from "lucide-react";
|
||||
import { AlertCircle, Moon, Sun, Wifi, WifiOff } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useWebSocket } from "@/lib/ws/context";
|
||||
|
||||
@@ -37,6 +37,11 @@ export function Header() {
|
||||
<Wifi className="size-3 text-yellow-500" />
|
||||
<span className="hidden sm:inline">Connecting</span>
|
||||
</>
|
||||
) : status === "error" ? (
|
||||
<>
|
||||
<AlertCircle className="size-3 text-destructive" />
|
||||
<span className="hidden sm:inline">Error</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<WifiOff className="size-3 text-destructive" />
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { LayoutDashboard, MessageSquare, Radio } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const tabs = [
|
||||
{ id: "messages", label: "Messages", icon: MessageSquare },
|
||||
{ id: "live", label: "Live", icon: Radio },
|
||||
{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
|
||||
] as const;
|
||||
|
||||
type TabId = (typeof tabs)[number]["id"];
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { type TabId, tabs } from "@/lib/tabs";
|
||||
|
||||
export function MobileTabBar({ activeTab }: { activeTab: TabId }) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
return (
|
||||
<nav className="md:hidden fixed bottom-0 inset-x-0 z-10 border-t bg-background">
|
||||
@@ -21,7 +14,11 @@ export function MobileTabBar({ activeTab }: { activeTab: TabId }) {
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => router.push(`/dashboard?tab=${id}`)}
|
||||
onClick={() => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set("tab", id);
|
||||
router.push(`/dashboard?${params}`);
|
||||
}}
|
||||
data-active={activeTab === id ? "" : undefined}
|
||||
className="flex-1 flex flex-col items-center gap-0.5 py-2 text-xs font-medium text-muted-foreground data-[active]:text-primary transition-colors"
|
||||
>
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { LayoutDashboard, MessageSquare, Radio } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const tabs = [
|
||||
{ id: "messages", label: "Messages", icon: MessageSquare },
|
||||
{ id: "live", label: "Live", icon: Radio },
|
||||
{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
|
||||
] as const;
|
||||
|
||||
type TabId = (typeof tabs)[number]["id"];
|
||||
import { Radio } from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { type TabId, tabs } from "@/lib/tabs";
|
||||
|
||||
export function Sidebar({ activeTab }: { activeTab: TabId }) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const handleTabClick = (tabId: TabId) => {
|
||||
router.push(`/dashboard?tab=${tabId}`);
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set("tab", tabId);
|
||||
router.push(`/dashboard?${params}`);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Shield,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { dashboardApi } from "@/lib/api";
|
||||
import type {
|
||||
@@ -123,7 +124,7 @@ export function DashboardPanel({ guildId }: { guildId: string }) {
|
||||
|
||||
// ── Stats View ────────────────────────────────────────────
|
||||
|
||||
function StatsView({ onNavigate }: { onNavigate: (view: View) => void }) {
|
||||
function StatsView(_props: { onNavigate: (view: View) => void }) {
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -166,8 +167,8 @@ function StatsView({ onNavigate }: { onNavigate: (view: View) => void }) {
|
||||
{/* Metric cards */}
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="rounded-lg border p-4 space-y-2">
|
||||
{Array.from({ length: 8 }, (_, i) => `stat-sk-${i}`).map((key) => (
|
||||
<div key={key} className="rounded-lg border p-4 space-y-2">
|
||||
<div className="h-3 w-16 bg-muted rounded animate-pulse" />
|
||||
<div className="h-8 w-20 bg-muted rounded animate-pulse" />
|
||||
</div>
|
||||
@@ -339,8 +340,8 @@ function UsersView({
|
||||
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="rounded-lg border p-4 space-y-2">
|
||||
{Array.from({ length: 6 }, (_, i) => `user-sk-${i}`).map((key) => (
|
||||
<div key={key} className="rounded-lg border p-4 space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-10 rounded-full bg-muted animate-pulse" />
|
||||
<div className="flex-1 space-y-1">
|
||||
@@ -362,9 +363,11 @@ function UsersView({
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-10 shrink-0 rounded-full bg-muted flex items-center justify-center text-sm font-medium overflow-hidden">
|
||||
{user.avatar_url ? (
|
||||
<img
|
||||
<Image
|
||||
src={user.avatar_url}
|
||||
alt=""
|
||||
width={40}
|
||||
height={40}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
@@ -407,21 +410,24 @@ function ChannelsView({
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const fetchChannels = useCallback(async (searchQuery?: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await dashboardApi.listChannels(
|
||||
20,
|
||||
searchQuery,
|
||||
guildId || undefined,
|
||||
);
|
||||
setChannels(result.data);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [guildId]);
|
||||
const fetchChannels = useCallback(
|
||||
async (searchQuery?: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await dashboardApi.listChannels(
|
||||
20,
|
||||
searchQuery,
|
||||
guildId || undefined,
|
||||
);
|
||||
setChannels(result.data);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[guildId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchChannels();
|
||||
@@ -450,8 +456,8 @@ function ChannelsView({
|
||||
|
||||
{loading ? (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="rounded-lg border p-4 space-y-2">
|
||||
{Array.from({ length: 6 }, (_, i) => `ch-sk-${i}`).map((key) => (
|
||||
<div key={key} className="rounded-lg border p-4 space-y-2">
|
||||
<div className="h-4 w-32 bg-muted rounded animate-pulse" />
|
||||
<div className="h-3 w-24 bg-muted rounded animate-pulse" />
|
||||
</div>
|
||||
@@ -517,9 +523,11 @@ function UserDetailView({
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="size-16 rounded-full bg-muted flex items-center justify-center text-xl font-medium overflow-hidden">
|
||||
{user.avatar_url ? (
|
||||
<img
|
||||
<Image
|
||||
src={user.avatar_url}
|
||||
alt=""
|
||||
width={64}
|
||||
height={64}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
@@ -541,7 +549,10 @@ function UserDetailView({
|
||||
value={user.flagged_count}
|
||||
variant="destructive"
|
||||
/>
|
||||
<DetailStat label="Clean Streak" value={user.clean_message_streak} />
|
||||
<DetailStat
|
||||
label="Clean Streak"
|
||||
value={user.clean_message_streak ?? 0}
|
||||
/>
|
||||
<DetailStat
|
||||
label="Trust Score"
|
||||
value={user.trust_score ?? 0}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Trash2,
|
||||
Volume2,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { recordingsApi, voiceApi } from "@/lib/api";
|
||||
import type {
|
||||
@@ -104,7 +105,7 @@ export function LivePanel() {
|
||||
const unsubSpeaker = ws.on("voice_active_user", (user) => {
|
||||
const speaker = user as ActiveSpeaker;
|
||||
setSpeakers((prev) => {
|
||||
const existing = prev.findIndex((s) => s.user_id === speaker.user_id);
|
||||
const existing = prev.findIndex((s) => s.userId === speaker.userId);
|
||||
if (existing >= 0) {
|
||||
const next = [...prev];
|
||||
next[existing] = speaker;
|
||||
@@ -307,7 +308,7 @@ export function LivePanel() {
|
||||
.filter((s) => s.speaking)
|
||||
.map((s) => (
|
||||
<div
|
||||
key={s.user_id}
|
||||
key={s.userId}
|
||||
className="flex items-center gap-2 rounded-full border bg-muted/50 px-3 py-1.5"
|
||||
>
|
||||
<span className="relative flex size-2">
|
||||
@@ -354,9 +355,11 @@ export function LivePanel() {
|
||||
<p className="text-xs text-muted-foreground">Now Playing</p>
|
||||
<div className="flex items-start gap-3">
|
||||
{mediaState.current.thumbnailUrl && (
|
||||
<img
|
||||
<Image
|
||||
src={mediaState.current.thumbnailUrl}
|
||||
alt=""
|
||||
width={48}
|
||||
height={48}
|
||||
className="size-12 rounded object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -120,9 +120,9 @@ export function MascotChatbot() {
|
||||
Ask me anything about the server!
|
||||
</p>
|
||||
)}
|
||||
{messages.map((msg, i) => (
|
||||
{messages.map((msg, _i) => (
|
||||
<div
|
||||
key={i}
|
||||
key={msg.timestamp + msg.role}
|
||||
className={`flex items-start gap-2 ${
|
||||
msg.role === "user" ? "flex-row-reverse" : ""
|
||||
}`}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Search,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { messagesApi, voiceApi } from "@/lib/api";
|
||||
import type { AttachmentRecord, Channel, MessageRecord } from "@/lib/types";
|
||||
@@ -26,7 +27,7 @@ export function MessagesPanel({ guildId }: { guildId: string }) {
|
||||
const [searchResults, setSearchResults] = useState<MessageRecord[] | null>(
|
||||
null,
|
||||
);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [_searching, setSearching] = useState(false);
|
||||
const [viewTab, setViewTab] = useState<"all" | "images" | "review">("all");
|
||||
const [imageMessages, setImageMessages] = useState<MessageRecord[]>([]);
|
||||
const [reviewMessages, setReviewMessages] = useState<MessageRecord[]>([]);
|
||||
@@ -42,22 +43,11 @@ export function MessagesPanel({ guildId }: { guildId: string }) {
|
||||
|
||||
const ws = useWebSocket();
|
||||
|
||||
// ── Guild placeholder (after all hooks) ───────────────────
|
||||
if (!guildId) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<AlertCircle className="size-8 text-muted-foreground mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No guild selected. Select a guild above to view messages.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Data-fetching side effects (guildId guaranteed non-empty) ──
|
||||
// ── Data-fetching side effects (all hooks before any early return) ──
|
||||
|
||||
// Fetch available text channels for filtering
|
||||
useEffect(() => {
|
||||
if (!guildId) return;
|
||||
voiceApi
|
||||
.getTextChannels(guildId)
|
||||
.then(setChannels)
|
||||
@@ -66,6 +56,7 @@ export function MessagesPanel({ guildId }: { guildId: string }) {
|
||||
|
||||
// Fetch initial messages
|
||||
const fetchMessages = useCallback(async () => {
|
||||
if (!guildId) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -86,6 +77,7 @@ export function MessagesPanel({ guildId }: { guildId: string }) {
|
||||
|
||||
// Fetch image messages
|
||||
const fetchImages = useCallback(async () => {
|
||||
if (!guildId) return;
|
||||
try {
|
||||
const result = await messagesApi.getImages(guildId, 50);
|
||||
setImageMessages(result.data);
|
||||
@@ -118,6 +110,7 @@ export function MessagesPanel({ guildId }: { guildId: string }) {
|
||||
|
||||
// WS subscription for real-time message updates
|
||||
useEffect(() => {
|
||||
if (!guildId) return;
|
||||
const unsubCreated = ws.on("message_created", (msg) => {
|
||||
setMessages((prev) => [msg as MessageRecord, ...prev]);
|
||||
});
|
||||
@@ -147,7 +140,7 @@ export function MessagesPanel({ guildId }: { guildId: string }) {
|
||||
unsubDeleted();
|
||||
unsubAnalyzed();
|
||||
};
|
||||
}, [ws]);
|
||||
}, [ws, guildId]);
|
||||
|
||||
// Search handler
|
||||
const handleSearch = useCallback(async () => {
|
||||
@@ -326,8 +319,8 @@ export function MessagesPanel({ guildId }: { guildId: string }) {
|
||||
<div className="space-y-2">
|
||||
{loading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="flex gap-3 rounded-lg border p-4">
|
||||
{Array.from({ length: 8 }, (_, i) => `msg-sk-${i}`).map((key) => (
|
||||
<div key={key} className="flex gap-3 rounded-lg border p-4">
|
||||
<div className="size-8 shrink-0 rounded-full bg-muted animate-pulse" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-4 w-32 bg-muted rounded animate-pulse" />
|
||||
@@ -441,9 +434,11 @@ export function MessagesPanel({ guildId }: { guildId: string }) {
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="size-10 shrink-0 rounded-full bg-muted flex items-center justify-center text-sm font-medium overflow-hidden">
|
||||
{detailMessage.avatar_url ? (
|
||||
<img
|
||||
<Image
|
||||
src={detailMessage.avatar_url}
|
||||
alt=""
|
||||
width={40}
|
||||
height={40}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
@@ -649,9 +644,11 @@ function MessageCard({
|
||||
{/* Avatar */}
|
||||
<div className="size-8 shrink-0 rounded-full bg-muted flex items-center justify-center text-xs font-medium overflow-hidden">
|
||||
{msg.avatar_url ? (
|
||||
<img
|
||||
<Image
|
||||
src={msg.avatar_url}
|
||||
alt=""
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
@@ -735,7 +732,7 @@ function MessageCard({
|
||||
<div
|
||||
className="h-full rounded-full bg-primary"
|
||||
style={{
|
||||
width: msg.ai_confidence * 100 + "%",
|
||||
width: `${msg.ai_confidence * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ export const dashboardApi = {
|
||||
const params = new URLSearchParams();
|
||||
if (limit) params.set("limit", String(limit));
|
||||
if (search) params.set("search", search);
|
||||
// Backend reads req.query.guild_id (snake_case) — see createDashboardRouter in dashboard.routes.ts
|
||||
if (guildId) params.set("guild_id", guildId);
|
||||
const qs = params.toString();
|
||||
return api.get<PaginatedChannels>(
|
||||
|
||||
@@ -8,6 +8,7 @@ export const messagesApi = {
|
||||
channelId?: string,
|
||||
cursor?: string,
|
||||
) => {
|
||||
// Backend messageQuerySchema expects camelCase guildId (see messages.schema.ts)
|
||||
const params = new URLSearchParams({ guildId });
|
||||
if (limit) params.set("limit", String(limit));
|
||||
if (channelId) params.set("channelId", channelId);
|
||||
@@ -31,6 +32,7 @@ export const messagesApi = {
|
||||
api.get<MessageRecord>(`/api/messages/detail/${id}`),
|
||||
|
||||
getImages: (guildId: string, limit?: number) => {
|
||||
// Backend reads req.query.guildId (camelCase) — see handleGetImageMessages in messages.controller.ts
|
||||
const params = new URLSearchParams({ guildId });
|
||||
if (limit) params.set("limit", String(limit));
|
||||
return api.get<{ data: MessageRecord[]; nextCursor: string | null }>(
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { LayoutDashboard, MessageSquare, Radio } from "lucide-react";
|
||||
|
||||
export const tabs = [
|
||||
{ id: "messages", label: "Messages", icon: MessageSquare },
|
||||
{ id: "live", label: "Live", icon: Radio },
|
||||
{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
|
||||
] as const;
|
||||
|
||||
export type TabId = (typeof tabs)[number]["id"];
|
||||
@@ -37,13 +37,13 @@ export interface DashboardUser {
|
||||
flagged_count: number;
|
||||
last_message_at?: number | null;
|
||||
trust_score?: number | null;
|
||||
clean_message_streak?: number;
|
||||
clean_message_streak?: number | null;
|
||||
}
|
||||
|
||||
export interface DashboardUserDetail extends DashboardUser {
|
||||
last_analyzed_at?: number | null;
|
||||
clean_message_streak: number;
|
||||
total_infractions: number;
|
||||
clean_message_streak: number | null;
|
||||
total_infractions: number | null;
|
||||
clean_count: number;
|
||||
recent_messages: MessageRecord[];
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
export interface Guild {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: string | null;
|
||||
icon: string | null;
|
||||
}
|
||||
|
||||
export interface Channel {
|
||||
id: string;
|
||||
name: string;
|
||||
type?: string | null; // "voice" | "text"
|
||||
parent_id?: string | null;
|
||||
type: "voice" | "text";
|
||||
}
|
||||
|
||||
/** Shape of the /api/config response (camelCase keys from backend). */
|
||||
|
||||
@@ -3,7 +3,7 @@ export type MediaMode = "music" | "screen";
|
||||
export interface MediaItem {
|
||||
id?: string | null;
|
||||
source: string;
|
||||
title?: string | null;
|
||||
title: string;
|
||||
mode?: MediaMode | null;
|
||||
durationMs?: number | null;
|
||||
thumbnailUrl?: string | null;
|
||||
@@ -12,6 +12,6 @@ export interface MediaItem {
|
||||
export interface MediaState {
|
||||
playing: boolean;
|
||||
musicVolume: number;
|
||||
current?: MediaItem | null;
|
||||
current: MediaItem | null;
|
||||
queue: MediaItem[];
|
||||
}
|
||||
|
||||
@@ -82,12 +82,14 @@ export interface MessageRecord {
|
||||
channel_id: string;
|
||||
thread_id?: string | null;
|
||||
reference_message_id?: string | null;
|
||||
reference_channel_id?: string | null;
|
||||
reference_guild_id?: string | null;
|
||||
user_id: string;
|
||||
username: string;
|
||||
avatar_url?: string | null;
|
||||
content: string;
|
||||
edited_content?: string | null;
|
||||
type: string; // "text" | "edited" | "deleted"
|
||||
type: "text" | "edited" | "deleted";
|
||||
is_reply?: boolean | null;
|
||||
is_forward?: boolean | null;
|
||||
is_crosspost?: boolean | null;
|
||||
|
||||
@@ -8,10 +8,10 @@ export interface VoiceRecording {
|
||||
channel_name?: string | null;
|
||||
filename: string;
|
||||
size_bytes: number;
|
||||
duration_bytes: number;
|
||||
download_url?: string | null;
|
||||
upload_status: string;
|
||||
upload_error?: string | null;
|
||||
transcription?: string | null;
|
||||
created_at: number;
|
||||
uploaded_at?: number | null;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@ export interface VoiceStatus {
|
||||
}
|
||||
|
||||
export interface ActiveSpeaker {
|
||||
id?: string | null;
|
||||
user_id: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
avatar?: string | null;
|
||||
speaking: boolean;
|
||||
|
||||
@@ -36,7 +36,8 @@ export interface WsEventMap {
|
||||
voice_recording_stopped: unknown;
|
||||
voice_recording_uploaded: VoiceRecording;
|
||||
voice_active_user: ActiveSpeaker;
|
||||
voice_pcm_data: { userId: string; pcm: string };
|
||||
/** NOT delivered as JSON — arrives only via onPcm() binary handler as PcmChunk */
|
||||
voice_pcm_data: never;
|
||||
voice_analyzed: unknown;
|
||||
analysis_queue_status: unknown;
|
||||
reaction_added: unknown;
|
||||
|
||||
Reference in New Issue
Block a user