重构 GlobalChatbox 组件,拆分为多个模块

This commit is contained in:
2026-04-03 14:07:27 +08:00
parent 56b4777dbd
commit d763876f86
5 changed files with 666 additions and 589 deletions
+158
View File
@@ -0,0 +1,158 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { SpeechState } from "./GlobalChatbox.types";
// WebKit Speech Recognition compatibility
interface SpeechRecognitionEvent extends Event {
readonly resultIndex: number;
readonly results: SpeechRecognitionResultList;
}
interface SpeechRecognition extends EventTarget {
lang: string;
continuous: boolean;
interimResults: boolean;
onresult: ((event: SpeechRecognitionEvent) => void) | null;
onerror: ((event: Event) => void) | null;
onend: (() => void) | null;
start(): void;
stop(): void;
abort(): void;
}
declare global {
interface Window {
SpeechRecognition?: {
new (): SpeechRecognition;
prototype: SpeechRecognition;
};
webkitSpeechRecognition?: {
new (): SpeechRecognition;
prototype: SpeechRecognition;
};
}
}
export function useSpeechSynthesis() {
const [speechState, setSpeechState] = useState<SpeechState>("idle");
const [speakingMessageId, setSpeakingMessageId] = useState<string | null>(null);
const utteranceRef = useRef<SpeechSynthesisUtterance | null>(null);
const isSupported = typeof window !== "undefined" && "speechSynthesis" in window;
const stop = useCallback(() => {
if (!isSupported) return;
window.speechSynthesis.cancel();
utteranceRef.current = null;
setSpeechState("idle");
setSpeakingMessageId(null);
}, [isSupported]);
const speak = useCallback(
(messageId: string, text: string) => {
if (!isSupported || !text) return;
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = "zh-CN";
utterance.rate = 1;
utterance.onend = () => {
setSpeechState("idle");
setSpeakingMessageId(null);
utteranceRef.current = null;
};
utterance.onerror = () => {
setSpeechState("idle");
setSpeakingMessageId(null);
utteranceRef.current = null;
};
utterance.onpause = () => setSpeechState("paused");
utterance.onresume = () => setSpeechState("playing");
utteranceRef.current = utterance;
setSpeakingMessageId(messageId);
setSpeechState("playing");
window.speechSynthesis.speak(utterance);
},
[isSupported],
);
const pause = useCallback(() => {
if (!isSupported) return;
window.speechSynthesis.pause();
}, [isSupported]);
const resume = useCallback(() => {
if (!isSupported) return;
window.speechSynthesis.resume();
}, [isSupported]);
useEffect(() => {
return () => {
if (typeof window !== "undefined" && "speechSynthesis" in window) {
window.speechSynthesis.cancel();
}
};
}, []);
return { speechState, speakingMessageId, speak, pause, resume, stop, isSupported };
}
export function useSpeechRecognition(onResult: (text: string) => void) {
const [isListening, setIsListening] = useState(false);
const recognitionRef = useRef<SpeechRecognition | null>(null);
const onResultRef = useRef(onResult);
useEffect(() => {
onResultRef.current = onResult;
}, [onResult]);
const isSupported =
typeof window !== "undefined" &&
("SpeechRecognition" in window || "webkitSpeechRecognition" in window);
const start = useCallback(() => {
if (!isSupported || recognitionRef.current) return;
const Ctor = window.SpeechRecognition ?? window.webkitSpeechRecognition;
if (!Ctor) return;
const recognition = new Ctor();
recognition.lang = "zh-CN";
recognition.continuous = true;
recognition.interimResults = false;
recognition.onresult = (event: SpeechRecognitionEvent) => {
for (let i = event.resultIndex; i < event.results.length; i++) {
if (event.results[i].isFinal) {
onResultRef.current(event.results[i][0].transcript);
}
}
};
recognition.onerror = () => {
setIsListening(false);
recognitionRef.current = null;
};
recognition.onend = () => {
setIsListening(false);
recognitionRef.current = null;
};
recognitionRef.current = recognition;
recognition.start();
setIsListening(true);
}, [isSupported]);
const stop = useCallback(() => {
recognitionRef.current?.stop();
recognitionRef.current = null;
setIsListening(false);
}, []);
useEffect(() => {
return () => {
recognitionRef.current?.stop();
};
}, []);
return { isListening, start, stop, isSupported };
}