增強 Web UI 功能,新增 WebSocket 自動重連機制,改善用戶回饋提交流程,並優化界面設計與交互體驗。重構代碼以支持多進程獨立管理器,並添加通知系統以提升用戶反饋。更新命令執行功能,增強錯誤處理與狀態提示。

This commit is contained in:
Minidoracat 2025-05-31 08:55:53 +08:00
parent 58c6630aec
commit c5a0521411
3 changed files with 593 additions and 145 deletions

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ venv*/
.DS_Store .DS_Store
.cursor/rules/ .cursor/rules/
uv.lock

View File

@ -720,63 +720,202 @@
<script> <script>
// 全域變數 // 全域變數
let selectedImages = []; let selectedImages = [];
let commandHistory = []; let currentLanguage = 'zh-TW';
let historyIndex = -1; let ws = null; // WebSocket 連接
let isConnected = false;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
const reconnectDelay = 2000; // 2 秒
let isCommandRunning = false;
// 初始化應用程式 // DOM 元素
const elements = {
connectionStatus: document.getElementById('connectionStatus'),
statusText: document.getElementById('statusText'),
feedbackTextarea: document.getElementById('feedbackText'),
commandInput: document.getElementById('commandInput'),
commandOutput: document.getElementById('commandOutput'),
runCommandBtn: document.getElementById('runBtn'),
stopCommandBtn: document.getElementById('stopCommandBtn'),
submitBtn: document.getElementById('submitBtn'),
cancelBtn: document.getElementById('cancelBtn')
};
// WebSocket 連接初始化
function initWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/{{ session_id }}`;
console.log('嘗試連接 WebSocket:', wsUrl);
updateConnectionStatus('connecting', '正在連接...');
try {
ws = new WebSocket(wsUrl);
setupWebSocketHandlers();
} catch (error) {
console.error('WebSocket 連接錯誤:', error);
handleConnectionError();
}
}
function setupWebSocketHandlers() {
ws.onopen = function() {
console.log('WebSocket 連接成功');
isConnected = true;
reconnectAttempts = 0;
updateConnectionStatus('connected', '已連接');
showNotification('WebSocket 連接成功', 'success');
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
handleWebSocketMessage(data);
} catch (error) {
console.error('解析 WebSocket 消息失敗:', error);
console.log('原始消息:', event.data);
}
};
ws.onclose = function(event) {
console.log('WebSocket 連接關閉:', event.code, event.reason);
isConnected = false;
updateConnectionStatus('disconnected', '連接中斷');
// 自動重連(除非是正常關閉)
if (event.code !== 1000 && reconnectAttempts < maxReconnectAttempts) {
setTimeout(attemptReconnect, reconnectDelay);
}
};
ws.onerror = function(error) {
console.error('WebSocket 錯誤:', error);
handleConnectionError();
};
}
function attemptReconnect() {
if (reconnectAttempts >= maxReconnectAttempts) {
console.log('重連次數已達上限');
updateConnectionStatus('disconnected', '連接失敗');
showNotification('無法重新連接,請重新整理頁面', 'error');
return;
}
reconnectAttempts++;
console.log(`嘗試重連 (${reconnectAttempts}/${maxReconnectAttempts})`);
updateConnectionStatus('connecting', `重連中... (${reconnectAttempts}/${maxReconnectAttempts})`);
initWebSocket();
}
function handleConnectionError() {
isConnected = false;
updateConnectionStatus('disconnected', '連接錯誤');
if (reconnectAttempts < maxReconnectAttempts) {
setTimeout(attemptReconnect, reconnectDelay);
}
}
function updateConnectionStatus(status, text) {
elements.connectionStatus.className = `status-indicator status-${status}`;
elements.statusText.textContent = text;
}
// 頁面初始化
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// 初始化 WebSocket 連接
initWebSocket();
// 初始化界面
initializeApp(); initializeApp();
setupEventListeners(); setupEventListeners();
applyTranslations(); changeLanguage(currentLanguage);
updateImagePreviewArea(); // 初始化預覽區域 setupDragAndDrop();
setupKeyboardShortcuts();
switchTab('feedback');
updateImagePreviewArea();
}); });
// 監聽語言變更事件 // 語言切換功能
document.addEventListener('languageChanged', function(event) { function changeLanguage(lang) {
currentLanguage = lang;
window.i18n.setLanguage(lang);
applyTranslations(); applyTranslations();
updatePlaceholders(); updatePlaceholders();
updateImagePreviewArea(); // 更新預覽區域文字 updateHtmlLang();
}); }
function initializeApp() { function initializeApp() {
// 設置語言選擇器 // 設置語言選擇器
const languageSelect = document.getElementById('languageSelect'); const languageSelect = document.getElementById('languageSelect');
languageSelect.value = window.i18n.getCurrentLanguage(); if (languageSelect) {
languageSelect.value = currentLanguage;
}
// 更新 HTML lang 屬性 // 更新 HTML lang 屬性
updateHtmlLang(); updateHtmlLang();
// 設置拖拽功能
setupDragAndDrop();
// 設置快捷鍵
setupKeyboardShortcuts();
} }
function setupEventListeners() { function setupEventListeners() {
// 語言選擇器事件 // 語言選擇器事件
document.getElementById('languageSelect').addEventListener('change', function(e) { const languageSelect = document.getElementById('languageSelect');
const newLanguage = e.target.value; if (languageSelect) {
window.i18n.setLanguage(newLanguage); languageSelect.addEventListener('change', function(e) {
updateHtmlLang(); changeLanguage(e.target.value);
}); });
}
// 文件輸入事件 // 文件輸入事件
document.getElementById('fileInput').addEventListener('change', handleFileSelect); const fileInput = document.getElementById('fileInput');
if (fileInput) {
fileInput.addEventListener('change', handleFileSelect);
}
// 命令執行
elements.runCommandBtn.addEventListener('click', runCommand);
elements.stopCommandBtn.addEventListener('click', stopCommand);
// 命令輸入框 Enter 鍵
elements.commandInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !isCommandRunning) {
runCommand();
}
});
// 回饋提交
elements.submitBtn.addEventListener('click', submitFeedback);
elements.cancelBtn.addEventListener('click', cancelFeedback);
// 快捷鍵支援
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
submitFeedback();
}
});
// 窗口關閉時清理 WebSocket
window.addEventListener('beforeunload', () => {
if (ws && isConnected) {
ws.close(1000, '頁面關閉');
}
});
} }
function updateHtmlLang() { function updateHtmlLang() {
const htmlRoot = document.getElementById('html-root'); const htmlRoot = document.getElementById('html-root');
const currentLang = window.i18n.getCurrentLanguage(); if (htmlRoot) {
// 語言代碼映射
// 語言代碼映射 const langMap = {
const langMap = { 'zh-TW': 'zh-TW',
'zh-TW': 'zh-TW', 'zh-CN': 'zh-CN',
'zh-CN': 'zh-CN', 'en': 'en'
'en': 'en' };
};
htmlRoot.setAttribute('lang', langMap[currentLanguage] || 'en');
htmlRoot.setAttribute('lang', langMap[currentLang] || 'en'); }
} }
function applyTranslations() { function applyTranslations() {
@ -784,56 +923,88 @@
document.title = t('app_title'); document.title = t('app_title');
// 更新標題區域 // 更新標題區域
document.getElementById('pageTitle').textContent = t('app_title'); const pageTitle = document.getElementById('pageTitle');
document.getElementById('projectDirLabel').textContent = t('project_directory'); if (pageTitle) pageTitle.textContent = t('app_title');
document.getElementById('languageLabel').textContent = t('language_selector') + ':';
const projectDirLabel = document.getElementById('projectDirLabel');
if (projectDirLabel) projectDirLabel.textContent = t('project_directory');
const languageLabel = document.getElementById('languageLabel');
if (languageLabel) languageLabel.textContent = t('language_selector') + ':';
// 更新語言選項 // 更新語言選項
const languageSelect = document.getElementById('languageSelect'); const languageSelect = document.getElementById('languageSelect');
const options = languageSelect.querySelectorAll('option'); if (languageSelect) {
options.forEach(option => { const options = languageSelect.querySelectorAll('option');
const value = option.value; options.forEach(option => {
option.textContent = window.i18n.getLanguageDisplayName(value); const value = option.value;
}); option.textContent = window.i18n.getLanguageDisplayName(value);
});
}
// 更新摘要區域 // 更新摘要區域
document.getElementById('summaryTitle').innerHTML = t('ai_summary'); const summaryTitle = document.getElementById('summaryTitle');
if (summaryTitle) summaryTitle.innerHTML = t('ai_summary');
// 更新分頁標籤 // 更新分頁標籤
document.getElementById('feedbackTabBtn').innerHTML = t('feedback_tab'); const feedbackTabBtn = document.getElementById('feedbackTabBtn');
document.getElementById('commandTabBtn').innerHTML = t('command_tab'); if (feedbackTabBtn) feedbackTabBtn.innerHTML = t('feedback_tab');
const commandTabBtn = document.getElementById('commandTabBtn');
if (commandTabBtn) commandTabBtn.innerHTML = t('command_tab');
// 更新回饋區域 // 更新回饋區域
document.getElementById('feedbackLabel').textContent = t('feedback_title'); const feedbackLabel = document.getElementById('feedbackLabel');
document.getElementById('feedbackDescription').textContent = t('feedback_description'); if (feedbackLabel) feedbackLabel.textContent = t('feedback_title');
const feedbackDescription = document.getElementById('feedbackDescription');
if (feedbackDescription) feedbackDescription.textContent = t('feedback_description');
// 更新命令區域 // 更新命令區域
document.getElementById('commandLabel').textContent = t('command_title'); const commandLabel = document.getElementById('commandLabel');
document.getElementById('commandDescription').textContent = t('command_description'); if (commandLabel) commandLabel.textContent = t('command_title');
document.getElementById('runBtn').innerHTML = t('btn_run_command');
const commandDescription = document.getElementById('commandDescription');
if (commandDescription) commandDescription.textContent = t('command_description');
const runBtn = document.getElementById('runBtn');
if (runBtn) runBtn.innerHTML = t('btn_run_command');
// 更新圖片區域 // 更新圖片區域
document.getElementById('imagesTitle').textContent = t('images_title'); const imagesTitle = document.getElementById('imagesTitle');
document.getElementById('selectFilesBtn').innerHTML = t('btn_select_files'); if (imagesTitle) imagesTitle.textContent = t('images_title');
document.getElementById('pasteBtn').innerHTML = t('btn_paste_clipboard');
document.getElementById('clearBtn').innerHTML = t('btn_clear_all'); const selectFilesBtn = document.getElementById('selectFilesBtn');
document.getElementById('dropZone').textContent = t('images_drag_hint'); if (selectFilesBtn) selectFilesBtn.innerHTML = t('btn_select_files');
const pasteBtn = document.getElementById('pasteBtn');
if (pasteBtn) pasteBtn.innerHTML = t('btn_paste_clipboard');
const clearBtn = document.getElementById('clearBtn');
if (clearBtn) clearBtn.innerHTML = t('btn_clear_all');
const dropZone = document.getElementById('dropZone');
if (dropZone) dropZone.textContent = t('images_drag_hint');
// 更新按鈕 // 更新按鈕
document.getElementById('cancelBtn').innerHTML = t('btn_cancel'); const cancelBtn = document.getElementById('cancelBtn');
document.getElementById('submitBtn').innerHTML = t('btn_submit_feedback'); if (cancelBtn) cancelBtn.innerHTML = t('btn_cancel');
// 更新圖片狀態 const submitBtn = document.getElementById('submitBtn');
if (submitBtn) submitBtn.innerHTML = t('btn_submit_feedback');
// 更新圖片狀態和預覽區域
updateImageStatus(); updateImageStatus();
// 更新預覽區域
updateImagePreviewArea(); updateImagePreviewArea();
} }
function updatePlaceholders() { function updatePlaceholders() {
// 更新輸入框的 placeholder // 更新輸入框的 placeholder
document.getElementById('feedbackText').placeholder = t('feedback_placeholder'); const feedbackText = document.getElementById('feedbackText');
document.getElementById('commandInput').placeholder = t('command_placeholder'); if (feedbackText) feedbackText.placeholder = t('feedback_placeholder');
const commandInput = document.getElementById('commandInput');
if (commandInput) commandInput.placeholder = t('command_placeholder');
} }
function setupKeyboardShortcuts() { function setupKeyboardShortcuts() {
@ -1106,66 +1277,121 @@
// 命令執行功能 // 命令執行功能
function runCommand() { function runCommand() {
const command = document.getElementById('commandInput').value.trim(); const command = elements.commandInput.value.trim();
if (!command) return; if (!command) return;
const outputElement = document.getElementById('commandOutput'); if (!isConnected) {
outputElement.textContent += `$ ${command}\n`; showStatusMessage('WebSocket 未連接', 'error');
return;
}
console.log('執行命令:', command);
// 這裡可以根據需要實現實際的命令執行 // 清空之前的輸出
// 暫時顯示一個模擬的輸出 elements.commandOutput.textContent = '';
setTimeout(() => { elements.commandOutput.style.display = 'block';
outputElement.textContent += t('command_running') + '\n';
setTimeout(() => {
outputElement.textContent += t('command_finished') + '\n\n';
outputElement.scrollTop = outputElement.scrollHeight;
}, 1000);
}, 100);
// 清空輸入框 // 更新 UI 狀態
document.getElementById('commandInput').value = ''; isCommandRunning = true;
elements.runCommandBtn.style.display = 'none';
elements.stopCommandBtn.style.display = 'inline-flex';
elements.commandInput.disabled = true;
// 發送命令執行請求
const success = sendWebSocketMessage({
type: 'run_command',
command: command
});
if (!success) {
handleCommandFinished(-1);
}
}
function stopCommand() {
console.log('停止命令執行');
sendWebSocketMessage({
type: 'stop_command'
});
handleCommandFinished(-1);
}
function appendCommandOutput(output) {
elements.commandOutput.textContent += output;
elements.commandOutput.scrollTop = elements.commandOutput.scrollHeight;
}
function handleCommandFinished(exitCode) {
console.log('命令執行完成,退出碼:', exitCode);
isCommandRunning = false;
elements.runCommandBtn.style.display = 'inline-flex';
elements.stopCommandBtn.style.display = 'none';
elements.commandInput.disabled = false;
const statusText = exitCode === 0 ? '命令執行成功' : '命令執行失敗';
const notificationType = exitCode === 0 ? 'success' : 'error';
showNotification(statusText, notificationType);
appendCommandOutput(`\n--- 命令執行完成 (退出碼: ${exitCode}) ---\n`);
} }
// 回饋提交功能 // 回饋提交功能
function submitFeedback() { function submitFeedback() {
const feedback = document.getElementById('feedbackText').value.trim(); const feedback = elements.feedbackTextarea.value.trim();
const commandLogs = document.getElementById('commandOutput').textContent;
if (!feedback && selectedImages.length === 0) { if (!feedback && selectedImages.length === 0) {
showStatusMessage(t('feedback_placeholder').split('\n')[0], 'error'); showStatusMessage(t('feedback_placeholder').split('\n')[0], 'error');
return; return;
} }
// 準備提交數據 if (!isConnected) {
const submitData = { showStatusMessage('WebSocket 未連接,無法提交', 'error');
interactive_feedback: feedback, return;
command_logs: commandLogs, }
console.log('提交回饋:', feedback);
// 顯示提交中狀態
elements.submitBtn.textContent = '提交中...';
elements.submitBtn.disabled = true;
const success = sendWebSocketMessage({
type: 'submit_feedback',
feedback: feedback,
images: selectedImages.map(img => ({ images: selectedImages.map(img => ({
filename: img.filename, name: img.filename,
data: img.data, data: img.data,
size: img.size, size: img.size
type: img.type
})) }))
}; });
// 顯示提交狀態 if (success) {
const submitBtn = document.getElementById('submitBtn'); showNotification('回饋已提交', 'success');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = t('uploading');
submitBtn.disabled = true;
// 模擬提交過程
setTimeout(() => {
showStatusMessage(t('upload_success'), 'success');
// 實際的提交邏輯應該在這裡實現
// 例如:發送到伺服器端點
console.log('提交數據:', submitData);
// 短暫延遲後關閉窗口
setTimeout(() => { setTimeout(() => {
if (ws) {
ws.close(1000, '回饋已提交');
}
window.close(); window.close();
}, 1000); }, 1500);
}, 500); } else {
// 恢復按鈕狀態
elements.submitBtn.textContent = '✅ 提交回饋';
elements.submitBtn.disabled = false;
}
}
function cancelFeedback() {
if (confirm('確定要取消回饋嗎?')) {
if (ws) {
ws.close(1000, '用戶取消');
}
window.close();
}
} }
// 狀態提示功能 // 狀態提示功能
@ -1193,6 +1419,75 @@
updateGridIndicator(); updateGridIndicator();
} }
}); });
// 通知系統
function showNotification(message, type = 'info') {
// 移除現有通知
const existingNotification = document.querySelector('.notification');
if (existingNotification) {
existingNotification.remove();
}
// 創建新通知
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
// 顯示動畫
setTimeout(() => notification.classList.add('show'), 100);
// 自動隱藏
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// 處理 WebSocket 消息
function handleWebSocketMessage(data) {
console.log('收到 WebSocket 消息:', data);
switch(data.type) {
case 'command_output':
appendCommandOutput(data.output);
break;
case 'command_finished':
handleCommandFinished(data.exit_code);
break;
case 'command_error':
appendCommandOutput(`錯誤: ${data.error}\n`);
handleCommandFinished(-1);
break;
case 'ping':
// 回應 ping
sendWebSocketMessage({ type: 'pong' });
break;
default:
console.log('未知的消息類型:', data.type);
}
}
function sendWebSocketMessage(message) {
if (ws && isConnected) {
try {
ws.send(JSON.stringify(message));
return true;
} catch (error) {
console.error('發送 WebSocket 消息失敗:', error);
return false;
}
} else {
console.warn('WebSocket 未連接,無法發送消息');
showNotification('連接中斷,無法發送消息', 'warning');
return false;
}
}
</script> </script>
</body> </body>
</html> </html>

View File

@ -12,21 +12,23 @@
增強功能: 圖片支援和現代化界面設計 增強功能: 圖片支援和現代化界面設計
""" """
import os
import sys
import json
import uuid
import asyncio import asyncio
import webbrowser import json
import threading import logging
import os
import socket
import subprocess import subprocess
import psutil import sys
import threading
import time import time
import webbrowser
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
import uuid
from datetime import datetime
import base64 import base64
import tempfile import tempfile
from typing import Dict, Optional, List from typing import Dict, Optional, List
from pathlib import Path
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, UploadFile, File, Form from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, UploadFile, File, Form
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
@ -230,14 +232,32 @@ class WebFeedbackSession:
class WebUIManager: class WebUIManager:
"""Web UI 管理器""" """Web UI 管理器"""
def __init__(self, host: str = "127.0.0.1", port: int = 8765): def __init__(self, host: str = "127.0.0.1", port: int = None):
self.host = host self.host = host
self.port = port self.port = port or self._find_free_port()
self.app = FastAPI(title="Interactive Feedback MCP Web UI") self.app = FastAPI(title="Interactive Feedback MCP Web UI")
self.sessions: Dict[str, WebFeedbackSession] = {} self.sessions: Dict[str, WebFeedbackSession] = {}
self.server_thread: Optional[threading.Thread] = None self.server_thread: Optional[threading.Thread] = None
self.setup_routes() self.setup_routes()
def _find_free_port(self, start_port: int = 8765, max_attempts: int = 100) -> int:
"""尋找可用的端口"""
for port in range(start_port, start_port + max_attempts):
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((self.host, port))
debug_log(f"找到可用端口: {port}")
return port
except OSError:
continue
# 如果沒有找到可用端口,使用系統分配
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((self.host, 0))
port = s.getsockname()[1]
debug_log(f"使用系統分配端口: {port}")
return port
def setup_routes(self): def setup_routes(self):
"""設置路由""" """設置路由"""
@ -346,20 +366,45 @@ class WebUIManager:
def start_server(self): def start_server(self):
"""啟動伺服器""" """啟動伺服器"""
def run_server(): max_retries = 10
uvicorn.run( retry_count = 0
self.app,
host=self.host, def run_server_with_retry():
port=self.port, nonlocal retry_count
log_level="error", while retry_count < max_retries:
access_log=False try:
) debug_log(f"嘗試在端口 {self.port} 啟動伺服器(第 {retry_count + 1} 次嘗試)")
uvicorn.run(
self.app,
host=self.host,
port=self.port,
log_level="error",
access_log=False
)
break # 成功啟動,跳出循環
except OSError as e:
if "10048" in str(e) or "Address already in use" in str(e):
retry_count += 1
debug_log(f"端口 {self.port} 被占用,尋找新端口(第 {retry_count} 次重試)")
if retry_count < max_retries:
# 尋找新的可用端口
self.port = self._find_free_port(self.port + 1)
debug_log(f"切換到新端口: {self.port}")
else:
debug_log(f"已達到最大重試次數 {max_retries},無法啟動伺服器")
raise Exception(f"無法找到可用端口,已嘗試 {max_retries}")
else:
debug_log(f"伺服器啟動失敗: {e}")
raise e
except Exception as e:
debug_log(f"伺服器啟動時發生未預期錯誤: {e}")
raise e
self.server_thread = threading.Thread(target=run_server, daemon=True) self.server_thread = threading.Thread(target=run_server_with_retry, daemon=True)
self.server_thread.start() self.server_thread.start()
# 等待伺服器啟動 # 等待伺服器啟動,並給足夠時間處理重試
time.sleep(2) time.sleep(3)
def open_browser(self, url: str): def open_browser(self, url: str):
"""開啟瀏覽器""" """開啟瀏覽器"""
@ -395,8 +440,13 @@ class WebUIManager:
<style> <style>
body {{ font-family: Arial, sans-serif; margin: 20px; background: #1e1e1e; color: white; }} body {{ font-family: Arial, sans-serif; margin: 20px; background: #1e1e1e; color: white; }}
.container {{ max-width: 800px; margin: 0 auto; }} .container {{ max-width: 800px; margin: 0 auto; }}
textarea {{ width: 100%; height: 200px; background: #2d2d30; color: white; border: 1px solid #464647; }} textarea {{ width: 100%; height: 200px; background: #2d2d30; color: white; border: 1px solid #464647; padding: 10px; }}
button {{ background: #007acc; color: white; padding: 10px 20px; border: none; cursor: pointer; }} button {{ background: #007acc; color: white; padding: 10px 20px; border: none; cursor: pointer; margin: 5px; }}
button:hover {{ background: #005a9e; }}
.notification {{ position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 6px; color: white; font-weight: bold; z-index: 10000; }}
.notification.error {{ background: #dc3545; }}
.notification.warning {{ background: #ffc107; }}
.notification.info {{ background: #007acc; }}
</style> </style>
</head> </head>
<body> <body>
@ -410,19 +460,113 @@ class WebUIManager:
<h3>您的回饋:</h3> <h3>您的回饋:</h3>
<textarea id="feedback" placeholder="請輸入您的回饋..."></textarea> <textarea id="feedback" placeholder="請輸入您的回饋..."></textarea>
</div> </div>
<button onclick="submitFeedback()">提交回饋</button> <button onclick="submitFeedback()" class="submit-btn">提交回饋</button>
<button onclick="cancelFeedback()">取消</button>
</div> </div>
<script> <script>
const ws = new WebSocket('ws://localhost:{self.port}/ws/{session_id}'); // ===== 全域變數 =====
function submitFeedback() {{ let ws = null;
const feedback = document.getElementById('feedback').value;
ws.send(JSON.stringify({{ // ===== WebSocket 連接 =====
type: 'submit_feedback', function connectWebSocket() {{
feedback: feedback, const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
images: [] const wsUrl = `${{protocol}}//${{window.location.host}}/ws/{session_id}`;
}}));
alert('回饋已提交!'); ws = new WebSocket(wsUrl);
ws.onopen = function() {{
console.log('WebSocket 連接成功');
}};
ws.onmessage = function(event) {{
const data = JSON.parse(event.data);
handleWebSocketMessage(data);
}};
ws.onclose = function() {{
console.log('WebSocket 連接已關閉');
}};
ws.onerror = function(error) {{
console.error('WebSocket 錯誤:', error);
}};
}} }}
function handleWebSocketMessage(data) {{
if (data.type === 'command_output') {{
// 處理命令輸出如果需要
console.log('命令輸出:', data.output);
}} else if (data.type === 'command_finished') {{
console.log('命令完成,返回碼:', data.exit_code);
}}
}}
// ===== 回饋提交 =====
function submitFeedback() {{
const feedback = document.getElementById('feedback').value.trim();
if (!feedback) {{
showNotification('請輸入回饋內容!', 'warning');
return;
}}
if (ws && ws.readyState === WebSocket.OPEN) {{
// 顯示提交中狀態
const submitBtn = document.querySelector('.submit-btn');
const originalText = submitBtn.textContent;
submitBtn.textContent = '提交中...';
submitBtn.disabled = true;
ws.send(JSON.stringify({{
type: 'submit_feedback',
feedback: feedback,
images: []
}}));
// 簡短延遲後自動關閉不顯示 alert
setTimeout(() => {{
window.close();
}}, 500);
}} else {{
showNotification('WebSocket 連接異常,請重新整理頁面', 'error');
}}
}}
// 添加通知函數替代 alert
function showNotification(message, type = 'info') {{
// 創建通知元素
const notification = document.createElement('div');
notification.className = `notification ${{type}}`;
notification.textContent = message;
document.body.appendChild(notification);
// 3 秒後自動移除
setTimeout(() => {{
if (notification.parentNode) {{
notification.parentNode.removeChild(notification);
}}
}}, 3000);
}}
function cancelFeedback() {{
if (confirm('確定要取消回饋嗎?')) {{
window.close();
}}
}}
// ===== 快捷鍵支援 =====
document.addEventListener('keydown', function(e) {{
if (e.ctrlKey && e.key === 'Enter') {{
e.preventDefault();
submitFeedback();
}}
}});
// ===== 初始化 =====
document.addEventListener('DOMContentLoaded', function() {{
connectWebSocket();
}});
</script> </script>
</body> </body>
</html> </html>
@ -430,15 +574,21 @@ class WebUIManager:
# ===== 全域管理器 ===== # ===== 全域管理器 =====
_web_ui_manager: Optional[WebUIManager] = None _web_ui_managers: Dict[int, WebUIManager] = {}
def get_web_ui_manager() -> WebUIManager: def get_web_ui_manager() -> WebUIManager:
"""獲取全域 Web UI 管理器""" """獲取 Web UI 管理器 - 每個進程獲得獨立的實例"""
global _web_ui_manager process_id = os.getpid()
if _web_ui_manager is None:
_web_ui_manager = WebUIManager() global _web_ui_managers
_web_ui_manager.start_server() if process_id not in _web_ui_managers:
return _web_ui_manager # 為每個進程創建獨立的管理器,使用不同的端口
manager = WebUIManager()
manager.start_server()
_web_ui_managers[process_id] = manager
debug_log(f"為進程 {process_id} 創建新的 Web UI 管理器,端口: {manager.port}")
return _web_ui_managers[process_id]
async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict: async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict:
"""啟動 Web 回饋 UI 並等待回饋""" """啟動 Web 回饋 UI 並等待回饋"""
@ -482,12 +632,14 @@ async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict:
def stop_web_ui(): def stop_web_ui():
"""停止 Web UI""" """停止 Web UI"""
global _web_ui_manager global _web_ui_managers
if _web_ui_manager: if _web_ui_managers:
# 清理所有會話 # 清理所有會話
for session_id in list(_web_ui_manager.sessions.keys()): for process_id, manager in list(_web_ui_managers.items()):
_web_ui_manager.remove_session(session_id) for session_id in list(manager.sessions.keys()):
_web_ui_manager = None manager.remove_session(session_id)
manager.sessions.clear()
_web_ui_managers.pop(process_id)
# ===== 主程式入口 ===== # ===== 主程式入口 =====