新增音效通知

This commit is contained in:
Minidoracat 2025-06-14 06:02:38 +08:00
parent a85b59ff63
commit c0324fdb23
10 changed files with 1867 additions and 2 deletions

View File

@ -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"
}
}
}

View File

@ -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": "找不到选择的音效"
}
}
}

View File

@ -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": "找不到選擇的音效"
}
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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);
};
/**
* 生成經典提示音440Hz0.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);
};
/**
* 生成輕柔鐘聲523Hz0.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;
})();

View File

@ -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 = `
<div class="audio-management-section">
<div class="audio-management-header">
<h4 class="audio-management-title" data-i18n="audio.notification.title">
🔊 音效通知設定
</h4>
</div>
<div class="audio-management-description" data-i18n="audio.notification.description">
設定會話更新時的音效通知
</div>
<div class="audio-settings-controls">
<!-- 啟用開關 -->
<div class="audio-setting-item">
<label class="audio-setting-label">
<input type="checkbox" id="audioNotificationEnabled" class="audio-toggle">
<span data-i18n="audio.notification.enabled">啟用音效通知</span>
</label>
</div>
<!-- 音量控制 -->
<div class="audio-setting-item">
<label class="audio-setting-label" data-i18n="audio.notification.volume">音量</label>
<div class="audio-volume-control">
<input type="range" id="audioVolumeSlider" class="audio-volume-slider"
min="0" max="100" value="50">
<span id="audioVolumeValue" class="audio-volume-value">50%</span>
</div>
</div>
<!-- 音效選擇 -->
<div class="audio-setting-item">
<label class="audio-setting-label" data-i18n="audio.notification.selectAudio">選擇音效</label>
<div class="audio-select-control">
<select id="audioSelect" class="audio-select">
<!-- 選項將動態生成 -->
</select>
<button type="button" id="audioTestButton" class="btn btn-secondary audio-test-btn">
<span data-i18n="audio.notification.testPlay">測試播放</span>
</button>
</div>
</div>
<!-- 自訂音效上傳 -->
<div class="audio-setting-item">
<label class="audio-setting-label" data-i18n="audio.notification.uploadCustom">上傳自訂音效</label>
<div class="audio-upload-control">
<input type="file" id="audioUploadInput" class="audio-upload-input"
accept="audio/mp3,audio/wav,audio/ogg" style="display: none;">
<button type="button" id="audioUploadButton" class="btn btn-primary audio-upload-btn">
📁 <span data-i18n="audio.notification.chooseFile">選擇檔案</span>
</button>
<span class="audio-upload-hint" data-i18n="audio.notification.supportedFormats">
支援 MP3WAVOGG 格式
</span>
</div>
</div>
<!-- 自訂音效列表 -->
<div class="audio-setting-item">
<label class="audio-setting-label" data-i18n="audio.notification.customAudios">自訂音效</label>
<div class="audio-custom-list" id="audioCustomList">
<!-- 自訂音效列表將在這裡動態生成 -->
</div>
</div>
</div>
</div>
`;
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 = '⏳ <span data-i18n="audio.notification.uploading">上傳中...</span>';
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 = '📁 <span data-i18n="audio.notification.chooseFile">選擇檔案</span>';
});
});
};
/**
* 處理刪除自訂音效
*/
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 = `
<div class="audio-empty-state">
<div style="font-size: 32px; margin-bottom: 8px;">🎵</div>
<div data-i18n="audio.notification.noCustomAudios">尚未上傳任何自訂音效</div>
</div>
`;
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 `
<div class="audio-custom-item" data-audio-id="${audio.id}">
<div class="audio-custom-info">
<div class="audio-custom-name">${Utils.escapeHtml(audio.name)}</div>
<div class="audio-custom-meta">
<span data-i18n="audio.notification.created">建立於</span>: ${createdDate}
| <span data-i18n="audio.notification.format">格式</span>: ${audio.mimeType}
</div>
</div>
<div class="audio-custom-actions">
<button type="button" class="btn btn-sm btn-secondary audio-play-btn"
data-audio-id="${audio.id}" title="播放">
</button>
<button type="button" class="btn btn-sm btn-danger audio-delete-btn"
data-audio-id="${audio.id}" title="刪除">
🗑
</button>
</div>
</div>
`;
};
/**
* 設置自訂音效項目事件
*/
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 = `
<div class="audio-name-modal-overlay" id="audioNameModalOverlay">
<div class="audio-name-modal">
<div class="audio-name-modal-header">
<h4 data-i18n="audio.notification.enterAudioName">輸入音效名稱</h4>
<button type="button" class="audio-name-modal-close" id="audioNameModalClose">×</button>
</div>
<div class="audio-name-modal-body">
<label for="audioNameInput" data-i18n="audio.notification.audioName">音效名稱:</label>
<input type="text" id="audioNameInput" class="audio-name-input"
value="${Utils.escapeHtml(defaultName)}"
placeholder="${this.t('audio.notification.audioNamePlaceholder', '請輸入音效名稱...')}"
maxlength="50">
<div class="audio-name-hint" data-i18n="audio.notification.audioNameHint">
留空將使用預設檔案名稱
</div>
</div>
<div class="audio-name-modal-footer">
<button type="button" class="btn btn-secondary" id="audioNameModalCancel">
<span data-i18n="buttons.cancel">取消</span>
</button>
<button type="button" class="btn btn-primary" id="audioNameModalConfirm">
<span data-i18n="buttons.ok">確定</span>
</button>
</div>
</div>
</div>
`;
// 新增模態框到頁面
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;
})();

View File

@ -30,7 +30,12 @@
// 自動定時提交設定
autoSubmitEnabled: false,
autoSubmitTimeout: 30,
autoSubmitPromptId: null
autoSubmitPromptId: null,
// 音效通知設定
audioNotificationEnabled: false,
audioNotificationVolume: 50,
selectedAudioId: 'default-beep',
customAudios: []
};
// 當前設定

View File

@ -7,6 +7,7 @@
<link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="/static/css/session-management.css">
<link rel="stylesheet" href="/static/css/prompt-management.css">
<link rel="stylesheet" href="/static/css/audio-management.css">
<style>
/* 僅保留必要的頁面特定樣式和響應式調整 */
@ -873,6 +874,16 @@
</div>
</div>
<!-- 音效通知設定卡片 -->
<div class="settings-card">
<div class="settings-card-header">
<h3 class="settings-card-title" data-i18n="audio.notification.title">🔊 音效通知設定</h3>
</div>
<div class="settings-card-body" id="audioManagementContainer">
<!-- 音效管理 UI 將在這裡動態生成 -->
</div>
</div>
<!-- 提示詞管理卡片 -->
<div class="settings-card">
<div class="settings-card-header">
@ -1018,6 +1029,10 @@
<script src="/static/js/modules/prompt/prompt-settings-ui.js?v=2025010510"></script>
<script src="/static/js/modules/prompt/prompt-input-buttons.js?v=2025010510"></script>
<!-- 音效管理模組 -->
<script src="/static/js/modules/audio/audio-manager.js?v=2025010510"></script>
<script src="/static/js/modules/audio/audio-settings-ui.js?v=2025010510"></script>
<!-- 其他模組 -->
<script src="/static/js/modules/utils.js?v=2025010510"></script>
<script src="/static/js/modules/tab-manager.js?v=2025010510"></script>