1592 lines
54 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-TW" id="html-root">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Feedback MCP</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
<style>
:root {
/* 深色主題顏色變數 */
--bg-primary: #1e1e1e;
--bg-secondary: #2d2d30;
--bg-tertiary: #252526;
--surface-color: #333333;
--text-primary: #cccccc;
--text-secondary: #9e9e9e;
--accent-color: #007acc;
--accent-hover: #005a9e;
--border-color: #464647;
--success-color: #4caf50;
--warning-color: #ff9800;
--error-color: #f44336;
--info-color: #2196f3;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
max-width: none;
width: 100%;
margin: 0 auto;
padding: 20px;
flex: 1;
}
.header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 15px 0;
margin-bottom: 20px;
border-radius: 8px 8px 0 0;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
max-width: none;
width: 100%;
margin: 0 auto;
padding: 0 20px;
}
.title {
font-size: 24px;
font-weight: bold;
color: var(--accent-color);
margin: 0;
}
.project-info {
color: var(--text-secondary);
font-size: 14px;
}
.language-selector {
display: flex;
align-items: center;
gap: 10px;
}
.language-selector select {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 5px 10px;
font-size: 14px;
}
.main-content {
width: 100%;
max-width: none;
}
@media (min-width: 768px) {
.main-content {
max-width: none;
}
}
.feedback-section,
.summary-section {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
width: 100%;
max-width: none;
}
.section-title {
font-size: 18px;
font-weight: bold;
color: var(--accent-color);
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.section-description {
color: var(--text-secondary);
font-size: 14px;
margin-bottom: 15px;
line-height: 1.5;
}
.tabs {
border-bottom: 2px solid var(--border-color);
margin-bottom: 20px;
}
.tab-buttons {
display: flex;
gap: 5px;
}
.tab-button {
background: transparent;
border: none;
color: var(--text-secondary);
padding: 12px 20px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.3s ease;
font-size: 14px;
font-weight: 500;
}
.tab-button.active {
color: var(--accent-color);
border-bottom-color: var(--accent-color);
}
.tab-button:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.input-group {
margin-bottom: 20px;
}
.input-label {
display: block;
font-weight: 500;
margin-bottom: 8px;
color: var(--text-primary);
}
.text-input,
.command-input {
width: 100%;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px;
color: var(--text-primary);
font-size: 14px;
line-height: 1.5;
resize: vertical;
min-height: 120px;
font-family: inherit;
}
.command-input {
min-height: auto;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
}
.text-input:focus,
.command-input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
}
.text-input::placeholder,
.command-input::placeholder {
color: var(--text-secondary);
}
.command-section {
display: flex;
gap: 10px;
align-items: flex-end;
}
.command-input-wrapper {
flex: 1;
}
.run-button {
background: var(--accent-color);
color: white;
border: none;
padding: 12px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.3s ease;
white-space: nowrap;
}
.run-button:hover {
background: var(--accent-hover);
}
.run-button:disabled {
background: var(--text-secondary);
cursor: not-allowed;
}
.command-output {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 15px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
color: var(--text-primary);
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
margin-top: 10px;
}
.image-section {
margin-top: 20px;
}
.upload-buttons {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.upload-btn {
background: var(--surface-color);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 8px 15px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 5px;
}
.upload-btn:hover {
background: var(--bg-tertiary);
}
.upload-btn.success {
background: var(--success-color);
border-color: var(--success-color);
}
.upload-btn.danger {
background: var(--error-color);
border-color: var(--error-color);
color: #ffffff;
}
.upload-btn.danger:hover {
background: #d32f2f;
border-color: #d32f2f;
color: #ffffff;
}
.drop-zone {
border: 2px dashed var(--border-color);
border-radius: 8px;
padding: 40px 20px;
text-align: center;
color: var(--text-secondary);
background: var(--bg-tertiary);
transition: all 0.3s ease;
margin-bottom: 10px;
}
.drop-zone.dragover {
border-color: var(--accent-color);
background: rgba(0, 122, 204, 0.1);
color: var(--accent-color);
}
.image-status {
color: var(--text-secondary);
font-size: 12px;
margin: 10px 0;
padding: 5px 0;
}
.image-preview-area {
flex: 1;
min-height: 140px;
max-height: 400px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 15px;
background: var(--bg-tertiary);
background-image:
linear-gradient(90deg, rgba(70, 70, 71, 0.1) 1px, transparent 1px),
linear-gradient(rgba(70, 70, 71, 0.1) 1px, transparent 1px);
background-size: 120px 120px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 12px;
align-content: flex-start;
position: relative;
}
.image-preview-area:empty::before {
content: attr(data-empty-text);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--text-secondary);
font-size: 14px;
text-align: center;
opacity: 0.6;
pointer-events: none;
white-space: pre-line;
}
.image-preview-area.has-images {
background-image: none;
}
.preview-grid-indicator {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0, 122, 204, 0.1);
color: var(--accent-color);
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
opacity: 0.7;
}
.image-preview {
position: relative;
width: 100%;
height: 100px;
border: 2px solid var(--border-color);
border-radius: 6px;
overflow: hidden;
background: var(--surface-color);
transition: transform 0.2s ease, border-color 0.2s ease;
}
.image-preview:hover {
transform: scale(1.05);
border-color: var(--accent-color);
}
.image-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-delete {
position: absolute;
top: 2px;
right: 2px;
background: var(--error-color);
color: #ffffff;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s ease;
}
.image-delete:hover {
background: #d32f2f;
color: #ffffff;
}
.summary-content {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 15px;
font-size: 14px;
line-height: 1.6;
max-height: 300px;
overflow-y: auto;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 15px;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
}
.btn {
padding: 12px 24px;
border-radius: 6px;
border: none;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: var(--success-color);
color: white;
}
.btn-primary:hover {
background: #45a049;
}
.btn-secondary {
background: var(--surface-color);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background: var(--bg-tertiary);
}
.status-message {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 6px;
color: white;
font-weight: 500;
z-index: 1000;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
}
.status-message.show {
opacity: 1;
transform: translateX(0);
}
.status-message.success {
background: var(--success-color);
}
.status-message.error {
background: var(--error-color);
}
.status-message.info {
background: var(--info-color);
}
/* 響應式設計 */
@media (max-width: 1200px) {
.container {
padding: 15px;
}
.header-content {
padding: 0 15px;
}
.image-preview-area {
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: 10px;
padding: 12px;
}
}
@media (max-width: 767px) {
.header-content {
flex-direction: column;
gap: 10px;
text-align: center;
}
.language-selector {
justify-content: center;
}
.container {
padding: 10px;
}
.tab-buttons {
flex-wrap: wrap;
}
.upload-buttons {
justify-content: center;
}
.actions {
flex-direction: column;
align-items: stretch;
}
.command-section {
flex-direction: column;
gap: 15px;
}
.image-preview-area {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 8px;
padding: 10px;
max-height: 300px;
}
.image-preview {
height: 80px;
}
}
@media (min-width: 1400px) {
.image-preview-area {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 15px;
padding: 20px;
}
.image-preview {
height: 120px;
}
}
/* 滾動條樣式 */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-tertiary);
}
::-webkit-scrollbar-thumb {
background: var(--surface-color);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--border-color);
}
/* 隱藏檔案輸入 */
#fileInput {
display: none;
}
</style>
</head>
<body>
<div class="header">
<div class="header-content">
<div>
<h1 class="title" id="pageTitle">Interactive Feedback MCP</h1>
<div class="project-info" id="projectInfo">
<span id="projectDirLabel">專案目錄</span>: {{ project_dir }}
</div>
</div>
<div class="language-selector">
<label for="languageSelect" id="languageLabel">🌐 語言選擇:</label>
<select id="languageSelect">
<option value="zh-TW">繁體中文</option>
<option value="en">English</option>
<option value="zh-CN">简体中文</option>
</select>
</div>
</div>
</div>
<div class="container">
<div class="main-content">
<!-- 主要回饋區域 -->
<div class="feedback-section">
<!-- AI 工作摘要 -->
<div class="summary-section" style="margin-bottom: 20px;">
<h2 class="section-title" id="summaryTitle">
📋 AI 工作摘要
</h2>
<div class="summary-content" id="summaryContent">{{ summary }}</div>
</div>
<!-- 分頁標籤 -->
<div class="tabs">
<div class="tab-buttons">
<button class="tab-button active" onclick="switchTab('feedback')" id="feedbackTabBtn">
💬 回饋
</button>
<button class="tab-button" onclick="switchTab('command')" id="commandTabBtn">
⚡ 命令
</button>
</div>
</div>
<!-- 回饋分頁內容 -->
<div id="feedback" class="tab-content active">
<div class="input-group">
<label class="input-label" id="feedbackLabel">💬 您的回饋</label>
<div class="section-description" id="feedbackDescription">
請在這裡輸入您的回饋、建議或問題。您的意見將幫助 AI 更好地理解您的需求。
</div>
<textarea id="feedbackText" class="text-input"
placeholder="請在這裡輸入您的回饋、建議或問題...&#10;&#10;💡 小提示:&#10;• 按 Ctrl+Enter 可快速提交回饋&#10;• 按 Ctrl+V 可直接貼上剪貼簿圖片"></textarea>
</div>
<!-- 圖片上傳區域 -->
<div class="image-section">
<h3 id="imagesTitle">🖼️ 圖片附件(可選)</h3>
<div class="upload-buttons">
<button class="upload-btn" onclick="selectFiles()" id="selectFilesBtn">📁 選擇文件</button>
<button class="upload-btn success" onclick="pasteFromClipboard()" id="pasteBtn">📋
剪貼板</button>
<button class="upload-btn danger" onclick="clearAllImages()" id="clearBtn">❌ 清除</button>
</div>
<div class="drop-zone" id="dropZone">
🎯 拖拽圖片到這裡 或 按 Ctrl+V 貼上剪貼簿圖片 (PNG、JPG、JPEG、GIF、BMP、WebP)
</div>
<div class="image-status" id="imageStatus">已選擇 0 張圖片</div>
<div class="image-preview-area" id="imagePreviewArea"></div>
</div>
</div>
<!-- 命令分頁內容 -->
<div id="command" class="tab-content">
<div class="input-group">
<label class="input-label" id="commandLabel">⚡ 命令執行</label>
<div class="section-description" id="commandDescription">
您可以在此執行系統命令來驗證結果或獲取更多資訊。
</div>
<div class="command-section">
<div class="command-input-wrapper">
<input type="text" id="commandInput" class="command-input" placeholder="輸入要執行的命令..."
onkeypress="if(event.key==='Enter') runCommand()">
</div>
<button class="run-button" onclick="runCommand()" id="runBtn">▶️ 執行</button>
</div>
<div class="command-output" id="commandOutput"></div>
</div>
</div>
<!-- 操作按鈕 -->
<div class="actions">
<button class="btn btn-secondary" onclick="window.close()" id="cancelBtn">❌ 取消</button>
<button class="btn btn-primary" onclick="submitFeedback()" id="submitBtn">✅ 提交回饋</button>
</div>
</div>
</div>
</div>
<!-- 隱藏的檔案輸入 -->
<input type="file" id="fileInput" multiple accept="image/*">
<!-- 狀態提示元素 -->
<div id="statusMessage" class="status-message"></div>
<!-- 引入國際化模組 -->
<script src="/static/i18n.js"></script>
<script>
// 全域變數
let selectedImages = [];
let currentLanguage = 'zh-TW';
let ws = null; // WebSocket 連接
let isConnected = false;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
const reconnectDelay = 2000; // 2 秒
let isCommandRunning = false;
// DOM 元素
const elements = {
feedbackTextarea: document.getElementById('feedbackText'),
commandInput: document.getElementById('commandInput'),
commandOutput: document.getElementById('commandOutput'),
runCommandBtn: document.getElementById('runBtn'),
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);
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;
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;
// 自動重連(除非是正常關閉)
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('重連次數已達上限');
showNotification('無法重新連接,請重新整理頁面', 'error');
return;
}
reconnectAttempts++;
console.log(`嘗試重連 (${reconnectAttempts}/${maxReconnectAttempts})`);
initWebSocket();
}
function handleConnectionError() {
isConnected = false;
if (reconnectAttempts < maxReconnectAttempts) {
setTimeout(attemptReconnect, reconnectDelay);
}
}
// 頁面初始化
document.addEventListener('DOMContentLoaded', async function () {
// 先初始化 i18n 載入翻譯
try {
await window.initI18n();
console.log('[I18N] 翻譯載入完成');
} catch (error) {
console.warn('[I18N] 翻譯載入失敗,使用內嵌翻譯:', error);
}
// 初始化 WebSocket 連接
initWebSocket();
// 初始化界面
initializeApp();
setupEventListeners();
changeLanguage(currentLanguage);
setupDragAndDrop();
setupKeyboardShortcuts();
switchTab('feedback');
updateImagePreviewArea();
});
// 語言切換功能
function changeLanguage(lang) {
currentLanguage = lang;
window.i18n.setLanguage(lang);
applyTranslations();
updatePlaceholders();
updateHtmlLang();
}
function initializeApp() {
// 設置語言選擇器
const languageSelect = document.getElementById('languageSelect');
if (languageSelect) {
languageSelect.value = currentLanguage;
}
// 更新 HTML lang 屬性
updateHtmlLang();
}
function setupEventListeners() {
// 語言選擇器事件
const languageSelect = document.getElementById('languageSelect');
if (languageSelect) {
languageSelect.addEventListener('change', function (e) {
changeLanguage(e.target.value);
});
}
// 文件輸入事件
const fileInput = document.getElementById('fileInput');
if (fileInput) {
fileInput.addEventListener('change', handleFileSelect);
}
// 命令執行
if (elements.runCommandBtn) {
elements.runCommandBtn.addEventListener('click', runCommand);
}
// 命令輸入框 Enter 鍵
if (elements.commandInput) {
elements.commandInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !isCommandRunning) {
runCommand();
}
});
}
// 回饋提交
if (elements.submitBtn) {
elements.submitBtn.addEventListener('click', submitFeedback);
}
if (elements.cancelBtn) {
elements.cancelBtn.addEventListener('click', cancelFeedback);
}
// 快捷鍵支援
document.addEventListener('keydown', (e) => {
// 支援主鍵盤和數字鍵盤的 Ctrl+Enter
if (e.ctrlKey && (e.key === 'Enter' || e.code === 'NumpadEnter')) {
e.preventDefault();
submitFeedback();
}
});
// 窗口關閉時清理 WebSocket
window.addEventListener('beforeunload', () => {
if (ws && isConnected) {
ws.close(1000, '頁面關閉');
}
});
}
function updateHtmlLang() {
const htmlRoot = document.getElementById('html-root');
if (htmlRoot) {
// 語言代碼映射
const langMap = {
'zh-TW': 'zh-TW',
'zh-CN': 'zh-CN',
'en': 'en'
};
htmlRoot.setAttribute('lang', langMap[currentLanguage] || 'en');
}
}
function applyTranslations() {
// 更新頁面標題
document.title = t('app_title');
// 更新標題區域
const pageTitle = document.getElementById('pageTitle');
if (pageTitle) pageTitle.textContent = t('app_title');
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');
if (languageSelect) {
const options = languageSelect.querySelectorAll('option');
options.forEach(option => {
const value = option.value;
option.textContent = window.i18n.getLanguageDisplayName(value);
});
}
// 更新摘要區域
const summaryTitle = document.getElementById('summaryTitle');
if (summaryTitle) summaryTitle.innerHTML = t('ai_summary');
// 動態更新 AI 工作摘要內容(如果是測試內容)
const summaryContent = document.getElementById('summaryContent');
if (summaryContent) {
const currentSummary = summaryContent.textContent || summaryContent.innerHTML;
// 更嚴格的測試摘要檢測邏輯 - 必須同時包含多個特徵
const isTestSummary = (
// Qt GUI 測試特徵組合
(currentSummary.includes('測試 Qt GUI 功能') && currentSummary.includes('🎯 **功能測試項目')) ||
(currentSummary.includes('Test Qt GUI Functionality') && currentSummary.includes('🎯 **Test Items')) ||
(currentSummary.includes('测试 Qt GUI 功能') && currentSummary.includes('🎯 **功能测试项目')) ||
// Web UI 測試特徵組合
(currentSummary.includes('測試 Web UI 功能') && currentSummary.includes('🎯 **功能測試項目')) ||
(currentSummary.includes('Test Web UI Functionality') && currentSummary.includes('🎯 **Test Items')) ||
(currentSummary.includes('测试 Web UI 功能') && currentSummary.includes('🎯 **功能测试项目')) ||
// 具體測試項目特徵組合
(currentSummary.includes('圖片上傳和預覽') && currentSummary.includes('智能 Ctrl+V 圖片貼上')) ||
(currentSummary.includes('Image upload and preview') && currentSummary.includes('Smart Ctrl+V image paste')) ||
(currentSummary.includes('图片上传和预览') && currentSummary.includes('智能 Ctrl+V 图片粘贴')) ||
// WebSocket 和服務器特徵組合
(currentSummary.includes('WebSocket 即時通訊') && currentSummary.includes('Web UI 服務器啟動')) ||
(currentSummary.includes('WebSocket real-time communication') && currentSummary.includes('Web UI server startup')) ||
(currentSummary.includes('WebSocket 即时通讯') && currentSummary.includes('Web UI 服务器启动'))
);
if (isTestSummary) {
// 使用對應語言的測試摘要
const testSummary = t('test.webUiSummary');
if (testSummary && testSummary !== 'test.webUiSummary') {
summaryContent.textContent = testSummary;
}
}
}
// 更新分頁標籤
const feedbackTabBtn = document.getElementById('feedbackTabBtn');
if (feedbackTabBtn) feedbackTabBtn.innerHTML = t('feedback_tab');
const commandTabBtn = document.getElementById('commandTabBtn');
if (commandTabBtn) commandTabBtn.innerHTML = t('command_tab');
// 更新回饋區域
const feedbackLabel = document.getElementById('feedbackLabel');
if (feedbackLabel) feedbackLabel.textContent = t('feedback_title');
const feedbackDescription = document.getElementById('feedbackDescription');
if (feedbackDescription) feedbackDescription.textContent = t('feedback_description');
// 更新命令區域
const commandLabel = document.getElementById('commandLabel');
if (commandLabel) commandLabel.textContent = t('command_title');
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');
// 更新圖片區域
const imagesTitle = document.getElementById('imagesTitle');
if (imagesTitle) imagesTitle.textContent = t('images_title');
const selectFilesBtn = document.getElementById('selectFilesBtn');
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');
// 更新按鈕
const cancelBtn = document.getElementById('cancelBtn');
if (cancelBtn) cancelBtn.innerHTML = t('btn_cancel');
const submitBtn = document.getElementById('submitBtn');
if (submitBtn) submitBtn.innerHTML = t('btn_submit_feedback');
// 更新圖片狀態和預覽區域
updateImageStatus();
updateImagePreviewArea();
}
function updatePlaceholders() {
// 更新輸入框的 placeholder
const feedbackText = document.getElementById('feedbackText');
if (feedbackText) feedbackText.placeholder = t('feedback_placeholder');
const commandInput = document.getElementById('commandInput');
if (commandInput) commandInput.placeholder = t('command_placeholder');
}
function setupKeyboardShortcuts() {
// Ctrl+Enter 快速提交 (支援主鍵盤和數字鍵盤)
document.addEventListener('keydown', function (e) {
if (e.ctrlKey && (e.key === 'Enter' || e.code === 'NumpadEnter')) {
e.preventDefault();
submitFeedback();
}
// Ctrl+V 智能貼上
if (e.ctrlKey && e.key === 'v') {
// 先檢查剪貼簿是否包含圖片
checkClipboardForImage().then(hasImage => {
if (hasImage) {
// 如果有圖片,無論焦點在哪裡都優先貼到圖片區域
e.preventDefault();
pasteFromClipboard();
// 提供額外的使用者提示
const activeElement = document.activeElement;
const isInTextArea = activeElement && activeElement.id === 'feedbackText';
if (isInTextArea) {
showStatusMessage(t('paste_image_from_textarea'), 'success');
}
} else {
// 如果沒有圖片,檢查是否在文字輸入區域
const activeElement = document.activeElement;
const isTextInput = activeElement && (
activeElement.tagName === 'TEXTAREA' ||
activeElement.tagName === 'INPUT' ||
activeElement.contentEditable === 'true'
);
if (isTextInput) {
// 在文字輸入區域且剪貼簿只有文字,允許正常的文字貼上
// 不需要 preventDefault(),讓瀏覽器執行預設行為
} else {
// 不在文字輸入區域且沒有圖片時,提示用戶
e.preventDefault();
showStatusMessage(t('paste_no_image'), 'info');
}
}
}).catch(err => {
// 如果檢查剪貼簿失敗,允許正常的文字貼上行為
console.warn('檢查剪貼簿失敗:', err);
});
}
});
}
// 檢查剪貼簿是否包含圖片
async function checkClipboardForImage() {
try {
const items = await navigator.clipboard.read();
for (const item of items) {
for (const type of item.types) {
if (type.startsWith('image/')) {
return true;
}
}
}
return false;
} catch (err) {
// 如果無法讀取剪貼簿,返回 false
return false;
}
}
// 分頁切換功能
function switchTab(tabName) {
// 隱藏所有分頁內容
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
// 移除所有按鈕的活動狀態
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
// 顯示選中的分頁
document.getElementById(tabName).classList.add('active');
// 設置對應按鈕為活動狀態
if (tabName === 'feedback') {
document.getElementById('feedbackTabBtn').classList.add('active');
} else if (tabName === 'command') {
document.getElementById('commandTabBtn').classList.add('active');
}
}
// 設置拖拽功能
function setupDragAndDrop() {
const dropZone = document.getElementById('dropZone');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, unhighlight, false);
});
dropZone.addEventListener('drop', handleDrop, false);
}
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
function highlight(e) {
document.getElementById('dropZone').classList.add('dragover');
}
function unhighlight(e) {
document.getElementById('dropZone').classList.remove('dragover');
}
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
}
// 檔案選擇功能
function selectFiles() {
document.getElementById('fileInput').click();
}
function handleFileSelect(event) {
const files = event.target.files;
handleFiles(files);
}
function handleFiles(files) {
[...files].forEach(processFile);
}
function processFile(file) {
if (!file.type.startsWith('image/')) {
showStatusMessage(t('invalid_file_type'), 'error');
return;
}
if (file.size > 1024 * 1024) { // 1MB 限制
showStatusMessage(t('file_too_large'), 'error');
return;
}
const reader = new FileReader();
reader.onload = function (e) {
const imageData = {
id: Date.now() + Math.random(),
filename: file.name,
data: e.target.result.split(',')[1], // 移除 data:image/xxx;base64, 前綴
size: file.size,
type: file.type
};
selectedImages.push(imageData);
updateImagePreview();
updateImageStatus();
showStatusMessage(t('upload_success'), 'success');
};
reader.readAsDataURL(file);
}
// 剪貼板功能
async function pasteFromClipboard() {
try {
const items = await navigator.clipboard.read();
let hasImage = false;
for (const item of items) {
for (const type of item.types) {
if (type.startsWith('image/')) {
const blob = await item.getType(type);
const file = new File([blob], `paste-${Date.now()}.png`, { type: type });
processFile(file);
hasImage = true;
}
}
}
if (!hasImage) {
showStatusMessage(t('paste_failed'), 'error');
}
} catch (err) {
showStatusMessage(t('paste_failed'), 'error');
}
}
// 圖片預覽更新
function updateImagePreview() {
const previewArea = document.getElementById('imagePreviewArea');
// 清除現有內容
previewArea.innerHTML = '';
if (selectedImages.length === 0) {
// 空狀態 - 移除 has-images 類別
previewArea.classList.remove('has-images');
updateImagePreviewArea();
} else {
// 有圖片 - 添加 has-images 類別
previewArea.classList.add('has-images');
selectedImages.forEach(img => {
const preview = document.createElement('div');
preview.className = 'image-preview';
const imgElement = document.createElement('img');
imgElement.src = `data:${img.type};base64,${img.data}`;
imgElement.alt = img.filename;
const deleteBtn = document.createElement('button');
deleteBtn.className = 'image-delete';
deleteBtn.innerHTML = '×';
deleteBtn.title = t('images_clear');
deleteBtn.onclick = () => {
if (confirm(t('images_delete_confirm', { filename: img.filename }))) {
removeImage(img.id);
}
};
preview.appendChild(imgElement);
preview.appendChild(deleteBtn);
previewArea.appendChild(preview);
});
// 添加網格指示器
updateGridIndicator();
}
}
function updateImagePreviewArea() {
const previewArea = document.getElementById('imagePreviewArea');
if (selectedImages.length === 0) {
// 計算當前可顯示的列數
const containerWidth = previewArea.clientWidth - 30; // 減去 padding
const minItemWidth = 100; // 最小項目寬度
const gap = 12; // 間距
const maxColumns = Math.floor((containerWidth + gap) / (minItemWidth + gap));
const actualColumns = Math.max(1, maxColumns);
const emptyText = `📋 圖片預覽區域\n\n💡 目前寬度可顯示 ${actualColumns} 列圖片\n區域寬度: ${Math.round(containerWidth)}px`;
previewArea.setAttribute('data-empty-text', emptyText);
// 移除網格指示器
const existingIndicator = previewArea.querySelector('.preview-grid-indicator');
if (existingIndicator) {
existingIndicator.remove();
}
}
}
function updateGridIndicator() {
const previewArea = document.getElementById('imagePreviewArea');
// 移除現有指示器
const existingIndicator = previewArea.querySelector('.preview-grid-indicator');
if (existingIndicator) {
existingIndicator.remove();
}
// 計算當前列數
const containerWidth = previewArea.clientWidth - 30;
const minItemWidth = 100;
const gap = 12;
const maxColumns = Math.floor((containerWidth + gap) / (minItemWidth + gap));
const actualColumns = Math.max(1, maxColumns);
// 添加新指示器
const indicator = document.createElement('div');
indicator.className = 'preview-grid-indicator';
indicator.textContent = `${actualColumns} 列佈局`;
previewArea.appendChild(indicator);
}
function removeImage(imageId) {
selectedImages = selectedImages.filter(img => img.id !== imageId);
updateImagePreview();
updateImageStatus();
}
function clearAllImages() {
if (selectedImages.length > 0) {
const confirmMessage = t('images_delete_confirm', {
filename: `${selectedImages.length} ${t('images_status', { count: selectedImages.length }).match(/\d+\s+(\S+)/)[1]}`
});
if (confirm(confirmMessage)) {
selectedImages = [];
updateImagePreview();
updateImageStatus();
}
}
}
function updateImageStatus() {
const count = selectedImages.length;
const statusElement = document.getElementById('imageStatus');
if (count === 0) {
statusElement.textContent = t('images_status', { count: 0 });
} else {
const totalSize = selectedImages.reduce((sum, img) => sum + img.size, 0);
let sizeStr;
if (totalSize >= 1024 * 1024) {
sizeStr = `${(totalSize / (1024 * 1024)).toFixed(1)} MB`;
} else {
sizeStr = `${(totalSize / 1024).toFixed(1)} KB`;
}
statusElement.textContent = t('images_status_with_size', { count, size: sizeStr });
}
}
// 命令執行功能
function runCommand() {
if (!elements.commandInput) return;
const command = elements.commandInput.value.trim();
if (!command) return;
if (!isConnected) {
showStatusMessage('WebSocket 未連接', 'error');
return;
}
console.log('執行命令:', command);
// 清空之前的輸出
if (elements.commandOutput) {
elements.commandOutput.textContent = '';
elements.commandOutput.style.display = 'block';
}
// 更新 UI 狀態
isCommandRunning = true;
if (elements.runCommandBtn) {
elements.runCommandBtn.style.display = 'none';
elements.runCommandBtn.disabled = true;
}
if (elements.commandInput) {
elements.commandInput.disabled = true;
}
// 發送命令執行請求
const success = sendWebSocketMessage({
type: 'run_command',
command: command
});
if (!success) {
handleCommandFinished(-1);
}
}
function appendCommandOutput(text) {
if (elements.commandOutput) {
elements.commandOutput.textContent += text;
elements.commandOutput.scrollTop = elements.commandOutput.scrollHeight;
}
}
function handleCommandFinished(exitCode) {
console.log('命令執行完成,退出碼:', exitCode);
isCommandRunning = false;
if (elements.runCommandBtn) {
elements.runCommandBtn.style.display = 'inline-flex';
elements.runCommandBtn.disabled = false;
}
if (elements.commandInput) {
elements.commandInput.disabled = false;
}
const statusText = exitCode === 0 ? '命令執行成功' : '命令執行失敗';
const notificationType = exitCode === 0 ? 'success' : 'error';
showNotification(statusText, notificationType);
appendCommandOutput(`\n--- 命令執行完成 (退出碼: ${exitCode}) ---\n`);
}
// 回饋提交功能
function submitFeedback() {
if (!elements.feedbackTextarea) return;
const feedback = elements.feedbackTextarea.value.trim();
if (!feedback && selectedImages.length === 0) {
showStatusMessage(t('feedback_placeholder').split('\n')[0], 'error');
return;
}
if (!isConnected) {
showStatusMessage('WebSocket 未連接,無法提交', 'error');
return;
}
console.log('提交回饋:', feedback);
// 顯示提交中狀態
if (elements.submitBtn) {
elements.submitBtn.textContent = '提交中...';
elements.submitBtn.disabled = true;
}
const success = sendWebSocketMessage({
type: 'submit_feedback',
feedback: feedback,
images: selectedImages.map(img => ({
name: img.filename,
data: img.data,
size: img.size
}))
});
if (success) {
showNotification('回饋已提交', 'success');
// 短暫延遲後關閉窗口
setTimeout(() => {
if (ws) {
ws.close(1000, '回饋已提交');
}
window.close();
}, 1500);
} else {
// 恢復按鈕狀態
if (elements.submitBtn) {
elements.submitBtn.textContent = '✅ 提交回饋';
elements.submitBtn.disabled = false;
}
}
}
function cancelFeedback() {
if (confirm('確定要取消回饋嗎?')) {
if (ws) {
ws.close(1000, '用戶取消');
}
window.close();
}
}
// 狀態提示功能
function showStatusMessage(message, type = 'info') {
const statusElement = document.getElementById('statusMessage');
statusElement.textContent = message;
statusElement.className = `status-message ${type} show`;
setTimeout(() => {
statusElement.classList.remove('show');
}, 3000);
}
// 監聽窗口大小變化
window.addEventListener('resize', function () {
updateImagePreviewArea();
if (selectedImages.length > 0) {
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>
</body>
</html>