mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 02:22:26 +08:00
✨ web ui 新增設定保存、載入及清除功能,並優化佈局模式選擇及相關UI,改善多語言支持。
This commit is contained in:
parent
4e1f6c8bb3
commit
ac05fd5b9a
3
.gitignore
vendored
3
.gitignore
vendored
@ -17,4 +17,5 @@ venv*/
|
||||
.DS_Store
|
||||
|
||||
.cursor/rules/
|
||||
uv.lock
|
||||
uv.lock
|
||||
.mcp_feedback_settings.json
|
@ -72,6 +72,11 @@
|
||||
"error": "Execution Error",
|
||||
"history": "Command History"
|
||||
},
|
||||
"combined": {
|
||||
"description": "Combined mode: AI summary and feedback input are on the same page for easy comparison.",
|
||||
"summaryTitle": "📋 AI Work Summary",
|
||||
"feedbackTitle": "💬 Provide Feedback"
|
||||
},
|
||||
"settings": {
|
||||
"title": "⚙️ Settings",
|
||||
"description": "Adjust interface settings and preference options.",
|
||||
@ -79,15 +84,25 @@
|
||||
"currentLanguage": "Current Language",
|
||||
"languageDesc": "Select interface display language",
|
||||
"interface": "Interface Settings",
|
||||
"combinedMode": "Combined Mode",
|
||||
"combinedModeDesc": "Merge AI summary and feedback input in the same tab",
|
||||
"layoutMode": "Interface Layout Mode",
|
||||
"layoutModeDesc": "Select how AI summary and feedback input are displayed",
|
||||
"separateMode": "Separate Mode",
|
||||
"separateModeDesc": "AI summary and feedback are in separate tabs",
|
||||
"combinedVertical": "Combined Mode (Vertical Layout)",
|
||||
"combinedVerticalDesc": "AI summary on top, feedback input below, both on the same page",
|
||||
"combinedHorizontal": "Combined Mode (Horizontal Layout)",
|
||||
"combinedHorizontalDesc": "AI summary on left, feedback input on right, expanding summary viewing area",
|
||||
"autoClose": "Auto Close Page",
|
||||
"autoCloseDesc": "Automatically close page after submitting feedback",
|
||||
"theme": "Theme",
|
||||
"notifications": "Notifications",
|
||||
"advanced": "Advanced Settings",
|
||||
"save": "Save Settings",
|
||||
"reset": "Reset",
|
||||
"reset": "Reset Settings",
|
||||
"resetDesc": "Clear all saved settings and restore to default state",
|
||||
"resetConfirm": "Are you sure you want to reset all settings? This will clear all saved preferences.",
|
||||
"resetSuccess": "Settings have been reset to default values",
|
||||
"resetError": "Error occurred while resetting settings",
|
||||
"timeout": "Connection Timeout (seconds)",
|
||||
"autorefresh": "Auto Refresh",
|
||||
"debug": "Debug Mode"
|
||||
|
@ -72,6 +72,11 @@
|
||||
"error": "执行错误",
|
||||
"history": "命令历史"
|
||||
},
|
||||
"combined": {
|
||||
"description": "合并模式:AI 摘要和反馈输入在同一页面中,方便对照查看。",
|
||||
"summaryTitle": "📋 AI 工作摘要",
|
||||
"feedbackTitle": "💬 提供反馈"
|
||||
},
|
||||
"settings": {
|
||||
"title": "⚙️ 设定",
|
||||
"description": "调整界面设定和偏好选项。",
|
||||
@ -79,15 +84,25 @@
|
||||
"currentLanguage": "当前语言",
|
||||
"languageDesc": "选择界面显示语言",
|
||||
"interface": "界面设定",
|
||||
"combinedMode": "合并模式",
|
||||
"combinedModeDesc": "将 AI 摘要和回馈输入合并在同一个分页中",
|
||||
"layoutMode": "界面布局模式",
|
||||
"layoutModeDesc": "选择 AI 摘要和反馈输入的显示方式",
|
||||
"separateMode": "分离模式",
|
||||
"separateModeDesc": "AI 摘要和反馈分别在不同页签",
|
||||
"combinedVertical": "合并模式(垂直布局)",
|
||||
"combinedVerticalDesc": "AI 摘要在上,反馈输入在下,摘要和反馈在同一页面",
|
||||
"combinedHorizontal": "合并模式(水平布局)",
|
||||
"combinedHorizontalDesc": "AI 摘要在左,反馈输入在右,增大摘要可视区域",
|
||||
"autoClose": "自动关闭页面",
|
||||
"autoCloseDesc": "提交回馈后自动关闭页面",
|
||||
"theme": "主题",
|
||||
"notifications": "通知",
|
||||
"advanced": "进阶设定",
|
||||
"save": "储存设定",
|
||||
"reset": "重设",
|
||||
"reset": "重置设定",
|
||||
"resetDesc": "清除所有已保存的设定,恢复到预设状态",
|
||||
"resetConfirm": "确定要重置所有设定吗?这将清除所有已保存的偏好设定。",
|
||||
"resetSuccess": "设定已重置为预设值",
|
||||
"resetError": "重置设定时发生错误",
|
||||
"timeout": "连线逾时 (秒)",
|
||||
"autorefresh": "自动重新整理",
|
||||
"debug": "除错模式"
|
||||
|
@ -72,6 +72,11 @@
|
||||
"error": "執行錯誤",
|
||||
"history": "命令歷史"
|
||||
},
|
||||
"combined": {
|
||||
"description": "合併模式:AI 摘要和回饋輸入在同一頁面中,方便對照查看。",
|
||||
"summaryTitle": "📋 AI 工作摘要",
|
||||
"feedbackTitle": "💬 提供回饋"
|
||||
},
|
||||
"settings": {
|
||||
"title": "⚙️ 設定",
|
||||
"description": "調整介面設定和偏好選項。",
|
||||
@ -79,15 +84,25 @@
|
||||
"currentLanguage": "當前語言",
|
||||
"languageDesc": "選擇界面顯示語言",
|
||||
"interface": "介面設定",
|
||||
"combinedMode": "合併模式",
|
||||
"combinedModeDesc": "將 AI 摘要和回饋輸入合併在同一個分頁中",
|
||||
"layoutMode": "界面佈局模式",
|
||||
"layoutModeDesc": "選擇 AI 摘要和回饋輸入的顯示方式",
|
||||
"separateMode": "分離模式",
|
||||
"separateModeDesc": "AI 摘要和回饋分別在不同頁籤",
|
||||
"combinedVertical": "合併模式(垂直布局)",
|
||||
"combinedVerticalDesc": "AI 摘要在上,回饋輸入在下,摘要和回饋在同一頁面",
|
||||
"combinedHorizontal": "合併模式(水平布局)",
|
||||
"combinedHorizontalDesc": "AI 摘要在左,回饋輸入在右,增大摘要可視區域",
|
||||
"autoClose": "自動關閉頁面",
|
||||
"autoCloseDesc": "提交回饋後自動關閉頁面",
|
||||
"theme": "主題",
|
||||
"notifications": "通知",
|
||||
"advanced": "進階設定",
|
||||
"save": "儲存設定",
|
||||
"reset": "重設",
|
||||
"reset": "重置設定",
|
||||
"resetDesc": "清除所有已保存的設定,恢復到預設狀態",
|
||||
"resetConfirm": "確定要重置所有設定嗎?這將清除所有已保存的偏好設定。",
|
||||
"resetSuccess": "設定已重置為預設值",
|
||||
"resetError": "重置設定時發生錯誤",
|
||||
"timeout": "連線逾時 (秒)",
|
||||
"autorefresh": "自動重新整理",
|
||||
"debug": "除錯模式"
|
||||
|
@ -37,7 +37,8 @@ class WebUIManager:
|
||||
|
||||
def __init__(self, host: str = "127.0.0.1", port: int = None):
|
||||
self.host = host
|
||||
self.port = port or find_free_port()
|
||||
# 優先使用固定端口 8765,確保 localStorage 的一致性
|
||||
self.port = port or find_free_port(preferred_port=8765)
|
||||
self.app = FastAPI(title="Interactive Feedback MCP")
|
||||
self.sessions: Dict[str, WebFeedbackSession] = {}
|
||||
self.server_thread = None
|
||||
@ -59,11 +60,8 @@ class WebUIManager:
|
||||
web_static_path = Path(__file__).parent / "static"
|
||||
if web_static_path.exists():
|
||||
self.app.mount("/static", StaticFiles(directory=str(web_static_path)), name="static")
|
||||
|
||||
# 備用:原有的靜態文件
|
||||
fallback_static_path = Path(__file__).parent.parent / "static"
|
||||
if fallback_static_path.exists():
|
||||
self.app.mount("/fallback_static", StaticFiles(directory=str(fallback_static_path)), name="fallback_static")
|
||||
else:
|
||||
raise RuntimeError(f"Static files directory not found: {web_static_path}")
|
||||
|
||||
def _setup_templates(self):
|
||||
"""設置模板引擎"""
|
||||
@ -72,9 +70,7 @@ class WebUIManager:
|
||||
if web_templates_path.exists():
|
||||
self.templates = Jinja2Templates(directory=str(web_templates_path))
|
||||
else:
|
||||
# 備用:原有的模板
|
||||
fallback_templates_path = Path(__file__).parent.parent / "templates"
|
||||
self.templates = Jinja2Templates(directory=str(fallback_templates_path))
|
||||
raise RuntimeError(f"Templates directory not found: {web_templates_path}")
|
||||
|
||||
def create_session(self, project_directory: str, summary: str) -> str:
|
||||
"""創建新的回饋會話"""
|
||||
|
@ -107,6 +107,74 @@ def setup_routes(manager: 'WebUIManager'):
|
||||
finally:
|
||||
session.websocket = None
|
||||
|
||||
@manager.app.post("/api/save-settings")
|
||||
async def save_settings(request: Request):
|
||||
"""保存設定到檔案"""
|
||||
try:
|
||||
data = await request.json()
|
||||
|
||||
# 構建設定檔案路徑
|
||||
settings_file = Path.cwd() / ".mcp_feedback_settings.json"
|
||||
|
||||
# 保存設定到檔案
|
||||
with open(settings_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
debug_log(f"設定已保存到: {settings_file}")
|
||||
|
||||
return JSONResponse(content={"status": "success", "message": "設定已保存"})
|
||||
|
||||
except Exception as e:
|
||||
debug_log(f"保存設定失敗: {e}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": f"保存失敗: {str(e)}"}
|
||||
)
|
||||
|
||||
@manager.app.get("/api/load-settings")
|
||||
async def load_settings():
|
||||
"""從檔案載入設定"""
|
||||
try:
|
||||
settings_file = Path.cwd() / ".mcp_feedback_settings.json"
|
||||
|
||||
if settings_file.exists():
|
||||
with open(settings_file, 'r', encoding='utf-8') as f:
|
||||
settings = json.load(f)
|
||||
|
||||
debug_log(f"設定已從檔案載入: {settings_file}")
|
||||
return JSONResponse(content=settings)
|
||||
else:
|
||||
debug_log("設定檔案不存在,返回空設定")
|
||||
return JSONResponse(content={})
|
||||
|
||||
except Exception as e:
|
||||
debug_log(f"載入設定失敗: {e}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": f"載入失敗: {str(e)}"}
|
||||
)
|
||||
|
||||
@manager.app.post("/api/clear-settings")
|
||||
async def clear_settings():
|
||||
"""清除設定檔案"""
|
||||
try:
|
||||
settings_file = Path.cwd() / ".mcp_feedback_settings.json"
|
||||
|
||||
if settings_file.exists():
|
||||
settings_file.unlink()
|
||||
debug_log(f"設定檔案已刪除: {settings_file}")
|
||||
else:
|
||||
debug_log("設定檔案不存在,無需刪除")
|
||||
|
||||
return JSONResponse(content={"status": "success", "message": "設定已清除"})
|
||||
|
||||
except Exception as e:
|
||||
debug_log(f"清除設定失敗: {e}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": f"清除失敗: {str(e)}"}
|
||||
)
|
||||
|
||||
|
||||
async def handle_websocket_message(manager: 'WebUIManager', session, data: dict):
|
||||
"""處理 WebSocket 消息"""
|
||||
|
@ -5,16 +5,101 @@
|
||||
* 處理 WebSocket 通信、分頁切換、圖片上傳、命令執行等功能
|
||||
*/
|
||||
|
||||
class PersistentSettings {
|
||||
constructor() {
|
||||
this.settingsFile = '.mcp_feedback_settings.json';
|
||||
this.storageKey = 'mcp_feedback_settings';
|
||||
}
|
||||
|
||||
async saveSettings(settings) {
|
||||
try {
|
||||
// 嘗試保存到伺服器端
|
||||
const response = await fetch('/api/save-settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('設定已保存到檔案');
|
||||
} else {
|
||||
throw new Error('伺服器端保存失敗');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('無法保存到檔案,使用 localStorage:', error);
|
||||
// 備用方案:保存到 localStorage
|
||||
this.saveToLocalStorage(settings);
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
try {
|
||||
// 嘗試從伺服器端載入
|
||||
const response = await fetch('/api/load-settings');
|
||||
if (response.ok) {
|
||||
const settings = await response.json();
|
||||
console.log('從檔案載入設定');
|
||||
return settings;
|
||||
} else {
|
||||
throw new Error('伺服器端載入失敗');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('無法從檔案載入,使用 localStorage:', error);
|
||||
// 備用方案:從 localStorage 載入
|
||||
return this.loadFromLocalStorage();
|
||||
}
|
||||
}
|
||||
|
||||
saveToLocalStorage(settings) {
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(settings));
|
||||
}
|
||||
|
||||
loadFromLocalStorage() {
|
||||
const saved = localStorage.getItem(this.storageKey);
|
||||
return saved ? JSON.parse(saved) : {};
|
||||
}
|
||||
|
||||
async clearSettings() {
|
||||
try {
|
||||
// 清除伺服器端設定
|
||||
await fetch('/api/clear-settings', { method: 'POST' });
|
||||
} catch (error) {
|
||||
console.warn('無法清除伺服器端設定:', error);
|
||||
}
|
||||
|
||||
// 清除 localStorage
|
||||
localStorage.removeItem(this.storageKey);
|
||||
|
||||
// 也清除個別設定項目(向後兼容)
|
||||
localStorage.removeItem('layoutMode');
|
||||
localStorage.removeItem('autoClose');
|
||||
localStorage.removeItem('activeTab');
|
||||
localStorage.removeItem('language');
|
||||
}
|
||||
}
|
||||
|
||||
class FeedbackApp {
|
||||
constructor(sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
this.websocket = null;
|
||||
this.images = [];
|
||||
this.isConnected = false;
|
||||
this.combinedMode = false;
|
||||
this.autoClose = true; // 預設開啟
|
||||
this.layoutMode = 'separate'; // 預設為分離模式
|
||||
this.autoClose = true; // 預設啟用自動關閉
|
||||
this.currentTab = 'feedback'; // 預設當前分頁
|
||||
this.persistentSettings = new PersistentSettings();
|
||||
this.images = []; // 初始化圖片陣列
|
||||
this.isConnected = false; // 初始化連接狀態
|
||||
this.websocket = null; // 初始化 WebSocket
|
||||
|
||||
this.init();
|
||||
// 立即檢查 DOM 狀態並初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
this.init();
|
||||
});
|
||||
} else {
|
||||
// DOM 已經載入完成,立即初始化
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
@ -41,8 +126,8 @@ class FeedbackApp {
|
||||
// 設置鍵盤快捷鍵
|
||||
this.setupKeyboardShortcuts();
|
||||
|
||||
// 載入設定
|
||||
this.loadSettings();
|
||||
// 載入設定(使用 await)
|
||||
await this.loadSettings();
|
||||
|
||||
// 初始化命令終端
|
||||
this.initCommandTerminal();
|
||||
@ -162,41 +247,10 @@ class FeedbackApp {
|
||||
}
|
||||
|
||||
showSuccessMessage() {
|
||||
// 創建成功訊息提示
|
||||
const message = document.createElement('div');
|
||||
message.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: var(--success-color);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
animation: slideIn 0.3s ease-out;
|
||||
`;
|
||||
message.textContent = '✅ 回饋提交成功!';
|
||||
|
||||
// 添加動畫樣式
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
document.body.appendChild(message);
|
||||
|
||||
// 3秒後移除訊息
|
||||
setTimeout(() => {
|
||||
if (message.parentNode) {
|
||||
message.remove();
|
||||
}
|
||||
}, 3000);
|
||||
const successMessage = window.i18nManager ?
|
||||
window.i18nManager.t('feedback.success', '✅ 回饋提交成功!') :
|
||||
'✅ 回饋提交成功!';
|
||||
this.showMessage(successMessage, 'success');
|
||||
}
|
||||
|
||||
updateConnectionStatus(connected) {
|
||||
@ -245,8 +299,46 @@ class FeedbackApp {
|
||||
});
|
||||
}
|
||||
|
||||
// 設置貼上監聽器
|
||||
this.setupPasteListener();
|
||||
|
||||
// 設定切換
|
||||
this.setupSettingsListeners();
|
||||
|
||||
// 設定重置按鈕(如果存在)
|
||||
const resetSettingsBtn = document.getElementById('resetSettingsBtn');
|
||||
if (resetSettingsBtn) {
|
||||
resetSettingsBtn.addEventListener('click', () => this.resetSettings());
|
||||
}
|
||||
}
|
||||
|
||||
setupSettingsListeners() {
|
||||
// 設置佈局模式單選按鈕監聽器
|
||||
const layoutModeRadios = document.querySelectorAll('input[name="layoutMode"]');
|
||||
layoutModeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
if (e.target.checked) {
|
||||
this.setLayoutMode(e.target.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 設置自動關閉開關監聽器
|
||||
const autoCloseToggle = document.getElementById('autoCloseToggle');
|
||||
if (autoCloseToggle) {
|
||||
autoCloseToggle.addEventListener('click', () => {
|
||||
this.toggleAutoClose();
|
||||
});
|
||||
}
|
||||
|
||||
// 設置語言選擇器
|
||||
const languageOptions = document.querySelectorAll('.language-option');
|
||||
languageOptions.forEach(option => {
|
||||
option.addEventListener('click', () => {
|
||||
const lang = option.getAttribute('data-lang');
|
||||
this.setLanguage(lang);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupTabs() {
|
||||
@ -395,31 +487,92 @@ class FeedbackApp {
|
||||
}
|
||||
}
|
||||
|
||||
setupSettingsListeners() {
|
||||
// 合併模式開關
|
||||
const combinedModeToggle = document.getElementById('combinedModeToggle');
|
||||
if (combinedModeToggle) {
|
||||
combinedModeToggle.addEventListener('click', () => {
|
||||
this.toggleCombinedMode();
|
||||
});
|
||||
setLayoutMode(mode) {
|
||||
if (this.layoutMode === mode) return;
|
||||
|
||||
this.layoutMode = mode;
|
||||
|
||||
// 保存設定到持久化存儲
|
||||
this.saveSettings();
|
||||
|
||||
// 只更新分頁可見性,不強制切換分頁
|
||||
this.updateTabVisibility();
|
||||
|
||||
// 數據同步
|
||||
if (mode === 'combined-vertical' || mode === 'combined-horizontal') {
|
||||
// 同步數據到合併模式
|
||||
this.syncDataToCombinedMode();
|
||||
} else {
|
||||
// 切換到分離模式時,同步數據回原始分頁
|
||||
this.syncDataFromCombinedMode();
|
||||
}
|
||||
|
||||
// 更新合併分頁的佈局樣式
|
||||
this.updateCombinedModeLayout();
|
||||
|
||||
console.log('佈局模式已切換至:', mode);
|
||||
}
|
||||
|
||||
updateTabVisibility() {
|
||||
const feedbackTab = document.querySelector('[data-tab="feedback"]');
|
||||
const summaryTab = document.querySelector('[data-tab="summary"]');
|
||||
const combinedTab = document.querySelector('[data-tab="combined"]');
|
||||
|
||||
if (this.layoutMode === 'separate') {
|
||||
// 分離模式:顯示原本的分頁,隱藏合併分頁
|
||||
if (feedbackTab) feedbackTab.classList.remove('hidden');
|
||||
if (summaryTab) summaryTab.classList.remove('hidden');
|
||||
if (combinedTab) {
|
||||
combinedTab.classList.add('hidden');
|
||||
// 只有在當前就在合併分頁時才切換到其他分頁
|
||||
if (combinedTab.classList.contains('active')) {
|
||||
this.switchToFeedbackTab();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 合併模式:隱藏原本的分頁,顯示合併分頁
|
||||
if (feedbackTab) feedbackTab.classList.add('hidden');
|
||||
if (summaryTab) summaryTab.classList.add('hidden');
|
||||
if (combinedTab) {
|
||||
combinedTab.classList.remove('hidden');
|
||||
// 不要強制切換到合併分頁,讓用戶手動選擇
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switchToFeedbackTab() {
|
||||
// 切換到回饋分頁的輔助方法
|
||||
const feedbackTab = document.querySelector('[data-tab="feedback"]');
|
||||
if (feedbackTab) {
|
||||
// 移除所有分頁按鈕的活躍狀態
|
||||
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
|
||||
// 移除所有分頁內容的活躍狀態
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
|
||||
// 設定回饋分頁為活躍
|
||||
feedbackTab.classList.add('active');
|
||||
document.getElementById('tab-feedback').classList.add('active');
|
||||
|
||||
console.log('已切換到回饋分頁');
|
||||
}
|
||||
}
|
||||
|
||||
updateCombinedModeLayout() {
|
||||
const combinedTabContent = document.getElementById('tab-combined');
|
||||
if (!combinedTabContent) {
|
||||
console.warn('找不到合併分頁元素 #tab-combined');
|
||||
return;
|
||||
}
|
||||
|
||||
// 自動關閉開關
|
||||
const autoCloseToggle = document.getElementById('autoCloseToggle');
|
||||
if (autoCloseToggle) {
|
||||
autoCloseToggle.addEventListener('click', () => {
|
||||
this.toggleAutoClose();
|
||||
});
|
||||
}
|
||||
// 移除所有佈局類
|
||||
combinedTabContent.classList.remove('combined-horizontal', 'combined-vertical');
|
||||
|
||||
// 語言選擇器
|
||||
const languageOptions = document.querySelectorAll('.language-option');
|
||||
languageOptions.forEach(option => {
|
||||
option.addEventListener('click', () => {
|
||||
const language = option.getAttribute('data-lang');
|
||||
this.setLanguage(language);
|
||||
});
|
||||
});
|
||||
// 根據當前模式添加對應的佈局類
|
||||
if (this.layoutMode === 'combined-horizontal') {
|
||||
combinedTabContent.classList.add('combined-horizontal');
|
||||
} else if (this.layoutMode === 'combined-vertical') {
|
||||
combinedTabContent.classList.add('combined-vertical');
|
||||
}
|
||||
}
|
||||
|
||||
setLanguage(language) {
|
||||
@ -438,10 +591,11 @@ class FeedbackApp {
|
||||
|
||||
// 語言切換後重新處理動態摘要內容
|
||||
setTimeout(() => {
|
||||
console.log('語言切換到:', language, '- 重新處理動態內容');
|
||||
this.processDynamicSummaryContent();
|
||||
}, 200); // 增加延遲時間確保翻譯加載完成
|
||||
}
|
||||
|
||||
console.log('語言已切換至:', language);
|
||||
}
|
||||
|
||||
handleFileSelection(files) {
|
||||
@ -598,7 +752,7 @@ class FeedbackApp {
|
||||
let feedbackText;
|
||||
|
||||
// 根據當前模式選擇正確的輸入框
|
||||
if (this.combinedMode) {
|
||||
if (this.layoutMode === 'combined-vertical' || this.layoutMode === 'combined-horizontal') {
|
||||
const combinedFeedbackInput = document.getElementById('combinedFeedbackText');
|
||||
feedbackText = combinedFeedbackInput?.value.trim() || '';
|
||||
} else {
|
||||
@ -650,67 +804,6 @@ class FeedbackApp {
|
||||
}
|
||||
}
|
||||
|
||||
toggleCombinedMode() {
|
||||
this.combinedMode = !this.combinedMode;
|
||||
|
||||
const toggle = document.getElementById('combinedModeToggle');
|
||||
if (toggle) {
|
||||
toggle.classList.toggle('active', this.combinedMode);
|
||||
}
|
||||
|
||||
// 顯示/隱藏分頁
|
||||
const feedbackTab = document.querySelector('[data-tab="feedback"]');
|
||||
const summaryTab = document.querySelector('[data-tab="summary"]');
|
||||
const combinedTab = document.querySelector('[data-tab="combined"]');
|
||||
|
||||
if (this.combinedMode) {
|
||||
// 啟用合併模式:隱藏原本的回饋和摘要分頁,顯示合併分頁
|
||||
if (feedbackTab) feedbackTab.classList.add('hidden');
|
||||
if (summaryTab) summaryTab.classList.add('hidden');
|
||||
if (combinedTab) {
|
||||
combinedTab.classList.remove('hidden');
|
||||
// 如果合併分頁顯示,並且當前在回饋或摘要分頁,則將合併分頁設為活躍
|
||||
const currentActiveTab = document.querySelector('.tab-button.active');
|
||||
if (currentActiveTab && (currentActiveTab.getAttribute('data-tab') === 'feedback' || currentActiveTab.getAttribute('data-tab') === 'summary')) {
|
||||
combinedTab.classList.add('active');
|
||||
currentActiveTab.classList.remove('active');
|
||||
|
||||
// 顯示對應的分頁內容
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
document.getElementById('tab-combined').classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 同步數據到合併模式
|
||||
this.syncDataToCombinedMode();
|
||||
|
||||
} else {
|
||||
// 停用合併模式:顯示原本的分頁,隱藏合併分頁
|
||||
if (feedbackTab) feedbackTab.classList.remove('hidden');
|
||||
if (summaryTab) summaryTab.classList.remove('hidden');
|
||||
if (combinedTab) {
|
||||
combinedTab.classList.add('hidden');
|
||||
// 如果當前在合併分頁,則切換到回饋分頁
|
||||
if (combinedTab.classList.contains('active')) {
|
||||
combinedTab.classList.remove('active');
|
||||
if (feedbackTab) {
|
||||
feedbackTab.classList.add('active');
|
||||
// 顯示對應的分頁內容
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
document.getElementById('tab-feedback').classList.add('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 同步數據回原始分頁
|
||||
this.syncDataFromCombinedMode();
|
||||
}
|
||||
|
||||
localStorage.setItem('combinedMode', this.combinedMode.toString());
|
||||
|
||||
console.log('合併模式已', this.combinedMode ? '啟用' : '停用');
|
||||
}
|
||||
|
||||
toggleAutoClose() {
|
||||
this.autoClose = !this.autoClose;
|
||||
|
||||
@ -719,7 +812,8 @@ class FeedbackApp {
|
||||
toggle.classList.toggle('active', this.autoClose);
|
||||
}
|
||||
|
||||
localStorage.setItem('autoClose', this.autoClose.toString());
|
||||
// 保存設定到持久化存儲
|
||||
this.saveSettings();
|
||||
|
||||
console.log('自動關閉頁面已', this.autoClose ? '啟用' : '停用');
|
||||
}
|
||||
@ -749,51 +843,107 @@ class FeedbackApp {
|
||||
}
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
// 載入合併模式設定
|
||||
const savedCombinedMode = localStorage.getItem('combinedMode');
|
||||
if (savedCombinedMode === 'true') {
|
||||
this.combinedMode = true;
|
||||
const toggle = document.getElementById('combinedModeToggle');
|
||||
if (toggle) {
|
||||
toggle.classList.add('active');
|
||||
syncLanguageSelector() {
|
||||
// 同步語言選擇器的狀態
|
||||
if (window.i18nManager) {
|
||||
const currentLang = window.i18nManager.currentLanguage;
|
||||
|
||||
// 更新現代化語言選擇器
|
||||
const languageOptions = document.querySelectorAll('.language-option');
|
||||
languageOptions.forEach(option => {
|
||||
const lang = option.getAttribute('data-lang');
|
||||
option.classList.toggle('active', lang === currentLang);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
try {
|
||||
// 使用持久化設定系統載入設定
|
||||
const settings = await this.persistentSettings.loadSettings();
|
||||
|
||||
// 載入佈局模式設定
|
||||
if (settings.layoutMode && ['separate', 'combined-vertical', 'combined-horizontal'].includes(settings.layoutMode)) {
|
||||
this.layoutMode = settings.layoutMode;
|
||||
} else {
|
||||
// 嘗試從舊的 localStorage 載入(向後兼容)
|
||||
const savedLayoutMode = localStorage.getItem('layoutMode');
|
||||
if (savedLayoutMode && ['separate', 'combined-vertical', 'combined-horizontal'].includes(savedLayoutMode)) {
|
||||
this.layoutMode = savedLayoutMode;
|
||||
} else {
|
||||
this.layoutMode = 'separate'; // 預設為分離模式
|
||||
}
|
||||
}
|
||||
|
||||
// 應用合併模式設定
|
||||
this.applyCombinedModeState();
|
||||
}
|
||||
// 更新佈局模式單選按鈕狀態
|
||||
const layoutRadios = document.querySelectorAll('input[name="layoutMode"]');
|
||||
layoutRadios.forEach((radio, index) => {
|
||||
radio.checked = radio.value === this.layoutMode;
|
||||
});
|
||||
|
||||
// 載入自動關閉設定
|
||||
const savedAutoClose = localStorage.getItem('autoClose');
|
||||
if (savedAutoClose !== null) {
|
||||
this.autoClose = savedAutoClose === 'true';
|
||||
} else {
|
||||
// 如果沒有保存的設定,使用預設值(true)
|
||||
// 載入自動關閉設定
|
||||
if (settings.autoClose !== undefined) {
|
||||
this.autoClose = settings.autoClose;
|
||||
} else {
|
||||
// 嘗試從舊的 localStorage 載入(向後兼容)
|
||||
const savedAutoClose = localStorage.getItem('autoClose');
|
||||
if (savedAutoClose !== null) {
|
||||
this.autoClose = savedAutoClose === 'true';
|
||||
} else {
|
||||
this.autoClose = true; // 預設啟用
|
||||
}
|
||||
}
|
||||
|
||||
// 更新自動關閉開關狀態
|
||||
const autoCloseToggle = document.getElementById('autoCloseToggle');
|
||||
if (autoCloseToggle) {
|
||||
autoCloseToggle.classList.toggle('active', this.autoClose);
|
||||
}
|
||||
|
||||
// 確保語言選擇器與當前語言同步
|
||||
this.syncLanguageSelector();
|
||||
|
||||
// 應用佈局模式設定
|
||||
this.applyCombinedModeState();
|
||||
|
||||
// 如果是合併模式,同步數據
|
||||
if (this.layoutMode === 'combined-vertical' || this.layoutMode === 'combined-horizontal') {
|
||||
this.syncDataToCombinedMode();
|
||||
}
|
||||
|
||||
console.log('設定已載入:', {
|
||||
layoutMode: this.layoutMode,
|
||||
autoClose: this.autoClose,
|
||||
currentLanguage: window.i18nManager?.currentLanguage,
|
||||
source: settings.layoutMode ? 'persistent' : 'localStorage'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.warn('載入設定時發生錯誤:', error);
|
||||
// 使用預設設定
|
||||
this.layoutMode = 'separate';
|
||||
this.autoClose = true;
|
||||
}
|
||||
|
||||
// 更新自動關閉開關狀態
|
||||
const autoCloseToggle = document.getElementById('autoCloseToggle');
|
||||
if (autoCloseToggle) {
|
||||
autoCloseToggle.classList.toggle('active', this.autoClose);
|
||||
|
||||
// 仍然需要更新 UI 狀態
|
||||
const layoutRadios = document.querySelectorAll('input[name="layoutMode"]');
|
||||
layoutRadios.forEach((radio, index) => {
|
||||
radio.checked = radio.value === this.layoutMode;
|
||||
});
|
||||
|
||||
const autoCloseToggle = document.getElementById('autoCloseToggle');
|
||||
if (autoCloseToggle) {
|
||||
autoCloseToggle.classList.toggle('active', this.autoClose);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyCombinedModeState() {
|
||||
const feedbackTab = document.querySelector('[data-tab="feedback"]');
|
||||
const summaryTab = document.querySelector('[data-tab="summary"]');
|
||||
const combinedTab = document.querySelector('[data-tab="combined"]');
|
||||
|
||||
if (this.combinedMode) {
|
||||
// 隱藏原本的回饋和摘要分頁,顯示合併分頁
|
||||
if (feedbackTab) feedbackTab.classList.add('hidden');
|
||||
if (summaryTab) summaryTab.classList.add('hidden');
|
||||
if (combinedTab) combinedTab.classList.remove('hidden');
|
||||
} else {
|
||||
// 顯示原本的分頁,隱藏合併分頁
|
||||
if (feedbackTab) feedbackTab.classList.remove('hidden');
|
||||
if (summaryTab) summaryTab.classList.remove('hidden');
|
||||
if (combinedTab) combinedTab.classList.add('hidden');
|
||||
// 更新分頁可見性
|
||||
this.updateTabVisibility();
|
||||
|
||||
// 更新合併分頁的佈局樣式
|
||||
if (this.layoutMode !== 'separate') {
|
||||
this.updateCombinedModeLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@ -818,6 +968,136 @@ Supported commands: ls, dir, pwd, cat, type, etc.
|
||||
$ `;
|
||||
this.appendCommandOutput(welcomeMessage);
|
||||
}
|
||||
|
||||
async resetSettings() {
|
||||
// 確認重置
|
||||
const confirmMessage = window.i18nManager ?
|
||||
window.i18nManager.t('settings.resetConfirm', '確定要重置所有設定嗎?這將清除所有已保存的偏好設定。') :
|
||||
'確定要重置所有設定嗎?這將清除所有已保存的偏好設定。';
|
||||
|
||||
if (!confirm(confirmMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用持久化設定系統清除設定
|
||||
await this.persistentSettings.clearSettings();
|
||||
|
||||
// 重置本地變數
|
||||
this.layoutMode = 'separate';
|
||||
this.autoClose = true;
|
||||
|
||||
// 更新佈局模式單選按鈕狀態
|
||||
const layoutRadios = document.querySelectorAll('input[name="layoutMode"]');
|
||||
layoutRadios.forEach((radio, index) => {
|
||||
radio.checked = radio.value === this.layoutMode;
|
||||
});
|
||||
|
||||
// 更新自動關閉開關狀態
|
||||
const autoCloseToggle = document.getElementById('autoCloseToggle');
|
||||
if (autoCloseToggle) {
|
||||
autoCloseToggle.classList.toggle('active', this.autoClose);
|
||||
}
|
||||
|
||||
// 確保語言選擇器與當前語言同步
|
||||
this.syncLanguageSelector();
|
||||
|
||||
// 應用佈局模式設定
|
||||
this.applyCombinedModeState();
|
||||
|
||||
// 切換到回饋分頁
|
||||
this.switchToFeedbackTab();
|
||||
|
||||
// 顯示成功訊息
|
||||
const successMessage = window.i18nManager ?
|
||||
window.i18nManager.t('settings.resetSuccess', '設定已重置為預設值') :
|
||||
'設定已重置為預設值';
|
||||
|
||||
this.showMessage(successMessage, 'success');
|
||||
|
||||
console.log('設定已重置');
|
||||
|
||||
} catch (error) {
|
||||
console.error('重置設定時發生錯誤:', error);
|
||||
|
||||
// 顯示錯誤訊息
|
||||
const errorMessage = window.i18nManager ?
|
||||
window.i18nManager.t('settings.resetError', '重置設定時發生錯誤') :
|
||||
'重置設定時發生錯誤';
|
||||
|
||||
this.showMessage(errorMessage, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
showMessage(text, type = 'info') {
|
||||
// 確保動畫樣式已添加
|
||||
if (!document.getElementById('slideInAnimation')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'slideInAnimation';
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// 創建訊息提示
|
||||
const message = document.createElement('div');
|
||||
const colors = {
|
||||
success: 'var(--success-color)',
|
||||
error: 'var(--error-color)',
|
||||
warning: 'var(--warning-color)',
|
||||
info: 'var(--info-color)'
|
||||
};
|
||||
|
||||
message.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${colors[type] || colors.info};
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
animation: slideIn 0.3s ease-out;
|
||||
`;
|
||||
message.textContent = text;
|
||||
|
||||
document.body.appendChild(message);
|
||||
|
||||
// 3秒後移除訊息
|
||||
setTimeout(() => {
|
||||
if (message.parentNode) {
|
||||
message.remove();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
try {
|
||||
const settings = {
|
||||
layoutMode: this.layoutMode,
|
||||
autoClose: this.autoClose,
|
||||
language: window.i18nManager?.currentLanguage || 'zh-TW',
|
||||
activeTab: localStorage.getItem('activeTab'),
|
||||
lastSaved: new Date().toISOString()
|
||||
};
|
||||
|
||||
await this.persistentSettings.saveSettings(settings);
|
||||
|
||||
// 同時保存到 localStorage 作為備用(向後兼容)
|
||||
localStorage.setItem('layoutMode', this.layoutMode);
|
||||
localStorage.setItem('autoClose', this.autoClose.toString());
|
||||
|
||||
console.log('設定已保存:', settings);
|
||||
} catch (error) {
|
||||
console.warn('保存設定時發生錯誤:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 全域函數,供 HTML 中的 onclick 使用
|
||||
|
@ -434,10 +434,6 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
/* 小屏幕下調整命令輸出區域高度 */
|
||||
.command-output {
|
||||
height: 250px;
|
||||
@ -657,6 +653,151 @@
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid var(--accent-color);
|
||||
}
|
||||
|
||||
/* 新增:水平佈局的 CSS 樣式 */
|
||||
.layout-mode-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.layout-option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-tertiary);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.layout-option:hover {
|
||||
border-color: var(--accent-color);
|
||||
background: rgba(0, 122, 204, 0.1);
|
||||
}
|
||||
|
||||
.layout-option input[type="radio"] {
|
||||
margin: 0;
|
||||
margin-right: 12px;
|
||||
margin-top: 2px;
|
||||
accent-color: var(--accent-color);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.layout-option label {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.layout-option-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.layout-option-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.layout-option input[type="radio"]:checked + label {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.layout-option input[type="radio"]:checked + label .layout-option-title {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.layout-option:has(input[type="radio"]:checked) {
|
||||
border-color: var(--accent-color);
|
||||
background: rgba(0, 122, 204, 0.15);
|
||||
}
|
||||
|
||||
/* 合併模式分頁的水平佈局樣式 */
|
||||
#tab-combined.active.combined-horizontal .combined-content {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
gap: 16px;
|
||||
height: calc(100% - 60px); /* 減去描述區塊的高度 */
|
||||
}
|
||||
|
||||
#tab-combined.active.combined-horizontal .combined-section:first-child {
|
||||
flex: 1 !important;
|
||||
min-width: 300px;
|
||||
max-width: 50%;
|
||||
overflow: hidden; /* 確保容器不超出範圍 */
|
||||
}
|
||||
|
||||
#tab-combined.active.combined-horizontal .combined-section:last-child {
|
||||
flex: 1 !important;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
#tab-combined.active.combined-horizontal .combined-summary {
|
||||
height: calc(100vh - 200px);
|
||||
max-height: 600px;
|
||||
overflow: hidden; /* 確保摘要容器不超出範圍 */
|
||||
}
|
||||
|
||||
#tab-combined.active.combined-horizontal #combinedSummaryContent {
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
overflow-y: auto; /* 添加垂直滾動條 */
|
||||
overflow-x: hidden; /* 隱藏水平滾動條 */
|
||||
}
|
||||
|
||||
#tab-combined.active.combined-horizontal .text-input {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* 合併模式分頁的垂直佈局樣式 */
|
||||
#tab-combined.active.combined-vertical .combined-content {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
gap: 16px;
|
||||
height: calc(100% - 60px); /* 減去描述區塊的高度 */
|
||||
}
|
||||
|
||||
#tab-combined.active.combined-vertical .combined-section:first-child {
|
||||
flex: 1 !important;
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
overflow: hidden; /* 確保容器不超出範圍 */
|
||||
}
|
||||
|
||||
#tab-combined.active.combined-vertical .combined-section:last-child {
|
||||
flex: 2 !important;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
#tab-combined.active.combined-vertical .combined-summary {
|
||||
height: 300px;
|
||||
max-height: 400px;
|
||||
overflow: hidden; /* 確保摘要容器不超出範圍 */
|
||||
}
|
||||
|
||||
#tab-combined.active.combined-vertical #combinedSummaryContent {
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
overflow-y: auto; /* 添加垂直滾動條 */
|
||||
overflow-x: hidden; /* 隱藏水平滾動條 */
|
||||
}
|
||||
|
||||
#tab-combined.active.combined-vertical .text-input {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* 預設的合併內容布局 */
|
||||
.combined-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -778,6 +919,53 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 合併模式分頁 - 移動到此位置 -->
|
||||
<div id="tab-combined" class="tab-content">
|
||||
<div class="section-description" style="margin-bottom: 12px; padding: 8px 12px; font-size: 13px;" data-i18n="combined.description">
|
||||
合併模式:AI 摘要和回饋輸入在同一頁面中,方便對照查看。
|
||||
</div>
|
||||
|
||||
<div class="combined-content">
|
||||
<!-- AI 摘要區域 -->
|
||||
<div class="combined-section">
|
||||
<h3 class="combined-section-title" data-i18n="combined.summaryTitle">📋 AI 工作摘要</h3>
|
||||
<div class="combined-summary">
|
||||
<div id="combinedSummaryContent" class="text-input" style="min-height: 200px; white-space: pre-wrap; cursor: text;" data-dynamic-content="aiSummary">
|
||||
{{ summary }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回饋輸入區域 -->
|
||||
<div class="combined-section">
|
||||
<h3 class="combined-section-title" data-i18n="combined.feedbackTitle">💬 提供回饋</h3>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label" data-i18n="feedback.textLabel">文字回饋</label>
|
||||
<textarea
|
||||
id="combinedFeedbackText"
|
||||
class="text-input"
|
||||
data-i18n-placeholder="feedback.detailedPlaceholder"
|
||||
placeholder="請在這裡輸入您的回饋..."
|
||||
style="min-height: 150px;"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label" data-i18n="feedback.imageLabel">圖片附件(可選)</label>
|
||||
<div id="combinedImageUploadArea" class="image-upload-area" style="min-height: 100px;">
|
||||
<div id="combinedImageUploadText" data-i18n="feedback.imageUploadText">
|
||||
📎 點擊選擇圖片或拖放圖片到此處<br>
|
||||
<small>支援 PNG、JPG、JPEG、GIF、BMP、WebP 等格式</small>
|
||||
</div>
|
||||
<div id="combinedImagePreviewContainer" class="image-preview-container"></div>
|
||||
<input type="file" id="combinedImageInput" multiple accept="image/*" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 設定分頁 -->
|
||||
<div id="tab-settings" class="tab-content">
|
||||
<div class="section-description" data-i18n="settings.description">
|
||||
@ -792,13 +980,33 @@
|
||||
<div class="settings-card-body">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label" data-i18n="settings.combinedMode">合併模式</div>
|
||||
<div class="setting-description" data-i18n="settings.combinedModeDesc">
|
||||
將 AI 摘要和回饋輸入合併在同一個分頁中
|
||||
<div class="setting-label" data-i18n="settings.layoutMode">界面佈局模式</div>
|
||||
<div class="setting-description" data-i18n="settings.layoutModeDesc">
|
||||
選擇 AI 摘要和回饋輸入的顯示方式
|
||||
</div>
|
||||
</div>
|
||||
<div id="combinedModeToggle" class="toggle-switch">
|
||||
<div class="toggle-knob"></div>
|
||||
<div class="layout-mode-selector">
|
||||
<div class="layout-option">
|
||||
<input type="radio" id="separateMode" name="layoutMode" value="separate" checked>
|
||||
<label for="separateMode">
|
||||
<div class="layout-option-title" data-i18n="settings.separateMode">分離模式</div>
|
||||
<div class="layout-option-desc" data-i18n="settings.separateModeDesc">AI 摘要和回饋分別在不同頁籤</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="layout-option">
|
||||
<input type="radio" id="combinedVertical" name="layoutMode" value="combined-vertical">
|
||||
<label for="combinedVertical">
|
||||
<div class="layout-option-title" data-i18n="settings.combinedVertical">合併模式(垂直布局)</div>
|
||||
<div class="layout-option-desc" data-i18n="settings.combinedVerticalDesc">AI 摘要在上,回饋輸入在下,摘要和回饋在同一頁面</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="layout-option">
|
||||
<input type="radio" id="combinedHorizontal" name="layoutMode" value="combined-horizontal">
|
||||
<label for="combinedHorizontal">
|
||||
<div class="layout-option-title" data-i18n="settings.combinedHorizontal">合併模式(水平布局)</div>
|
||||
<div class="layout-option-desc" data-i18n="settings.combinedHorizontalDesc">AI 摘要在左,回饋輸入在右,增大摘要可視區域</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
@ -847,48 +1055,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 合併模式分頁 - 新增 -->
|
||||
<div id="tab-combined" class="tab-content">
|
||||
<div class="section-description">
|
||||
合併模式:AI 摘要和回饋輸入在同一頁面中,方便對照查看。
|
||||
</div>
|
||||
|
||||
<!-- AI 摘要區域 -->
|
||||
<div class="combined-section">
|
||||
<h3 class="combined-section-title">📋 AI 工作摘要</h3>
|
||||
<div class="combined-summary">
|
||||
<div id="combinedSummaryContent" class="text-input" style="min-height: 200px; white-space: pre-wrap; cursor: text;" data-dynamic-content="aiSummary">
|
||||
{{ summary }}
|
||||
</div>
|
||||
<!-- 重置設定卡片 -->
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<h3 class="settings-card-title" data-i18n="settings.advanced">🔧 進階設定</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回饋輸入區域 -->
|
||||
<div class="combined-section">
|
||||
<h3 class="combined-section-title">💬 提供回饋</h3>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label" data-i18n="feedback.textLabel">文字回饋</label>
|
||||
<textarea
|
||||
id="combinedFeedbackText"
|
||||
class="text-input"
|
||||
data-i18n-placeholder="feedback.detailedPlaceholder"
|
||||
placeholder="請在這裡輸入您的回饋..."
|
||||
style="min-height: 150px;"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label" data-i18n="feedback.imageLabel">圖片附件(可選)</label>
|
||||
<div id="combinedImageUploadArea" class="image-upload-area" style="min-height: 100px;">
|
||||
<div id="combinedImageUploadText" data-i18n="feedback.imageUploadText">
|
||||
📎 點擊選擇圖片或拖放圖片到此處<br>
|
||||
<small>支援 PNG、JPG、JPEG、GIF、BMP、WebP 等格式</small>
|
||||
<div class="settings-card-body">
|
||||
<div class="setting-item" style="border-bottom: none;">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label" data-i18n="settings.reset">重置設定</div>
|
||||
<div class="setting-description" data-i18n="settings.resetDesc">
|
||||
清除所有已保存的設定,恢復到預設狀態
|
||||
</div>
|
||||
</div>
|
||||
<div id="combinedImagePreviewContainer" class="image-preview-container"></div>
|
||||
<input type="file" id="combinedImageInput" multiple accept="image/*" style="display: none;">
|
||||
<button id="resetSettingsBtn" class="btn btn-secondary" style="font-size: 12px; padding: 6px 16px;">
|
||||
<span data-i18n="settings.reset">重置設定</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -992,12 +1175,9 @@
|
||||
<script src="/static/js/i18n.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
// 初始化頁面
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化 WebSocket 連接
|
||||
const sessionId = '{{ session_id }}';
|
||||
window.feedbackApp = new FeedbackApp(sessionId);
|
||||
});
|
||||
// 初始化全域應用程式實例
|
||||
const sessionId = '{{ session_id }}';
|
||||
window.feedbackApp = new FeedbackApp(sessionId);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -11,13 +11,14 @@ import socket
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def find_free_port(start_port: int = 8765, max_attempts: int = 100) -> int:
|
||||
def find_free_port(start_port: int = 8765, max_attempts: int = 100, preferred_port: int = 8765) -> int:
|
||||
"""
|
||||
尋找可用的端口
|
||||
尋找可用的端口,優先使用偏好端口
|
||||
|
||||
Args:
|
||||
start_port: 起始端口號
|
||||
max_attempts: 最大嘗試次數
|
||||
preferred_port: 偏好端口號(用於保持設定持久性)
|
||||
|
||||
Returns:
|
||||
int: 可用的端口號
|
||||
@ -25,8 +26,15 @@ def find_free_port(start_port: int = 8765, max_attempts: int = 100) -> int:
|
||||
Raises:
|
||||
RuntimeError: 如果找不到可用端口
|
||||
"""
|
||||
# 首先嘗試偏好端口(通常是 8765)
|
||||
if is_port_available("127.0.0.1", preferred_port):
|
||||
return preferred_port
|
||||
|
||||
# 如果偏好端口不可用,嘗試其他端口
|
||||
for i in range(max_attempts):
|
||||
port = start_port + i
|
||||
if port == preferred_port: # 跳過已經嘗試過的偏好端口
|
||||
continue
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.bind(("127.0.0.1", port))
|
||||
|
Loading…
x
Reference in New Issue
Block a user