diff --git a/src/mcp_feedback_enhanced/web/locales/en/translation.json b/src/mcp_feedback_enhanced/web/locales/en/translation.json index f67356d..0ccc010 100644 --- a/src/mcp_feedback_enhanced/web/locales/en/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/en/translation.json @@ -356,5 +356,40 @@ "disabled": "Disabled", "executing": "Executing auto submit...", "countdownLabel": "Submit Countdown" + }, + "audio": { + "notification": { + "title": "Audio Notification Settings", + "description": "Configure audio notifications for session updates", + "enabled": "Enable Audio Notifications", + "volume": "Volume", + "selectAudio": "Select Audio", + "testPlay": "Test Play", + "uploadCustom": "Upload Custom Audio", + "chooseFile": "Choose File", + "supportedFormats": "Supports MP3, WAV, OGG formats", + "customAudios": "Custom Audio Files", + "defaultBeep": "Classic Beep", + "notificationDing": "Notification Ding", + "softChime": "Soft Chime", + "default": "Default", + "customAudio": "Custom Audio", + "noCustomAudios": "No custom audio files uploaded yet", + "created": "Created", + "format": "Format", + "enterAudioName": "Enter Audio Name", + "audioName": "Audio Name", + "audioNamePlaceholder": "Please enter audio name...", + "audioNameHint": "Leave empty to use default filename", + "nameRequired": "Audio name cannot be empty", + "uploading": "Uploading...", + "uploadSuccess": "Audio uploaded successfully: ", + "deleteConfirm": "Are you sure you want to delete audio \"{name}\"?", + "deleteSuccess": "Audio deleted", + "enabledChanged": "Audio notification settings updated", + "audioSelected": "Audio selected", + "testPlaying": "Playing test audio", + "audioNotFound": "Selected audio not found" + } } } diff --git a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json index 4d1d69b..5f8a3ea 100644 --- a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json @@ -356,5 +356,40 @@ "disabled": "已停用", "executing": "正在执行自动提交...", "countdownLabel": "提交倒数" + }, + "audio": { + "notification": { + "title": "音效通知设定", + "description": "设定会话更新时的音效通知", + "enabled": "启用音效通知", + "volume": "音量", + "selectAudio": "选择音效", + "testPlay": "测试播放", + "uploadCustom": "上传自定义音效", + "chooseFile": "选择文件", + "supportedFormats": "支持 MP3、WAV、OGG 格式", + "customAudios": "自定义音效", + "defaultBeep": "经典提示音", + "notificationDing": "通知铃声", + "softChime": "轻柔钟声", + "default": "默认", + "customAudio": "自定义音效", + "noCustomAudios": "尚未上传任何自定义音效", + "created": "创建于", + "format": "格式", + "enterAudioName": "输入音效名称", + "audioName": "音效名称", + "audioNamePlaceholder": "请输入音效名称...", + "audioNameHint": "留空将使用默认文件名称", + "nameRequired": "音效名称不能为空", + "uploading": "上传中...", + "uploadSuccess": "音效上传成功: ", + "deleteConfirm": "确定要删除音效 \"{name}\" 吗?", + "deleteSuccess": "音效已删除", + "enabledChanged": "音效通知设定已更新", + "audioSelected": "音效已选择", + "testPlaying": "正在播放测试音效", + "audioNotFound": "找不到选择的音效" + } } } diff --git a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json index 32bdb6a..64d8995 100644 --- a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json @@ -361,5 +361,40 @@ "disabled": "已停用", "executing": "正在執行自動提交...", "countdownLabel": "提交倒數" + }, + "audio": { + "notification": { + "title": "音效通知設定", + "description": "設定會話更新時的音效通知", + "enabled": "啟用音效通知", + "volume": "音量", + "selectAudio": "選擇音效", + "testPlay": "測試播放", + "uploadCustom": "上傳自訂音效", + "chooseFile": "選擇檔案", + "supportedFormats": "支援 MP3、WAV、OGG 格式", + "customAudios": "自訂音效", + "defaultBeep": "經典提示音", + "notificationDing": "通知鈴聲", + "softChime": "輕柔鐘聲", + "default": "預設", + "customAudio": "自訂音效", + "noCustomAudios": "尚未上傳任何自訂音效", + "created": "建立於", + "format": "格式", + "enterAudioName": "輸入音效名稱", + "audioName": "音效名稱", + "audioNamePlaceholder": "請輸入音效名稱...", + "audioNameHint": "留空將使用預設檔案名稱", + "nameRequired": "音效名稱不能為空", + "uploading": "上傳中...", + "uploadSuccess": "音效上傳成功: ", + "deleteConfirm": "確定要刪除音效 \"{name}\" 嗎?", + "deleteSuccess": "音效已刪除", + "enabledChanged": "音效通知設定已更新", + "audioSelected": "音效已選擇", + "testPlaying": "正在播放測試音效", + "audioNotFound": "找不到選擇的音效" + } } } diff --git a/src/mcp_feedback_enhanced/web/static/css/audio-management.css b/src/mcp_feedback_enhanced/web/static/css/audio-management.css new file mode 100644 index 0000000..cdbf443 --- /dev/null +++ b/src/mcp_feedback_enhanced/web/static/css/audio-management.css @@ -0,0 +1,564 @@ +/** + * 音效管理功能樣式 + * ================= + * + * 包含音效通知設定相關的所有 UI 樣式 + * 參考 prompt-management.css 的設計風格 + */ + +/* ===== 音效管理區塊樣式 ===== */ + +.audio-management-section { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 20px; + margin-bottom: 20px; + transition: all 0.3s ease; +} + +.audio-management-section:hover { + border-color: var(--accent-color); + box-shadow: 0 2px 8px rgba(0, 122, 204, 0.1); +} + +.audio-management-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border-color); +} + +.audio-management-title { + color: var(--text-primary); + font-size: 16px; + font-weight: 600; + margin: 0; + display: flex; + align-items: center; + gap: 8px; +} + +.audio-management-description { + color: var(--text-secondary); + font-size: 14px; + margin-bottom: 20px; + line-height: 1.4; +} + +/* ===== 音效設定控制項樣式 ===== */ + +.audio-settings-controls { + display: flex; + flex-direction: column; + gap: 16px; +} + +.audio-setting-item { + display: flex; + flex-direction: column; + gap: 8px; +} + +.audio-setting-label { + color: var(--text-primary); + font-size: 14px; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +/* ===== 開關控制項樣式 ===== */ + +.audio-toggle { + width: 18px; + height: 18px; + accent-color: var(--accent-color); + cursor: pointer; +} + +.audio-toggle:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* ===== 音量控制項樣式 ===== */ + +.audio-volume-control { + display: flex; + align-items: center; + gap: 12px; +} + +.audio-volume-slider { + flex: 1; + height: 6px; + background: var(--bg-secondary); + border-radius: 3px; + outline: none; + cursor: pointer; + -webkit-appearance: none; + appearance: none; +} + +.audio-volume-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + background: var(--accent-color); + border-radius: 50%; + cursor: pointer; + transition: all 0.2s ease; +} + +.audio-volume-slider::-webkit-slider-thumb:hover { + background: var(--accent-hover); + transform: scale(1.1); +} + +.audio-volume-slider::-moz-range-thumb { + width: 18px; + height: 18px; + background: var(--accent-color); + border-radius: 50%; + cursor: pointer; + border: none; + transition: all 0.2s ease; +} + +.audio-volume-slider::-moz-range-thumb:hover { + background: var(--accent-hover); + transform: scale(1.1); +} + +.audio-volume-slider:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.audio-volume-value { + color: var(--text-secondary); + font-size: 14px; + font-weight: 500; + min-width: 40px; + text-align: right; +} + +/* ===== 音效選擇控制項樣式 ===== */ + +.audio-select-control { + display: flex; + gap: 12px; + align-items: center; +} + +.audio-select { + flex: 1; + padding: 8px 12px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-primary); + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.audio-select:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2); +} + +.audio-select:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.audio-test-btn { + padding: 8px 16px; + font-size: 14px; + white-space: nowrap; +} + +/* ===== 檔案上傳控制項樣式 ===== */ + +.audio-upload-control { + display: flex; + flex-direction: column; + gap: 8px; +} + +.audio-upload-btn { + align-self: flex-start; + padding: 8px 16px; + font-size: 14px; + display: flex; + align-items: center; + gap: 8px; +} + +.audio-upload-hint { + color: var(--text-secondary); + font-size: 12px; + font-style: italic; +} + +/* ===== 自訂音效列表樣式 ===== */ + +.audio-custom-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 300px; + overflow-y: auto; +} + +.audio-custom-item { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 12px; + display: flex; + justify-content: space-between; + align-items: center; + transition: all 0.2s ease; +} + +.audio-custom-item:hover { + border-color: var(--accent-color); + background: rgba(0, 122, 204, 0.05); +} + +.audio-custom-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.audio-custom-name { + color: var(--text-primary); + font-size: 14px; + font-weight: 500; +} + +.audio-custom-meta { + color: var(--text-secondary); + font-size: 12px; +} + +.audio-custom-actions { + display: flex; + gap: 8px; +} + +.audio-play-btn, +.audio-delete-btn { + padding: 6px 8px; + font-size: 12px; + min-width: auto; + border-radius: 4px; +} + +.audio-play-btn:hover { + background: var(--success-color); + border-color: var(--success-color); +} + +.audio-delete-btn:hover { + background: var(--error-color); + border-color: var(--error-color); +} + +/* ===== 空狀態樣式 ===== */ + +.audio-empty-state { + text-align: center; + padding: 40px 20px; + color: var(--text-secondary); + background: var(--bg-primary); + border: 2px dashed var(--border-color); + border-radius: 8px; +} + +.audio-empty-state div:first-child { + font-size: 48px; + margin-bottom: 12px; +} + +/* ===== 響應式設計 ===== */ + +@media (max-width: 768px) { + .audio-select-control { + flex-direction: column; + align-items: stretch; + } + + .audio-test-btn { + align-self: stretch; + } + + .audio-volume-control { + flex-direction: column; + align-items: stretch; + gap: 8px; + } + + .audio-volume-value { + text-align: center; + } + + .audio-custom-item { + flex-direction: column; + align-items: stretch; + gap: 12px; + } + + .audio-custom-actions { + justify-content: center; + } +} + +/* ===== 動畫效果 ===== */ + +@keyframes audioFadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.audio-custom-item { + animation: audioFadeIn 0.3s ease; +} + +/* ===== 無障礙改進 ===== */ + +.audio-toggle:focus, +.audio-volume-slider:focus, +.audio-select:focus, +.audio-test-btn:focus, +.audio-upload-btn:focus, +.audio-play-btn:focus, +.audio-delete-btn:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + +/* ===== 禁用狀態樣式 ===== */ + +.audio-setting-item.disabled { + opacity: 0.6; +} + +.audio-setting-item.disabled .audio-setting-label { + color: var(--text-secondary); +} + +/* ===== 載入狀態樣式 ===== */ + +.audio-upload-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.audio-upload-btn.loading { + position: relative; +} + +.audio-upload-btn.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + border: 2px solid transparent; + border-top: 2px solid var(--text-primary); + border-radius: 50%; + animation: audioSpin 1s linear infinite; +} + +@keyframes audioSpin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* ===== 成功/錯誤狀態樣式 ===== */ + +.audio-setting-item.success { + border-left: 4px solid var(--success-color); + background: rgba(76, 175, 80, 0.05); +} + +.audio-setting-item.error { + border-left: 4px solid var(--error-color); + background: rgba(244, 67, 54, 0.05); +} + +/* ===== 音效名稱輸入模態框樣式 ===== */ + +.audio-name-modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + animation: audioModalFadeIn 0.2s ease; +} + +.audio-name-modal { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + width: 90%; + max-width: 400px; + animation: audioModalSlideIn 0.3s ease; +} + +.audio-name-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px 16px; + border-bottom: 1px solid var(--border-color); +} + +.audio-name-modal-header h4 { + margin: 0; + color: var(--text-primary); + font-size: 18px; + font-weight: 600; +} + +.audio-name-modal-close { + background: none; + border: none; + font-size: 24px; + color: var(--text-secondary); + cursor: pointer; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + transition: all 0.2s ease; +} + +.audio-name-modal-close:hover { + background: var(--bg-secondary); + color: var(--text-primary); +} + +.audio-name-modal-body { + padding: 24px; +} + +.audio-name-modal-body label { + display: block; + color: var(--text-primary); + font-size: 14px; + font-weight: 500; + margin-bottom: 8px; +} + +.audio-name-input { + width: 100%; + padding: 12px 16px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-size: 14px; + transition: all 0.2s ease; + box-sizing: border-box; +} + +.audio-name-input:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.1); +} + +.audio-name-hint { + color: var(--text-secondary); + font-size: 12px; + margin-top: 8px; + font-style: italic; +} + +.audio-name-modal-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 16px 24px 24px; +} + +.audio-name-modal-footer .btn { + padding: 10px 20px; + font-size: 14px; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid transparent; +} + +.audio-name-modal-footer .btn-secondary { + background: var(--bg-secondary); + color: var(--text-primary); + border-color: var(--border-color); +} + +.audio-name-modal-footer .btn-secondary:hover { + background: var(--bg-tertiary); +} + +.audio-name-modal-footer .btn-primary { + background: var(--accent-color); + color: white; +} + +.audio-name-modal-footer .btn-primary:hover { + background: var(--accent-hover); +} + +/* ===== 模態框動畫 ===== */ + +@keyframes audioModalFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes audioModalSlideIn { + from { + opacity: 0; + transform: translateY(-20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} diff --git a/src/mcp_feedback_enhanced/web/static/js/app.js b/src/mcp_feedback_enhanced/web/static/js/app.js index c116f83..454d4bb 100644 --- a/src/mcp_feedback_enhanced/web/static/js/app.js +++ b/src/mcp_feedback_enhanced/web/static/js/app.js @@ -38,6 +38,10 @@ this.promptSettingsUI = null; this.promptInputButtons = null; + // 音效管理器 + this.audioManager = null; + this.audioSettingsUI = null; + // 自動提交管理器 this.autoSubmitManager = null; @@ -197,7 +201,10 @@ // 9. 初始化提示詞管理器 self.initializePromptManagers(); - // 10. 初始化自動提交管理器 + // 10. 初始化音效管理器 + self.initializeAudioManagers(); + + // 11. 初始化自動提交管理器 self.initializeAutoSubmitManager(); // 11. 應用設定到 UI @@ -440,6 +447,43 @@ } }; + /** + * 初始化音效管理器 + */ + FeedbackApp.prototype.initializeAudioManagers = function() { + console.log('🔊 初始化音效管理器...'); + + try { + // 檢查音效模組是否已載入 + if (!window.MCPFeedback.AudioManager) { + console.warn('⚠️ 音效模組未載入,跳過初始化'); + return; + } + + // 1. 初始化音效管理器 + this.audioManager = new window.MCPFeedback.AudioManager({ + settingsManager: this.settingsManager, + onSettingsChange: function(settings) { + console.log('🔊 音效設定已變更:', settings); + } + }); + this.audioManager.initialize(); + + // 2. 初始化音效設定 UI + this.audioSettingsUI = new window.MCPFeedback.AudioSettingsUI({ + container: document.querySelector('#audioManagementContainer'), + audioManager: this.audioManager, + t: window.i18nManager ? window.i18nManager.t.bind(window.i18nManager) : function(key, defaultValue) { return defaultValue || key; } + }); + this.audioSettingsUI.initialize(); + + console.log('✅ 音效管理器初始化完成'); + + } catch (error) { + console.error('❌ 音效管理器初始化失敗:', error); + } + }; + /** * 處理 WebSocket 開啟 */ @@ -525,6 +569,11 @@ FeedbackApp.prototype.handleSessionUpdated = function(data) { console.log('🔄 處理會話更新:', data.session_info); + // 播放音效通知 + if (this.audioManager) { + this.audioManager.playNotification(); + } + // 顯示更新通知 window.MCPFeedback.Utils.showMessage(data.message || '會話已更新,正在局部更新內容...', window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS); diff --git a/src/mcp_feedback_enhanced/web/static/js/i18n.js b/src/mcp_feedback_enhanced/web/static/js/i18n.js index 2cd9f8a..f022ce5 100644 --- a/src/mcp_feedback_enhanced/web/static/js/i18n.js +++ b/src/mcp_feedback_enhanced/web/static/js/i18n.js @@ -164,6 +164,9 @@ class I18nManager { // 更新動態內容 this.updateDynamicContent(); + // 更新音效選擇器翻譯 + this.updateAudioSelectTranslations(); + console.log('翻譯已應用:', this.currentLanguage); } @@ -309,6 +312,15 @@ class I18nManager { } } + updateAudioSelectTranslations() { + // 更新音效選擇器的翻譯 + if (window.feedbackApp && window.feedbackApp.audioSettingsUI) { + if (typeof window.feedbackApp.audioSettingsUI.updateAudioSelectTranslations === 'function') { + window.feedbackApp.audioSettingsUI.updateAudioSelectTranslations(); + } + } + } + getCurrentLanguage() { return this.currentLanguage; } diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/audio/audio-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/audio/audio-manager.js new file mode 100644 index 0000000..7bf1840 --- /dev/null +++ b/src/mcp_feedback_enhanced/web/static/js/modules/audio/audio-manager.js @@ -0,0 +1,446 @@ +/** + * MCP Feedback Enhanced - 音效管理模組 + * =================================== + * + * 處理音效通知的播放、管理和設定功能 + * 使用 HTML5 Audio API 進行音效播放 + * 支援自訂音效上傳和 base64 儲存 + */ + +(function() { + 'use strict'; + + // 確保命名空間存在 + window.MCPFeedback = window.MCPFeedback || {}; + const Utils = window.MCPFeedback.Utils; + + /** + * 音效管理器建構函數 + */ + function AudioManager(options) { + options = options || {}; + + // 設定管理器引用 + this.settingsManager = options.settingsManager || null; + + // 當前音效設定 + this.currentAudioSettings = { + enabled: false, + volume: 50, + selectedAudioId: 'default-beep', + customAudios: [] + }; + + // 預設音效(base64 編碼的簡單提示音) + this.defaultAudios = { + 'default-beep': { + id: 'default-beep', + name: '經典提示音', + data: this.generateBeepSound(), + mimeType: 'audio/wav', + isDefault: true + }, + 'notification-ding': { + id: 'notification-ding', + name: '通知鈴聲', + data: this.generateDingSound(), + mimeType: 'audio/wav', + isDefault: true + }, + 'soft-chime': { + id: 'soft-chime', + name: '輕柔鐘聲', + data: this.generateChimeSound(), + mimeType: 'audio/wav', + isDefault: true + } + }; + + // 當前播放的 Audio 物件 + this.currentAudio = null; + + // 回調函數 + this.onSettingsChange = options.onSettingsChange || null; + + console.log('🔊 AudioManager 初始化完成'); + } + + /** + * 初始化音效管理器 + */ + AudioManager.prototype.initialize = function() { + this.loadAudioSettings(); + console.log('✅ AudioManager 初始化完成'); + }; + + /** + * 載入音效設定 + */ + AudioManager.prototype.loadAudioSettings = function() { + if (!this.settingsManager) { + console.warn('⚠️ SettingsManager 未設定,使用預設音效設定'); + return; + } + + try { + // 從設定管理器載入音效相關設定 + this.currentAudioSettings.enabled = this.settingsManager.get('audioNotificationEnabled', false); + this.currentAudioSettings.volume = this.settingsManager.get('audioNotificationVolume', 50); + this.currentAudioSettings.selectedAudioId = this.settingsManager.get('selectedAudioId', 'default-beep'); + this.currentAudioSettings.customAudios = this.settingsManager.get('customAudios', []); + + console.log('📥 音效設定已載入:', this.currentAudioSettings); + } catch (error) { + console.error('❌ 載入音效設定失敗:', error); + } + }; + + /** + * 儲存音效設定 + */ + AudioManager.prototype.saveAudioSettings = function() { + if (!this.settingsManager) { + console.warn('⚠️ SettingsManager 未設定,無法儲存音效設定'); + return; + } + + try { + this.settingsManager.set('audioNotificationEnabled', this.currentAudioSettings.enabled); + this.settingsManager.set('audioNotificationVolume', this.currentAudioSettings.volume); + this.settingsManager.set('selectedAudioId', this.currentAudioSettings.selectedAudioId); + this.settingsManager.set('customAudios', this.currentAudioSettings.customAudios); + + console.log('💾 音效設定已儲存'); + + // 觸發回調 + if (this.onSettingsChange) { + this.onSettingsChange(this.currentAudioSettings); + } + } catch (error) { + console.error('❌ 儲存音效設定失敗:', error); + } + }; + + /** + * 播放通知音效 + */ + AudioManager.prototype.playNotification = function() { + if (!this.currentAudioSettings.enabled) { + console.log('🔇 音效通知已停用'); + return; + } + + try { + const audioData = this.getAudioById(this.currentAudioSettings.selectedAudioId); + if (!audioData) { + console.warn('⚠️ 找不到指定的音效,使用預設音效'); + this.playAudio(this.defaultAudios['default-beep']); + return; + } + + this.playAudio(audioData); + } catch (error) { + console.error('❌ 播放通知音效失敗:', error); + } + }; + + /** + * 播放指定的音效 + */ + AudioManager.prototype.playAudio = function(audioData) { + try { + // 停止當前播放的音效 + if (this.currentAudio) { + this.currentAudio.pause(); + this.currentAudio = null; + } + + // 建立新的 Audio 物件 + this.currentAudio = new Audio(); + this.currentAudio.src = 'data:' + audioData.mimeType + ';base64,' + audioData.data; + this.currentAudio.volume = this.currentAudioSettings.volume / 100; + + // 播放音效 + const playPromise = this.currentAudio.play(); + + if (playPromise !== undefined) { + playPromise + .then(() => { + console.log('🔊 音效播放成功:', audioData.name); + }) + .catch(error => { + console.error('❌ 音效播放失敗:', error); + // 可能是瀏覽器的自動播放政策限制 + if (error.name === 'NotAllowedError') { + console.warn('⚠️ 瀏覽器阻止自動播放,需要用戶互動'); + } + }); + } + } catch (error) { + console.error('❌ 播放音效時發生錯誤:', error); + } + }; + + /** + * 根據 ID 獲取音效資料 + */ + AudioManager.prototype.getAudioById = function(audioId) { + // 先檢查預設音效 + if (this.defaultAudios[audioId]) { + return this.defaultAudios[audioId]; + } + + // 再檢查自訂音效 + return this.currentAudioSettings.customAudios.find(audio => audio.id === audioId) || null; + }; + + /** + * 獲取所有可用的音效 + */ + AudioManager.prototype.getAllAudios = function() { + const allAudios = []; + + // 新增預設音效 + Object.values(this.defaultAudios).forEach(audio => { + allAudios.push(audio); + }); + + // 新增自訂音效 + this.currentAudioSettings.customAudios.forEach(audio => { + allAudios.push(audio); + }); + + return allAudios; + }; + + /** + * 新增自訂音效 + */ + AudioManager.prototype.addCustomAudio = function(name, file) { + return new Promise((resolve, reject) => { + if (!name || !file) { + reject(new Error('音效名稱和檔案不能為空')); + return; + } + + // 檢查檔案類型 + if (!this.isValidAudioFile(file)) { + reject(new Error('不支援的音效檔案格式')); + return; + } + + // 檢查名稱是否重複 + if (this.isAudioNameExists(name)) { + reject(new Error('音效名稱已存在')); + return; + } + + // 轉換為 base64 + this.fileToBase64(file) + .then(base64Data => { + const audioData = { + id: this.generateAudioId(), + name: name.trim(), + data: base64Data, + mimeType: file.type, + createdAt: new Date().toISOString(), + isDefault: false + }; + + this.currentAudioSettings.customAudios.push(audioData); + this.saveAudioSettings(); + + console.log('➕ 新增自訂音效:', audioData.name); + resolve(audioData); + }) + .catch(error => { + reject(error); + }); + }); + }; + + /** + * 刪除自訂音效 + */ + AudioManager.prototype.removeCustomAudio = function(audioId) { + const index = this.currentAudioSettings.customAudios.findIndex(audio => audio.id === audioId); + if (index === -1) { + throw new Error('找不到指定的音效'); + } + + const removedAudio = this.currentAudioSettings.customAudios.splice(index, 1)[0]; + + // 如果刪除的是當前選中的音效,切換到預設音效 + if (this.currentAudioSettings.selectedAudioId === audioId) { + this.currentAudioSettings.selectedAudioId = 'default-beep'; + } + + this.saveAudioSettings(); + console.log('🗑️ 刪除自訂音效:', removedAudio.name); + + return removedAudio; + }; + + /** + * 設定音量 + */ + AudioManager.prototype.setVolume = function(volume) { + if (volume < 0 || volume > 100) { + throw new Error('音量必須在 0-100 之間'); + } + + this.currentAudioSettings.volume = volume; + this.saveAudioSettings(); + console.log('🔊 音量已設定為:', volume); + }; + + /** + * 設定是否啟用音效通知 + */ + AudioManager.prototype.setEnabled = function(enabled) { + this.currentAudioSettings.enabled = !!enabled; + this.saveAudioSettings(); + console.log('🔊 音效通知已', enabled ? '啟用' : '停用'); + }; + + /** + * 設定選中的音效 + */ + AudioManager.prototype.setSelectedAudio = function(audioId) { + if (!this.getAudioById(audioId)) { + throw new Error('找不到指定的音效'); + } + + this.currentAudioSettings.selectedAudioId = audioId; + this.saveAudioSettings(); + console.log('🎵 已選擇音效:', audioId); + }; + + /** + * 檢查是否為有效的音效檔案 + */ + AudioManager.prototype.isValidAudioFile = function(file) { + const validTypes = ['audio/mp3', 'audio/wav', 'audio/ogg', 'audio/mpeg']; + return validTypes.includes(file.type); + }; + + /** + * 檢查音效名稱是否已存在 + */ + AudioManager.prototype.isAudioNameExists = function(name) { + // 檢查預設音效 + const defaultExists = Object.values(this.defaultAudios).some(audio => audio.name === name); + if (defaultExists) return true; + + // 檢查自訂音效 + return this.currentAudioSettings.customAudios.some(audio => audio.name === name); + }; + + /** + * 檔案轉 base64 + */ + AudioManager.prototype.fileToBase64 = function(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = function() { + // 移除 data URL 前綴,只保留 base64 資料 + const base64 = reader.result.split(',')[1]; + resolve(base64); + }; + reader.onerror = function() { + reject(new Error('檔案讀取失敗')); + }; + reader.readAsDataURL(file); + }); + }; + + /** + * 生成音效 ID + */ + AudioManager.prototype.generateAudioId = function() { + return 'audio_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + }; + + /** + * 生成經典提示音(440Hz,0.3秒) + */ + AudioManager.prototype.generateBeepSound = function() { + return this.generateToneWAV(440, 0.3, 0.5); + }; + + /** + * 生成通知鈴聲(800Hz + 600Hz 和弦,0.4秒) + */ + AudioManager.prototype.generateDingSound = function() { + return this.generateToneWAV(800, 0.4, 0.4); + }; + + /** + * 生成輕柔鐘聲(523Hz,0.5秒,漸弱) + */ + AudioManager.prototype.generateChimeSound = function() { + return this.generateToneWAV(523, 0.5, 0.3); + }; + + /** + * 生成指定頻率和時長的 WAV 音效 + * @param {number} frequency - 頻率(Hz) + * @param {number} duration - 持續時間(秒) + * @param {number} volume - 音量(0-1) + */ + AudioManager.prototype.generateToneWAV = function(frequency, duration, volume) { + const sampleRate = 44100; + const numSamples = Math.floor(sampleRate * duration); + const buffer = new ArrayBuffer(44 + numSamples * 2); + const view = new DataView(buffer); + + // WAV 檔案標頭 + const writeString = (offset, string) => { + for (let i = 0; i < string.length; i++) { + view.setUint8(offset + i, string.charCodeAt(i)); + } + }; + + writeString(0, 'RIFF'); + view.setUint32(4, 36 + numSamples * 2, true); + writeString(8, 'WAVE'); + writeString(12, 'fmt '); + view.setUint32(16, 16, true); + view.setUint16(20, 1, true); + view.setUint16(22, 1, true); + view.setUint32(24, sampleRate, true); + view.setUint32(28, sampleRate * 2, true); + view.setUint16(32, 2, true); + view.setUint16(34, 16, true); + writeString(36, 'data'); + view.setUint32(40, numSamples * 2, true); + + // 生成音效資料 + for (let i = 0; i < numSamples; i++) { + const t = i / sampleRate; + const fadeOut = Math.max(0, 1 - (t / duration) * 0.5); // 漸弱效果 + const sample = Math.sin(2 * Math.PI * frequency * t) * volume * fadeOut; + const intSample = Math.max(-32768, Math.min(32767, Math.floor(sample * 32767))); + view.setInt16(44 + i * 2, intSample, true); + } + + // 轉換為 base64 + const bytes = new Uint8Array(buffer); + let binary = ''; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); + }; + + /** + * 獲取當前設定 + */ + AudioManager.prototype.getSettings = function() { + return Utils.deepClone(this.currentAudioSettings); + }; + + // 匯出到全域命名空間 + window.MCPFeedback.AudioManager = AudioManager; + +})(); diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/audio/audio-settings-ui.js b/src/mcp_feedback_enhanced/web/static/js/modules/audio/audio-settings-ui.js new file mode 100644 index 0000000..e0da350 --- /dev/null +++ b/src/mcp_feedback_enhanced/web/static/js/modules/audio/audio-settings-ui.js @@ -0,0 +1,669 @@ +/** + * MCP Feedback Enhanced - 音效設定 UI 模組 + * ====================================== + * + * 處理音效通知設定的使用者介面 + * 參考 prompt-settings-ui.js 的設計模式 + */ + +(function() { + 'use strict'; + + // 確保命名空間存在 + window.MCPFeedback = window.MCPFeedback || {}; + const Utils = window.MCPFeedback.Utils; + + /** + * 音效設定 UI 建構函數 + */ + function AudioSettingsUI(options) { + options = options || {}; + + // 容器元素 + this.container = options.container || null; + + // 音效管理器引用 + this.audioManager = options.audioManager || null; + + // i18n 翻譯函數 + this.t = options.t || function(key, defaultValue) { return defaultValue || key; }; + + // UI 元素引用 + this.enabledToggle = null; + this.volumeSlider = null; + this.volumeValue = null; + this.audioSelect = null; + this.testButton = null; + this.uploadButton = null; + this.uploadInput = null; + this.audioList = null; + + console.log('🎨 AudioSettingsUI 初始化完成'); + } + + /** + * 初始化 UI + */ + AudioSettingsUI.prototype.initialize = function() { + if (!this.container) { + console.error('❌ AudioSettingsUI 容器未設定'); + return; + } + + if (!this.audioManager) { + console.error('❌ AudioManager 未設定'); + return; + } + + this.createUI(); + this.setupEventListeners(); + this.refreshUI(); + + console.log('✅ AudioSettingsUI 初始化完成'); + }; + + /** + * 創建 UI 結構 + */ + AudioSettingsUI.prototype.createUI = function() { + const html = ` +
+
+

+ 🔊 音效通知設定 +

+
+
+ 設定會話更新時的音效通知 +
+ +
+ +
+ +
+ + +
+ +
+ + 50% +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + + + 支援 MP3、WAV、OGG 格式 + +
+
+ + +
+ +
+ +
+
+
+
+ `; + + this.container.insertAdjacentHTML('beforeend', html); + + // 獲取 UI 元素引用 + this.enabledToggle = this.container.querySelector('#audioNotificationEnabled'); + this.volumeSlider = this.container.querySelector('#audioVolumeSlider'); + this.volumeValue = this.container.querySelector('#audioVolumeValue'); + this.audioSelect = this.container.querySelector('#audioSelect'); + this.testButton = this.container.querySelector('#audioTestButton'); + this.uploadButton = this.container.querySelector('#audioUploadButton'); + this.uploadInput = this.container.querySelector('#audioUploadInput'); + this.audioList = this.container.querySelector('#audioCustomList'); + }; + + /** + * 設置事件監聽器 + */ + AudioSettingsUI.prototype.setupEventListeners = function() { + const self = this; + + // 啟用開關事件 + if (this.enabledToggle) { + this.enabledToggle.addEventListener('change', function(e) { + self.handleEnabledChange(e.target.checked); + }); + } + + // 音量滑桿事件 + if (this.volumeSlider) { + this.volumeSlider.addEventListener('input', function(e) { + self.handleVolumeChange(parseInt(e.target.value)); + }); + } + + // 音效選擇事件 + if (this.audioSelect) { + this.audioSelect.addEventListener('change', function(e) { + self.handleAudioSelect(e.target.value); + }); + } + + // 測試播放事件 + if (this.testButton) { + this.testButton.addEventListener('click', function() { + self.handleTestPlay(); + }); + } + + // 上傳按鈕事件 + if (this.uploadButton) { + this.uploadButton.addEventListener('click', function() { + self.uploadInput.click(); + }); + } + + // 檔案上傳事件 + if (this.uploadInput) { + this.uploadInput.addEventListener('change', function(e) { + self.handleFileUpload(e.target.files[0]); + }); + } + + // 設置音效管理器回調 + if (this.audioManager) { + this.audioManager.onSettingsChange = function(settings) { + console.log('🎨 音效設定變更,重新渲染 UI'); + self.refreshUI(); + }; + } + + // 語言變更將由 i18n.js 直接調用 updateAudioSelectTranslations 方法 + }; + + /** + * 處理啟用狀態變更 + */ + AudioSettingsUI.prototype.handleEnabledChange = function(enabled) { + try { + this.audioManager.setEnabled(enabled); + this.updateControlsState(); + this.showSuccess(this.t('audio.notification.enabledChanged', '音效通知設定已更新')); + } catch (error) { + console.error('❌ 設定啟用狀態失敗:', error); + this.showError(error.message); + // 恢復原狀態 + this.enabledToggle.checked = this.audioManager.getSettings().enabled; + } + }; + + /** + * 處理音量變更 + */ + AudioSettingsUI.prototype.handleVolumeChange = function(volume) { + try { + this.audioManager.setVolume(volume); + this.volumeValue.textContent = volume + '%'; + } catch (error) { + console.error('❌ 設定音量失敗:', error); + this.showError(error.message); + } + }; + + /** + * 處理音效選擇 + */ + AudioSettingsUI.prototype.handleAudioSelect = function(audioId) { + try { + this.audioManager.setSelectedAudio(audioId); + this.showSuccess(this.t('audio.notification.audioSelected', '音效已選擇')); + } catch (error) { + console.error('❌ 選擇音效失敗:', error); + this.showError(error.message); + // 恢復原選擇 + this.audioSelect.value = this.audioManager.getSettings().selectedAudioId; + } + }; + + /** + * 處理測試播放 + */ + AudioSettingsUI.prototype.handleTestPlay = function() { + try { + const selectedAudioId = this.audioSelect.value; + const audioData = this.audioManager.getAudioById(selectedAudioId); + + if (audioData) { + this.audioManager.playAudio(audioData); + this.showSuccess(this.t('audio.notification.testPlaying', '正在播放測試音效')); + } else { + this.showError(this.t('audio.notification.audioNotFound', '找不到選擇的音效')); + } + } catch (error) { + console.error('❌ 測試播放失敗:', error); + this.showError(error.message); + } + }; + + /** + * 處理檔案上傳 + */ + AudioSettingsUI.prototype.handleFileUpload = function(file) { + if (!file) return; + + // 生成預設檔案名稱(去除副檔名) + const defaultName = file.name.replace(/\.[^/.]+$/, ''); + + // 顯示美觀的名稱輸入模態框 + this.showAudioNameModal(defaultName, (audioName) => { + if (!audioName || !audioName.trim()) { + this.showError(this.t('audio.notification.nameRequired', '音效名稱不能為空')); + return; + } + + // 顯示上傳中狀態 + this.uploadButton.disabled = true; + this.uploadButton.innerHTML = '⏳ 上傳中...'; + + this.audioManager.addCustomAudio(audioName.trim(), file) + .then(audioData => { + this.showSuccess(this.t('audio.notification.uploadSuccess', '音效上傳成功: ') + audioData.name); + this.refreshAudioSelect(); + this.refreshCustomAudioList(); + // 清空檔案輸入 + this.uploadInput.value = ''; + }) + .catch(error => { + console.error('❌ 上傳音效失敗:', error); + this.showError(error.message); + }) + .finally(() => { + // 恢復按鈕狀態 + this.uploadButton.disabled = false; + this.uploadButton.innerHTML = '📁 選擇檔案'; + }); + }); + }; + + /** + * 處理刪除自訂音效 + */ + AudioSettingsUI.prototype.handleDeleteCustomAudio = function(audioId) { + const audioData = this.audioManager.getAudioById(audioId); + if (!audioData) return; + + const confirmMessage = this.t('audio.notification.deleteConfirm', '確定要刪除音效 "{name}" 嗎?') + .replace('{name}', audioData.name); + + if (!confirm(confirmMessage)) return; + + try { + this.audioManager.removeCustomAudio(audioId); + this.showSuccess(this.t('audio.notification.deleteSuccess', '音效已刪除')); + this.refreshAudioSelect(); + this.refreshCustomAudioList(); + } catch (error) { + console.error('❌ 刪除音效失敗:', error); + this.showError(error.message); + } + }; + + /** + * 刷新整個 UI + */ + AudioSettingsUI.prototype.refreshUI = function() { + const settings = this.audioManager.getSettings(); + + // 更新啟用狀態 + if (this.enabledToggle) { + this.enabledToggle.checked = settings.enabled; + } + + // 更新音量 + if (this.volumeSlider && this.volumeValue) { + this.volumeSlider.value = settings.volume; + this.volumeValue.textContent = settings.volume + '%'; + } + + // 更新音效選擇 + this.refreshAudioSelect(); + + // 更新自訂音效列表 + this.refreshCustomAudioList(); + + // 更新控制項狀態 + this.updateControlsState(); + }; + + /** + * 刷新音效選擇下拉選單 + */ + AudioSettingsUI.prototype.refreshAudioSelect = function() { + if (!this.audioSelect) return; + + const settings = this.audioManager.getSettings(); + const allAudios = this.audioManager.getAllAudios(); + + // 清空現有選項 + this.audioSelect.innerHTML = ''; + + // 新增音效選項 + allAudios.forEach(audio => { + const option = document.createElement('option'); + option.value = audio.id; + + // 使用翻譯後的名稱 + let displayName = audio.name; + if (audio.isDefault) { + // 為預設音效提供翻譯 + const translationKey = this.getDefaultAudioTranslationKey(audio.id); + if (translationKey) { + displayName = this.t(translationKey, audio.name); + } + displayName += ' (' + this.t('audio.notification.default', '預設') + ')'; + } + + option.textContent = displayName; + + // 為預設音效選項新增 data-i18n 屬性,以便語言切換時自動更新 + if (audio.isDefault) { + const translationKey = this.getDefaultAudioTranslationKey(audio.id); + if (translationKey) { + option.setAttribute('data-audio-id', audio.id); + option.setAttribute('data-is-default', 'true'); + option.setAttribute('data-translation-key', translationKey); + } + } + + if (audio.id === settings.selectedAudioId) { + option.selected = true; + } + this.audioSelect.appendChild(option); + }); + }; + + /** + * 刷新自訂音效列表 + */ + AudioSettingsUI.prototype.refreshCustomAudioList = function() { + if (!this.audioList) return; + + const customAudios = this.audioManager.getSettings().customAudios; + + if (customAudios.length === 0) { + this.audioList.innerHTML = ` +
+
🎵
+
尚未上傳任何自訂音效
+
+ `; + return; + } + + let html = ''; + customAudios.forEach(audio => { + html += this.createCustomAudioItemHTML(audio); + }); + + this.audioList.innerHTML = html; + this.setupCustomAudioEvents(); + }; + + /** + * 創建自訂音效項目 HTML + */ + AudioSettingsUI.prototype.createCustomAudioItemHTML = function(audio) { + const createdDate = new Date(audio.createdAt).toLocaleDateString(); + + return ` +
+
+
${Utils.escapeHtml(audio.name)}
+
+ 建立於: ${createdDate} + | 格式: ${audio.mimeType} +
+
+
+ + +
+
+ `; + }; + + /** + * 設置自訂音效項目事件 + */ + AudioSettingsUI.prototype.setupCustomAudioEvents = function() { + const self = this; + + // 播放按鈕事件 + const playButtons = this.audioList.querySelectorAll('.audio-play-btn'); + playButtons.forEach(button => { + button.addEventListener('click', function() { + const audioId = button.getAttribute('data-audio-id'); + const audioData = self.audioManager.getAudioById(audioId); + if (audioData) { + self.audioManager.playAudio(audioData); + } + }); + }); + + // 刪除按鈕事件 + const deleteButtons = this.audioList.querySelectorAll('.audio-delete-btn'); + deleteButtons.forEach(button => { + button.addEventListener('click', function() { + const audioId = button.getAttribute('data-audio-id'); + self.handleDeleteCustomAudio(audioId); + }); + }); + }; + + /** + * 更新控制項狀態 + */ + AudioSettingsUI.prototype.updateControlsState = function() { + const enabled = this.enabledToggle ? this.enabledToggle.checked : false; + + // 根據啟用狀態禁用/啟用控制項 + const controls = [ + this.volumeSlider, + this.audioSelect, + this.testButton, + this.uploadButton + ]; + + controls.forEach(control => { + if (control) { + control.disabled = !enabled; + } + }); + }; + + /** + * 顯示成功訊息 + */ + AudioSettingsUI.prototype.showSuccess = function(message) { + if (Utils && Utils.showMessage) { + Utils.showMessage(message, Utils.CONSTANTS.MESSAGE_SUCCESS); + } else { + console.log('✅', message); + } + }; + + /** + * 顯示錯誤訊息 + */ + AudioSettingsUI.prototype.showError = function(message) { + if (Utils && Utils.showMessage) { + Utils.showMessage(message, Utils.CONSTANTS.MESSAGE_ERROR); + } else { + console.error('❌', message); + } + }; + + /** + * 顯示音效名稱輸入模態框 + */ + AudioSettingsUI.prototype.showAudioNameModal = function(defaultName, onConfirm) { + const self = this; + + // 創建模態框 HTML + const modalHTML = ` +
+
+
+

輸入音效名稱

+ +
+
+ + +
+ 留空將使用預設檔案名稱 +
+
+ +
+
+ `; + + // 新增模態框到頁面 + document.body.insertAdjacentHTML('beforeend', modalHTML); + + // 獲取元素引用 + const overlay = document.getElementById('audioNameModalOverlay'); + const input = document.getElementById('audioNameInput'); + const closeBtn = document.getElementById('audioNameModalClose'); + const cancelBtn = document.getElementById('audioNameModalCancel'); + const confirmBtn = document.getElementById('audioNameModalConfirm'); + + // 聚焦輸入框並選中文字 + setTimeout(() => { + input.focus(); + input.select(); + }, 100); + + // 關閉模態框函數 + const closeModal = () => { + if (overlay && overlay.parentNode) { + overlay.parentNode.removeChild(overlay); + } + }; + + // 確認函數 + const confirm = () => { + const audioName = input.value.trim() || defaultName; + closeModal(); + if (onConfirm) { + onConfirm(audioName); + } + }; + + // 事件監聽器 + closeBtn.addEventListener('click', closeModal); + cancelBtn.addEventListener('click', closeModal); + confirmBtn.addEventListener('click', confirm); + + // 點擊遮罩關閉 + overlay.addEventListener('click', function(e) { + if (e.target === overlay) { + closeModal(); + } + }); + + // Enter 鍵確認,Escape 鍵取消 + input.addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + confirm(); + } else if (e.key === 'Escape') { + e.preventDefault(); + closeModal(); + } + }); + }; + + + + /** + * 更新音效選擇器的翻譯 + */ + AudioSettingsUI.prototype.updateAudioSelectTranslations = function() { + if (!this.audioSelect) return; + + const options = this.audioSelect.querySelectorAll('option[data-is-default="true"]'); + options.forEach(option => { + const audioId = option.getAttribute('data-audio-id'); + const translationKey = option.getAttribute('data-translation-key'); + + if (audioId && translationKey) { + const audioData = this.audioManager.getAudioById(audioId); + if (audioData) { + const translatedName = this.t(translationKey, audioData.name); + const defaultText = this.t('audio.notification.default', '預設'); + option.textContent = translatedName + ' (' + defaultText + ')'; + } + } + }); + }; + + /** + * 獲取預設音效的翻譯鍵值 + */ + AudioSettingsUI.prototype.getDefaultAudioTranslationKey = function(audioId) { + const translationMap = { + 'default-beep': 'audio.notification.defaultBeep', + 'notification-ding': 'audio.notification.notificationDing', + 'soft-chime': 'audio.notification.softChime' + }; + return translationMap[audioId] || null; + }; + + // 匯出到全域命名空間 + window.MCPFeedback.AudioSettingsUI = AudioSettingsUI; + +})(); diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js index d6077b6..5a5c982 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js @@ -30,7 +30,12 @@ // 自動定時提交設定 autoSubmitEnabled: false, autoSubmitTimeout: 30, - autoSubmitPromptId: null + autoSubmitPromptId: null, + // 音效通知設定 + audioNotificationEnabled: false, + audioNotificationVolume: 50, + selectedAudioId: 'default-beep', + customAudios: [] }; // 當前設定 diff --git a/src/mcp_feedback_enhanced/web/templates/feedback.html b/src/mcp_feedback_enhanced/web/templates/feedback.html index 3c0f854..65261b0 100644 --- a/src/mcp_feedback_enhanced/web/templates/feedback.html +++ b/src/mcp_feedback_enhanced/web/templates/feedback.html @@ -7,6 +7,7 @@ +