@hirohsu/user-web-feedback 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +953 -0
- package/dist/cli.cjs +95778 -0
- package/dist/index.cjs +92818 -0
- package/dist/static/app.js +385 -0
- package/dist/static/components/navbar.css +406 -0
- package/dist/static/components/navbar.html +49 -0
- package/dist/static/components/navbar.js +211 -0
- package/dist/static/dashboard.css +495 -0
- package/dist/static/dashboard.html +95 -0
- package/dist/static/dashboard.js +540 -0
- package/dist/static/favicon.svg +27 -0
- package/dist/static/index.html +541 -0
- package/dist/static/logs.html +376 -0
- package/dist/static/logs.js +442 -0
- package/dist/static/mcp-settings.html +797 -0
- package/dist/static/mcp-settings.js +884 -0
- package/dist/static/modules/app-core.js +124 -0
- package/dist/static/modules/conversation-panel.js +247 -0
- package/dist/static/modules/feedback-handler.js +1420 -0
- package/dist/static/modules/image-handler.js +155 -0
- package/dist/static/modules/log-viewer.js +296 -0
- package/dist/static/modules/mcp-manager.js +474 -0
- package/dist/static/modules/prompt-manager.js +364 -0
- package/dist/static/modules/settings-manager.js +299 -0
- package/dist/static/modules/socket-manager.js +170 -0
- package/dist/static/modules/state-manager.js +352 -0
- package/dist/static/modules/timer-controller.js +243 -0
- package/dist/static/modules/ui-helpers.js +246 -0
- package/dist/static/settings.html +355 -0
- package/dist/static/settings.js +425 -0
- package/dist/static/socket.io.min.js +7 -0
- package/dist/static/style.css +2157 -0
- package/dist/static/terminals.html +357 -0
- package/dist/static/terminals.js +321 -0
- package/package.json +91 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* image-handler.js
|
|
3
|
+
* 圖片處理模組
|
|
4
|
+
* 包含圖片上傳、預覽、移除等功能
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getCurrentImages,
|
|
9
|
+
addImage,
|
|
10
|
+
removeImageAt,
|
|
11
|
+
clearImages as clearImageState,
|
|
12
|
+
} from "./state-manager.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 處理檔案選擇事件
|
|
16
|
+
* @param {Event} e - 檔案選擇事件
|
|
17
|
+
*/
|
|
18
|
+
export function handleFileSelect(e) {
|
|
19
|
+
handleFileDrop(e.target.files);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 處理檔案拖放
|
|
24
|
+
* @param {FileList} files - 檔案列表
|
|
25
|
+
*/
|
|
26
|
+
export function handleFileDrop(files) {
|
|
27
|
+
Array.from(files).forEach((file) => {
|
|
28
|
+
if (file.type.startsWith("image/")) {
|
|
29
|
+
readImageFile(file);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 處理貼上事件
|
|
36
|
+
* @param {ClipboardEvent} e - 貼上事件
|
|
37
|
+
*/
|
|
38
|
+
export function handlePaste(e) {
|
|
39
|
+
const items = e.clipboardData.items;
|
|
40
|
+
|
|
41
|
+
for (let item of items) {
|
|
42
|
+
if (item.type.startsWith("image/")) {
|
|
43
|
+
const file = item.getAsFile();
|
|
44
|
+
readImageFile(file);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 讀取圖片檔案
|
|
51
|
+
* @param {File} file - 圖片檔案
|
|
52
|
+
*/
|
|
53
|
+
export function readImageFile(file) {
|
|
54
|
+
const reader = new FileReader();
|
|
55
|
+
|
|
56
|
+
reader.onload = (e) => {
|
|
57
|
+
const imageData = {
|
|
58
|
+
name: file.name,
|
|
59
|
+
data: e.target.result.split(",")[1],
|
|
60
|
+
size: file.size,
|
|
61
|
+
type: file.type,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
addImage(imageData);
|
|
65
|
+
const currentImages = getCurrentImages();
|
|
66
|
+
addImagePreview(e.target.result, currentImages.length - 1);
|
|
67
|
+
updateImageCount();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
reader.readAsDataURL(file);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 新增圖片預覽
|
|
75
|
+
* @param {string} dataUrl - 圖片 Data URL
|
|
76
|
+
* @param {number} index - 圖片索引
|
|
77
|
+
*/
|
|
78
|
+
export function addImagePreview(dataUrl, index) {
|
|
79
|
+
const container = document.getElementById("imagePreviewContainer");
|
|
80
|
+
const dropZone = document.getElementById("imageDropZone");
|
|
81
|
+
const currentImages = getCurrentImages();
|
|
82
|
+
|
|
83
|
+
if (currentImages.length > 0) {
|
|
84
|
+
dropZone.style.display = "none";
|
|
85
|
+
container.style.display = "flex";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const preview = document.createElement("div");
|
|
89
|
+
preview.className = "image-preview";
|
|
90
|
+
preview.innerHTML = `
|
|
91
|
+
<img src="${dataUrl}" alt="Preview">
|
|
92
|
+
<button class="image-preview-remove" onclick="removeImage(${index})">✖</button>
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
container.appendChild(preview);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 移除圖片
|
|
100
|
+
* @param {number} index - 圖片索引
|
|
101
|
+
*/
|
|
102
|
+
export function removeImage(index) {
|
|
103
|
+
removeImageAt(index);
|
|
104
|
+
|
|
105
|
+
const container = document.getElementById("imagePreviewContainer");
|
|
106
|
+
container.innerHTML = "";
|
|
107
|
+
|
|
108
|
+
const currentImages = getCurrentImages();
|
|
109
|
+
currentImages.forEach((img, i) => {
|
|
110
|
+
const dataUrl = `data:${img.type};base64,${img.data}`;
|
|
111
|
+
addImagePreview(dataUrl, i);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
updateImageCount();
|
|
115
|
+
|
|
116
|
+
if (currentImages.length === 0) {
|
|
117
|
+
document.getElementById("imageDropZone").style.display = "flex";
|
|
118
|
+
container.style.display = "none";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 清除所有圖片
|
|
124
|
+
*/
|
|
125
|
+
export function clearImages() {
|
|
126
|
+
clearImageState();
|
|
127
|
+
document.getElementById("imagePreviewContainer").innerHTML = "";
|
|
128
|
+
document.getElementById("imageDropZone").style.display = "flex";
|
|
129
|
+
document.getElementById("imagePreviewContainer").style.display = "none";
|
|
130
|
+
updateImageCount();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 更新圖片數量顯示
|
|
135
|
+
*/
|
|
136
|
+
export function updateImageCount() {
|
|
137
|
+
const countEl = document.getElementById("imageCount");
|
|
138
|
+
if (countEl) {
|
|
139
|
+
countEl.textContent = getCurrentImages().length;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 暴露到 window 供 HTML onclick 使用
|
|
144
|
+
window.removeImage = removeImage;
|
|
145
|
+
|
|
146
|
+
export default {
|
|
147
|
+
handleFileSelect,
|
|
148
|
+
handleFileDrop,
|
|
149
|
+
handlePaste,
|
|
150
|
+
readImageFile,
|
|
151
|
+
addImagePreview,
|
|
152
|
+
removeImage,
|
|
153
|
+
clearImages,
|
|
154
|
+
updateImageCount,
|
|
155
|
+
};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* log-viewer.js
|
|
3
|
+
* 日誌檢視器模組
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
showToast,
|
|
8
|
+
showLoadingOverlay,
|
|
9
|
+
hideLoadingOverlay,
|
|
10
|
+
escapeHtml,
|
|
11
|
+
} from "./ui-helpers.js";
|
|
12
|
+
|
|
13
|
+
// 模組內部狀態
|
|
14
|
+
let currentLogPage = 1;
|
|
15
|
+
let totalLogPages = 1;
|
|
16
|
+
let logSources = [];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 開啟日誌檢視器彈窗
|
|
20
|
+
*/
|
|
21
|
+
export async function openLogViewerModal() {
|
|
22
|
+
const modal = document.getElementById("logViewerModal");
|
|
23
|
+
if (modal) {
|
|
24
|
+
modal.classList.add("show");
|
|
25
|
+
|
|
26
|
+
// 載入日誌來源列表
|
|
27
|
+
await loadLogSources();
|
|
28
|
+
|
|
29
|
+
// 載入第一頁日誌
|
|
30
|
+
await loadLogs(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 關閉日誌檢視器彈窗
|
|
36
|
+
*/
|
|
37
|
+
export function closeLogViewerModal() {
|
|
38
|
+
const modal = document.getElementById("logViewerModal");
|
|
39
|
+
if (modal) {
|
|
40
|
+
modal.classList.remove("show");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 載入日誌來源
|
|
46
|
+
*/
|
|
47
|
+
async function loadLogSources() {
|
|
48
|
+
try {
|
|
49
|
+
const response = await fetch("/api/logs/sources");
|
|
50
|
+
if (response.ok) {
|
|
51
|
+
const data = await response.json();
|
|
52
|
+
logSources = data.sources || [];
|
|
53
|
+
|
|
54
|
+
// 更新來源下拉選單
|
|
55
|
+
const sourceFilter = document.getElementById("logSourceFilter");
|
|
56
|
+
if (sourceFilter) {
|
|
57
|
+
// 保留第一個選項
|
|
58
|
+
sourceFilter.innerHTML = '<option value="">全部來源</option>';
|
|
59
|
+
logSources.forEach((source) => {
|
|
60
|
+
const option = document.createElement("option");
|
|
61
|
+
option.value = source;
|
|
62
|
+
option.textContent = source;
|
|
63
|
+
sourceFilter.appendChild(option);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error("載入日誌來源失敗:", error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 載入日誌
|
|
74
|
+
* @param {number} page 頁碼
|
|
75
|
+
*/
|
|
76
|
+
export async function loadLogs(page = 1) {
|
|
77
|
+
const container = document.getElementById("logEntriesContainer");
|
|
78
|
+
if (!container) return;
|
|
79
|
+
|
|
80
|
+
// 顯示載入中
|
|
81
|
+
container.innerHTML =
|
|
82
|
+
'<div class="log-loading"><div class="spinner"></div>載入中...</div>';
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// 收集篩選參數
|
|
86
|
+
const params = new URLSearchParams();
|
|
87
|
+
params.set("page", page.toString());
|
|
88
|
+
params.set("limit", "50");
|
|
89
|
+
|
|
90
|
+
const level = document.getElementById("logLevelFilter").value;
|
|
91
|
+
if (level) params.set("level", level);
|
|
92
|
+
|
|
93
|
+
const source = document.getElementById("logSourceFilter").value;
|
|
94
|
+
if (source) params.set("source", source);
|
|
95
|
+
|
|
96
|
+
const search = document.getElementById("logSearch").value.trim();
|
|
97
|
+
if (search) params.set("search", search);
|
|
98
|
+
|
|
99
|
+
const startDate = document.getElementById("logStartDate").value;
|
|
100
|
+
if (startDate) params.set("startDate", startDate);
|
|
101
|
+
|
|
102
|
+
const endDate = document.getElementById("logEndDate").value;
|
|
103
|
+
if (endDate) params.set("endDate", endDate);
|
|
104
|
+
|
|
105
|
+
const response = await fetch(`/api/logs?${params.toString()}`);
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
throw new Error(`HTTP ${response.status}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const data = await response.json();
|
|
111
|
+
const logs = data.logs || [];
|
|
112
|
+
currentLogPage = data.pagination?.page || 1;
|
|
113
|
+
totalLogPages = data.pagination?.totalPages || 1;
|
|
114
|
+
|
|
115
|
+
// 渲染日誌條目
|
|
116
|
+
if (logs.length === 0) {
|
|
117
|
+
container.innerHTML = `
|
|
118
|
+
<div class="placeholder">
|
|
119
|
+
<span class="icon">📭</span>
|
|
120
|
+
<p>沒有符合條件的日誌記錄</p>
|
|
121
|
+
</div>
|
|
122
|
+
`;
|
|
123
|
+
} else {
|
|
124
|
+
container.innerHTML = logs.map((log) => renderLogEntry(log)).join("");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 更新分頁控制
|
|
128
|
+
updateLogPagination();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error("載入日誌失敗:", error);
|
|
131
|
+
container.innerHTML = `
|
|
132
|
+
<div class="placeholder">
|
|
133
|
+
<span class="icon">❌</span>
|
|
134
|
+
<p>載入日誌失敗: ${error.message}</p>
|
|
135
|
+
</div>
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 渲染單條日誌
|
|
142
|
+
*/
|
|
143
|
+
function renderLogEntry(log) {
|
|
144
|
+
const timestamp = new Date(log.timestamp).toLocaleString("zh-TW", {
|
|
145
|
+
year: "numeric",
|
|
146
|
+
month: "2-digit",
|
|
147
|
+
day: "2-digit",
|
|
148
|
+
hour: "2-digit",
|
|
149
|
+
minute: "2-digit",
|
|
150
|
+
second: "2-digit",
|
|
151
|
+
hour12: false,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const levelClass = `log-level-${log.level}`;
|
|
155
|
+
const searchTerm = document.getElementById("logSearch").value.trim();
|
|
156
|
+
|
|
157
|
+
// 高亮搜尋詞
|
|
158
|
+
let message = escapeHtml(log.message);
|
|
159
|
+
if (searchTerm) {
|
|
160
|
+
const regex = new RegExp(`(${escapeRegex(searchTerm)})`, "gi");
|
|
161
|
+
message = message.replace(regex, "<mark>$1</mark>");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 格式化 meta 資訊
|
|
165
|
+
let metaHtml = "";
|
|
166
|
+
if (log.meta) {
|
|
167
|
+
try {
|
|
168
|
+
const metaObj =
|
|
169
|
+
typeof log.meta === "string" ? JSON.parse(log.meta) : log.meta;
|
|
170
|
+
if (Object.keys(metaObj).length > 0) {
|
|
171
|
+
metaHtml = `<div class="log-meta"><pre>${escapeHtml(
|
|
172
|
+
JSON.stringify(metaObj, null, 2)
|
|
173
|
+
)}</pre></div>`;
|
|
174
|
+
}
|
|
175
|
+
} catch (e) {
|
|
176
|
+
// 如果無法解析,顯示原始字串
|
|
177
|
+
if (log.meta) {
|
|
178
|
+
metaHtml = `<div class="log-meta">${escapeHtml(
|
|
179
|
+
String(log.meta)
|
|
180
|
+
)}</div>`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return `
|
|
186
|
+
<div class="log-entry">
|
|
187
|
+
<div class="log-entry-header">
|
|
188
|
+
<span class="log-timestamp">${timestamp}</span>
|
|
189
|
+
<span class="log-level ${levelClass}">${log.level}</span>
|
|
190
|
+
<span class="log-source">[${escapeHtml(log.source)}]</span>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="log-message">${message}</div>
|
|
193
|
+
${metaHtml}
|
|
194
|
+
</div>
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 更新分頁控制
|
|
200
|
+
*/
|
|
201
|
+
function updateLogPagination() {
|
|
202
|
+
const pageInfo = document.getElementById("logPageInfo");
|
|
203
|
+
const prevBtn = document.getElementById("logPrevPage");
|
|
204
|
+
const nextBtn = document.getElementById("logNextPage");
|
|
205
|
+
|
|
206
|
+
if (pageInfo) {
|
|
207
|
+
pageInfo.textContent = `${currentLogPage} / ${totalLogPages}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (prevBtn) {
|
|
211
|
+
prevBtn.disabled = currentLogPage <= 1;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (nextBtn) {
|
|
215
|
+
nextBtn.disabled = currentLogPage >= totalLogPages;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 搜尋日誌
|
|
221
|
+
*/
|
|
222
|
+
export function searchLogs() {
|
|
223
|
+
loadLogs(1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 清除舊日誌
|
|
228
|
+
*/
|
|
229
|
+
export async function clearOldLogs() {
|
|
230
|
+
// 預設清除 7 天前的日誌
|
|
231
|
+
const daysToKeep = 7;
|
|
232
|
+
const cutoffDate = new Date();
|
|
233
|
+
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|
234
|
+
|
|
235
|
+
if (!confirm(`確定要清除 ${daysToKeep} 天前的所有日誌嗎?此操作無法復原。`)) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
showLoadingOverlay("清除舊日誌中...");
|
|
241
|
+
|
|
242
|
+
const response = await fetch(
|
|
243
|
+
`/api/logs?endDate=${cutoffDate.toISOString().split("T")[0]}`,
|
|
244
|
+
{
|
|
245
|
+
method: "DELETE",
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (!response.ok) {
|
|
250
|
+
throw new Error(`HTTP ${response.status}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const data = await response.json();
|
|
254
|
+
showToast(
|
|
255
|
+
"success",
|
|
256
|
+
"清除成功",
|
|
257
|
+
`已刪除 ${data.deletedCount || 0} 條舊日誌`
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// 重新載入日誌
|
|
261
|
+
await loadLogs(1);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error("清除舊日誌失敗:", error);
|
|
264
|
+
showToast("error", "清除失敗", error.message);
|
|
265
|
+
} finally {
|
|
266
|
+
hideLoadingOverlay();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 正則表達式轉義
|
|
272
|
+
*/
|
|
273
|
+
function escapeRegex(str) {
|
|
274
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 處理分頁點擊
|
|
279
|
+
* @param {string} direction 'prev' or 'next'
|
|
280
|
+
*/
|
|
281
|
+
export function handlePagination(direction) {
|
|
282
|
+
if (direction === "prev" && currentLogPage > 1) {
|
|
283
|
+
loadLogs(currentLogPage - 1);
|
|
284
|
+
} else if (direction === "next" && currentLogPage < totalLogPages) {
|
|
285
|
+
loadLogs(currentLogPage + 1);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export default {
|
|
290
|
+
openLogViewerModal,
|
|
291
|
+
closeLogViewerModal,
|
|
292
|
+
loadLogs,
|
|
293
|
+
searchLogs,
|
|
294
|
+
clearOldLogs,
|
|
295
|
+
handlePagination,
|
|
296
|
+
};
|