1493 lines
50 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;💡 小提示:按 Ctrl+Enter 可快速提交回饋"
></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">
🎯 拖拽圖片到這裡 (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 = {
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() {
// 初始化 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);
}
// 命令執行
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() {
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');
// 更新分頁標籤
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.preventDefault();
submitFeedback();
}
});
}
// 分頁切換功能
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() {
const command = elements.commandInput.value.trim();
if (!command) return;
if (!isConnected) {
showStatusMessage('WebSocket 未連接', 'error');
return;
}
console.log('執行命令:', command);
// 清空之前的輸出
elements.commandOutput.textContent = '';
elements.commandOutput.style.display = 'block';
// 更新 UI 狀態
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() {
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);
// 顯示提交中狀態
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 {
// 恢復按鈕狀態
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);
}
// 追加命令輸出
function appendCommandOutput(text) {
const outputElement = document.getElementById('commandOutput');
outputElement.textContent += text;
outputElement.scrollTop = outputElement.scrollHeight;
}
// 監聽窗口大小變化
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>