feat: add voice recordings feature with database integration

- Implemented PostgreSQL and SQLite schemas for voice recordings.
- Created repository functions for inserting, updating, and listing voice recordings.
- Developed uploader logic to handle file uploads and database updates.
- Added routes for accessing voice recordings via API.
- Enhanced UI state to include recordings tab.
- Introduced moderation WebSocket event for uploaded recordings.
This commit is contained in:
MythEclipse
2026-05-18 20:15:38 +07:00
parent 9d5559a45b
commit 3b9e87ee6c
17 changed files with 1404 additions and 13 deletions
+14 -2
View File
@@ -5,6 +5,7 @@ import { MessagesPanel } from "./components/messages/MessagesPanel";
import { ReviewPanel } from "./components/review/ReviewPanel";
import { Tabs, TabsContent } from "./components/ui/tabs";
import { VoicePanel } from "./components/voice/VoicePanel";
import { RecordingsPanel } from "./components/recordings/RecordingsPanel";
import { AuthOverlay } from "./components/layout/AuthOverlay";
import { useDashboardSocket } from "./hooks/useDashboardSocket";
import { mergeMessages, useMessages } from "./hooks/useMessages";
@@ -75,6 +76,10 @@ export default function App() {
onMessageAnalyzed: (message) => messages.setMessages((prev) => mergeMessages(prev, [message])),
onAttachmentUploaded: () => messages.fetchMessages(selectedTextChannel).catch(() => undefined),
onMediaState: media.setMediaState,
onVoiceRecordingUploaded: (recording) => {
const event = new CustomEvent("voice_recording_uploaded", { detail: recording });
window.dispatchEvent(event);
},
onPcm: handleIncomingPcm,
});
@@ -178,7 +183,7 @@ export default function App() {
await patchUIState({ isListening: true });
}, [isListening, patchUIState]);
const tabs = useMemo(() => ["voice", "media", "messages", "review"] as DashboardTab[], []);
const tabs = useMemo(() => ["voice", "media", "messages", "recordings", "review"] as DashboardTab[], []);
return (
<DashboardLayout
@@ -189,7 +194,7 @@ export default function App() {
>
<div className="md:hidden">
<Tabs value={activeTab} onValueChange={(value) => patchUIState({ activeTab: value as DashboardTab })}>
<div className="mb-4 grid grid-cols-4 gap-2 rounded-2xl bg-muted p-1">
<div className="mb-4 grid grid-cols-5 gap-2 rounded-2xl bg-muted p-1">
{tabs.map((tab) => (
<button key={tab} className={`rounded-xl px-2 py-2 text-xs font-medium ${activeTab === tab ? "bg-background text-foreground" : "text-muted-foreground"}`} onClick={() => patchUIState({ activeTab: tab })}>
{tab}
@@ -250,6 +255,13 @@ export default function App() {
onReanalyze={messages.reanalyze}
/>
</TabsContent>
<TabsContent value="recordings">
{!isAuthenticated ? (
<AuthOverlay onAuthenticated={() => setIsAuthenticated(true)} />
) : (
<RecordingsPanel />
)}
</TabsContent>
<TabsContent value="review">
<ReviewPanel messages={messages.messages} onReanalyze={messages.reanalyze} />
</TabsContent>
+2 -1
View File
@@ -1,4 +1,4 @@
import { Bot, MessageSquare, Music2, ShieldAlert, Volume2 } from "lucide-react";
import { Bot, MessageSquare, Music2, ShieldAlert, Volume2, Mic } from "lucide-react";
import type { DashboardTab } from "../../types/ui";
import { cn } from "../../lib/utils";
import { Button } from "../ui/button";
@@ -7,6 +7,7 @@ const navItems: Array<{ id: DashboardTab; label: string; icon: typeof Volume2 }>
{ id: "voice", label: "Voice", icon: Volume2 },
{ id: "media", label: "Media", icon: Music2 },
{ id: "messages", label: "Messages", icon: MessageSquare },
{ id: "recordings", label: "Recordings", icon: Mic },
{ id: "review", label: "Review", icon: ShieldAlert },
];
+4
View File
@@ -15,6 +15,7 @@ export interface DashboardSocketHandlers {
onMessageAnalyzed?: (message: MessageRecord) => void;
onAttachmentUploaded?: () => void;
onMediaState?: (state: MediaState) => void;
onVoiceRecordingUploaded?: (recording: any) => void;
onPcm?: (data: ArrayBuffer) => void;
}
@@ -75,6 +76,9 @@ export function useDashboardSocket(handlers: DashboardSocketHandlers) {
case "media_state":
handlersRef.current.onMediaState?.(message.state);
break;
case "voice_recording_uploaded":
handlersRef.current.onVoiceRecordingUploaded?.(message.data);
break;
}
} catch {
// ignore malformed socket messages
+1 -1
View File
@@ -1,4 +1,4 @@
export type DashboardTab = "voice" | "media" | "messages" | "review";
export type DashboardTab = "voice" | "media" | "messages" | "review" | "recordings";
export interface UIState {
selectedGuild?: string;