mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 10:42:25 +08:00
1592 lines
54 KiB
HTML
1592 lines
54 KiB
HTML
<!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="請在這裡輸入您的回饋、建議或問題... 💡 小提示: • 按 Ctrl+Enter 可快速提交回饋 • 按 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> |