Integrate React Query for data management and enhance Debug Preferences
- Added React Query for managing API calls related to assistants and voices. - Introduced `useAssistantsQuery` and `useVoicesQuery` hooks for fetching data. - Implemented mutations for creating, updating, and deleting voices using React Query. - Integrated a global `QueryClient` for managing query states and configurations. - Refactored components to utilize the new query hooks, improving data handling and performance. - Added a Zustand store for managing debug preferences, including WebSocket URL and audio settings.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import App from './App';
|
||||
import { queryClient } from './services/queryClient';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
@@ -10,6 +12,8 @@ if (!rootElement) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
);
|
||||
|
||||
59
web/package-lock.json
generated
59
web/package-lock.json
generated
@@ -9,10 +9,12 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.39.0",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-router-dom": "^7.13.0"
|
||||
"react-router-dom": "^7.13.0",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
@@ -1265,6 +1267,32 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.90.20",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
|
||||
"integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.90.21",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
|
||||
"integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.20"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -2772,6 +2800,35 @@
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.11",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz",
|
||||
"integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"lucide-react": "^0.563.0",
|
||||
"zustand": "^5.0.8",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"@google/genai": "^1.39.0",
|
||||
"react": "^19.2.4",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Plus, Search, Play, Square, Copy, Trash2, Mic, MessageSquare, Save, Vid
|
||||
import { Button, Input, Badge, Drawer, Dialog, Switch } from '../components/UI';
|
||||
import { ASRModel, Assistant, AssistantOpenerToolCall, KnowledgeBase, LLMModel, TabValue, Tool, Voice } from '../types';
|
||||
import { createAssistant, deleteAssistant, fetchASRModels, fetchAssistantOpenerAudioPcmBuffer, fetchAssistants, fetchKnowledgeBases, fetchLLMModels, fetchTools, fetchVoices, generateAssistantOpenerAudio, previewVoice, updateAssistant as updateAssistantApi } from '../services/backendApi';
|
||||
import { useDebugPrefsStore } from '../stores/debugPrefsStore';
|
||||
|
||||
const isOpenAICompatibleVendor = (vendor?: string) => {
|
||||
const normalized = String(vendor || '').trim().toLowerCase();
|
||||
@@ -2365,12 +2366,17 @@ export const DebugDrawer: React.FC<{
|
||||
const [dynamicVariables, setDynamicVariables] = useState<DynamicVariableEntry[]>([]);
|
||||
const [dynamicVariablesError, setDynamicVariablesError] = useState('');
|
||||
const dynamicVariableSeqRef = useRef(0);
|
||||
const [wsUrl, setWsUrl] = useState<string>(() => {
|
||||
const fromStorage = localStorage.getItem('debug_ws_url');
|
||||
if (fromStorage) return fromStorage;
|
||||
const defaultHost = window.location.hostname || 'localhost';
|
||||
return `ws://${defaultHost}:8000/ws`;
|
||||
});
|
||||
const wsUrl = useDebugPrefsStore((state) => state.wsUrl);
|
||||
const setWsUrl = useDebugPrefsStore((state) => state.setWsUrl);
|
||||
const aecEnabled = useDebugPrefsStore((state) => state.aecEnabled);
|
||||
const setAecEnabled = useDebugPrefsStore((state) => state.setAecEnabled);
|
||||
const nsEnabled = useDebugPrefsStore((state) => state.nsEnabled);
|
||||
const setNsEnabled = useDebugPrefsStore((state) => state.setNsEnabled);
|
||||
const agcEnabled = useDebugPrefsStore((state) => state.agcEnabled);
|
||||
const setAgcEnabled = useDebugPrefsStore((state) => state.setAgcEnabled);
|
||||
const clientToolEnabledMap = useDebugPrefsStore((state) => state.clientToolEnabledMap);
|
||||
const setClientToolEnabled = useDebugPrefsStore((state) => state.setClientToolEnabled);
|
||||
const hydrateClientToolDefaults = useDebugPrefsStore((state) => state.hydrateClientToolDefaults);
|
||||
const nextDynamicVariableId = () => {
|
||||
dynamicVariableSeqRef.current += 1;
|
||||
return `var_${dynamicVariableSeqRef.current}`;
|
||||
@@ -2432,16 +2438,6 @@ export const DebugDrawer: React.FC<{
|
||||
const [selectedCamera, setSelectedCamera] = useState<string>('');
|
||||
const [selectedMic, setSelectedMic] = useState<string>('');
|
||||
const [isSwapped, setIsSwapped] = useState(false);
|
||||
const [aecEnabled, setAecEnabled] = useState<boolean>(() => localStorage.getItem('debug_audio_aec') !== '0');
|
||||
const [nsEnabled, setNsEnabled] = useState<boolean>(() => localStorage.getItem('debug_audio_ns') !== '0');
|
||||
const [agcEnabled, setAgcEnabled] = useState<boolean>(() => localStorage.getItem('debug_audio_agc') !== '0');
|
||||
const [clientToolEnabledMap, setClientToolEnabledMap] = useState<Record<string, boolean>>(() => {
|
||||
const result: Record<string, boolean> = {};
|
||||
for (const tool of DEBUG_CLIENT_TOOLS) {
|
||||
result[tool.id] = localStorage.getItem(`debug_client_tool_enabled_${tool.id}`) !== '0';
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
const micAudioCtxRef = useRef<AudioContext | null>(null);
|
||||
const micSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
|
||||
@@ -2557,8 +2553,8 @@ export const DebugDrawer: React.FC<{
|
||||
}, [isOpen, assistant, mode]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('debug_ws_url', wsUrl);
|
||||
}, [wsUrl]);
|
||||
hydrateClientToolDefaults(DEBUG_CLIENT_TOOLS.map((tool) => tool.id));
|
||||
}, [hydrateClientToolDefaults]);
|
||||
|
||||
useEffect(() => {
|
||||
wsStatusRef.current = wsStatus;
|
||||
@@ -2578,24 +2574,6 @@ export const DebugDrawer: React.FC<{
|
||||
setDynamicVariablesError('');
|
||||
}, [assistant.id, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('debug_audio_aec', aecEnabled ? '1' : '0');
|
||||
}, [aecEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('debug_audio_ns', nsEnabled ? '1' : '0');
|
||||
}, [nsEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('debug_audio_agc', agcEnabled ? '1' : '0');
|
||||
}, [agcEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
for (const tool of DEBUG_CLIENT_TOOLS) {
|
||||
localStorage.setItem(`debug_client_tool_enabled_${tool.id}`, isClientToolEnabled(tool.id) ? '1' : '0');
|
||||
}
|
||||
}, [clientToolEnabledMap]);
|
||||
|
||||
useEffect(() => {
|
||||
clientToolEnabledMapRef.current = clientToolEnabledMap;
|
||||
}, [clientToolEnabledMap]);
|
||||
@@ -4223,7 +4201,7 @@ export const DebugDrawer: React.FC<{
|
||||
</div>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={(next) => setClientToolEnabledMap((prev) => ({ ...prev, [tool.id]: next }))}
|
||||
onCheckedChange={(next) => setClientToolEnabled(tool.id, next)}
|
||||
title={enabled ? '点击关闭' : '点击开启'}
|
||||
aria-label={`${tool.name} ${enabled ? '开启' : '关闭'}`}
|
||||
/>
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Search, Play, Copy, Trash2, Zap, MessageSquare, Mic, AlertTriangle, ClipboardCheck, X } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge, Dialog } from '../components/UI';
|
||||
import { mockAutoTestAssistants } from '../services/mockData';
|
||||
import { Assistant, AutoTestAssistant, TestType, TestMethod } from '../types';
|
||||
import { fetchAssistants } from '../services/backendApi';
|
||||
import { AutoTestAssistant, TestType, TestMethod } from '../types';
|
||||
import { useAssistantsQuery } from '../services/queries';
|
||||
|
||||
export const AutoTestPage: React.FC = () => {
|
||||
const [testAssistants, setTestAssistants] = useState<AutoTestAssistant[]>(mockAutoTestAssistants);
|
||||
const [assistants, setAssistants] = useState<Assistant[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadAssistants = async () => {
|
||||
try {
|
||||
const list = await fetchAssistants();
|
||||
setAssistants(list);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
loadAssistants();
|
||||
}, []);
|
||||
const { data: assistants = [] } = useAssistantsQuery();
|
||||
|
||||
const filteredTests = testAssistants.filter(t =>
|
||||
t.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
|
||||
@@ -4,17 +4,16 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Phone, CheckCircle, Clock, UserCheck, ChevronDown, BarChart3, HelpCircle, Mail, ArrowDown, Bot, Zap, Rocket, LineChart, Layers, Fingerprint, Network, MonitorPlay, Plus } from 'lucide-react';
|
||||
import { Card, Button } from '../components/UI';
|
||||
import { getDashboardStats } from '../services/mockData';
|
||||
import { Assistant } from '../types';
|
||||
import { fetchAssistants } from '../services/backendApi';
|
||||
import { useAssistantsQuery } from '../services/queries';
|
||||
|
||||
export const DashboardPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [timeRange, setTimeRange] = useState<'week' | 'month' | 'year'>('week');
|
||||
const [selectedAssistantId, setSelectedAssistantId] = useState<string>('all');
|
||||
const [assistants, setAssistants] = useState<Assistant[]>([]);
|
||||
const [topRailHeight, setTopRailHeight] = useState(0);
|
||||
const [isOverviewVisible, setIsOverviewVisible] = useState(true);
|
||||
const [scrollRootCenterX, setScrollRootCenterX] = useState<number | null>(null);
|
||||
const { data: assistants = [] } = useAssistantsQuery();
|
||||
|
||||
const scrollRootRef = useRef<HTMLDivElement>(null);
|
||||
const topRailRef = useRef<HTMLDivElement>(null);
|
||||
@@ -26,29 +25,6 @@ export const DashboardPage: React.FC = () => {
|
||||
return getDashboardStats(timeRange, selectedAssistantId);
|
||||
}, [timeRange, selectedAssistantId]);
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
|
||||
const loadAssistants = async () => {
|
||||
try {
|
||||
const list = await fetchAssistants();
|
||||
if (!disposed) {
|
||||
setAssistants(list);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!disposed) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadAssistants();
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const updateTopRailHeight = () => {
|
||||
const nextHeight = topRailRef.current?.offsetHeight ?? 0;
|
||||
|
||||
@@ -2,7 +2,13 @@ import React, { useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { Search, Mic2, Play, Pause, Upload, Filter, Plus, Volume2, Pencil, Trash2 } from 'lucide-react';
|
||||
import { Button, Input, Select, TableHeader, TableRow, TableHead, TableCell, Dialog, Badge, LibraryPageShell, TableStatusRow, LibraryActionCell } from '../components/UI';
|
||||
import { Voice } from '../types';
|
||||
import { createVoice, deleteVoice, fetchVoices, previewVoice, updateVoice } from '../services/backendApi';
|
||||
import { previewVoice } from '../services/backendApi';
|
||||
import {
|
||||
useCreateVoiceMutation,
|
||||
useDeleteVoiceMutation,
|
||||
useUpdateVoiceMutation,
|
||||
useVoicesQuery,
|
||||
} from '../services/queries';
|
||||
|
||||
const OPENAI_COMPATIBLE_DEFAULT_MODEL = 'FunAudioLLM/CosyVoice2-0.5B';
|
||||
const OPENAI_COMPATIBLE_DEFAULT_VOICE = 'FunAudioLLM/CosyVoice2-0.5B:anna';
|
||||
@@ -19,7 +25,6 @@ const buildOpenAICompatibleVoiceKey = (rawId: string, model: string): string =>
|
||||
};
|
||||
|
||||
export const VoiceLibraryPage: React.FC = () => {
|
||||
const [voices, setVoices] = useState<Voice[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [vendorFilter, setVendorFilter] = useState<string>('all');
|
||||
const [genderFilter, setGenderFilter] = useState<'all' | 'Male' | 'Female'>('all');
|
||||
@@ -29,24 +34,15 @@ export const VoiceLibraryPage: React.FC = () => {
|
||||
const [isCloneModalOpen, setIsCloneModalOpen] = useState(false);
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
const [editingVoice, setEditingVoice] = useState<Voice | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [playLoadingId, setPlayLoadingId] = useState<string | null>(null);
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadVoices = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
setVoices(await fetchVoices());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setVoices([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
loadVoices();
|
||||
}, []);
|
||||
const voicesQuery = useVoicesQuery();
|
||||
const voices = voicesQuery.data || [];
|
||||
const isLoading = voicesQuery.isLoading;
|
||||
const createVoiceMutation = useCreateVoiceMutation();
|
||||
const updateVoiceMutation = useUpdateVoiceMutation();
|
||||
const deleteVoiceMutation = useDeleteVoiceMutation();
|
||||
|
||||
const vendorOptions = useMemo(
|
||||
() => Array.from(new Set(voices.map((v) => String(v.vendor || '').trim()).filter(Boolean))).sort(),
|
||||
@@ -100,22 +96,19 @@ export const VoiceLibraryPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleAddSuccess = async (newVoice: Voice) => {
|
||||
const created = await createVoice(newVoice);
|
||||
setVoices((prev) => [created, ...prev]);
|
||||
await createVoiceMutation.mutateAsync(newVoice);
|
||||
setIsAddModalOpen(false);
|
||||
setIsCloneModalOpen(false);
|
||||
};
|
||||
|
||||
const handleUpdateSuccess = async (id: string, data: Voice) => {
|
||||
const updated = await updateVoice(id, data);
|
||||
setVoices((prev) => prev.map((voice) => (voice.id === id ? updated : voice)));
|
||||
await updateVoiceMutation.mutateAsync({ id, data });
|
||||
setEditingVoice(null);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('确认删除该声音吗?该操作不可恢复。')) return;
|
||||
await deleteVoice(id);
|
||||
setVoices((prev) => prev.filter((voice) => voice.id !== id));
|
||||
await deleteVoiceMutation.mutateAsync(id);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,9 +3,10 @@ import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { ArrowLeft, Play, Save, Rocket, Plus, Bot, UserCheck, Wrench, Ban, Zap, X, Copy, MousePointer2 } from 'lucide-react';
|
||||
import { Button, Input, Badge } from '../components/UI';
|
||||
import { Assistant, WorkflowNode, WorkflowEdge, Workflow } from '../types';
|
||||
import { WorkflowNode, WorkflowEdge, Workflow } from '../types';
|
||||
import { DebugDrawer } from './Assistants';
|
||||
import { createWorkflow, fetchAssistants, fetchWorkflowById, updateWorkflow } from '../services/backendApi';
|
||||
import { createWorkflow, fetchWorkflowById, updateWorkflow } from '../services/backendApi';
|
||||
import { useAssistantsQuery } from '../services/queries';
|
||||
|
||||
const toWorkflowNodeType = (type: WorkflowNode['type']): WorkflowNode['type'] => {
|
||||
if (type === 'conversation') return 'assistant';
|
||||
@@ -80,7 +81,7 @@ export const WorkflowEditorPage: React.FC = () => {
|
||||
const [nodes, setNodes] = useState<WorkflowNode[]>(() => getTemplateNodes(templateType));
|
||||
const [edges, setEdges] = useState<WorkflowEdge[]>([]);
|
||||
const [createdAt, setCreatedAt] = useState('');
|
||||
const [assistants, setAssistants] = useState<Assistant[]>([]);
|
||||
const { data: assistants = [] } = useAssistantsQuery();
|
||||
|
||||
const [selectedNodeName, setSelectedNodeName] = useState<string | null>(null);
|
||||
const [isAddMenuOpen, setIsAddMenuOpen] = useState(false);
|
||||
@@ -215,18 +216,6 @@ export const WorkflowEditorPage: React.FC = () => {
|
||||
};
|
||||
}, [draggingNodeName, isPanning, zoom]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const assistantList = await fetchAssistants();
|
||||
setAssistants(assistantList);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
const loadWorkflow = async () => {
|
||||
|
||||
59
web/services/queries.ts
Normal file
59
web/services/queries.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Voice } from '../types';
|
||||
import {
|
||||
createVoice,
|
||||
deleteVoice,
|
||||
fetchAssistants,
|
||||
fetchVoices,
|
||||
updateVoice,
|
||||
} from './backendApi';
|
||||
import { queryKeys } from './queryKeys';
|
||||
|
||||
export const useAssistantsQuery = () =>
|
||||
useQuery({
|
||||
queryKey: queryKeys.assistants,
|
||||
queryFn: fetchAssistants,
|
||||
});
|
||||
|
||||
export const useVoicesQuery = () =>
|
||||
useQuery({
|
||||
queryKey: queryKeys.voices,
|
||||
queryFn: fetchVoices,
|
||||
});
|
||||
|
||||
export const useCreateVoiceMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (payload: Partial<Voice>) => createVoice(payload),
|
||||
onSuccess: (created) => {
|
||||
queryClient.setQueryData<Voice[]>(queryKeys.voices, (prev = []) => [created, ...prev]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateVoiceMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: Partial<Voice> }) => updateVoice(id, data),
|
||||
onSuccess: (updated) => {
|
||||
queryClient.setQueryData<Voice[]>(queryKeys.voices, (prev = []) =>
|
||||
prev.map((voice) => (voice.id === updated.id ? updated : voice))
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteVoiceMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
await deleteVoice(id);
|
||||
return id;
|
||||
},
|
||||
onSuccess: (deletedId) => {
|
||||
queryClient.setQueryData<Voice[]>(queryKeys.voices, (prev = []) =>
|
||||
prev.filter((voice) => voice.id !== deletedId)
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
12
web/services/queryClient.ts
Normal file
12
web/services/queryClient.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30_000,
|
||||
gcTime: 5 * 60_000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
4
web/services/queryKeys.ts
Normal file
4
web/services/queryKeys.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const queryKeys = {
|
||||
assistants: ['assistants'] as const,
|
||||
voices: ['voices'] as const,
|
||||
};
|
||||
65
web/stores/debugPrefsStore.ts
Normal file
65
web/stores/debugPrefsStore.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
type DebugPrefsState = {
|
||||
wsUrl: string;
|
||||
aecEnabled: boolean;
|
||||
nsEnabled: boolean;
|
||||
agcEnabled: boolean;
|
||||
clientToolEnabledMap: Record<string, boolean>;
|
||||
setWsUrl: (value: string) => void;
|
||||
setAecEnabled: (value: boolean) => void;
|
||||
setNsEnabled: (value: boolean) => void;
|
||||
setAgcEnabled: (value: boolean) => void;
|
||||
setClientToolEnabled: (toolId: string, enabled: boolean) => void;
|
||||
hydrateClientToolDefaults: (toolIds: string[]) => void;
|
||||
};
|
||||
|
||||
const getDefaultWsUrl = (): string => {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'ws://localhost:8000/ws';
|
||||
}
|
||||
const defaultHost = window.location.hostname || 'localhost';
|
||||
return `ws://${defaultHost}:8000/ws`;
|
||||
};
|
||||
|
||||
export const useDebugPrefsStore = create<DebugPrefsState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
wsUrl: getDefaultWsUrl(),
|
||||
aecEnabled: true,
|
||||
nsEnabled: true,
|
||||
agcEnabled: true,
|
||||
clientToolEnabledMap: {},
|
||||
setWsUrl: (value) => set({ wsUrl: value }),
|
||||
setAecEnabled: (value) => set({ aecEnabled: value }),
|
||||
setNsEnabled: (value) => set({ nsEnabled: value }),
|
||||
setAgcEnabled: (value) => set({ agcEnabled: value }),
|
||||
setClientToolEnabled: (toolId, enabled) =>
|
||||
set((state) => ({
|
||||
clientToolEnabledMap: {
|
||||
...state.clientToolEnabledMap,
|
||||
[toolId]: enabled,
|
||||
},
|
||||
})),
|
||||
hydrateClientToolDefaults: (toolIds) => {
|
||||
const current = get().clientToolEnabledMap;
|
||||
let changed = false;
|
||||
const nextMap: Record<string, boolean> = { ...current };
|
||||
for (const toolId of toolIds) {
|
||||
if (!(toolId in nextMap)) {
|
||||
nextMap[toolId] = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
set({ clientToolEnabledMap: nextMap });
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'assistants-debug-prefs',
|
||||
version: 1,
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user