Bloom_Ware / static /frontend /js /agent.js
LawrenceBai's picture
feat: overhaul MCP architecture with structured tool schemas, comprehensive care-mode skill definitions, and enhanced test coverage for pipelines and service integration.
79df050
Raw
History Blame Contribute Delete
8.78 kB
window.currentState = 'idle';
window.thinkingTimeout = null;
window.stateBufferTimeout = null;
/**
* 設置 Agent 狀態,加入緩衝機制防止動畫閃爍
*/
function setState(newState, options = {}) {
if (window.currentState === newState && !window.stateBufferTimeout) {
return;
}
// 如果有待處理的狀態轉換,先清除它
if (window.stateBufferTimeout) {
clearTimeout(window.stateBufferTimeout);
window.stateBufferTimeout = null;
}
// 針對進入 idle 狀態加入微小延遲,避免在快速切換(如 speaking -> idle -> recording)時花瓣動畫跳轉
if (newState === 'idle') {
window.stateBufferTimeout = setTimeout(() => {
window.stateBufferTimeout = null;
applyStateChange('idle', options);
}, 150);
} else {
applyStateChange(newState, options);
}
}
/**
* 實際執行狀態變更與 UI 更新
*/
function applyStateChange(newState, options) {
const oldState = window.currentState;
window.currentState = newState;
console.log(`🔄 狀態切換: ${oldState} -> ${newState}`);
// 清除之前的思考超時計時器
if (window.thinkingTimeout) {
clearTimeout(window.thinkingTimeout);
window.thinkingTimeout = null;
}
// 更新麥克風容器樣式
micContainer.classList.remove('recording', 'thinking', 'speaking', 'disconnected');
switch(newState) {
case 'idle':
// idle 狀態不應主動隱藏輸出,讓使用者能看清最後的回覆
if (typeof stopSpeaking === 'function') {
stopSpeaking(true, 'state_idle');
}
if (options.clearCards !== false) {
clearAllCards();
}
break;
case 'recording':
micContainer.classList.add('recording');
if (!options.keepOutput) {
hideAgentOutput();
}
if (!options.keepCards) {
clearAllCards();
}
transcript.textContent = '聆聽中...';
transcript.className = 'voice-transcript provisional';
break;
case 'thinking':
micContainer.classList.add('thinking');
hideAgentOutput();
if (typeof stopSpeaking === 'function') {
stopSpeaking(true, 'state_thinking');
}
// 設定思考超時重置 (45秒)
window.thinkingTimeout = setTimeout(() => {
if (window.currentState === 'thinking') {
console.warn('⚠️ 思考時間過長,自動重置');
showErrorNotification('抱歉,處理時間過長,請再試一次。');
resetAgent({clearCards: false});
}
}, 45000);
break;
case 'speaking':
micContainer.classList.add('speaking');
if (options.outputText) {
typewriterEffect(options.outputText, 40, options.enableTTS);
}
break;
case 'disconnected':
micContainer.classList.add('disconnected');
hideAgentOutput();
if (typeof stopSpeaking === 'function') {
stopSpeaking(true, 'state_disconnected');
}
clearAllCards();
break;
default:
console.error(`❌ 未知狀態: ${newState}`);
}
}
window.currentEmotion = 'neutral';
window.isInCareMode = false;
function applyEmotion(emotion, careMode = null) {
if (careMode !== null) {
window.isInCareMode = !!careMode;
}
const validEmotions = ['neutral', 'happy', 'sad', 'angry', 'fear', 'surprise'];
// 🎯 支援多語言/原始標籤映射,確保 100% 信心
const mapping = {
'悲傷(sad)': 'sad', '悲傷': 'sad',
'開心(happy)': 'happy', '開心': 'happy',
'生氣(angry)': 'angry', '生氣': 'angry',
'恐懼(fear)': 'fear', '恐懼': 'fear',
'驚訝(surprise)': 'surprise', '驚訝': 'surprise',
'中性(neutral)': 'neutral', '中性': 'neutral'
};
if (mapping[emotion]) {
emotion = mapping[emotion];
}
if (!validEmotions.includes(emotion)) {
emotion = 'neutral';
}
background.className = `voice-immersive-background emotion-${emotion} active`;
emotionIndicator.textContent = `當前情緒: ${emotionEmojis[emotion]}`;
// 🎯 保存到全域狀態,供 TTS 使用
window.currentEmotion = emotion;
}
function showErrorNotification(message) {
console.error('🚨 錯誤:', message);
// 立即重置 Agent 狀態(關閉花朵,停止錄音/語音)
resetAgent({ clearCards: false });
// 顯示錯誤訊息於輸出區域,但不切換到 speaking 狀態(讓花朵保持 idle)
if (typeof typewriterEffect === 'function') {
typewriterEffect(`抱歉,發生錯誤:${message}`, 40, false);
}
// 3秒後自動隱藏錯誤訊息
setTimeout(() => {
if (currentState === 'idle') {
hideAgentOutput();
}
}, 5000);
}
let isThinking = false;
let isDisconnected = false;
let isRecording = false;
let isSpeaking = false;
function resetAgent(options = {}) {
isRecording = false;
isThinking = false;
isSpeaking = false;
isDisconnected = false;
if (typeof stopSpeaking === 'function') {
stopSpeaking(true, 'reset_agent');
}
if (typeof stopRealAudioAnalysis === 'function') {
stopRealAudioAnalysis();
}
if (wsManager && typeof wsManager.stopRecording === 'function') {
wsManager.stopRecording();
}
if (typeof transcript !== 'undefined' && transcript) {
transcript.textContent = '';
}
setState('idle', options);
}
function initAgentControls() {
const handleMicInteraction = async () => {
if (currentState === 'recording') {
isRecording = false;
if (typeof stopRealAudioAnalysis === 'function') {
stopRealAudioAnalysis();
}
if (wsManager && typeof wsManager.stopRecording === 'function') {
wsManager.stopRecording();
}
setState('thinking');
return;
}
if (currentState === 'idle' || currentState === 'disconnected' || currentState === 'speaking') {
if (currentState === 'speaking' && typeof stopSpeaking === 'function') {
stopSpeaking(true, 'mic_interrupt');
}
isRecording = true;
setState('recording', {
keepOutput: true, // 保留前次 Agent 回應
keepCards: true, // 保留前次工具卡片
detect_timeout: 20.0, // 考量到 Function Calling 可能較慢
feature_timeout: 30.0, // MCP 工具內部超時
ai_timeout: 25.0 // 配合 Streaming
});
if (typeof startRealAudioAnalysis === 'function') {
await startRealAudioAnalysis();
}
if (wsManager && typeof wsManager.startRecording === 'function') {
const success = await wsManager.startRecording();
if (!success) {
console.error('❌ 錄音啟動失敗');
resetAgent();
}
} else {
console.error('❌ WebSocket 管理器未初始化');
resetAgent();
}
}
};
// 點擊麥克風中心
micContainer.addEventListener('click', handleMicInteraction);
// 點擊波形容器(較大區域)也觸發交互,提高可用性
const waveformContainer = document.querySelector('.voice-waveform-container');
if (waveformContainer) {
waveformContainer.style.cursor = 'pointer';
waveformContainer.addEventListener('click', (e) => {
// 如果點擊的是 micContainer 內部,就不重複觸發(事件冒泡)
if (e.target === micContainer || micContainer.contains(e.target)) {
return;
}
handleMicInteraction();
});
}
document.getElementById('toggle-recording').addEventListener('click', async () => {
isRecording = !isRecording;
if (isRecording) {
setState('recording');
await startRealAudioAnalysis();
} else {
setState('idle');
stopRealAudioAnalysis();
}
});
document.getElementById('toggle-thinking').addEventListener('click', () => {
isThinking = !isThinking;
if (isThinking) {
setState('thinking');
} else {
setState('idle', {clearCards: false}); // 保留工具卡片
}
});
document.getElementById('toggle-speaking').addEventListener('click', () => {
isSpeaking = !isSpeaking;
if (isSpeaking) {
clearAllCards();
setTimeout(() => addToolCard('weather'), 300);
const responseText = '根據目前的天氣資料,台北今天氣溫約 23°C,天氣晴朗,濕度 65%。建議您外出時可以穿著輕便舒適的衣物,並記得攜帶太陽眼鏡。';
setState('speaking', {outputText: responseText});
} else {
setState('idle', {clearCards: false}); // 保留工具卡片
}
});
document.getElementById('toggle-disconnected').addEventListener('click', () => {
isDisconnected = !isDisconnected;
if (isDisconnected) {
setState('disconnected');
} else {
setState('idle');
}
});
}