Files
bigwo/test2/client/src/App.jsx
User 3e72cd54d3 feat(app): add textEngine toggle for chat mode (Coze ↔ S2S)
- localStorage-persistent textEngine state ('coze' | 's2s')
- Header button toggles between the two engines when in chat mode
- ChatPanel remounts on engine switch via key=sessionId-textEngine
- Voice mode completely unaffected
2026-04-17 09:36:13 +08:00

281 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback, useRef } from 'react';
import { Settings2, Zap, Mic, MessageSquare, History, Plus, Film } from 'lucide-react';
import VoicePanel from './components/VoicePanel';
import ChatPanel from './components/ChatPanel';
import SettingsPanel from './components/SettingsPanel';
import { getVoiceConfig } from './services/voiceApi';
import SessionHistoryPanel from './components/SessionHistoryPanel';
import VideoPanel from './components/VideoPanel';
export default function App() {
const [showSettings, setShowSettings] = useState(false);
const [showHistory, setShowHistory] = useState(false);
const [voiceConfig, setVoiceConfig] = useState(null);
// 'voice' | 'chat'
const [mode, setMode] = useState('voice');
// 统一会话 ID贯穿语音和文字模式数据库用此 ID 关联所有消息)
const [currentSessionId, setCurrentSessionId] = useState(null);
// 语音转文字的交接数据(保留兼容)
const [handoff, setHandoff] = useState(null);
// 文字聊天消息(用于切回语音时注入上下文)
const [chatMessages, setChatMessages] = useState([]);
const [settings, setSettings] = useState({
modelVersion: 'O',
speaker: 'zh_female_vv_jupiter_bigtts',
enableWebSearch: false,
});
// 文字对话引擎: 'coze' (原方舟 HTTP/SSE) | 's2s' (S2S WebSocket, event 501)
const [textEngine, setTextEngine] = useState(() => {
try {
return localStorage.getItem('bigwo_text_engine') || 'coze';
} catch (_) {
return 'coze';
}
});
const toggleTextEngine = useCallback(() => {
setTextEngine((prev) => {
const next = prev === 'coze' ? 's2s' : 'coze';
try { localStorage.setItem('bigwo_text_engine', next); } catch (_) {}
return next;
});
}, []);
useEffect(() => {
getVoiceConfig()
.then(setVoiceConfig)
.catch((err) => console.warn('Failed to load config:', err));
}, []);
// 语音通话结束后,无缝切换到文字对话(携带同一个 sessionId
const handleVoiceEnd = useCallback((data) => {
if (data?.sessionId) {
const sid = data.sessionId;
setCurrentSessionId(sid);
setHandoff({
sessionId: sid,
subtitles: data.subtitles || [],
});
setMode('chat');
console.log(`[App] Voice→Chat, sessionId=${sid}`);
}
}, []);
// 从文字模式返回语音模式(使用同一个 sessionId数据库已有完整历史
const handleBackToVoice = useCallback(() => {
console.log(`[App] Chat→Voice, sessionId=${currentSessionId}`);
setMode('voice');
}, [currentSessionId]);
// 切换到文字模式(复用已有 sessionId没有时新建
const handleStartChat = useCallback(() => {
const sid = currentSessionId || `chat_${Date.now().toString(36)}`;
setCurrentSessionId(sid);
setHandoff({
sessionId: sid,
subtitles: [],
});
setMode('chat');
console.log(`[App] Switch to chat, sessionId=${sid}`);
}, [currentSessionId]);
// 语音会话创建时同步 sessionId 到 App 状态
const handleSessionCreated = useCallback((sessionId) => {
if (sessionId && sessionId !== currentSessionId) {
setCurrentSessionId(sessionId);
console.log(`[App] Voice session synced: ${sessionId}`);
}
}, [currentSessionId]);
// 新建会话:重置所有状态
const handleNewSession = useCallback(() => {
setCurrentSessionId(null);
setHandoff(null);
setChatMessages([]);
setMode('voice');
console.log('[App] New session created');
}, []);
// 从历史记录中选择会话
const handleSelectSession = useCallback((session) => {
const sid = session.id;
setCurrentSessionId(sid);
setChatMessages([]);
// 根据会话最后的模式决定打开方式,默认用文字模式查看历史
setHandoff({
sessionId: sid,
subtitles: [],
});
setMode('chat');
console.log(`[App] Selected session: ${sid}, mode: ${session.mode}`);
}, []);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
{/* Header */}
<header className="border-b border-slate-700/50 bg-slate-900/80 backdrop-blur-sm sticky top-0 z-30">
<div className="max-w-5xl mx-auto px-3 sm:px-4 h-14 flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-500 to-indigo-600 flex items-center justify-center">
<Zap className="w-4 h-4 text-white" />
</div>
<div>
<h1 className="text-sm font-semibold text-white leading-tight">
{mode === 'voice' ? '语音通话' : mode === 'chat' ? '文字对话' : 'AI 视频'}
</h1>
<p className="text-[11px] text-slate-400 leading-tight">
{mode === 'voice'
? '直连 S2S 语音 · ChatTTSText'
: mode === 'chat'
? (textEngine === 's2s'
? 'S2S 文字 · event 501 ChatTextQuery'
: (handoff?.subtitles?.length > 0
? '语音转接 · 上下文已延续'
: '方舟 LLM · Function Calling'))
: 'Seedance · AI 视频生成'}
</p>
</div>
</div>
<div className="flex items-center gap-1.5">
{/* History button */}
<button
onClick={() => setShowHistory(true)}
className="p-2 rounded-lg hover:bg-slate-700/50 text-slate-400 hover:text-white transition-colors mr-1"
title="会话历史"
>
<History className="w-4 h-4" />
</button>
{/* New session button */}
<button
onClick={handleNewSession}
className="p-2 rounded-lg hover:bg-slate-700/50 text-slate-400 hover:text-white transition-colors"
title="新建会话"
>
<Plus className="w-4 h-4" />
</button>
{/* Mode toggle buttons */}
<div className="flex items-center bg-slate-800/60 rounded-lg border border-slate-700/40 p-0.5 mr-2">
<button
onClick={() => { if (mode !== 'voice') handleBackToVoice(); }}
className={`flex items-center gap-1 px-2.5 py-1 rounded-md text-xs transition-all ${
mode === 'voice'
? 'bg-violet-500/20 text-violet-300 font-medium'
: 'text-slate-500 hover:text-slate-300'
}`}
>
<Mic className="w-3 h-3" /> 语音
</button>
<button
onClick={() => { if (mode !== 'chat') handleStartChat(); }}
className={`flex items-center gap-1 px-2.5 py-1 rounded-md text-xs transition-all ${
mode === 'chat'
? 'bg-violet-500/20 text-violet-300 font-medium'
: 'text-slate-500 hover:text-slate-300'
}`}
>
<MessageSquare className="w-3 h-3" /> 文字
</button>
<button
onClick={() => setMode('video')}
className={`flex items-center gap-1 px-2.5 py-1 rounded-md text-xs transition-all ${
mode === 'video'
? 'bg-violet-500/20 text-violet-300 font-medium'
: 'text-slate-500 hover:text-slate-300'
}`}
>
<Film className="w-3 h-3" /> 视频
</button>
</div>
{mode === 'chat' && (
<button
onClick={toggleTextEngine}
className={`px-2 py-1 rounded-md text-[11px] font-medium transition-colors border ${
textEngine === 's2s'
? 'bg-emerald-500/15 text-emerald-300 border-emerald-500/30'
: 'bg-slate-700/40 text-slate-300 border-slate-600/40'
}`}
title="切换文字对话引擎 (S2S 走 event 501, Coze 走 HTTP/SSE)"
>
{textEngine === 's2s' ? 'S2S' : 'Coze'}
</button>
)}
{mode === 'voice' && (
<button
onClick={() => setShowSettings(!showSettings)}
className="p-2 rounded-lg hover:bg-slate-700/50 text-slate-400 hover:text-white transition-colors"
title="语音设置"
>
<Settings2 className="w-5 h-5" />
</button>
)}
</div>
</div>
</header>
<main className="max-w-5xl mx-auto px-4 py-6">
{/* Settings Panel */}
{showSettings && mode === 'voice' && (
<SettingsPanel
settings={settings}
onChange={setSettings}
voiceConfig={voiceConfig}
onClose={() => setShowSettings(false)}
/>
)}
{mode === 'voice' ? (
<>
{/* Voice Panel */}
<VoicePanel settings={settings} onVoiceEnd={handleVoiceEnd} chatHistory={chatMessages} sessionId={currentSessionId} onSessionCreated={handleSessionCreated} />
{/* Architecture Info */}
<div className="mt-6 p-4 rounded-xl bg-slate-800/40 border border-slate-700/40">
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">RTC 直路由语音架构</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-xs">
<div className="p-3 rounded-lg bg-slate-700/30 border border-slate-600/30">
<div className="text-emerald-400 font-medium mb-1">上行链路</div>
<div className="text-slate-400">浏览器 RTC 麦克风 房间字幕/消息 后端前置路由</div>
</div>
<div className="p-3 rounded-lg bg-slate-700/30 border border-slate-600/30">
<div className="text-amber-400 font-medium mb-1">应答链路</div>
<div className="text-slate-400">知识库/工具结果 ExternalTextToSpeech 语音播报</div>
</div>
<div className="p-3 rounded-lg bg-slate-700/30 border border-slate-600/30">
<div className="text-violet-400 font-medium mb-1">当前目标</div>
<div className="text-slate-400">彻底绕开原生链纯 S2S 抢答保证知识库结果能播报</div>
</div>
</div>
</div>
</>
) : mode === 'chat' ? (
/* Chat Panel */
handoff && (
<ChatPanel
key={`${handoff.sessionId}-${textEngine}`}
sessionId={handoff.sessionId}
voiceSubtitles={handoff.subtitles}
settings={settings}
onBack={handleBackToVoice}
onMessagesChange={setChatMessages}
useS2S={textEngine === 's2s'}
playAudioReply={false}
/>
)
) : (
/* Video Panel */
<VideoPanel />
)}
</main>
{/* Session History Sidebar */}
{showHistory && (
<SessionHistoryPanel
currentSessionId={currentSessionId}
onSelectSession={handleSelectSession}
onNewSession={handleNewSession}
onClose={() => setShowHistory(false)}
/>
)}
</div>
);
}