feat: 聊天框与视频功能解耦,输入文字即可开始对话
之前必须点击「开始对话」连接 WebSocket 后才能使用聊天框。 现在聊天输入框始终可用,输入文字自动连接 WebSocket 并发送消息; 左侧按钮改为「开始视频通话」,单独控制摄像头/麦克风。 - ChatPanel 移除 !isConnected early return,始终显示输入框 - useVisionSession 新增待发消息队列,sendTextMessage 支持自动连接 - startSession 改为仅负责视频(摄像头 + 麦克风 + VAD) - 修复 statusRef 在 useWebSocketManager 之前声明导致的 TDZ 错误 - 更新 i18n 翻译(新增 controls.startVideo、video.placeholder)
This commit is contained in:
@@ -47,6 +47,9 @@ export function useVisionSession() {
|
||||
// 对话历史(role + content),用于多轮上下文
|
||||
const historyRef = useRef<Array<{ role: string; content: string }>>([]);
|
||||
|
||||
// 待发消息队列(未连接时暂存,连接后自动发送)
|
||||
const pendingMessagesRef = useRef<Array<{ text: string; requestId: string }>>([]);
|
||||
|
||||
// TTS 播放器
|
||||
const ttsPlayerRef = useRef<TTSPlayer | null>(null);
|
||||
const getTTSPlayer = useCallback(() => {
|
||||
@@ -68,6 +71,12 @@ export function useVisionSession() {
|
||||
isProcessingRef.current = isProcessing;
|
||||
}, [isProcessing]);
|
||||
|
||||
// 用 ref 跟踪连接状态,避免回调闭包问题
|
||||
const statusRef = useRef(status);
|
||||
useEffect(() => {
|
||||
statusRef.current = status;
|
||||
}, [status]);
|
||||
|
||||
// 观察模式:画面变化时自动发送 query
|
||||
const { isObserving, startObserving, stopObserving } = useObservationMode({
|
||||
onChange: useCallback(
|
||||
@@ -110,7 +119,7 @@ export function useVisionSession() {
|
||||
});
|
||||
}, [videoRef, captureFrame, startObserving, stopObserving]);
|
||||
|
||||
// WebSocket 连接成功后发送 config
|
||||
// WebSocket 连接成功后发送 config + flush 待发消息
|
||||
useEffect(() => {
|
||||
if (status === "connected") {
|
||||
send({
|
||||
@@ -121,6 +130,27 @@ export function useVisionSession() {
|
||||
language: config.language,
|
||||
},
|
||||
});
|
||||
|
||||
// flush 待发消息队列
|
||||
const pending = pendingMessagesRef.current;
|
||||
pendingMessagesRef.current = [];
|
||||
for (const msg of pending) {
|
||||
const frame = captureFrame();
|
||||
send({
|
||||
type: "query",
|
||||
request_id: msg.requestId,
|
||||
image: frame ? dataUrlToBase64(frame) : "",
|
||||
audio: "",
|
||||
text: msg.text,
|
||||
});
|
||||
setStats((prev) => ({ ...prev, queryCount: prev.queryCount + 1 }));
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{ role: "user", content: msg.text, timestamp: Date.now() },
|
||||
]);
|
||||
historyRef.current.push({ role: "user", content: msg.text });
|
||||
setIsProcessing(true);
|
||||
}
|
||||
}
|
||||
}, [status]); // eslint-disable-line react-hooks/exhaustive-deps -- 仅在连接状态变化时发送
|
||||
|
||||
@@ -282,10 +312,13 @@ export function useVisionSession() {
|
||||
return unsub;
|
||||
}, [getTTSPlayer]);
|
||||
|
||||
/** 启动会话 */
|
||||
/** 启动视频通话(摄像头 + 麦克风 + VAD) */
|
||||
const startSession = useCallback(async () => {
|
||||
// 1. 连接 WebSocket(必须)
|
||||
connect();
|
||||
// 1. 确保 WebSocket 已连接
|
||||
if (statusRef.current !== "connected") {
|
||||
connect();
|
||||
// 等待连接完成(通过 status 变化触发后续流程,这里直接继续)
|
||||
}
|
||||
|
||||
// 2. 尝试获取摄像头(可选)
|
||||
try {
|
||||
@@ -381,10 +414,23 @@ export function useVisionSession() {
|
||||
ttsPlayerRef.current?.stop();
|
||||
setIsAudioPlaying(false);
|
||||
|
||||
// 捕获当前摄像头画面
|
||||
const frame = captureFrame();
|
||||
|
||||
const requestId = uuidv4();
|
||||
|
||||
// 未连接时:自动连接,消息加入待发队列
|
||||
if (statusRef.current !== "connected") {
|
||||
pendingMessagesRef.current.push({ text: text.trim(), requestId });
|
||||
// 添加用户消息到 UI(立即反馈)
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{ role: "user", content: text.trim(), timestamp: Date.now() }],
|
||||
);
|
||||
// 自动连接 WebSocket
|
||||
connect();
|
||||
return;
|
||||
}
|
||||
|
||||
// 已连接:直接发送
|
||||
const frame = captureFrame();
|
||||
send({
|
||||
type: "query",
|
||||
request_id: requestId,
|
||||
@@ -407,7 +453,7 @@ export function useVisionSession() {
|
||||
|
||||
setIsProcessing(true);
|
||||
},
|
||||
[captureFrame, send],
|
||||
[captureFrame, send, connect],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user