@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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +953 -0
  3. package/dist/cli.cjs +95778 -0
  4. package/dist/index.cjs +92818 -0
  5. package/dist/static/app.js +385 -0
  6. package/dist/static/components/navbar.css +406 -0
  7. package/dist/static/components/navbar.html +49 -0
  8. package/dist/static/components/navbar.js +211 -0
  9. package/dist/static/dashboard.css +495 -0
  10. package/dist/static/dashboard.html +95 -0
  11. package/dist/static/dashboard.js +540 -0
  12. package/dist/static/favicon.svg +27 -0
  13. package/dist/static/index.html +541 -0
  14. package/dist/static/logs.html +376 -0
  15. package/dist/static/logs.js +442 -0
  16. package/dist/static/mcp-settings.html +797 -0
  17. package/dist/static/mcp-settings.js +884 -0
  18. package/dist/static/modules/app-core.js +124 -0
  19. package/dist/static/modules/conversation-panel.js +247 -0
  20. package/dist/static/modules/feedback-handler.js +1420 -0
  21. package/dist/static/modules/image-handler.js +155 -0
  22. package/dist/static/modules/log-viewer.js +296 -0
  23. package/dist/static/modules/mcp-manager.js +474 -0
  24. package/dist/static/modules/prompt-manager.js +364 -0
  25. package/dist/static/modules/settings-manager.js +299 -0
  26. package/dist/static/modules/socket-manager.js +170 -0
  27. package/dist/static/modules/state-manager.js +352 -0
  28. package/dist/static/modules/timer-controller.js +243 -0
  29. package/dist/static/modules/ui-helpers.js +246 -0
  30. package/dist/static/settings.html +355 -0
  31. package/dist/static/settings.js +425 -0
  32. package/dist/static/socket.io.min.js +7 -0
  33. package/dist/static/style.css +2157 -0
  34. package/dist/static/terminals.html +357 -0
  35. package/dist/static/terminals.js +321 -0
  36. package/package.json +91 -0
@@ -0,0 +1,540 @@
1
+ /**
2
+ * Dashboard 前端邏輯
3
+ * 負責載入和顯示專案概覽,並提供即時更新
4
+ */
5
+
6
+ (function () {
7
+ "use strict";
8
+
9
+ // 配置
10
+ const POLLING_INTERVAL = 3000; // 3秒輪詢間隔
11
+ const API_BASE = "";
12
+
13
+ // 狀態
14
+ let socket = null;
15
+ let pollTimer = null;
16
+ let currentData = null;
17
+ let searchFilter = "";
18
+
19
+ // DOM 元素
20
+ const elements = {
21
+ connectionStatus: document.getElementById("connectionStatus"),
22
+ versionDisplay: document.getElementById("version-display"),
23
+ refreshBtn: document.getElementById("refreshBtn"),
24
+ totalProjects: document.getElementById("totalProjects"),
25
+ activeSessions: document.getElementById("activeSessions"),
26
+ completedSessions: document.getElementById("completedSessions"),
27
+ projectsList: document.getElementById("projectsList"),
28
+ emptyState: document.getElementById("emptyState"),
29
+ searchInput: document.getElementById("searchInput"),
30
+ };
31
+
32
+ // 初始化
33
+ function init() {
34
+ initSocket();
35
+ initEventListeners();
36
+ loadDashboardData();
37
+ startPolling();
38
+ loadVersion();
39
+ }
40
+
41
+ // 載入版本資訊
42
+ async function loadVersion() {
43
+ try {
44
+ const response = await fetch(`${API_BASE}/api/version`);
45
+ const data = await response.json();
46
+ if (data.version) {
47
+ elements.versionDisplay.textContent = `v${data.version}`;
48
+ }
49
+ } catch (error) {
50
+ console.error("Failed to load version:", error);
51
+ }
52
+ }
53
+
54
+ // 初始化 Socket.IO 連接
55
+ function initSocket() {
56
+ socket = io({
57
+ reconnection: true,
58
+ reconnectionDelay: 1000,
59
+ reconnectionAttempts: Infinity,
60
+ });
61
+
62
+ socket.on("connect", () => {
63
+ updateConnectionStatus(true);
64
+ console.log("[Dashboard] Socket connected");
65
+ });
66
+
67
+ socket.on("disconnect", () => {
68
+ updateConnectionStatus(false);
69
+ console.log("[Dashboard] Socket disconnected");
70
+ });
71
+
72
+ // 監聽 Dashboard 事件
73
+ socket.on("dashboard:session_created", (data) => {
74
+ console.log("[Dashboard] Session created:", data);
75
+ loadDashboardData();
76
+ });
77
+
78
+ socket.on("dashboard:session_updated", (data) => {
79
+ console.log("[Dashboard] Session updated:", data);
80
+ loadDashboardData();
81
+ });
82
+
83
+ socket.on("dashboard:project_activity", (data) => {
84
+ console.log("[Dashboard] Project activity:", data);
85
+ loadDashboardData();
86
+ });
87
+ }
88
+
89
+ // 更新連接狀態顯示
90
+ function updateConnectionStatus(connected) {
91
+ elements.connectionStatus.classList.toggle("connected", connected);
92
+ elements.connectionStatus.classList.toggle("disconnected", !connected);
93
+ const statusText = elements.connectionStatus.querySelector(".status-text");
94
+ if (statusText) {
95
+ statusText.textContent = connected ? "已連接" : "已斷開";
96
+ }
97
+ }
98
+
99
+ // 初始化事件監聽器
100
+ function initEventListeners() {
101
+ elements.refreshBtn.addEventListener("click", () => {
102
+ loadDashboardData();
103
+ });
104
+
105
+ elements.searchInput.addEventListener("input", (e) => {
106
+ searchFilter = e.target.value.toLowerCase();
107
+ renderProjects();
108
+ });
109
+ }
110
+
111
+ // 開始輪詢
112
+ function startPolling() {
113
+ if (pollTimer) {
114
+ clearInterval(pollTimer);
115
+ }
116
+ pollTimer = setInterval(loadDashboardData, POLLING_INTERVAL);
117
+ }
118
+
119
+ // 載入 Dashboard 資料
120
+ async function loadDashboardData() {
121
+ try {
122
+ const response = await fetch(`${API_BASE}/api/dashboard/overview`);
123
+ if (!response.ok) {
124
+ throw new Error(`HTTP error! status: ${response.status}`);
125
+ }
126
+
127
+ currentData = await response.json();
128
+ updateStats();
129
+ renderProjects();
130
+ } catch (error) {
131
+ console.error("[Dashboard] Failed to load data:", error);
132
+ }
133
+ }
134
+
135
+ // 更新統計數據
136
+ function updateStats() {
137
+ if (!currentData) return;
138
+
139
+ elements.totalProjects.textContent = currentData.totalProjects || 0;
140
+ elements.activeSessions.textContent = currentData.totalActiveSessions || 0;
141
+
142
+ // 計算已完成的會話數
143
+ let completed = 0;
144
+ if (currentData.projects) {
145
+ currentData.projects.forEach((p) => {
146
+ if (p.sessions) {
147
+ completed += p.sessions.filter(
148
+ (s) => s.status === "completed"
149
+ ).length;
150
+ }
151
+ });
152
+ }
153
+ elements.completedSessions.textContent = completed;
154
+ }
155
+
156
+ // 渲染專案列表(智能DOM更新,無閃爍)
157
+ function renderProjects() {
158
+ if (!currentData || !currentData.projects) {
159
+ showEmptyState();
160
+ return;
161
+ }
162
+
163
+ let projects = currentData.projects;
164
+
165
+ // 搜尋過濾
166
+ if (searchFilter) {
167
+ projects = projects.filter((p) => {
168
+ const name = p.project?.name?.toLowerCase() || "";
169
+ const path = p.project?.path?.toLowerCase() || "";
170
+ return name.includes(searchFilter) || path.includes(searchFilter);
171
+ });
172
+ }
173
+
174
+ if (projects.length === 0) {
175
+ showEmptyState();
176
+ return;
177
+ }
178
+
179
+ hideEmptyState();
180
+
181
+ // 按活躍會話數排序
182
+ projects.sort((a, b) => (b.activeSessions || 0) - (a.activeSessions || 0));
183
+
184
+ // 使用智能DOM更新
185
+ updateProjectsList(projects);
186
+ }
187
+
188
+ // 智能DOM更新:只更新變化的卡片,避免閃爍
189
+ function updateProjectsList(newProjects) {
190
+ const container = elements.projectsList;
191
+ const existingCards = new Map();
192
+
193
+ // 索引現有卡片
194
+ container.querySelectorAll(".project-card").forEach((card) => {
195
+ const projectId = card.dataset.projectId;
196
+ existingCards.set(projectId, card);
197
+ });
198
+
199
+ // 建立一個臨時映射用於排序
200
+ const newProjectsMap = new Map();
201
+ newProjects.forEach((project, index) => {
202
+ const projectId = String(project.project?.id || "");
203
+ newProjectsMap.set(projectId, { project, index });
204
+ });
205
+
206
+ // 更新或創建卡片
207
+ newProjects.forEach((projectData, targetIndex) => {
208
+ const projectId = String(projectData.project?.id || "");
209
+ const existingCard = existingCards.get(projectId);
210
+
211
+ if (existingCard) {
212
+ // 更新現有卡片內容
213
+ updateProjectCard(existingCard, projectData);
214
+ existingCards.delete(projectId);
215
+
216
+ // 確保順序正確(如果需要移動)
217
+ const currentIndex = Array.from(container.children).indexOf(
218
+ existingCard
219
+ );
220
+ if (currentIndex !== targetIndex) {
221
+ const referenceNode = container.children[targetIndex];
222
+ if (referenceNode && referenceNode !== existingCard) {
223
+ container.insertBefore(existingCard, referenceNode);
224
+ } else if (targetIndex >= container.children.length) {
225
+ container.appendChild(existingCard);
226
+ }
227
+ }
228
+ } else {
229
+ // 創建新卡片
230
+ const newCard = createProjectCard(projectData);
231
+
232
+ // 插入到正確位置
233
+ if (targetIndex >= container.children.length) {
234
+ container.appendChild(newCard);
235
+ } else {
236
+ container.insertBefore(newCard, container.children[targetIndex]);
237
+ }
238
+
239
+ // 添加淡入動畫
240
+ requestAnimationFrame(() => {
241
+ newCard.classList.add("fade-in");
242
+ });
243
+ }
244
+ });
245
+
246
+ // 移除不再存在的卡片
247
+ existingCards.forEach((card) => {
248
+ card.classList.add("fade-out");
249
+ setTimeout(() => {
250
+ if (card.parentNode === container) {
251
+ container.removeChild(card);
252
+ }
253
+ }, 300);
254
+ });
255
+ }
256
+
257
+ // 創建專案卡片 DOM 元素
258
+ function createProjectCard(projectData) {
259
+ const div = document.createElement("div");
260
+ const projectId = String(projectData.project?.id || "");
261
+ div.className = "project-card";
262
+ div.dataset.projectId = projectId;
263
+
264
+ if (projectData.activeSessions > 0) {
265
+ div.classList.add("has-active");
266
+ }
267
+
268
+ // 設置內容
269
+ div.innerHTML = renderProjectCardHTML(projectData);
270
+
271
+ // 綁定點擊事件
272
+ div.addEventListener("click", () => {
273
+ navigateToSession(projectId);
274
+ });
275
+
276
+ // 綁定會話項點擊事件
277
+ div.querySelectorAll(".session-item").forEach((item) => {
278
+ item.addEventListener("click", (e) => {
279
+ e.stopPropagation();
280
+ const sessionId = item.dataset.sessionId;
281
+ navigateToSessionPage(sessionId);
282
+ });
283
+ });
284
+
285
+ return div;
286
+ }
287
+
288
+ // 更新現有專案卡片
289
+ function updateProjectCard(card, projectData) {
290
+ const projectId = String(projectData.project?.id || "");
291
+ const hasActive = projectData.activeSessions > 0;
292
+
293
+ // 更新類別
294
+ card.classList.toggle("has-active", hasActive);
295
+
296
+ // 更新專案名稱
297
+ const nameEl = card.querySelector(".project-name");
298
+ const newName = projectData.project?.name || "Unknown";
299
+ if (nameEl) {
300
+ const iconSpan = nameEl.querySelector(".icon");
301
+ const currentName = nameEl.textContent.trim().substring(2); // 移除圖標字符
302
+ if (currentName !== newName) {
303
+ nameEl.innerHTML = '<span class="icon">📁</span>' + escapeHtml(newName);
304
+ }
305
+ }
306
+
307
+ // 更新徽章
308
+ const badgeEl = card.querySelector(".project-badge");
309
+ if (badgeEl) {
310
+ const badgeClass = hasActive ? "active" : "idle";
311
+ const badgeText = hasActive
312
+ ? `${projectData.activeSessions} 等待中`
313
+ : "無等待";
314
+
315
+ badgeEl.className = `project-badge ${badgeClass}`;
316
+ if (badgeEl.textContent !== badgeText) {
317
+ badgeEl.textContent = badgeText;
318
+ }
319
+ }
320
+
321
+ // 更新活躍會話數
322
+ const activeStatEl = card.querySelector(
323
+ ".project-stat.active .value, .project-stat .value"
324
+ );
325
+ if (activeStatEl) {
326
+ const newValue = String(projectData.activeSessions);
327
+ if (activeStatEl.textContent !== newValue) {
328
+ activeStatEl.textContent = newValue;
329
+ }
330
+ }
331
+
332
+ // 更新總會話數
333
+ const stats = card.querySelectorAll(".project-stat .value");
334
+ if (stats.length > 1) {
335
+ const newValue = String(projectData.totalSessions);
336
+ if (stats[1].textContent !== newValue) {
337
+ stats[1].textContent = newValue;
338
+ }
339
+ }
340
+
341
+ // 更新會話列表(簡化版:完全替換)
342
+ const sessionsContainer = card.querySelector(".project-sessions");
343
+ const newSessions = projectData.sessions || [];
344
+ const displaySessions = newSessions.slice(0, 3);
345
+
346
+ if (displaySessions.length > 0) {
347
+ const newSessionsHTML = `
348
+ <div class="project-sessions">
349
+ <div class="session-list">
350
+ ${displaySessions
351
+ .map((s) => renderSessionItem(s))
352
+ .join("")}
353
+ </div>
354
+ </div>
355
+ `;
356
+
357
+ if (sessionsContainer) {
358
+ const parent = sessionsContainer.parentNode;
359
+ const temp = document.createElement("div");
360
+ temp.innerHTML = newSessionsHTML;
361
+ parent.replaceChild(temp.firstElementChild, sessionsContainer);
362
+ } else {
363
+ // 如果之前沒有會話,添加會話列表
364
+ const bodyEl = card.querySelector(".project-card-body");
365
+ if (bodyEl) {
366
+ const temp = document.createElement("div");
367
+ temp.innerHTML = newSessionsHTML;
368
+ bodyEl.appendChild(temp.firstElementChild);
369
+ }
370
+ }
371
+
372
+ // 重新綁定會話項點擊事件
373
+ card.querySelectorAll(".session-item").forEach((item) => {
374
+ item.addEventListener("click", (e) => {
375
+ e.stopPropagation();
376
+ const sessionId = item.dataset.sessionId;
377
+ navigateToSessionPage(sessionId);
378
+ });
379
+ });
380
+ } else if (sessionsContainer) {
381
+ // 移除會話列表
382
+ sessionsContainer.remove();
383
+ }
384
+ }
385
+
386
+ // 渲染專案卡片 HTML(用於創建新卡片)
387
+ function renderProjectCardHTML(projectData) {
388
+ const project = projectData.project || {};
389
+ const sessions = projectData.sessions || [];
390
+ const activeSessions = projectData.activeSessions || 0;
391
+ const totalSessions = projectData.totalSessions || 0;
392
+
393
+ const hasActive = activeSessions > 0;
394
+ const badgeClass = hasActive ? "active" : "idle";
395
+ const badgeText = hasActive ? `${activeSessions} 等待中` : "無等待";
396
+
397
+ // 最多顯示 3 個會話
398
+ const displaySessions = sessions.slice(0, 3);
399
+
400
+ // 注意:此函數僅返回卡片內部內容,外層 div.project-card 由 createProjectCard 建立
401
+ return `
402
+ <div class="project-card-header">
403
+ <div class="project-name">
404
+ <span class="icon">📁</span>
405
+ ${escapeHtml(project.name || "Unknown")}
406
+ </div>
407
+ <span class="project-badge ${badgeClass}">${badgeText}</span>
408
+ </div>
409
+ <div class="project-card-body">
410
+ ${
411
+ project.path
412
+ ? `<div class="project-path">${escapeHtml(
413
+ project.path
414
+ )}</div>`
415
+ : ""
416
+ }
417
+ <div class="project-stats">
418
+ <div class="project-stat ${hasActive ? "active" : ""}">
419
+ <span class="icon">⏳</span>
420
+ <span class="value">${activeSessions}</span>
421
+ <span>等待</span>
422
+ </div>
423
+ <div class="project-stat">
424
+ <span class="icon">📋</span>
425
+ <span class="value">${totalSessions}</span>
426
+ <span>總計</span>
427
+ </div>
428
+ </div>
429
+ ${
430
+ displaySessions.length > 0
431
+ ? `
432
+ <div class="project-sessions">
433
+ <div class="session-list">
434
+ ${displaySessions
435
+ .map((s) => renderSessionItem(s))
436
+ .join("")}
437
+ </div>
438
+ </div>
439
+ `
440
+ : ""
441
+ }
442
+ </div>
443
+ `;
444
+ }
445
+
446
+ // 渲染會話項
447
+ function renderSessionItem(session) {
448
+ const status = session.status || "active";
449
+ const statusText = getStatusText(status);
450
+ const summary = session.workSummary || "無摘要";
451
+ const truncatedSummary =
452
+ summary.length > 50 ? summary.substring(0, 50) + "..." : summary;
453
+
454
+ return `
455
+ <div class="session-item ${status}" data-session-id="${
456
+ session.sessionId
457
+ }">
458
+ <span class="session-summary">${escapeHtml(
459
+ truncatedSummary
460
+ )}</span>
461
+ <span class="session-status ${status}">${statusText}</span>
462
+ </div>
463
+ `;
464
+ }
465
+
466
+ // 獲取狀態文字
467
+ function getStatusText(status) {
468
+ const statusMap = {
469
+ waiting: "等待中",
470
+ active: "進行中",
471
+ completed: "已完成",
472
+ timeout: "已逾時",
473
+ };
474
+ return statusMap[status] || status;
475
+ }
476
+
477
+ // 顯示空狀態
478
+ function showEmptyState() {
479
+ elements.projectsList.innerHTML = "";
480
+ elements.emptyState.style.display = "flex";
481
+ }
482
+
483
+ // 隱藏空狀態
484
+ function hideEmptyState() {
485
+ elements.emptyState.style.display = "none";
486
+ // 移除載入中佔位符
487
+ const loadingPlaceholder = elements.projectsList.querySelector('.loading-placeholder');
488
+ if (loadingPlaceholder) {
489
+ loadingPlaceholder.remove();
490
+ }
491
+ }
492
+
493
+ // 導航到專案的第一個活躍會話
494
+ function navigateToSession(projectId) {
495
+ if (!currentData) return;
496
+
497
+ const projectData = currentData.projects.find(
498
+ (p) => p.project?.id === projectId
499
+ );
500
+ if (
501
+ !projectData ||
502
+ !projectData.sessions ||
503
+ projectData.sessions.length === 0
504
+ ) {
505
+ console.log("[Dashboard] No sessions for project:", projectId);
506
+ return;
507
+ }
508
+
509
+ // 優先選擇等待中的會話
510
+ const activeSession = projectData.sessions.find(
511
+ (s) => s.status === "active" || s.status === "waiting"
512
+ );
513
+ const sessionId = activeSession
514
+ ? activeSession.sessionId
515
+ : projectData.sessions[0].sessionId;
516
+
517
+ navigateToSessionPage(sessionId);
518
+ }
519
+
520
+ // 導航到會話頁面
521
+ function navigateToSessionPage(sessionId) {
522
+ // 導航到會話回饋頁面
523
+ window.location.href = `/?sessionId=${sessionId}`;
524
+ }
525
+
526
+ // HTML 轉義
527
+ function escapeHtml(text) {
528
+ if (!text) return "";
529
+ const div = document.createElement("div");
530
+ div.textContent = text;
531
+ return div.innerHTML;
532
+ }
533
+
534
+ // 頁面載入完成後初始化
535
+ if (document.readyState === "loading") {
536
+ document.addEventListener("DOMContentLoaded", init);
537
+ } else {
538
+ init();
539
+ }
540
+ })();
@@ -0,0 +1,27 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
2
+ <!-- 黃色方框背景 -->
3
+ <rect x="0" y="0" width="100" height="100" fill="#FFC107" rx="10"/>
4
+
5
+ <!-- 黑色交談氣泡圖示 -->
6
+ <g transform="translate(50, 50)">
7
+ <!-- 主要對話框 -->
8
+ <path d="M -25 -20
9
+ Q -30 -20 -30 -15
10
+ L -30 5
11
+ Q -30 10 -25 10
12
+ L -5 10
13
+ L 0 15
14
+ L 0 10
15
+ L 15 10
16
+ Q 20 10 20 5
17
+ L 20 -15
18
+ Q 20 -20 15 -20
19
+ Z"
20
+ fill="#212121"/>
21
+
22
+ <!-- 對話框內的三個點 -->
23
+ <circle cx="-12" cy="-5" r="3" fill="#FFC107"/>
24
+ <circle cx="0" cy="-5" r="3" fill="#FFC107"/>
25
+ <circle cx="12" cy="-5" r="3" fill="#FFC107"/>
26
+ </g>
27
+ </svg>