feat: expand intelligent investigation workspace
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s

This commit is contained in:
2026-10-30 17:30:19 +08:00
parent ddd03dbff6
commit c0e4231b83
223 changed files with 13155 additions and 5304 deletions
+52
View File
@@ -0,0 +1,52 @@
import { useSyncExternalStore } from "react";
import { speechVoiceIdSchema, type SpeechVoice } from "@/shared/tts/voices";
export const SPEECH_VOICE_STORAGE_KEY = "tjwater.agent.speech-voice";
const VOICE_CHANGE_EVENT = "tjwater:speech-voice-change";
let memoryVoice = "";
let useMemoryPreference = false;
export function getSelectedSpeechVoice(): string {
let value: string;
try { value = useMemoryPreference ? memoryVoice : localStorage.getItem(SPEECH_VOICE_STORAGE_KEY) ?? ""; }
catch { value = memoryVoice; }
return speechVoiceIdSchema.safeParse(value).success ? value : "";
}
export function setSelectedSpeechVoice(value: string) {
const voice = speechVoiceIdSchema.safeParse(value).success ? value : "";
memoryVoice = voice;
try {
if (voice) localStorage.setItem(SPEECH_VOICE_STORAGE_KEY, voice);
else localStorage.removeItem(SPEECH_VOICE_STORAGE_KEY);
useMemoryPreference = false;
} catch { useMemoryPreference = true; }
window.dispatchEvent(new Event(VOICE_CHANGE_EVENT));
}
function subscribe(onChange: () => void) {
window.addEventListener(VOICE_CHANGE_EVENT, onChange);
window.addEventListener("storage", onChange);
return () => {
window.removeEventListener(VOICE_CHANGE_EVENT, onChange);
window.removeEventListener("storage", onChange);
};
}
export function useSelectedSpeechVoice() {
return useSyncExternalStore(subscribe, getSelectedSpeechVoice, () => "");
}
const chineseNames: Record<string, string> = {
"zh-CN-XiaoxiaoNeural": "晓晓", "zh-CN-XiaoyiNeural": "晓伊",
"zh-CN-YunxiNeural": "云希", "zh-CN-YunyangNeural": "云扬",
"zh-CN-YunjianNeural": "云健", "zh-CN-YunxiaNeural": "云夏"
};
export function getSpeechVoiceName(voice: SpeechVoice) {
return chineseNames[voice.id] ?? voice.id.replace(`${voice.locale}-`, "").replace(/Neural$/, "");
}
export function getSpeechVoiceDescription(voice: SpeechVoice) {
return `普通话 · ${voice.gender === "Female" ? "女声" : "男声"}`;
}