@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,406 @@
1
+ /**
2
+ * Navbar Styles
3
+ * 統一導覽列樣式
4
+ */
5
+
6
+ :root {
7
+ --navbar-height: 60px;
8
+ --navbar-bg: #ffffff;
9
+ --navbar-border: #e1e5e9;
10
+ --navbar-text: #1a1a2e;
11
+ --navbar-text-hover: #4a90d9;
12
+ --navbar-active: #4a90d9;
13
+ --navbar-active-bg: #e8f4fd;
14
+ --navbar-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
15
+ --navbar-z-index: 1000;
16
+ }
17
+
18
+ /* 為固定導覽列預留空間 */
19
+ body {
20
+ padding-top: var(--navbar-height);
21
+ }
22
+
23
+ /* 導覽列容器 */
24
+ .navbar {
25
+ position: fixed;
26
+ top: 0;
27
+ left: 0;
28
+ right: 0;
29
+ height: var(--navbar-height);
30
+ background: var(--navbar-bg);
31
+ border-bottom: 1px solid var(--navbar-border);
32
+ box-shadow: var(--navbar-shadow);
33
+ z-index: var(--navbar-z-index);
34
+ }
35
+
36
+ .navbar-container {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: space-between;
40
+ height: 100%;
41
+ max-width: 1600px;
42
+ margin: 0 auto;
43
+ padding: 0 20px;
44
+ }
45
+
46
+ /* 品牌區域 */
47
+ .navbar-brand {
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 8px;
51
+ font-size: 18px;
52
+ font-weight: 600;
53
+ color: var(--navbar-text);
54
+ white-space: nowrap;
55
+ }
56
+
57
+ .navbar-logo {
58
+ font-size: 24px;
59
+ }
60
+
61
+ .navbar-title {
62
+ display: inline-block;
63
+ }
64
+
65
+ /* 選單區域 */
66
+ .navbar-menu {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 32px;
70
+ flex: 1;
71
+ justify-content: space-between;
72
+ margin-left: 32px;
73
+ }
74
+
75
+ .navbar-links {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 4px;
79
+ }
80
+
81
+ /* 導覽連結 */
82
+ .navbar-link {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 6px;
86
+ padding: 8px 16px;
87
+ border-radius: 6px;
88
+ font-size: 14px;
89
+ font-weight: 500;
90
+ color: var(--navbar-text);
91
+ text-decoration: none;
92
+ transition: all 0.2s ease;
93
+ white-space: nowrap;
94
+ }
95
+
96
+ .navbar-link:hover {
97
+ color: var(--navbar-text-hover);
98
+ background: var(--navbar-active-bg);
99
+ }
100
+
101
+ .navbar-link.active {
102
+ color: var(--navbar-active);
103
+ background: var(--navbar-active-bg);
104
+ }
105
+
106
+ .navbar-link .link-icon {
107
+ font-size: 16px;
108
+ }
109
+
110
+ /* 狀態區域 */
111
+ .navbar-status {
112
+ display: flex;
113
+ align-items: center;
114
+ gap: 12px;
115
+ }
116
+
117
+ /* 自動回覆計時器 (navbar) */
118
+ .navbar-timer {
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 6px;
122
+ padding: 6px 12px;
123
+ border-radius: 20px;
124
+ background: #fff3e0;
125
+ font-size: 13px;
126
+ font-weight: 500;
127
+ cursor: pointer;
128
+ transition: all 0.3s ease;
129
+ animation: timerPulseNavbar 1s ease-in-out infinite;
130
+ }
131
+
132
+ .navbar-timer:hover {
133
+ background: #ffe0b2;
134
+ }
135
+
136
+ .navbar-timer.paused {
137
+ background: #f5f5f5;
138
+ animation: none;
139
+ }
140
+
141
+ .navbar-timer.paused .timer-icon,
142
+ .navbar-timer.paused .timer-text,
143
+ .navbar-timer.paused .timer-seconds,
144
+ .navbar-timer.paused .timer-unit {
145
+ color: #9e9e9e;
146
+ }
147
+
148
+ .navbar-timer .timer-icon {
149
+ font-size: 14px;
150
+ }
151
+
152
+ .navbar-timer .timer-text {
153
+ color: #e65100;
154
+ }
155
+
156
+ .navbar-timer .timer-seconds {
157
+ color: #e65100;
158
+ font-weight: 700;
159
+ min-width: 20px;
160
+ text-align: center;
161
+ }
162
+
163
+ .navbar-timer .timer-unit {
164
+ color: #f57c00;
165
+ }
166
+
167
+ @keyframes timerPulseNavbar {
168
+ 0%, 100% {
169
+ opacity: 1;
170
+ }
171
+ 50% {
172
+ opacity: 0.8;
173
+ }
174
+ }
175
+
176
+ .connection-status {
177
+ display: flex;
178
+ align-items: center;
179
+ gap: 6px;
180
+ padding: 6px 12px;
181
+ border-radius: 20px;
182
+ background: #f5f7fa;
183
+ font-size: 13px;
184
+ font-weight: 500;
185
+ }
186
+
187
+ .connection-status .status-dot {
188
+ width: 8px;
189
+ height: 8px;
190
+ border-radius: 50%;
191
+ background: #94a3b8;
192
+ transition: background 0.3s ease;
193
+ }
194
+
195
+ .connection-status.connected .status-dot {
196
+ background: #22c55e;
197
+ box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
198
+ }
199
+
200
+ .connection-status.disconnected .status-dot {
201
+ background: #ef4444;
202
+ box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
203
+ }
204
+
205
+ .connection-status .status-text {
206
+ color: #64748b;
207
+ }
208
+
209
+ .version-badge {
210
+ padding: 4px 10px;
211
+ border-radius: 12px;
212
+ background: #e8f4fd;
213
+ color: #4a90d9;
214
+ font-size: 12px;
215
+ font-weight: 600;
216
+ white-space: nowrap;
217
+ }
218
+
219
+ /* 移動端選單切換按鈕 */
220
+ .navbar-toggle {
221
+ display: none;
222
+ flex-direction: column;
223
+ justify-content: center;
224
+ align-items: center;
225
+ width: 40px;
226
+ height: 40px;
227
+ border: none;
228
+ background: transparent;
229
+ cursor: pointer;
230
+ padding: 0;
231
+ }
232
+
233
+ .toggle-icon {
234
+ position: relative;
235
+ width: 24px;
236
+ height: 2px;
237
+ background: var(--navbar-text);
238
+ transition: all 0.3s ease;
239
+ }
240
+
241
+ .toggle-icon::before,
242
+ .toggle-icon::after {
243
+ content: '';
244
+ position: absolute;
245
+ left: 0;
246
+ width: 24px;
247
+ height: 2px;
248
+ background: var(--navbar-text);
249
+ transition: all 0.3s ease;
250
+ }
251
+
252
+ .toggle-icon::before {
253
+ top: -8px;
254
+ }
255
+
256
+ .toggle-icon::after {
257
+ bottom: -8px;
258
+ }
259
+
260
+ .navbar-toggle.active .toggle-icon {
261
+ background: transparent;
262
+ }
263
+
264
+ .navbar-toggle.active .toggle-icon::before {
265
+ top: 0;
266
+ transform: rotate(45deg);
267
+ }
268
+
269
+ .navbar-toggle.active .toggle-icon::after {
270
+ bottom: 0;
271
+ transform: rotate(-45deg);
272
+ }
273
+
274
+ /* 響應式設計 - 平板 */
275
+ @media (max-width: 1024px) {
276
+ .navbar-container {
277
+ padding: 0 16px;
278
+ }
279
+
280
+ .navbar-menu {
281
+ gap: 24px;
282
+ margin-left: 24px;
283
+ }
284
+
285
+ .navbar-links {
286
+ gap: 2px;
287
+ }
288
+
289
+ .navbar-link {
290
+ padding: 6px 12px;
291
+ font-size: 13px;
292
+ }
293
+
294
+ .link-text {
295
+ display: none;
296
+ }
297
+
298
+ .navbar-link .link-icon {
299
+ font-size: 18px;
300
+ }
301
+ }
302
+
303
+ /* 響應式設計 - 手機 */
304
+ @media (max-width: 768px) {
305
+ .navbar-toggle {
306
+ display: flex;
307
+ }
308
+
309
+ .navbar-menu {
310
+ position: fixed;
311
+ top: var(--navbar-height);
312
+ left: 0;
313
+ right: 0;
314
+ flex-direction: column;
315
+ align-items: stretch;
316
+ gap: 0;
317
+ margin: 0;
318
+ padding: 16px;
319
+ background: var(--navbar-bg);
320
+ border-bottom: 1px solid var(--navbar-border);
321
+ box-shadow: var(--navbar-shadow);
322
+ transform: translateY(-100%);
323
+ opacity: 0;
324
+ visibility: hidden;
325
+ transition: all 0.3s ease;
326
+ }
327
+
328
+ .navbar-menu.active {
329
+ transform: translateY(0);
330
+ opacity: 1;
331
+ visibility: visible;
332
+ }
333
+
334
+ .navbar-links {
335
+ flex-direction: column;
336
+ gap: 4px;
337
+ width: 100%;
338
+ }
339
+
340
+ .navbar-link {
341
+ width: 100%;
342
+ justify-content: flex-start;
343
+ padding: 12px 16px;
344
+ font-size: 15px;
345
+ }
346
+
347
+ .link-text {
348
+ display: inline;
349
+ }
350
+
351
+ .navbar-link .link-icon {
352
+ font-size: 18px;
353
+ }
354
+
355
+ .navbar-status {
356
+ width: 100%;
357
+ justify-content: space-between;
358
+ padding-top: 16px;
359
+ margin-top: 16px;
360
+ border-top: 1px solid var(--navbar-border);
361
+ }
362
+
363
+ .navbar-title {
364
+ display: none;
365
+ }
366
+ }
367
+
368
+ /* 高對比度模式支援 */
369
+ @media (prefers-contrast: high) {
370
+ .navbar {
371
+ border-bottom-width: 2px;
372
+ }
373
+
374
+ .navbar-link {
375
+ border: 1px solid transparent;
376
+ }
377
+
378
+ .navbar-link.active {
379
+ border-color: var(--navbar-active);
380
+ }
381
+ }
382
+
383
+ /* 深色模式支援(可選) */
384
+ @media (prefers-color-scheme: dark) {
385
+ :root {
386
+ --navbar-bg: #1a1a2e;
387
+ --navbar-border: #2d2d44;
388
+ --navbar-text: #e1e5e9;
389
+ --navbar-text-hover: #60a5fa;
390
+ --navbar-active: #60a5fa;
391
+ --navbar-active-bg: #1e3a5f;
392
+ }
393
+
394
+ .connection-status {
395
+ background: #2d2d44;
396
+ }
397
+
398
+ .connection-status .status-text {
399
+ color: #94a3b8;
400
+ }
401
+
402
+ .version-badge {
403
+ background: #1e3a5f;
404
+ color: #60a5fa;
405
+ }
406
+ }
@@ -0,0 +1,49 @@
1
+ <nav class="navbar" role="navigation" aria-label="主導覽">
2
+ <div class="navbar-container">
3
+ <div class="navbar-brand">
4
+ <span class="navbar-logo">🎯</span>
5
+ <span class="navbar-title">User Feedback</span>
6
+ </div>
7
+
8
+ <button class="navbar-toggle" aria-label="展開選單" aria-expanded="false">
9
+ <span class="toggle-icon"></span>
10
+ </button>
11
+
12
+ <div class="navbar-menu">
13
+ <div class="navbar-links">
14
+ <a href="/dashboard" class="navbar-link" data-page="dashboard">
15
+ <span class="link-icon">📊</span>
16
+ <span class="link-text">Dashboard</span>
17
+ </a>
18
+ <a href="/" class="navbar-link" data-page="feedback">
19
+ <span class="link-icon">💬</span>
20
+ <span class="link-text">回覆</span>
21
+ </a>
22
+ <a href="/mcp-settings" class="navbar-link" data-page="mcp-settings">
23
+ <span class="link-icon">🔧</span>
24
+ <span class="link-text">MCP</span>
25
+ </a>
26
+ <a href="/logs" class="navbar-link" data-page="logs">
27
+ <span class="link-icon">📋</span>
28
+ <span class="link-text">日誌</span>
29
+ </a>
30
+ <a href="/terminals" class="navbar-link" data-page="terminals">
31
+ <span class="link-icon">💻</span>
32
+ <span class="link-text">終端機</span>
33
+ </a>
34
+ <a href="/settings" class="navbar-link" data-page="settings">
35
+ <span class="link-icon">⚙️</span>
36
+ <span class="link-text">設定</span>
37
+ </a>
38
+ </div>
39
+
40
+ <div class="navbar-status">
41
+ <div class="connection-status" id="navbar-connection-status">
42
+ <span class="status-dot"></span>
43
+ <span class="status-text">連接中...</span>
44
+ </div>
45
+ <span class="version-badge" id="navbar-version">v--</span>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </nav>
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Unified Navigation Bar Component
3
+ * 可重用的導覽列組件,注入到所有頁面
4
+ */
5
+
6
+ (function () {
7
+ "use strict";
8
+
9
+ const NAVBAR_HTML_PATH = "/components/navbar.html";
10
+
11
+ class Navbar {
12
+ constructor() {
13
+ this.container = null;
14
+ this.socket = null;
15
+ this.currentPage = this.detectCurrentPage();
16
+ }
17
+
18
+ /**
19
+ * 初始化導覽列
20
+ */
21
+ async init() {
22
+ try {
23
+ await this.loadAndInject();
24
+ this.setupEventListeners();
25
+ this.highlightActivePage();
26
+ this.loadVersion();
27
+
28
+ // 如果頁面有 Socket.IO,連接狀態同步
29
+ if (window.io) {
30
+ this.connectSocket();
31
+ }
32
+ } catch (error) {
33
+ console.error("[Navbar] Initialization failed:", error);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * 載入並注入 HTML
39
+ */
40
+ async loadAndInject() {
41
+ const response = await fetch(NAVBAR_HTML_PATH);
42
+ if (!response.ok) {
43
+ throw new Error(`Failed to load navbar: ${response.status}`);
44
+ }
45
+
46
+ const html = await response.text();
47
+ const tempContainer = document.createElement("div");
48
+ tempContainer.innerHTML = html;
49
+
50
+ const navbar = tempContainer.querySelector(".navbar");
51
+ if (!navbar) {
52
+ throw new Error("Navbar element not found in HTML");
53
+ }
54
+
55
+ // 注入到 body 的最前面
56
+ document.body.insertBefore(navbar, document.body.firstChild);
57
+ this.container = navbar;
58
+ }
59
+
60
+ /**
61
+ * 偵測當前頁面
62
+ */
63
+ detectCurrentPage() {
64
+ const path = window.location.pathname;
65
+
66
+ if (path === "/" || path === "/index.html") {
67
+ return "feedback";
68
+ } else if (path.startsWith("/dashboard")) {
69
+ return "dashboard";
70
+ } else if (path.startsWith("/mcp-settings")) {
71
+ return "mcp-settings";
72
+ } else if (path.startsWith("/logs")) {
73
+ return "logs";
74
+ } else if (path.startsWith("/terminals")) {
75
+ return "terminals";
76
+ } else if (path.startsWith("/settings")) {
77
+ return "settings";
78
+ }
79
+
80
+ return null;
81
+ }
82
+
83
+ /**
84
+ * 高亮當前頁面連結
85
+ */
86
+ highlightActivePage() {
87
+ if (!this.container || !this.currentPage) return;
88
+
89
+ const links = this.container.querySelectorAll(".navbar-link");
90
+ links.forEach((link) => {
91
+ if (link.dataset.page === this.currentPage) {
92
+ link.classList.add("active");
93
+ link.setAttribute("aria-current", "page");
94
+ } else {
95
+ link.classList.remove("active");
96
+ link.removeAttribute("aria-current");
97
+ }
98
+ });
99
+ }
100
+
101
+ /**
102
+ * 設置事件監聽器
103
+ */
104
+ setupEventListeners() {
105
+ if (!this.container) return;
106
+
107
+ // 移動端選單切換
108
+ const toggle = this.container.querySelector(".navbar-toggle");
109
+ const menu = this.container.querySelector(".navbar-menu");
110
+
111
+ if (toggle && menu) {
112
+ toggle.addEventListener("click", () => {
113
+ const isExpanded = menu.classList.toggle("active");
114
+ toggle.setAttribute("aria-expanded", String(isExpanded));
115
+ toggle.classList.toggle("active");
116
+ });
117
+ }
118
+
119
+ // 點擊連結時關閉移動端選單
120
+ const links = this.container.querySelectorAll(".navbar-link");
121
+ links.forEach((link) => {
122
+ link.addEventListener("click", () => {
123
+ if (menu.classList.contains("active")) {
124
+ menu.classList.remove("active");
125
+ toggle.classList.remove("active");
126
+ toggle.setAttribute("aria-expanded", "false");
127
+ }
128
+ });
129
+ });
130
+ }
131
+
132
+ /**
133
+ * 載入版本資訊
134
+ */
135
+ async loadVersion() {
136
+ try {
137
+ const response = await fetch("/api/version");
138
+ const data = await response.json();
139
+
140
+ const versionEl = this.container.querySelector("#navbar-version");
141
+ if (versionEl && data.version) {
142
+ versionEl.textContent = `v${data.version}`;
143
+ }
144
+ } catch (error) {
145
+ console.warn("[Navbar] Failed to load version:", error);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * 連接 Socket.IO 並同步狀態
151
+ */
152
+ connectSocket() {
153
+ // 尋找全局的 socket 實例
154
+ if (window.socket) {
155
+ this.socket = window.socket;
156
+ this.syncSocketStatus();
157
+ return;
158
+ }
159
+
160
+ // 如果還沒有 socket,創建一個用於狀態監聽
161
+ if (window.io) {
162
+ this.socket = io();
163
+ this.syncSocketStatus();
164
+ }
165
+ }
166
+
167
+ /**
168
+ * 同步 Socket.IO 連接狀態
169
+ */
170
+ syncSocketStatus() {
171
+ if (!this.socket || !this.container) return;
172
+
173
+ const statusEl = this.container.querySelector(
174
+ "#navbar-connection-status"
175
+ );
176
+ if (!statusEl) return;
177
+
178
+ // 更新狀態顯示
179
+ const updateStatus = (connected) => {
180
+ statusEl.classList.toggle("connected", connected);
181
+ statusEl.classList.toggle("disconnected", !connected);
182
+
183
+ const textEl = statusEl.querySelector(".status-text");
184
+ if (textEl) {
185
+ textEl.textContent = connected ? "已連接" : "已斷開";
186
+ }
187
+ };
188
+
189
+ // 監聽連接事件
190
+ this.socket.on("connect", () => updateStatus(true));
191
+ this.socket.on("disconnect", () => updateStatus(false));
192
+
193
+ // 初始狀態
194
+ updateStatus(this.socket.connected);
195
+ }
196
+ }
197
+
198
+ // 頁面載入時自動初始化
199
+ if (document.readyState === "loading") {
200
+ document.addEventListener("DOMContentLoaded", () => {
201
+ const navbar = new Navbar();
202
+ navbar.init();
203
+ });
204
+ } else {
205
+ const navbar = new Navbar();
206
+ navbar.init();
207
+ }
208
+
209
+ // 將 Navbar 暴露為全局對象(可選)
210
+ window.Navbar = Navbar;
211
+ })();