diff --git a/client/src/lib/luxsinApi.ts b/client/src/lib/luxsinApi.ts index 1a23847..96e476b 100644 --- a/client/src/lib/luxsinApi.ts +++ b/client/src/lib/luxsinApi.ts @@ -171,6 +171,7 @@ export interface PeqFilter { export interface PeqState { filters: PeqFilter[]; peqSelect?: number; + peqEnable?: number; // 0: off, 1: on peq?: Array<{ name: string; filters?: PeqFilter[] | string; diff --git a/client/src/pages/AIPage.tsx b/client/src/pages/AIPage.tsx index ba8abb5..a059752 100644 --- a/client/src/pages/AIPage.tsx +++ b/client/src/pages/AIPage.tsx @@ -8,6 +8,16 @@ ============================================================ */ import BottomNav from "@/components/BottomNav"; import ConnectionPlaceholder from "@/components/ConnectionPlaceholder"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { useDevice } from "@/contexts/DeviceContext"; import { AI_SPLIT_RESTORE_PATH_KEY, @@ -30,7 +40,6 @@ import { } from "@/lib/aiApi"; import { buildPeqSvgCurveData, getFilterType, PeqBandForResponse } from "@/lib/peqAudio"; -import { buildPeqPresetBody } from "@/lib/luxsinApi"; import { cn } from "@/lib/utils"; import { ArrowUp, @@ -206,12 +215,18 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) const [routePath, setLocation] = useLocation(); const isLgUp = useIsLgUp(); const { setOpen: setAiPanelOpen } = useAIDrawer(); - const { isConnected, deviceState, api } = useDevice(); + const { isConnected, deviceState, api, updateSetting } = useDevice(); const mac = deviceState?.mac ?? ""; const language = resolveDeviceLanguage(deviceState?.language); const deviceName = deviceState?.device ?? "Luxsin X9"; const aiText = useMemo(() => resolveAILocale(language), [language]); + const promptText = useMemo(() => resolveLocalePack(language).prompt, [language]); + const eqUi = useMemo(() => { + const pack = resolveLocalePack(language); + const peq = pack.peq as typeof localeZh.peq; + return peq.eqUi ?? (localeEn.peq as typeof localeEn.peq).eqUi; + }, [language]); const restoreDesktopSplit = useCallback(() => { const path = readAndClearAiSplitRestorePath(); @@ -224,6 +239,8 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) const [input, setInput] = useState(""); const [sending, setSending] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); + const [pendingDeleteId, setPendingDeleteId] = useState(null); + const [pendingApplyMsg, setPendingApplyMsg] = useState(null); const [chats, setChats] = useState([]); // 初次加载历史会话期间,避免先闪一下欢迎页再切到消息 const [initializing, setInitializing] = useState(true); @@ -383,27 +400,22 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) } }, [aiText, chatId, newChat]); - // ── 历史面板里删除某条会话 ── - const deleteChatById = useCallback( - async (id: string) => { - const ok = window.confirm(aiText.history.deleteConfirm); - if (!ok) return; - try { - await clearChat(id); - setChats((prev) => prev.filter((c) => c.id !== id)); - if (chatId === id) { - setChatId(null); - setMessages([]); - } - toast.success(aiText.history.deleted); - } catch (e: any) { - toast.error( - formatText(aiText.history.deleteFailed, { message: e?.message ?? e }), - ); + // ── 历史面板里删除某条会话(确认框用 AlertDialog,WebView 不支持 window.confirm) ── + const confirmDeleteChat = useCallback(async (id: string) => { + try { + await clearChat(id); + setChats((prev) => prev.filter((c) => c.id !== id)); + if (chatId === id) { + setChatId(null); + setMessages([]); } - }, - [aiText, chatId], - ); + toast.success(aiText.history.deleted); + } catch (e: any) { + toast.error( + formatText(aiText.history.deleteFailed, { message: e?.message ?? e }), + ); + } + }, [aiText, chatId]); // ── 发送提问 ── const send = useCallback( @@ -621,8 +633,6 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) autoPre: selected?.autoPre, brand: selected?.brand, model: selected?.model, - target: selected?.target, - form: selected?.form, filters: (selectedFilters ?? peqState.filters ?? []) as OptimizePeqPayload["filters"], }); } catch { @@ -634,7 +644,7 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) if (isConnected) void refreshCurrentDevicePeq(); }, [isConnected, refreshCurrentDevicePeq]); - const handleApplyToggle = useCallback( + const executeApplyToggle = useCallback( async (msg: OptimizeUIMessage) => { if (!api) { toast.error(aiText.deviceDisconnected); @@ -642,28 +652,21 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) } const nextApplied = !msg.applied; const targetPeq = nextApplied ? msg.afterPeq : msg.beforePeq; - const metaSource = currentDevicePeq ?? targetPeq; setApplyingId(msg.id); try { await api.upgradePeqChange({ - peqChange: buildPeqPresetBody( - { - name: targetPeq.name ?? msg.name ?? metaSource?.name ?? "", - brand: targetPeq.brand ?? metaSource?.brand, - model: targetPeq.model ?? metaSource?.model, - target: targetPeq.target ?? metaSource?.target, - form: targetPeq.form ?? metaSource?.form, - autoPre: targetPeq.autoPre ?? metaSource?.autoPre, - preamp: targetPeq.preamp ?? metaSource?.preamp, - canDel: targetPeq.canDel ?? metaSource?.canDel, - }, - (targetPeq.filters ?? []).map((f) => ({ + peqChange: { + name: targetPeq.name ?? msg.name, + filters: (targetPeq.filters ?? []).map((f) => ({ type: getFilterType(f.type), fc: (f.fc ?? f.frequency ?? 1000) as number, gain: f.gain, q: f.q, })), - ), + autoPre: targetPeq.autoPre, + preamp: targetPeq.preamp, + canDel: targetPeq.canDel, + }, }); await updateMessageApplied(msg.id, nextApplied); await api.refreshState(); @@ -687,7 +690,35 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) setApplyingId(null); } }, - [aiText, api, currentDevicePeq, refreshCurrentDevicePeq], + [aiText, api, refreshCurrentDevicePeq], + ); + + const handleApplyToggle = useCallback( + (msg: OptimizeUIMessage) => { + if (!api) { + toast.error(aiText.deviceDisconnected); + return; + } + if ((deviceState?.peqEnable ?? 0) !== 1) { + setPendingApplyMsg(msg); + return; + } + void executeApplyToggle(msg); + }, + [aiText, api, deviceState?.peqEnable, executeApplyToggle], + ); + + const confirmEnableAndApply = useCallback( + async (msg: OptimizeUIMessage) => { + try { + const bypassOn = (deviceState?.dsp_enable ?? 0) === 0; + await updateSetting(bypassOn ? { peqEnable: 1, dsp_enable: 1 } : { peqEnable: 1 }); + await executeApplyToggle(msg); + } catch (e: any) { + toast.error(formatText(aiText.optimize.applyFailed, { message: e?.message ?? e })); + } + }, + [aiText, deviceState?.dsp_enable, executeApplyToggle, updateSetting], ); const openCompare = useCallback( @@ -907,10 +938,72 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) onClose={() => setHistoryOpen(false)} onSelect={(id) => loadChat(id)} onNew={newChat} - onDelete={deleteChatById} + onDelete={setPendingDeleteId} /> )} + { + if (!open) setPendingDeleteId(null); + }} + > + + + + {aiText.history.delete} + + + {aiText.history.deleteConfirm} + + + + + {promptText.cancel} + + { + if (pendingDeleteId) void confirmDeleteChat(pendingDeleteId); + }} + > + {promptText.delete} + + + + + + { + if (!open) setPendingApplyMsg(null); + }} + > + + + + {eqUi.enableConfirmTitle} + + + {eqUi.enableConfirmDesc} + + + + + {eqUi.cancel} + + { + if (pendingApplyMsg) void confirmEnableAndApply(pendingApplyMsg); + }} + > + {eqUi.enableConfirmOk} + + + + + {/* ── 对比对话框 ── */} {compareMsg && ( )}