controller.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  1. // UI controller module
  2. import { loadConfig, saveConfig } from '../config/manager.js?v=0205';
  3. import { getAudioPlayer } from '../core/audio/player.js?v=0205';
  4. import { getAudioRecorder } from '../core/audio/recorder.js?v=0205';
  5. import { getWebSocketHandler } from '../core/network/websocket.js?v=0205';
  6. // UI controller class
  7. class UIController {
  8. constructor() {
  9. this.isEditing = false;
  10. this.visualizerCanvas = null;
  11. this.visualizerContext = null;
  12. this.audioStatsTimer = null;
  13. this.currentBackgroundIndex = localStorage.getItem('backgroundIndex') ? parseInt(localStorage.getItem('backgroundIndex')) : 0;
  14. this.backgroundImages = ['1.png', '2.png', '3.png'];
  15. this.dialBtnDisabled = false;
  16. // Bind methods
  17. this.init = this.init.bind(this);
  18. this.initEventListeners = this.initEventListeners.bind(this);
  19. this.updateDialButton = this.updateDialButton.bind(this);
  20. this.addChatMessage = this.addChatMessage.bind(this);
  21. this.switchBackground = this.switchBackground.bind(this);
  22. this.switchLive2DModel = this.switchLive2DModel.bind(this);
  23. this.showModal = this.showModal.bind(this);
  24. this.hideModal = this.hideModal.bind(this);
  25. this.switchTab = this.switchTab.bind(this);
  26. }
  27. // Initialize
  28. init() {
  29. console.log('UIController init started');
  30. this.visualizerCanvas = document.getElementById('audioVisualizer');
  31. if (this.visualizerCanvas) {
  32. this.visualizerContext = this.visualizerCanvas.getContext('2d');
  33. this.initVisualizer();
  34. }
  35. // Check if connect button exists during initialization
  36. const connectBtn = document.getElementById('connectBtn');
  37. console.log('connectBtn during init:', connectBtn);
  38. this.initEventListeners();
  39. this.startAudioStatsMonitor();
  40. loadConfig();
  41. // Register recording callback
  42. const audioRecorder = getAudioRecorder();
  43. audioRecorder.onRecordingStart = (seconds) => {
  44. this.updateRecordButtonState(true, seconds);
  45. };
  46. // Initialize status display
  47. this.updateConnectionUI(false);
  48. // Apply saved background
  49. const backgroundContainer = document.querySelector('.background-container');
  50. if (backgroundContainer) {
  51. backgroundContainer.style.backgroundImage = `url('./images/${this.backgroundImages[this.currentBackgroundIndex]}')`;
  52. }
  53. this.updateDialButton(false);
  54. console.log('UIController init completed');
  55. }
  56. // Initialize visualizer
  57. initVisualizer() {
  58. if (this.visualizerCanvas) {
  59. this.visualizerCanvas.width = this.visualizerCanvas.clientWidth;
  60. this.visualizerCanvas.height = this.visualizerCanvas.clientHeight;
  61. this.visualizerContext.fillStyle = '#fafafa';
  62. this.visualizerContext.fillRect(0, 0, this.visualizerCanvas.width, this.visualizerCanvas.height);
  63. }
  64. }
  65. // Initialize event listeners
  66. initEventListeners() {
  67. // Settings button
  68. const settingsBtn = document.getElementById('settingsBtn');
  69. if (settingsBtn) {
  70. settingsBtn.addEventListener('click', () => {
  71. this.showModal('settingsModal');
  72. });
  73. }
  74. // Background switch button
  75. const backgroundBtn = document.getElementById('backgroundBtn');
  76. if (backgroundBtn) {
  77. backgroundBtn.addEventListener('click', this.switchBackground);
  78. }
  79. // Model select change event
  80. const modelSelect = document.getElementById('live2dModelSelect');
  81. if (modelSelect) {
  82. modelSelect.addEventListener('change', () => {
  83. this.switchLive2DModel();
  84. });
  85. }
  86. // Camera switch button
  87. const cameraSwitch = document.getElementById('cameraSwitch');
  88. const cameraSwitchMask = document.getElementById('cameraSwitchMask');
  89. if (cameraSwitchMask) {
  90. cameraSwitchMask.addEventListener('click', () => {
  91. const isCameraActive = cameraSwitch.classList.contains('active');
  92. if (isCameraActive) {
  93. window.switchCamera();
  94. }
  95. })
  96. }
  97. // Dial button
  98. const dialBtn = document.getElementById('dialBtn');
  99. if (dialBtn) {
  100. dialBtn.addEventListener('click', () => {
  101. dialBtn.disabled = true;
  102. this.dialBtnDisabled = true;
  103. setTimeout(() => {
  104. dialBtn.disabled = false;
  105. this.dialBtnDisabled = false;
  106. }, 3000);
  107. const wsHandler = getWebSocketHandler();
  108. const isConnected = wsHandler.isConnected();
  109. if (isConnected) {
  110. wsHandler.disconnect();
  111. this.updateDialButton(false);
  112. if (cameraSwitch) cameraSwitch.classList.remove('active');
  113. this.addChatMessage('Disconnected, see you next time~😊', false);
  114. } else {
  115. // Check if OTA URL is filled
  116. const otaUrlInput = document.getElementById('otaUrl');
  117. if (!otaUrlInput || !otaUrlInput.value.trim()) {
  118. // If OTA URL is not filled, show settings modal and switch to device tab
  119. this.showModal('settingsModal');
  120. this.switchTab('device');
  121. this.addChatMessage('Please fill in OTA server URL', false);
  122. return;
  123. }
  124. // Start connection process
  125. this.handleConnect();
  126. }
  127. });
  128. }
  129. // Camera button
  130. const cameraBtn = document.getElementById('cameraBtn');
  131. let cameraTimer = null;
  132. if (cameraBtn) {
  133. cameraBtn.addEventListener('click', () => {
  134. if (cameraTimer) {
  135. clearTimeout(cameraTimer);
  136. cameraTimer = null;
  137. }
  138. cameraTimer = setTimeout(() => {
  139. const cameraContainer = document.getElementById('cameraContainer');
  140. if (!cameraContainer) {
  141. log('摄像头容器不存在', 'warning');
  142. return;
  143. }
  144. const isActive = cameraContainer.classList.contains('active');
  145. if (isActive) {
  146. // 关闭摄像头
  147. if (typeof window.stopCamera === 'function') {
  148. if (cameraSwitch) cameraSwitch.classList.remove('active');
  149. window.stopCamera();
  150. }
  151. cameraContainer.classList.remove('active');
  152. cameraBtn.classList.remove('camera-active');
  153. cameraBtn.querySelector('.btn-text').textContent = '摄像头';
  154. log('摄像头已关闭', 'info');
  155. } else {
  156. // 打开摄像头
  157. if (typeof window.startCamera === 'function') {
  158. window.startCamera().then(success => {
  159. if (success) {
  160. cameraBtn.classList.add('camera-active');
  161. cameraBtn.querySelector('.btn-text').textContent = '关闭';
  162. } else {
  163. this.addChatMessage('⚠️ 摄像头启动失败,请检查浏览器权限', false);
  164. }
  165. }).catch(error => {
  166. log(`启动摄像头异常: ${error.message}`, 'error');
  167. });
  168. } else {
  169. log('startCamera函数未定义', 'warning');
  170. }
  171. }
  172. }, 300);
  173. });
  174. }
  175. // Record button
  176. const recordBtn = document.getElementById('recordBtn');
  177. if (recordBtn) {
  178. let recordTimer = null;
  179. recordBtn.addEventListener('click', () => {
  180. if (recordTimer) {
  181. clearTimeout(recordTimer);
  182. recordTimer = null;
  183. }
  184. recordTimer = setTimeout(() => {
  185. const audioRecorder = getAudioRecorder();
  186. if (audioRecorder.isRecording) {
  187. audioRecorder.stop();
  188. // Restore record button to normal state
  189. recordBtn.classList.remove('recording');
  190. recordBtn.querySelector('.btn-text').textContent = '录音';
  191. } else {
  192. // Update button state to recording
  193. recordBtn.classList.add('recording');
  194. recordBtn.querySelector('.btn-text').textContent = '录音中';
  195. // Start recording, update button state after delay
  196. setTimeout(() => {
  197. audioRecorder.start();
  198. }, 100);
  199. }
  200. }, 300);
  201. });
  202. }
  203. // Chat input event listener
  204. const chatIpt = document.getElementById('chatIpt');
  205. if (chatIpt) {
  206. const wsHandler = getWebSocketHandler();
  207. chatIpt.addEventListener('keydown', (e) => {
  208. if (e.key === 'Enter') {
  209. if (e.target.value) {
  210. wsHandler.sendTextMessage(e.target.value);
  211. e.target.value = '';
  212. return;
  213. }
  214. }
  215. });
  216. }
  217. // Close button
  218. const closeButtons = document.querySelectorAll('.close-btn');
  219. closeButtons.forEach(btn => {
  220. btn.addEventListener('click', (e) => {
  221. e.stopPropagation();
  222. const modal = e.target.closest('.modal');
  223. if (modal) {
  224. if (modal.id === 'settingsModal') {
  225. saveConfig();
  226. }
  227. this.hideModal(modal.id);
  228. }
  229. });
  230. });
  231. // Settings tab switch
  232. const tabBtns = document.querySelectorAll('.tab-btn');
  233. tabBtns.forEach(btn => {
  234. btn.addEventListener('click', (e) => {
  235. this.switchTab(e.target.dataset.tab);
  236. });
  237. });
  238. // 点击模态框背景关闭(仅对特定模态框禁用此功能)
  239. const modals = document.querySelectorAll('.modal');
  240. modals.forEach(modal => {
  241. modal.addEventListener('click', (e) => {
  242. if (e.target === modal) {
  243. // settingsModal、mcpToolModal、mcpPropertyModal 只能通过点击X关闭
  244. const nonClosableModals = ['settingsModal', 'mcpToolModal', 'mcpPropertyModal'];
  245. if (nonClosableModals.includes(modal.id)) {
  246. return; // 禁止点击背景关闭
  247. }
  248. this.hideModal(modal.id);
  249. }
  250. });
  251. });
  252. // Add MCP tool button
  253. const addMCPToolBtn = document.getElementById('addMCPToolBtn');
  254. if (addMCPToolBtn) {
  255. addMCPToolBtn.addEventListener('click', (e) => {
  256. e.stopPropagation();
  257. this.addMCPTool();
  258. });
  259. }
  260. // Connect button and send button are not removed, can be added to dial button later
  261. }
  262. // Update connection status UI
  263. updateConnectionUI(isConnected) {
  264. const connectionStatus = document.getElementById('connectionStatus');
  265. const statusDot = document.querySelector('.status-dot');
  266. if (connectionStatus) {
  267. if (isConnected) {
  268. connectionStatus.textContent = '已连接';
  269. if (statusDot) {
  270. statusDot.className = 'status-dot status-connected';
  271. }
  272. } else {
  273. connectionStatus.textContent = '离线';
  274. if (statusDot) {
  275. statusDot.className = 'status-dot status-disconnected';
  276. }
  277. }
  278. }
  279. }
  280. // Update dial button state
  281. updateDialButton(isConnected) {
  282. const dialBtn = document.getElementById('dialBtn');
  283. const recordBtn = document.getElementById('recordBtn');
  284. const cameraBtn = document.getElementById('cameraBtn');
  285. if (dialBtn) {
  286. if (isConnected) {
  287. dialBtn.classList.add('dial-active');
  288. dialBtn.querySelector('.btn-text').textContent = '挂断';
  289. // Update dial button icon to hang up icon
  290. dialBtn.querySelector('svg').innerHTML = `
  291. <path d="M12,9C10.4,9 9,10.4 9,12C9,13.6 10.4,15 12,15C13.6,15 15,13.6 15,12C15,10.4 13.6,9 12,9M12,17C9.2,17 7,14.8 7,12C7,9.2 9.2,7 12,7C14.8,7 17,9.2 17,12C17,14.8 14.8,17 12,17M12,4.5C7,4.5 2.7,7.6 1,12C2.7,16.4 7,19.5 12,19.5C17,19.5 21.3,16.4 23,12C21.3,7.6 17,4.5 12,4.5Z"/>
  292. `;
  293. } else {
  294. dialBtn.classList.remove('dial-active');
  295. dialBtn.querySelector('.btn-text').textContent = '拨号';
  296. // Restore dial button icon
  297. dialBtn.querySelector('svg').innerHTML = `
  298. <path d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z"/>
  299. `;
  300. }
  301. }
  302. // Update camera button state - reset to default when disconnected
  303. if (cameraBtn && !isConnected) {
  304. const cameraContainer = document.getElementById('cameraContainer');
  305. if (cameraContainer && cameraContainer.classList.contains('active')) {
  306. cameraContainer.classList.remove('active');
  307. }
  308. cameraBtn.classList.remove('camera-active');
  309. cameraBtn.querySelector('.btn-text').textContent = '摄像头';
  310. cameraBtn.disabled = true;
  311. cameraBtn.title = '请先连接服务器';
  312. // 关闭摄像头
  313. if (typeof window.stopCamera === 'function') {
  314. window.stopCamera();
  315. }
  316. }
  317. // Update camera button state - enable when connected and camera is available
  318. if (cameraBtn && isConnected) {
  319. if (window.cameraAvailable) {
  320. cameraBtn.disabled = false;
  321. cameraBtn.title = '打开/关闭摄像头';
  322. } else {
  323. cameraBtn.disabled = true;
  324. cameraBtn.title = '请先绑定验证码';
  325. }
  326. }
  327. // Update record button state
  328. if (recordBtn) {
  329. const microphoneAvailable = window.microphoneAvailable !== false;
  330. if (isConnected && microphoneAvailable) {
  331. recordBtn.disabled = false;
  332. recordBtn.title = '开始录音';
  333. // Restore record button to normal state
  334. recordBtn.querySelector('.btn-text').textContent = '录音';
  335. recordBtn.classList.remove('recording');
  336. } else {
  337. recordBtn.disabled = true;
  338. if (!microphoneAvailable) {
  339. recordBtn.title = window.isHttpNonLocalhost ? '当前由于是http访问,无法录音,只能用文字交互' : '麦克风不可用';
  340. } else {
  341. recordBtn.title = '请先连接服务器';
  342. }
  343. // Restore record button to normal state
  344. recordBtn.querySelector('.btn-text').textContent = '录音';
  345. recordBtn.classList.remove('recording');
  346. }
  347. }
  348. }
  349. // Update record button state
  350. updateRecordButtonState(isRecording, seconds = 0) {
  351. const recordBtn = document.getElementById('recordBtn');
  352. if (recordBtn) {
  353. if (isRecording) {
  354. recordBtn.querySelector('.btn-text').textContent = `录音中`;
  355. recordBtn.classList.add('recording');
  356. } else {
  357. recordBtn.querySelector('.btn-text').textContent = '录音';
  358. recordBtn.classList.remove('recording');
  359. }
  360. // Only enable button when microphone is available
  361. recordBtn.disabled = window.microphoneAvailable === false;
  362. }
  363. }
  364. /**
  365. * Update microphone availability state
  366. * @param {boolean} isAvailable - Whether microphone is available
  367. * @param {boolean} isHttpNonLocalhost - Whether it is HTTP non-localhost access
  368. */
  369. updateMicrophoneAvailability(isAvailable, isHttpNonLocalhost) {
  370. const recordBtn = document.getElementById('recordBtn');
  371. if (!recordBtn) return;
  372. if (!isAvailable) {
  373. // Disable record button
  374. recordBtn.disabled = true;
  375. // Update button text and title
  376. recordBtn.querySelector('.btn-text').textContent = '录音';
  377. recordBtn.title = isHttpNonLocalhost ? '当前由于是http访问,无法录音,只能用文字交互' : '麦克风不可用';
  378. } else {
  379. // If connected, enable record button
  380. const wsHandler = getWebSocketHandler();
  381. if (wsHandler && wsHandler.isConnected()) {
  382. recordBtn.disabled = false;
  383. recordBtn.title = '开始录音';
  384. }
  385. }
  386. }
  387. // Add chat message
  388. addChatMessage(content, isUser = false) {
  389. const chatStream = document.getElementById('chatStream');
  390. if (!chatStream) return;
  391. const messageDiv = document.createElement('div');
  392. messageDiv.className = `chat-message ${isUser ? 'user' : 'ai'}`;
  393. messageDiv.innerHTML = `<div class="message-bubble">${content}</div>`;
  394. chatStream.appendChild(messageDiv);
  395. // Scroll to bottom
  396. chatStream.scrollTop = chatStream.scrollHeight;
  397. }
  398. // Switch background
  399. switchBackground() {
  400. this.currentBackgroundIndex = (this.currentBackgroundIndex + 1) % this.backgroundImages.length;
  401. const backgroundContainer = document.querySelector('.background-container');
  402. if (backgroundContainer) {
  403. backgroundContainer.style.backgroundImage = `url('./images/${this.backgroundImages[this.currentBackgroundIndex]}')`;
  404. }
  405. localStorage.setItem('backgroundIndex', this.currentBackgroundIndex);
  406. }
  407. // Switch Live2D model
  408. switchLive2DModel() {
  409. const modelSelect = document.getElementById('live2dModelSelect');
  410. if (!modelSelect) {
  411. console.error('模型选择下拉框不存在');
  412. return;
  413. }
  414. const selectedModel = modelSelect.value;
  415. const app = window.chatApp;
  416. if (app && app.live2dManager) {
  417. app.live2dManager.switchModel(selectedModel)
  418. .then(success => {
  419. if (success) {
  420. this.addChatMessage(`已切换到模型: ${selectedModel}`, false);
  421. } else {
  422. this.addChatMessage('模型切换失败', false);
  423. }
  424. })
  425. .catch(error => {
  426. console.error('模型切换错误:', error);
  427. this.addChatMessage('模型切换出错', false);
  428. });
  429. } else {
  430. this.addChatMessage('Live2D管理器未初始化', false);
  431. }
  432. }
  433. // Show modal
  434. showModal(modalId) {
  435. const modal = document.getElementById(modalId);
  436. if (modal) {
  437. modal.style.display = 'flex';
  438. }
  439. }
  440. // Hide modal
  441. hideModal(modalId) {
  442. const modal = document.getElementById(modalId);
  443. if (modal) {
  444. modal.style.display = 'none';
  445. }
  446. }
  447. // Switch tab
  448. switchTab(tabName) {
  449. // Remove active class from all tabs
  450. const tabBtns = document.querySelectorAll('.tab-btn');
  451. const tabContents = document.querySelectorAll('.tab-content');
  452. tabBtns.forEach(btn => btn.classList.remove('active'));
  453. tabContents.forEach(content => content.classList.remove('active'));
  454. // Activate selected tab
  455. const activeTabBtn = document.querySelector(`[data-tab="${tabName}"]`);
  456. const activeTabContent = document.getElementById(`${tabName}Tab`);
  457. if (activeTabBtn && activeTabContent) {
  458. activeTabBtn.classList.add('active');
  459. activeTabContent.classList.add('active');
  460. }
  461. }
  462. // Start AI chat session after connection
  463. startAIChatSession() {
  464. this.addChatMessage('连接成功,开始聊天吧~😊', false);
  465. // Check microphone availability and show error messages if needed
  466. if (!window.microphoneAvailable) {
  467. if (window.isHttpNonLocalhost) {
  468. this.addChatMessage('⚠️ 当前由于是http访问,无法录音,只能用文字交互', false);
  469. } else {
  470. this.addChatMessage('⚠️ 麦克风不可用,请检查权限设置,只能用文字交互', false);
  471. }
  472. }
  473. // Start recording only if microphone is available
  474. if (window.microphoneAvailable) {
  475. const recordBtn = document.getElementById('recordBtn');
  476. if (recordBtn) {
  477. recordBtn.click();
  478. }
  479. }
  480. // Start camera only if camera is available (bound with verification code)
  481. if (window.cameraAvailable && typeof window.startCamera === 'function') {
  482. window.startCamera().then(success => {
  483. if (success) {
  484. const cameraBtn = document.getElementById('cameraBtn');
  485. if (cameraBtn) {
  486. cameraBtn.classList.add('camera-active');
  487. cameraBtn.querySelector('.btn-text').textContent = '关闭';
  488. }
  489. } else {
  490. this.addChatMessage('⚠️ 摄像头启动失败,可能被浏览器拒绝', false);
  491. }
  492. }).catch(error => {
  493. log(`启动摄像头异常: ${error.message}`, 'error');
  494. });
  495. }
  496. }
  497. // Handle connect button click
  498. async handleConnect() {
  499. console.log('handleConnect called');
  500. // Switch to device settings tab
  501. this.switchTab('device');
  502. // Wait for DOM update
  503. await new Promise(resolve => setTimeout(resolve, 50));
  504. const otaUrlInput = document.getElementById('otaUrl');
  505. console.log('otaUrl element:', otaUrlInput);
  506. if (!otaUrlInput || !otaUrlInput.value) {
  507. this.addChatMessage('请输入OTA服务器地址', false);
  508. return;
  509. }
  510. const otaUrl = otaUrlInput.value;
  511. console.log('otaUrl value:', otaUrl);
  512. // Update dial button state to connecting
  513. const dialBtn = document.getElementById('dialBtn');
  514. if (dialBtn) {
  515. dialBtn.classList.add('dial-active');
  516. dialBtn.querySelector('.btn-text').textContent = '连接中...';
  517. dialBtn.disabled = true;
  518. }
  519. // Show connecting message
  520. this.addChatMessage('正在连接服务器...', false);
  521. const chatIpt = document.getElementById('chatIpt');
  522. if (chatIpt) {
  523. chatIpt.style.display = 'flex';
  524. }
  525. try {
  526. // Get WebSocket handler instance
  527. const wsHandler = getWebSocketHandler();
  528. // Register connection state callback BEFORE connecting
  529. wsHandler.onConnectionStateChange = (isConnected) => {
  530. this.updateConnectionUI(isConnected);
  531. this.updateDialButton(isConnected);
  532. };
  533. // Register chat message callback BEFORE connecting
  534. wsHandler.onChatMessage = (text, isUser) => {
  535. this.addChatMessage(text, isUser);
  536. };
  537. // Register record button state callback BEFORE connecting
  538. wsHandler.onRecordButtonStateChange = (isRecording) => {
  539. const recordBtn = document.getElementById('recordBtn');
  540. if (recordBtn) {
  541. if (isRecording) {
  542. recordBtn.classList.add('recording');
  543. recordBtn.querySelector('.btn-text').textContent = '录音中';
  544. } else {
  545. recordBtn.classList.remove('recording');
  546. recordBtn.querySelector('.btn-text').textContent = '录音';
  547. }
  548. }
  549. };
  550. const isConnected = await wsHandler.connect();
  551. if (isConnected) {
  552. // Check microphone availability (check again after connection)
  553. const { checkMicrophoneAvailability } = await import('../core/audio/recorder.js?v=0205');
  554. const micAvailable = await checkMicrophoneAvailability();
  555. if (!micAvailable) {
  556. const isHttp = window.isHttpNonLocalhost;
  557. if (isHttp) {
  558. this.addChatMessage('⚠️ 当前由于是http访问,无法录音,只能用文字交互', false);
  559. }
  560. // Update global state
  561. window.microphoneAvailable = false;
  562. }
  563. // Update dial button state
  564. const dialBtn = document.getElementById('dialBtn');
  565. if (dialBtn) {
  566. if (!this.dialBtnDisabled) {
  567. dialBtn.disabled = false;
  568. }
  569. dialBtn.querySelector('.btn-text').textContent = '挂断';
  570. dialBtn.classList.add('dial-active');
  571. }
  572. this.hideModal('settingsModal');
  573. } else {
  574. throw new Error('OTA连接失败');
  575. }
  576. } catch (error) {
  577. console.error('Connection error details:', {
  578. message: error.message,
  579. stack: error.stack,
  580. name: error.name
  581. });
  582. // Show error message
  583. const errorMessage = error.message.includes('Cannot set properties of null')
  584. ? '连接失败:请检查设备连接'
  585. : `连接失败: ${error.message}`;
  586. this.addChatMessage(errorMessage, false);
  587. // Restore dial button state
  588. const dialBtn = document.getElementById('dialBtn');
  589. if (dialBtn) {
  590. if (!this.dialBtnDisabled) {
  591. dialBtn.disabled = false;
  592. }
  593. dialBtn.querySelector('.btn-text').textContent = '拨号';
  594. dialBtn.classList.remove('dial-active');
  595. console.log('Dial button state restored successfully');
  596. }
  597. }
  598. }
  599. // Add MCP tool
  600. addMCPTool() {
  601. const mcpToolsList = document.getElementById('mcpToolsList');
  602. if (!mcpToolsList) return;
  603. const toolId = `mcp-tool-${Date.now()}`;
  604. const toolDiv = document.createElement('div');
  605. toolDiv.className = 'properties-container';
  606. toolDiv.innerHTML = `
  607. <div class="property-item">
  608. <input type="text" placeholder="工具名称" value="新工具">
  609. <input type="text" placeholder="工具描述" value="工具描述">
  610. <button class="remove-property" onclick="uiController.removeMCPTool('${toolId}')">删除</button>
  611. </div>
  612. `;
  613. mcpToolsList.appendChild(toolDiv);
  614. }
  615. // Remove MCP tool
  616. removeMCPTool(toolId) {
  617. const toolElement = document.getElementById(toolId);
  618. if (toolElement) {
  619. toolElement.remove();
  620. }
  621. }
  622. // Update audio statistics display
  623. updateAudioStats() {
  624. const audioPlayer = getAudioPlayer();
  625. if (!audioPlayer) return;
  626. const stats = audioPlayer.getAudioStats();
  627. // Here can add audio statistics UI update logic
  628. }
  629. // Start audio statistics monitor
  630. startAudioStatsMonitor() {
  631. // Update audio statistics every 100ms
  632. this.audioStatsTimer = setInterval(() => {
  633. this.updateAudioStats();
  634. }, 100);
  635. }
  636. // Stop audio statistics monitor
  637. stopAudioStatsMonitor() {
  638. if (this.audioStatsTimer) {
  639. clearInterval(this.audioStatsTimer);
  640. this.audioStatsTimer = null;
  641. }
  642. }
  643. // Draw audio visualizer waveform
  644. drawVisualizer(dataArray) {
  645. if (!this.visualizerContext || !this.visualizerCanvas) return;
  646. this.visualizerContext.fillStyle = '#fafafa';
  647. this.visualizerContext.fillRect(0, 0, this.visualizerCanvas.width, this.visualizerCanvas.height);
  648. const barWidth = (this.visualizerCanvas.width / dataArray.length) * 2.5;
  649. let barHeight;
  650. let x = 0;
  651. for (let i = 0; i < dataArray.length; i++) {
  652. barHeight = dataArray[i] / 2;
  653. // Create gradient color: from purple to blue to green
  654. const gradient = this.visualizerContext.createLinearGradient(0, 0, 0, this.visualizerCanvas.height);
  655. gradient.addColorStop(0, '#8e44ad');
  656. gradient.addColorStop(0.5, '#3498db');
  657. gradient.addColorStop(1, '#1abc9c');
  658. this.visualizerContext.fillStyle = gradient;
  659. this.visualizerContext.fillRect(x, this.visualizerCanvas.height - barHeight, barWidth, barHeight);
  660. x += barWidth + 1;
  661. }
  662. }
  663. // Update session status UI
  664. updateSessionStatus(isSpeaking) {
  665. // Here can add session status UI update logic
  666. // For example: update Live2D model's mouth movement status
  667. }
  668. // Update session emotion
  669. updateSessionEmotion(emoji) {
  670. // Here can add emotion update logic
  671. // For example: display emoji in status indicator
  672. }
  673. }
  674. // Create singleton instance
  675. export const uiController = new UIController();
  676. // Export class for module usage
  677. export { UIController };