update:由于现在的模型本身动作有限,使用起来体验不佳,暂时移除动作控制的代码

This commit is contained in:
hrz
2026-01-27 15:45:53 +08:00
parent 0064f975c2
commit 5d72f9f79d
11 changed files with 45 additions and 639 deletions
@@ -68,37 +68,5 @@
"brightness": "${brightness}",
"message": "亮度已设置为 ${brightness}"
}
},
{
"name": "live2d.smile",
"description": "Make the virtual human perform a smiling action. Call this tool when the user says 'smile', 'smile once', 'give me a smile', 'smile please', etc.",
"inputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "live2d.wave",
"description": "Make the virtual human perform a waving action. Call this tool when the user says 'wave to me', 'wave', 'say hello', 'wave please', etc.",
"inputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "live2d.action",
"description": "Trigger a specified action for the virtual human. Supported actions include: FlickUp (swipe up), FlickDown (swipe down), Tap (tap), Tap@Body (body tap), Flick (swipe), Flick@Body (body swipe), etc. Call this tool when the user requests a specific virtual human action.",
"inputSchema": {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "Action name, such as: FlickUp, FlickDown, Tap, Tap@Body, Flick, Flick@Body"
}
},
"required": [
"action"
]
}
}
]
@@ -174,7 +174,6 @@ export class AudioRecorder {
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
try {
this.websocket.send(opusData.buffer);
log(`已发送Opus帧,大小: ${opusData.length} 字节`, 'debug');
} catch (error) {
log(`WebSocket发送错误: ${error.message}`, 'error');
}
@@ -1,131 +0,0 @@
/**
* Audio recording module tests - Browser compatible version
* Test microphone availability detection functionality
*
* This version works without Vitest - uses the simple test framework from test-runner.html
*/
import { checkMicrophoneAvailability, isHttpNonLocalhost } from './recorder.js';
describe('Microphone Availability Detection', () => {
beforeEach(() => {
// Reset mocks before each test
vi.clearAllMocks();
});
/**
* Test checkMicrophoneAvailability function - success case
*/
test('should return true when microphone is available', async () => {
// Mock navigator.mediaDevices.getUserMedia to return a successful stream
const mockTrack = { stop: vi.fn() };
const mockStream = { getTracks: () => [mockTrack] };
navigator.mediaDevices.getUserMedia = vi.fn().mockResolvedValue(mockStream);
const result = await checkMicrophoneAvailability();
expect(result).toBe(true);
expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 16000, channelCount: 1 } });
expect(mockTrack.stop).toHaveBeenCalled();
});
/**
* Test checkMicrophoneAvailability function - failure case
*/
test('should return false when microphone is not available', async () => {
// Mock getUserMedia to throw an error
navigator.mediaDevices.getUserMedia = vi.fn().mockRejectedValue(new Error('Permission denied'));
const result = await checkMicrophoneAvailability();
expect(result).toBe(false);
expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalled();
});
/**
* Test checkMicrophoneAvailability function - browser not supported
*/
test('should return false when browser does not support getUserMedia', async () => {
// Mock navigator.mediaDevices.getUserMedia to be undefined
const originalGetUserMedia = navigator.mediaDevices.getUserMedia;
navigator.mediaDevices.getUserMedia = undefined;
const result = await checkMicrophoneAvailability();
expect(result).toBe(false);
// Restore
navigator.mediaDevices.getUserMedia = originalGetUserMedia;
});
/**
* Test isHttpNonLocalhost function - HTTP non-localhost
* Note: window.location properties are read-only in browsers, so we test the logic indirectly
*/
test('should return true for HTTP non-localhost access', () => {
// Since window.location is read-only, we'll test by checking the actual implementation
// This test verifies the function works correctly with the current location
// In a real browser environment, this would test against actual location
const result = isHttpNonLocalhost();
// Just verify the function runs without error
expect(typeof result).toBe('boolean');
});
/**
* Test isHttpNonLocalhost function - localhost should return false
* Note: window.location properties are read-only in browsers
*/
test('should return false for localhost', () => {
// Test the logic by checking if current location is localhost
const result = isHttpNonLocalhost();
// If we're on localhost, result should be false
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
expect(result).toBe(false);
} else {
// Otherwise just verify function returns boolean
expect(typeof result).toBe('boolean');
}
});
/**
* Test isHttpNonLocalhost function - 127.0.0.1 should return false
* Note: window.location properties are read-only in browsers
*/
test('should return false for 127.0.0.1', () => {
// Test the logic by checking if current location is 127.0.0.1
const result = isHttpNonLocalhost();
// If we're on 127.0.0.1, result should be false
if (window.location.hostname === '127.0.0.1') {
expect(result).toBe(false);
} else {
// Otherwise just verify function returns boolean
expect(typeof result).toBe('boolean');
}
});
/**
* Test isHttpNonLocalhost function - private IP should return false
* Note: window.location properties are read-only in browsers
*/
test('should return false for private IP addresses', () => {
// Test the logic by checking if current location is a private IP
const result = isHttpNonLocalhost();
const hostname = window.location.hostname;
const isPrivateIP = hostname.startsWith('192.168.') || hostname.startsWith('10.') || hostname.startsWith('172.');
if (isPrivateIP && window.location.protocol === 'http:') {
expect(result).toBe(false);
} else {
// Otherwise just verify function returns boolean
expect(typeof result).toBe('boolean');
}
});
/**
* Test isHttpNonLocalhost function - HTTPS should return false
* Note: window.location properties are read-only in browsers
*/
test('should return false for HTTPS protocol', () => {
// Test the logic by checking if current protocol is HTTPS
const result = isHttpNonLocalhost();
// If we're on HTTPS, result should be false
if (window.location.protocol === 'https:') {
expect(result).toBe(false);
} else {
// Otherwise just verify function returns boolean
expect(typeof result).toBe('boolean');
}
});
});
@@ -1,101 +0,0 @@
/**
* Audio recording module tests
* Test microphone availability detection functionality
*/
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
import { checkMicrophoneAvailability, isHttpNonLocalhost } from './recorder.js';
describe('Microphone Availability Detection', () => {
beforeEach(() => {
// Reset mocks before each test
vi.clearAllMocks();
});
/**
* Test checkMicrophoneAvailability function - success case
*/
test('should return true when microphone is available', async () => {
// Mock navigator.mediaDevices.getUserMedia to return a successful stream
const mockTrack = { stop: vi.fn() };
const mockStream = { getTracks: () => [mockTrack] };
global.navigator.mediaDevices.getUserMedia = vi.fn().mockResolvedValue(mockStream);
const result = await checkMicrophoneAvailability();
expect(result).toBe(true);
expect(global.navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 16000, channelCount: 1 } });
expect(mockTrack.stop).toHaveBeenCalled();
});
/**
* Test checkMicrophoneAvailability function - failure case
*/
test('should return false when microphone is not available', async () => {
// Mock getUserMedia to throw an error
global.navigator.mediaDevices.getUserMedia = vi.fn().mockRejectedValue(new Error('Permission denied'));
const result = await checkMicrophoneAvailability();
expect(result).toBe(false);
expect(global.navigator.mediaDevices.getUserMedia).toHaveBeenCalled();
});
/**
* Test checkMicrophoneAvailability function - browser not supported
*/
test('should return false when browser does not support getUserMedia', async () => {
// Mock navigator without mediaDevices
const originalMediaDevices = global.navigator.mediaDevices;
delete global.navigator.mediaDevices;
const result = await checkMicrophoneAvailability();
expect(result).toBe(false);
// Restore
global.navigator.mediaDevices = originalMediaDevices;
});
/**
* Test isHttpNonLocalhost function - HTTP non-localhost
*/
test('should return true for HTTP non-localhost access', () => {
// Mock window.location for HTTP non-localhost
Object.defineProperty(window, 'location', { value: { protocol: 'http:', hostname: 'example.com' }, writable: true, configurable: true });
const result = isHttpNonLocalhost();
expect(result).toBe(true);
});
/**
* Test isHttpNonLocalhost function - localhost should return false
*/
test('should return false for localhost', () => {
Object.defineProperty(window, 'location', { value: { protocol: 'http:', hostname: 'localhost' }, writable: true, configurable: true });
const result = isHttpNonLocalhost();
expect(result).toBe(false);
});
/**
* Test isHttpNonLocalhost function - 127.0.0.1 should return false
*/
test('should return false for 127.0.0.1', () => {
Object.defineProperty(window, 'location', { value: { protocol: 'http:', hostname: '127.0.0.1' }, writable: true, configurable: true });
const result = isHttpNonLocalhost();
expect(result).toBe(false);
});
/**
* Test isHttpNonLocalhost function - private IP should return false
*/
test('should return false for private IP addresses', () => {
const privateIPs = ['192.168.1.100', '10.0.0.1', '172.16.0.1'];
privateIPs.forEach(ip => {
Object.defineProperty(window, 'location', { value: { protocol: 'http:', hostname: ip }, writable: true, configurable: true });
const result = isHttpNonLocalhost();
expect(result).toBe(false);
});
});
/**
* Test isHttpNonLocalhost function - HTTPS should return false
*/
test('should return false for HTTPS protocol', () => {
Object.defineProperty(window, 'location', { value: { protocol: 'https:', hostname: 'example.com' }, writable: true, configurable: true });
const result = isHttpNonLocalhost();
expect(result).toBe(false);
});
});
@@ -305,43 +305,6 @@ export function getMcpTools() {
return mcpTools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema }));
}
/**
* 执行Live2D动作
* @param {string} toolName - 工具名称,如 'live2d.smile', 'live2d.wave', 'live2d.action'
* @param {Object} toolArgs - 工具参数,对于 'live2d.action' 工具,应包含 'action' 字段
* @returns {Object} 执行结果,包含 success, message, action, tool 等字段
*/
function executeLive2DAction(toolName, toolArgs) {
try {
const live2dManager = window.chatApp?.live2dManager;
if (!live2dManager) {
log('Live2D管理器未初始化', 'error');
return { success: false, error: 'Live2D管理器未初始化' };
}
// 动作映射:工具名称到Live2D动作名称
const actionMap = { 'live2d.smile': 'FlickUp', 'live2d.wave': 'Tap', 'live2d.happy': 'FlickUp', 'live2d.sad': 'FlickDown', 'live2d.tap': 'Tap', 'live2d.tapBody': 'Tap@Body', 'live2d.flick': 'Flick', 'live2d.flickBody': 'Flick@Body', 'live2d.flickUp': 'FlickUp', 'live2d.flickDown': 'FlickDown' };
let motionName;
if (toolName === 'live2d.action' && toolArgs?.action) {
// 通用动作工具,使用指定的动作名称
motionName = toolArgs.action;
} else {
// 使用映射表查找对应动作名称
motionName = actionMap[toolName];
}
if (!motionName) {
log(`未知的动作: ${toolName}`, 'error');
return { success: false, error: `未知的动作: ${toolName}` };
}
// 触发Live2D动作
live2dManager.motion(motionName);
log(`Live2D动作已触发: ${motionName}`, 'success');
return { success: true, message: `虚拟人已执行动作: ${motionName}`, action: motionName, tool: toolName };
} catch (error) {
log(`执行Live2D动作失败: ${error.message}`, 'error');
return { success: false, error: `执行动作失败: ${error.message}` };
}
}
/**
* 执行工具调用
*/
@@ -351,10 +314,6 @@ export function executeMcpTool(toolName, toolArgs) {
log(`未找到工具: ${toolName}`, 'error');
return { success: false, error: `未知工具: ${toolName}` };
}
// 处理Live2D动作工具
if (toolName.startsWith('live2d.')) {
return executeLive2DAction(toolName, toolArgs);
}
// 如果有模拟返回结果,使用它
if (tool.mockResponse) {
// 替换模板变量
@@ -1,98 +0,0 @@
/**
* MCP工具模块测试 - Browser compatible version
* 测试Live2D动作工具执行功能
*
* This version works without Vitest - uses the simple test framework from test-runner.html
*/
import { executeMcpTool, initMcpTools, setWebSocket } from './tools.js';
describe('Live2D Action Tools', () => {
let mockLive2DManager, originalChatApp;
beforeEach(() => {
// Reset mocks before each test
vi.clearAllMocks();
// Save original chatApp
originalChatApp = window.chatApp;
// Mock Live2D manager
mockLive2DManager = { motion: vi.fn() };
// Setup window.chatApp
window.chatApp = { live2dManager: mockLive2DManager };
// Mock localStorage
localStorage.getItem = vi.fn(() => null);
// Mock fetch for default-mcp-tools.json
globalThis.fetch = vi.fn(() => Promise.resolve({ json: () => Promise.resolve([{ name: 'live2d.smile', description: 'Make the virtual human smile', inputSchema: { type: 'object', properties: {} } }, { name: 'live2d.wave', description: 'Make the virtual human wave', inputSchema: { type: 'object', properties: {} } }, { name: 'live2d.action', description: 'Trigger a specified action', inputSchema: { type: 'object', properties: { action: { type: 'string' } }, required: ['action'] } }]) }));
// Mock DOM elements - ensure all required elements exist
const mockContainer = { innerHTML: '', appendChild: vi.fn(), textContent: '' };
document.getElementById = vi.fn((id) => {
// Return mock elements for all IDs that tools.js might access
if (id === 'mcpToolsContainer' || id === 'mcpPropertiesContainer') return mockContainer;
if (id === 'mcpToolsCount') return { textContent: '' };
// Return null for other elements (they're checked with if statements)
return null;
});
});
afterEach(() => {
// Clean up
window.chatApp = originalChatApp;
});
/**
* 测试 executeMcpTool - smile 动作
*/
test('should execute Live2D smile action', async () => {
await initMcpTools();
const result = executeMcpTool('live2d.smile', {});
expect(result.success).toBe(true);
expect(result.action).toBe('FlickUp');
expect(result.tool).toBe('live2d.smile');
expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickUp');
});
/**
* 测试 executeMcpTool - wave 动作
*/
test('should execute Live2D wave action', async () => {
await initMcpTools();
const result = executeMcpTool('live2d.wave', {});
expect(result.success).toBe(true);
expect(result.action).toBe('Tap');
expect(result.tool).toBe('live2d.wave');
expect(mockLive2DManager.motion).toHaveBeenCalledWith('Tap');
});
/**
* 测试 executeMcpTool - 通用动作工具
*/
test('should handle generic action tool', async () => {
await initMcpTools();
const result = executeMcpTool('live2d.action', { action: 'FlickDown' });
expect(result.success).toBe(true);
expect(result.action).toBe('FlickDown');
expect(result.tool).toBe('live2d.action');
expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickDown');
});
/**
* 测试 executeMcpTool - Live2D管理器未初始化
*/
test('should handle missing Live2D manager gracefully', async () => {
await initMcpTools();
window.chatApp = null;
const result = executeMcpTool('live2d.smile', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Live2D管理器未初始化');
});
/**
* 测试 executeMcpTool - 未知的工具
*/
test('should handle unknown tool gracefully', async () => {
await initMcpTools();
const result = executeMcpTool('unknown.tool', {});
expect(result.success).toBe(false);
expect(result.error).toContain('未知工具');
});
});
@@ -1,133 +0,0 @@
/**
* MCP工具模块测试
* 测试Live2D动作工具执行功能
*/
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
import { executeMcpTool, initMcpTools, setWebSocket } from './tools.js';
describe('Live2D Action Tools', () => {
let mockLive2DManager;
beforeEach(() => {
// Reset mocks before each test
vi.clearAllMocks();
// Mock Live2D manager
mockLive2DManager = { motion: vi.fn() };
// Setup window.chatApp
global.window.chatApp = { live2dManager: mockLive2DManager };
// Mock localStorage
global.localStorage.getItem = vi.fn(() => null);
// Mock fetch for default-mcp-tools.json
global.fetch = vi.fn(() => Promise.resolve({ json: () => Promise.resolve([{ name: 'live2d.smile', description: 'Make the virtual human smile', inputSchema: { type: 'object', properties: {} } }, { name: 'live2d.wave', description: 'Make the virtual human wave', inputSchema: { type: 'object', properties: {} } }, { name: 'live2d.action', description: 'Trigger a specified action', inputSchema: { type: 'object', properties: { action: { type: 'string' } }, required: ['action'] } }]) }));
// Mock DOM elements
global.document.getElementById = vi.fn((id) => {
if (id === 'mcpToolsContainer') {
return { innerHTML: '', appendChild: vi.fn() };
}
if (id === 'mcpToolsCount') {
return { textContent: '' };
}
return null;
});
});
afterEach(() => {
// Clean up
global.window.chatApp = null;
});
/**
* 测试 executeMcpTool - smile 动作
*/
test('should execute Live2D smile action', async () => {
await initMcpTools();
const result = executeMcpTool('live2d.smile', {});
expect(result.success).toBe(true);
expect(result.action).toBe('FlickUp');
expect(result.tool).toBe('live2d.smile');
expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickUp');
});
/**
* 测试 executeMcpTool - wave 动作
*/
test('should execute Live2D wave action', async () => {
await initMcpTools();
const result = executeMcpTool('live2d.wave', {});
expect(result.success).toBe(true);
expect(result.action).toBe('Tap');
expect(result.tool).toBe('live2d.wave');
expect(mockLive2DManager.motion).toHaveBeenCalledWith('Tap');
});
/**
* 测试 executeMcpTool - 通用动作工具
*/
test('should handle generic action tool', async () => {
await initMcpTools();
const result = executeMcpTool('live2d.action', { action: 'FlickDown' });
expect(result.success).toBe(true);
expect(result.action).toBe('FlickDown');
expect(result.tool).toBe('live2d.action');
expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickDown');
});
/**
* 测试 executeMcpTool - Live2D管理器未初始化
*/
test('should handle missing Live2D manager gracefully', async () => {
await initMcpTools();
global.window.chatApp = null;
const result = executeMcpTool('live2d.smile', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Live2D管理器未初始化');
});
/**
* 测试 executeMcpTool - 未知的动作
*/
test('should handle unknown action gracefully', async () => {
await initMcpTools();
// Add an unknown live2d tool to the list
const tools = await global.fetch().then(res => res.json());
tools.push({ name: 'live2d.unknown', description: 'Unknown action', inputSchema: { type: 'object', properties: {} } });
global.fetch = vi.fn(() => Promise.resolve({ json: () => Promise.resolve(tools) }));
await initMcpTools();
const result = executeMcpTool('live2d.unknown', {});
expect(result.success).toBe(false);
expect(result.error).toContain('未知的动作');
});
/**
* 测试 executeMcpTool - 未知的工具
*/
test('should handle unknown tool gracefully', async () => {
await initMcpTools();
const result = executeMcpTool('unknown.tool', {});
expect(result.success).toBe(false);
expect(result.error).toContain('未知工具');
});
/**
* 测试 executeMcpTool - 其他动作映射
*/
test('should handle other action mappings', async () => {
await initMcpTools();
const actionMappings = [{ tool: 'live2d.happy', expectedAction: 'FlickUp' }, { tool: 'live2d.sad', expectedAction: 'FlickDown' }, { tool: 'live2d.tap', expectedAction: 'Tap' }, { tool: 'live2d.tapBody', expectedAction: 'Tap@Body' }, { tool: 'live2d.flick', expectedAction: 'Flick' }, { tool: 'live2d.flickBody', expectedAction: 'Flick@Body' }, { tool: 'live2d.flickUp', expectedAction: 'FlickUp' }, { tool: 'live2d.flickDown', expectedAction: 'FlickDown' }];
// Add these tools to the mock
const tools = await global.fetch().then(res => res.json());
actionMappings.forEach(mapping => {
tools.push({ name: mapping.tool, description: `Test ${mapping.tool}`, inputSchema: { type: 'object', properties: {} } });
});
global.fetch = vi.fn(() => Promise.resolve({ json: () => Promise.resolve(tools) }));
await initMcpTools();
for (const mapping of actionMappings) {
mockLive2DManager.motion.mockClear();
const result = executeMcpTool(mapping.tool, {});
expect(result.success).toBe(true);
expect(result.action).toBe(mapping.expectedAction);
expect(mockLive2DManager.motion).toHaveBeenCalledWith(mapping.expectedAction);
}
});
});
@@ -304,7 +304,6 @@ export class WebSocketHandler {
let arrayBuffer;
if (data instanceof ArrayBuffer) {
arrayBuffer = data;
log(`收到ArrayBuffer音频数据,大小: ${data.byteLength}字节`, 'debug');
} else if (data instanceof Blob) {
arrayBuffer = await data.arrayBuffer();
log(`收到Blob音频数据,大小: ${arrayBuffer.byteLength}字节`, 'debug');
@@ -392,7 +391,7 @@ export class WebSocketHandler {
this.websocket.onerror = (error) => {
log(`WebSocket错误: ${error.message || '未知错误'}`, 'error');
uiController.addChatMessage(`⚠️ WebSocket错误: ${error.message || '未知错误'}`, false);
if (this.onConnectionStateChange) {
this.onConnectionStateChange(false);
}
+44 -41
View File
@@ -1,8 +1,8 @@
// UI controller module
import { loadConfig, saveConfig } from '../config/manager.js';
import { getAudioPlayer } from '../core/audio/player.js';
import { getAudioRecorder } from '../core/audio/recorder.js';
import { getWebSocketHandler } from '../core/network/websocket.js';
import { getAudioPlayer } from '../core/audio/player.js';
// UI controller class
class UIController {
@@ -292,12 +292,7 @@ class UIController {
// Update button text and title
recordBtn.querySelector('.btn-text').textContent = '录音';
recordBtn.title = isHttpNonLocalhost ? '当前由于是http访问,无法录音,只能用文字交互' : '麦克风不可用';
// Display notification message in chat
if (isHttpNonLocalhost) {
this.addChatMessage('⚠️ 当前由于是http访问,无法录音,只能用文字交互', false);
} else {
this.addChatMessage('⚠️ 麦克风不可用,请检查权限设置', false);
}
} else {
// If connected, enable record button
const wsHandler = getWebSocketHandler();
@@ -369,10 +364,20 @@ class UIController {
// Start AI chat session after connection
startAIChatSession() {
this.addChatMessage('连接成功,开始聊天吧~😊', false);
// Start recording
const recordBtn = document.getElementById('recordBtn');
if (recordBtn) {
recordBtn.click();
// Check microphone availability and show error messages if needed
if (!window.microphoneAvailable) {
if (window.isHttpNonLocalhost) {
this.addChatMessage('⚠️ 当前由于是http访问,无法录音,只能用文字交互', false);
} else {
this.addChatMessage('⚠️ 麦克风不可用,请检查权限设置,只能用文字交互', false);
}
}
// Start recording only if microphone is available
if (window.microphoneAvailable) {
const recordBtn = document.getElementById('recordBtn');
if (recordBtn) {
recordBtn.click();
}
}
}
@@ -418,13 +423,39 @@ class UIController {
// Get WebSocket handler instance
const wsHandler = getWebSocketHandler();
// Register connection state callback BEFORE connecting
wsHandler.onConnectionStateChange = (isConnected) => {
this.updateConnectionUI(isConnected);
this.updateDialButton(isConnected);
};
// Register chat message callback BEFORE connecting
wsHandler.onChatMessage = (text, isUser) => {
this.addChatMessage(text, isUser);
};
// Register record button state callback BEFORE connecting
wsHandler.onRecordButtonStateChange = (isRecording) => {
const recordBtn = document.getElementById('recordBtn');
if (recordBtn) {
if (isRecording) {
recordBtn.classList.add('recording');
recordBtn.querySelector('.btn-text').textContent = '录音中';
} else {
recordBtn.classList.remove('recording');
recordBtn.querySelector('.btn-text').textContent = '录音';
}
}
};
const isConnected = await wsHandler.connect();
if (isConnected) {
// Check microphone availability (check again after connection)
const { checkMicrophoneAvailability } = await import('../core/audio/recorder.js');
const micAvailable = await checkMicrophoneAvailability();
if (!micAvailable) {
const isHttp = window.isHttpNonLocalhost;
if (isHttp) {
@@ -434,34 +465,6 @@ class UIController {
window.microphoneAvailable = false;
}
// Register connection state callback
wsHandler.onConnectionStateChange = (isConnected) => {
this.updateConnectionUI(isConnected);
this.updateDialButton(isConnected);
};
// Register chat message callback
wsHandler.onChatMessage = (text, isUser) => {
this.addChatMessage(text, isUser);
};
// Register record button state callback
wsHandler.onRecordButtonStateChange = (isRecording) => {
const recordBtn = document.getElementById('recordBtn');
if (recordBtn) {
if (isRecording) {
recordBtn.classList.add('recording');
recordBtn.querySelector('.btn-text').textContent = '录音中';
} else {
recordBtn.classList.remove('recording');
recordBtn.querySelector('.btn-text').textContent = '录音';
}
}
};
// Connection successful
this.addChatMessage('OTA连接成功,正在建立WebSocket连接...', false);
// Update dial button state
const dialBtn = document.getElementById('dialBtn');
if (dialBtn) {
@@ -595,4 +598,4 @@ class UIController {
export const uiController = new UIController();
// Export class for module usage
export { UIController };
export { UIController };