psyclaw 0.29.2 → 0.29.4

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 (51) hide show
  1. package/README.md +4 -4
  2. package/dist/apps/panel/index.html +2222 -54
  3. package/dist/src/adapters/pi/extension.js +136 -41
  4. package/dist/src/adapters/pi/extension.js.map +1 -1
  5. package/dist/src/analysis/plan.d.ts +23 -0
  6. package/dist/src/analysis/plan.js +39 -2
  7. package/dist/src/analysis/plan.js.map +1 -1
  8. package/dist/src/analysis/stats-router.js +3 -2
  9. package/dist/src/analysis/stats-router.js.map +1 -1
  10. package/dist/src/ars/keybindings.d.ts +10 -5
  11. package/dist/src/ars/keybindings.js +20 -10
  12. package/dist/src/ars/keybindings.js.map +1 -1
  13. package/dist/src/branding.js +3 -1
  14. package/dist/src/branding.js.map +1 -1
  15. package/dist/src/index.d.ts +1 -0
  16. package/dist/src/index.js +1 -0
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/panel/extension.js +14 -5
  19. package/dist/src/panel/extension.js.map +1 -1
  20. package/dist/src/panel/server.js +69 -1
  21. package/dist/src/panel/server.js.map +1 -1
  22. package/dist/src/session/help.d.ts +5 -0
  23. package/dist/src/session/help.js +46 -0
  24. package/dist/src/session/help.js.map +1 -0
  25. package/dist/src/session/modes.d.ts +1 -1
  26. package/dist/src/session/modes.js +5 -4
  27. package/dist/src/session/modes.js.map +1 -1
  28. package/dist/src/style/cli-ui.js +7 -6
  29. package/dist/src/style/cli-ui.js.map +1 -1
  30. package/dist/src/verify/checklist.d.ts +9 -2
  31. package/dist/src/verify/checklist.js +70 -15
  32. package/dist/src/verify/checklist.js.map +1 -1
  33. package/package.json +4 -1
  34. package/scripts/rebrand-pi.mjs +4 -2
  35. package/skills/core/analysis-plan/SKILL.md +21 -11
  36. package/skills/recommended/catalog.json +84 -2
  37. package/skills/scholars/README.md +20 -0
  38. package/skills/scholars/freud-perspective/SKILL.md +70 -0
  39. package/skills/scholars/gelman-perspective/SKILL.md +66 -0
  40. package/skills/scholars/kahneman-perspective/SKILL.md +71 -0
  41. package/vendor/nuwa-skill/LICENSE +21 -0
  42. package/vendor/nuwa-skill/NOTICE.md +9 -0
  43. package/vendor/nuwa-skill/PROVENANCE.json +1 -0
  44. package/vendor/nuwa-skill/README.md +378 -0
  45. package/vendor/nuwa-skill/README_EN.md +290 -0
  46. package/vendor/nuwa-skill/SKILL.md +682 -0
  47. package/vendor/nuwa-skill/references/extraction-framework.md +151 -0
  48. package/vendor/nuwa-skill/references/fidelity-scorecard.md +63 -0
  49. package/vendor/nuwa-skill/references/skill-template.md +117 -0
  50. package/vendor/nuwa-skill/scripts/merge_research.py +150 -0
  51. package/vendor/nuwa-skill/scripts/quality_check.py +152 -0
@@ -3,87 +3,2255 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <title>PsyClaw Panel</title>
6
+ <title>PsyClaw Panel · 学术科研智能体控制台</title>
7
7
  <style>
8
- :root{color-scheme:light;--bg:#f4f6f8;--surface:#fff;--line:#dfe3e8;--text:#17202a;--muted:#66717d;--accent:#087f73;--accent-soft:#e8f5f3;--ok:#15803d;--warn:#a16207;--bad:#b42318;--radius:6px;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
9
- *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-size:14px}button,input,select{font:inherit}button{cursor:pointer}.shell{min-height:100vh;display:grid;grid-template-rows:56px 1fr}.topbar{display:flex;align-items:center;justify-content:space-between;padding:0 24px;background:var(--surface);border-bottom:1px solid var(--line)}.brand{display:flex;align-items:center;gap:10px;font-weight:750;font-size:17px}.brand-mark{display:grid;place-items:center;width:30px;height:30px;background:var(--text);color:#fff;border-radius:5px}.local{font-size:12px;color:var(--muted)}.layout{display:grid;grid-template-columns:190px minmax(0,1fr);min-height:0}.nav{padding:18px 12px;background:#eef1f3;border-right:1px solid var(--line)}.nav button{width:100%;display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:4px;border:0;background:transparent;color:var(--muted);text-align:left;border-radius:5px}.nav button.active{background:var(--surface);color:var(--text);box-shadow:0 0 0 1px var(--line);font-weight:650}.main{padding:22px;min-width:0}.view{display:none}.view.active{display:block}.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:16px}.page-head h1{font-size:20px;margin:0 0 4px}.page-head p{margin:0;color:var(--muted);font-size:13px}.button{border:1px solid var(--line);background:var(--surface);padding:7px 11px;border-radius:5px;color:var(--text)}.button.primary{background:var(--accent);border-color:var(--accent);color:#fff}.button.small{padding:4px 8px;font-size:12px}.button:disabled{opacity:.5;cursor:default}.panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius)}.workspace{display:grid;grid-template-columns:280px minmax(0,1fr);height:calc(100vh - 124px);overflow:hidden}.tree{border-right:1px solid var(--line);overflow:auto;padding:10px}.tree-section{margin:12px 8px 5px;color:var(--muted);font-size:11px;font-weight:700;text-transform:uppercase}.tree-item{display:flex;align-items:center;gap:8px;width:100%;border:0;background:transparent;padding:7px 8px;border-radius:4px;text-align:left;color:var(--text);overflow:hidden}.tree-item:hover,.tree-item.active{background:var(--accent-soft);color:#075e56}.tree-item span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.viewer{overflow:auto}.viewer-head{position:sticky;top:0;z-index:2;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 18px;background:rgba(255,255,255,.96);border-bottom:1px solid var(--line)}.viewer-path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.viewer-body{max-width:900px;margin:0 auto;padding:28px 34px 50px}.empty{display:grid;place-items:center;min-height:320px;color:var(--muted)}.markdown{line-height:1.75}.markdown h1,.markdown h2,.markdown h3{line-height:1.3;margin:1.5em 0 .65em}.markdown h1{font-size:26px;border-bottom:1px solid var(--line);padding-bottom:10px}.markdown h2{font-size:20px}.markdown h3{font-size:16px}.markdown p{margin:.8em 0}.markdown blockquote{margin:1em 0;padding:4px 14px;border-left:3px solid var(--accent);color:var(--muted);background:#f8fafb}.markdown code,.code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.markdown code{background:#f0f2f4;padding:2px 4px;border-radius:3px}.markdown pre,.raw{white-space:pre-wrap;word-break:break-word;background:#111827;color:#e5e7eb;padding:16px;border-radius:5px;overflow:auto}.markdown table{width:100%;border-collapse:collapse;margin:1em 0}.markdown th,.markdown td{padding:7px 9px;border-bottom:1px solid var(--line);text-align:left}.markdown th{font-weight:700;background:#f7f8f9}.toolbar{display:flex;gap:8px;align-items:center}.segmented{display:inline-flex;border:1px solid var(--line);background:var(--surface);border-radius:5px;padding:2px}.segmented button{border:0;background:transparent;padding:5px 10px;border-radius:3px;color:var(--muted)}.segmented button.active{background:var(--text);color:#fff}.trace-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:18px}.metric{padding:12px;border:1px solid var(--line);border-radius:5px}.metric small{display:block;color:var(--muted);margin-bottom:4px}.metric strong{font-size:20px}.timeline{border-left:2px solid var(--line);margin-left:8px}.event{position:relative;padding:0 0 18px 22px}.event:before{content:"";position:absolute;left:-6px;top:4px;width:9px;height:9px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 3px #fff}.event.error:before{background:var(--bad)}.event-head{display:flex;justify-content:space-between;gap:12px;font-weight:650}.event-meta{margin-top:3px;color:var(--muted);font-size:12px}.controls{display:flex;gap:8px;flex-wrap:wrap}.controls input,.controls select,.form input,.form select{border:1px solid var(--line);border-radius:5px;background:var(--surface);padding:7px 9px;color:var(--text)}.table-wrap{overflow:auto;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius)}table.data{width:100%;border-collapse:collapse;min-width:760px}table.data th,table.data td{padding:9px 11px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}table.data th{position:sticky;top:0;background:#f7f8f9;color:var(--muted);font-size:12px;font-weight:700}table.data tr:last-child td{border-bottom:0}.name{font-weight:650}.subtle{color:var(--muted);font-size:12px;margin-top:2px}.tag{display:inline-block;padding:2px 6px;border-radius:999px;font-size:11px;background:#eef1f3;color:var(--muted)}.tag.ok{background:#eaf7ee;color:var(--ok)}.tag.warn{background:#fff7df;color:var(--warn)}.provider-grid{display:grid;grid-template-columns:minmax(300px,420px) minmax(0,1fr);gap:16px}.section{padding:16px}.section h2{font-size:15px;margin:0 0 14px}.form{display:grid;gap:12px}.field{display:grid;gap:5px}.field label{font-size:12px;color:var(--muted)}.feedback{font-size:12px;color:var(--muted);min-height:18px}.cost{margin-top:16px}.cost-metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:12px}.cost-note{font-size:12px;color:var(--muted);margin:0 0 12px}.hidden{display:none!important}
10
- @media(max-width:850px){.layout{grid-template-columns:1fr}.nav{display:flex;border-right:0;border-bottom:1px solid var(--line);padding:8px;overflow:auto}.nav button{width:auto;white-space:nowrap;margin:0 4px 0 0}.main{padding:12px}.workspace{grid-template-columns:1fr;height:auto}.tree{max-height:280px;border-right:0;border-bottom:1px solid var(--line)}.viewer{min-height:520px}.provider-grid{grid-template-columns:1fr}.trace-summary,.cost-metrics{grid-template-columns:1fr}.topbar{padding:0 14px}}
11
- .event-detail{white-space:pre-wrap;word-break:break-word;background:#f8fafc;border:1px solid var(--line);border-radius:4px;padding:8px;margin:6px 0 2px;font-size:12px;color:var(--text)}
8
+ :root {
9
+ color-scheme: light;
10
+ --brand-primary: #0d766e;
11
+ --brand-primary-hover: #0f766e;
12
+ --brand-primary-active: #115e59;
13
+ --brand-primary-light: #f0fdfa;
14
+ --brand-primary-border: #ccfbf1;
15
+ --brand-accent: #0284c7;
16
+
17
+ --bg-app: #f8fafc;
18
+ --bg-surface: #ffffff;
19
+ --bg-surface-subtle: #f1f5f9;
20
+ --bg-surface-hover: #f8fafc;
21
+ --bg-active: #e2e8f0;
22
+ --bg-sidebar: #f8fafc;
23
+
24
+ --border-subtle: #e2e8f0;
25
+ --border-default: #cbd5e1;
26
+ --border-strong: #94a3b8;
27
+ --border-divider: #f1f5f9;
28
+
29
+ --text-main: #0f172a;
30
+ --text-secondary: #334155;
31
+ --text-muted: #64748b;
32
+ --text-tertiary: #94a3b8;
33
+
34
+ --ok-text: #047857;
35
+ --ok-bg: #ecfdf5;
36
+ --ok-border: #a7f3d0;
37
+
38
+ --warn-text: #b45309;
39
+ --warn-bg: #fffbeb;
40
+ --warn-border: #fde68a;
41
+
42
+ --bad-text: #b91c1c;
43
+ --bad-bg: #fef2f2;
44
+ --bad-border: #fecaca;
45
+
46
+ --info-text: #1d4ed8;
47
+ --info-bg: #eff6ff;
48
+ --info-border: #bfdbfe;
49
+
50
+ --shadow-xs: 0 1px 2px 0 rgba(15, 23, 42, 0.04);
51
+ --shadow-sm: 0 1px 3px 0 rgba(15, 23, 42, 0.06), 0 1px 2px -1px rgba(15, 23, 42, 0.04);
52
+ --shadow-md: 0 4px 6px -1px rgba(15, 23, 42, 0.07), 0 2px 4px -2px rgba(15, 23, 42, 0.04);
53
+ --shadow-card: 0 1px 3px 0 rgba(15, 23, 42, 0.05), 0 0 0 1px rgba(15, 23, 42, 0.04);
54
+
55
+ --radius-xs: 4px;
56
+ --radius-sm: 6px;
57
+ --radius-md: 8px;
58
+ --radius-lg: 12px;
59
+
60
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
61
+ --font-mono: ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, Monaco, Consolas, monospace;
62
+ --font-serif: "Charter", "Bitstream Charter", Cambria, Georgia, serif;
63
+ }
64
+
65
+ * { box-sizing: border-box; margin: 0; padding: 0; }
66
+ body {
67
+ background: var(--bg-app);
68
+ color: var(--text-main);
69
+ font-family: var(--font-sans);
70
+ font-size: 13.5px;
71
+ line-height: 1.5;
72
+ -webkit-font-smoothing: antialiased;
73
+ -moz-osx-font-smoothing: grayscale;
74
+ }
75
+ button, input, select { font: inherit; color: inherit; }
76
+ button { cursor: pointer; border: none; background: none; }
77
+
78
+ .shell {
79
+ min-height: 100vh;
80
+ display: grid;
81
+ grid-template-rows: 54px 1fr;
82
+ }
83
+
84
+ .topbar {
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: space-between;
88
+ padding: 0 20px;
89
+ background: var(--bg-surface);
90
+ border-bottom: 1px solid var(--border-subtle);
91
+ z-index: 10;
92
+ position: sticky;
93
+ top: 0;
94
+ }
95
+ .brand {
96
+ display: flex;
97
+ align-items: center;
98
+ gap: 10px;
99
+ text-decoration: none;
100
+ }
101
+ .brand-mark {
102
+ display: grid;
103
+ place-items: center;
104
+ width: 28px;
105
+ height: 28px;
106
+ background: linear-gradient(135deg, #0f172a 0%, #0d766e 100%);
107
+ color: #ffffff;
108
+ border-radius: var(--radius-sm);
109
+ font-weight: 700;
110
+ font-size: 16px;
111
+ line-height: 1;
112
+ box-shadow: var(--shadow-xs);
113
+ }
114
+ .brand-title {
115
+ font-weight: 650;
116
+ font-size: 15px;
117
+ letter-spacing: -0.01em;
118
+ color: var(--text-main);
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 8px;
122
+ }
123
+ .brand-badge {
124
+ font-size: 10.5px;
125
+ font-weight: 550;
126
+ color: var(--brand-primary);
127
+ background: var(--brand-primary-light);
128
+ border: 1px solid var(--brand-primary-border);
129
+ padding: 1px 6px;
130
+ border-radius: 999px;
131
+ letter-spacing: 0.02em;
132
+ }
133
+ .topbar-meta {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 16px;
137
+ }
138
+ .local-badge {
139
+ display: inline-flex;
140
+ align-items: center;
141
+ gap: 6px;
142
+ font-size: 11.5px;
143
+ font-weight: 500;
144
+ color: var(--text-secondary);
145
+ background: var(--bg-surface-subtle);
146
+ padding: 3px 9px;
147
+ border-radius: 999px;
148
+ border: 1px solid var(--border-subtle);
149
+ }
150
+ .pulse-dot {
151
+ width: 6px;
152
+ height: 6px;
153
+ border-radius: 50%;
154
+ background: #10b981;
155
+ box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
156
+ }
157
+
158
+ .layout {
159
+ display: grid;
160
+ grid-template-columns: 210px minmax(0, 1fr);
161
+ min-height: 0;
162
+ }
163
+
164
+ .nav {
165
+ padding: 16px 10px;
166
+ background: var(--bg-sidebar);
167
+ border-right: 1px solid var(--border-subtle);
168
+ display: flex;
169
+ flex-direction: column;
170
+ justify-content: space-between;
171
+ }
172
+ .nav-group {
173
+ display: flex;
174
+ flex-direction: column;
175
+ gap: 3px;
176
+ }
177
+ .nav-label {
178
+ font-size: 10.5px;
179
+ font-weight: 700;
180
+ color: var(--text-tertiary);
181
+ letter-spacing: 0.05em;
182
+ text-transform: uppercase;
183
+ padding: 6px 10px 4px;
184
+ margin-bottom: 2px;
185
+ }
186
+ .nav button {
187
+ width: 100%;
188
+ display: flex;
189
+ align-items: center;
190
+ gap: 9px;
191
+ padding: 8px 10px;
192
+ border-radius: var(--radius-sm);
193
+ color: var(--text-secondary);
194
+ font-weight: 500;
195
+ font-size: 12.5px;
196
+ text-align: left;
197
+ transition: all 0.15s ease;
198
+ position: relative;
199
+ }
200
+ .nav button svg {
201
+ width: 16px;
202
+ height: 16px;
203
+ color: var(--text-muted);
204
+ flex-shrink: 0;
205
+ transition: color 0.15s ease;
206
+ }
207
+ .nav button:hover {
208
+ background: var(--bg-surface-hover);
209
+ color: var(--text-main);
210
+ }
211
+ .nav button:hover svg {
212
+ color: var(--text-main);
213
+ }
214
+ .nav button.active {
215
+ background: var(--bg-surface);
216
+ color: var(--brand-primary);
217
+ font-weight: 600;
218
+ box-shadow: var(--shadow-sm);
219
+ border: 1px solid var(--border-subtle);
220
+ }
221
+ .nav button.active svg {
222
+ color: var(--brand-primary);
223
+ }
224
+ .nav-footer {
225
+ padding: 12px 10px;
226
+ border-top: 1px solid var(--border-subtle);
227
+ font-size: 11px;
228
+ color: var(--text-muted);
229
+ line-height: 1.4;
230
+ }
231
+ .nav-footer code {
232
+ font-family: var(--font-mono);
233
+ font-size: 10.5px;
234
+ background: var(--bg-surface-subtle);
235
+ padding: 1px 4px;
236
+ border-radius: 3px;
237
+ border: 1px solid var(--border-subtle);
238
+ }
239
+
240
+ .main {
241
+ padding: 24px 28px;
242
+ min-width: 0;
243
+ overflow-y: auto;
244
+ }
245
+ .view { display: none; }
246
+ .view.active { display: block; animation: fadeIn 0.18s ease; }
247
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: translateY(0); } }
248
+
249
+ .page-head {
250
+ display: flex;
251
+ align-items: flex-end;
252
+ justify-content: space-between;
253
+ gap: 16px;
254
+ margin-bottom: 20px;
255
+ }
256
+ .page-head h1 {
257
+ font-size: 19px;
258
+ font-weight: 650;
259
+ letter-spacing: -0.02em;
260
+ color: var(--text-main);
261
+ margin: 0 0 3px;
262
+ }
263
+ .page-head p {
264
+ margin: 0;
265
+ color: var(--text-muted);
266
+ font-size: 12.5px;
267
+ }
268
+
269
+ .button {
270
+ display: inline-flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ gap: 6px;
274
+ border: 1px solid var(--border-default);
275
+ background: var(--bg-surface);
276
+ padding: 6px 12px;
277
+ border-radius: var(--radius-sm);
278
+ color: var(--text-secondary);
279
+ font-weight: 500;
280
+ font-size: 12.5px;
281
+ box-shadow: var(--shadow-xs);
282
+ transition: all 0.15s ease;
283
+ white-space: nowrap;
284
+ }
285
+ .button:hover {
286
+ background: var(--bg-surface-hover);
287
+ border-color: var(--border-strong);
288
+ color: var(--text-main);
289
+ }
290
+ .button:active {
291
+ transform: translateY(1px);
292
+ }
293
+ .button.primary {
294
+ background: var(--brand-primary);
295
+ border-color: var(--brand-primary);
296
+ color: #ffffff;
297
+ box-shadow: 0 1px 2px rgba(13, 118, 110, 0.2);
298
+ }
299
+ .button.primary:hover {
300
+ background: var(--brand-primary-hover);
301
+ border-color: var(--brand-primary-hover);
302
+ }
303
+ .button.small {
304
+ padding: 3.5px 8px;
305
+ font-size: 11.5px;
306
+ border-radius: var(--radius-xs);
307
+ }
308
+ .button:disabled {
309
+ opacity: 0.5;
310
+ cursor: not-allowed;
311
+ transform: none;
312
+ }
313
+
314
+ .panel {
315
+ background: var(--bg-surface);
316
+ border: 1px solid var(--border-subtle);
317
+ border-radius: var(--radius-md);
318
+ box-shadow: var(--shadow-card);
319
+ }
320
+
321
+ .workspace {
322
+ display: grid;
323
+ grid-template-columns: 260px minmax(0, 1fr);
324
+ height: calc(100vh - 130px);
325
+ overflow: hidden;
326
+ background: var(--bg-surface);
327
+ }
328
+ .tree {
329
+ border-right: 1px solid var(--border-subtle);
330
+ overflow-y: auto;
331
+ padding: 12px 8px;
332
+ background: #fafbfc;
333
+ }
334
+ .tree-section {
335
+ margin: 12px 6px 4px;
336
+ color: var(--text-muted);
337
+ font-size: 10.5px;
338
+ font-weight: 700;
339
+ text-transform: uppercase;
340
+ letter-spacing: 0.05em;
341
+ display: flex;
342
+ align-items: center;
343
+ gap: 6px;
344
+ }
345
+ .tree-item {
346
+ display: flex;
347
+ align-items: center;
348
+ gap: 8px;
349
+ width: 100%;
350
+ border: 1px solid transparent;
351
+ background: transparent;
352
+ padding: 6px 8px;
353
+ border-radius: var(--radius-xs);
354
+ text-align: left;
355
+ color: var(--text-secondary);
356
+ font-size: 12.5px;
357
+ transition: all 0.12s ease;
358
+ cursor: pointer;
359
+ }
360
+ .tree-item svg {
361
+ width: 14px;
362
+ height: 14px;
363
+ color: var(--text-tertiary);
364
+ flex-shrink: 0;
365
+ transition: color 0.12s ease;
366
+ }
367
+ .tree-item:hover {
368
+ background: var(--bg-surface);
369
+ color: var(--text-main);
370
+ border-color: var(--border-subtle);
371
+ }
372
+ .tree-item:hover svg {
373
+ color: var(--text-secondary);
374
+ }
375
+ .tree-item.active {
376
+ background: var(--brand-primary-light);
377
+ color: var(--brand-primary);
378
+ font-weight: 600;
379
+ border-color: var(--brand-primary-border);
380
+ }
381
+ .tree-item.active svg {
382
+ color: var(--brand-primary);
383
+ }
384
+ .tree-item span:last-child {
385
+ overflow: hidden;
386
+ text-overflow: ellipsis;
387
+ white-space: nowrap;
388
+ flex: 1;
389
+ }
390
+
391
+ .viewer {
392
+ overflow-y: auto;
393
+ display: flex;
394
+ flex-direction: column;
395
+ background: var(--bg-surface);
396
+ }
397
+ .viewer-head {
398
+ position: sticky;
399
+ top: 0;
400
+ z-index: 2;
401
+ display: flex;
402
+ align-items: center;
403
+ justify-content: space-between;
404
+ gap: 12px;
405
+ padding: 10px 20px;
406
+ background: rgba(255, 255, 255, 0.94);
407
+ backdrop-filter: blur(8px);
408
+ border-bottom: 1px solid var(--border-subtle);
409
+ }
410
+ .viewer-path-box {
411
+ display: flex;
412
+ align-items: center;
413
+ gap: 8px;
414
+ min-width: 0;
415
+ }
416
+ .viewer-path {
417
+ font-family: var(--font-mono);
418
+ font-size: 12px;
419
+ color: var(--text-secondary);
420
+ font-weight: 550;
421
+ overflow: hidden;
422
+ text-overflow: ellipsis;
423
+ white-space: nowrap;
424
+ }
425
+ .viewer-actions {
426
+ display: flex;
427
+ align-items: center;
428
+ gap: 8px;
429
+ }
430
+ .viewer-body {
431
+ max-width: 860px;
432
+ width: 100%;
433
+ margin: 0 auto;
434
+ padding: 32px 36px 64px;
435
+ color: var(--text-main);
436
+ }
437
+ .viewer-body.empty {
438
+ display: grid;
439
+ place-items: center;
440
+ min-height: 380px;
441
+ color: var(--text-muted);
442
+ font-size: 13px;
443
+ }
444
+
445
+ .markdown {
446
+ line-height: 1.72;
447
+ font-size: 14px;
448
+ color: #1e293b;
449
+ }
450
+ .markdown h1, .markdown h2, .markdown h3, .markdown h4 {
451
+ line-height: 1.35;
452
+ color: #0f172a;
453
+ font-weight: 650;
454
+ letter-spacing: -0.015em;
455
+ }
456
+ .markdown h1 {
457
+ font-size: 23px;
458
+ margin: 1.6em 0 0.7em;
459
+ padding-bottom: 8px;
460
+ border-bottom: 1px solid var(--border-subtle);
461
+ }
462
+ .markdown h1:first-child { margin-top: 0; }
463
+ .markdown h2 {
464
+ font-size: 18px;
465
+ margin: 1.4em 0 0.6em;
466
+ }
467
+ .markdown h3 {
468
+ font-size: 15px;
469
+ margin: 1.2em 0 0.5em;
470
+ }
471
+ .markdown p {
472
+ margin: 0.85em 0;
473
+ }
474
+ .markdown blockquote {
475
+ margin: 1.2em 0;
476
+ padding: 8px 16px;
477
+ border-left: 3px solid var(--brand-primary);
478
+ color: var(--text-secondary);
479
+ background: #f8fafc;
480
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
481
+ font-style: normal;
482
+ }
483
+ .markdown blockquote p { margin: 0.3em 0; }
484
+ .markdown ul, .markdown ol {
485
+ margin: 0.8em 0;
486
+ padding-left: 22px;
487
+ }
488
+ .markdown li {
489
+ margin: 0.3em 0;
490
+ }
491
+ .markdown code, .code {
492
+ font-family: var(--font-mono);
493
+ font-size: 12px;
494
+ }
495
+ .markdown code {
496
+ background: var(--bg-surface-subtle);
497
+ color: #0f766e;
498
+ padding: 2px 5px;
499
+ border-radius: var(--radius-xs);
500
+ border: 1px solid #e2e8f0;
501
+ }
502
+ .markdown pre, .raw {
503
+ font-family: var(--font-mono);
504
+ font-size: 12.5px;
505
+ line-height: 1.6;
506
+ white-space: pre-wrap;
507
+ word-break: break-word;
508
+ background: #0f172a;
509
+ color: #f1f5f9;
510
+ padding: 16px 20px;
511
+ border-radius: var(--radius-md);
512
+ overflow-x: auto;
513
+ margin: 1.2em 0;
514
+ border: 1px solid #1e293b;
515
+ box-shadow: var(--shadow-sm);
516
+ }
517
+ .markdown pre code {
518
+ background: transparent;
519
+ color: inherit;
520
+ padding: 0;
521
+ border: none;
522
+ font-size: inherit;
523
+ }
524
+ .markdown table {
525
+ width: 100%;
526
+ border-collapse: collapse;
527
+ margin: 1.4em 0;
528
+ font-size: 13px;
529
+ }
530
+ .markdown th, .markdown td {
531
+ padding: 8px 12px;
532
+ border-bottom: 1px solid var(--border-subtle);
533
+ text-align: left;
534
+ }
535
+ .markdown th {
536
+ font-weight: 600;
537
+ color: var(--text-secondary);
538
+ background: #f8fafc;
539
+ border-top: 1px solid var(--border-subtle);
540
+ border-bottom: 2px solid var(--border-default);
541
+ }
542
+ .markdown tr:hover td {
543
+ background: #fafbfc;
544
+ }
545
+
546
+ .tag {
547
+ display: inline-flex;
548
+ align-items: center;
549
+ gap: 4px;
550
+ padding: 2px 7px;
551
+ border-radius: 999px;
552
+ font-size: 11px;
553
+ font-weight: 550;
554
+ background: var(--bg-surface-subtle);
555
+ color: var(--text-secondary);
556
+ border: 1px solid var(--border-subtle);
557
+ white-space: nowrap;
558
+ }
559
+ .tag.ok {
560
+ background: var(--ok-bg);
561
+ color: var(--ok-text);
562
+ border-color: var(--ok-border);
563
+ }
564
+ .tag.warn {
565
+ background: var(--warn-bg);
566
+ color: var(--warn-text);
567
+ border-color: var(--warn-border);
568
+ }
569
+ .tag.bad {
570
+ background: var(--bad-bg);
571
+ color: var(--bad-text);
572
+ border-color: var(--bad-border);
573
+ }
574
+ .tag.info {
575
+ background: var(--info-bg);
576
+ color: var(--info-text);
577
+ border-color: var(--info-border);
578
+ }
579
+
580
+ .segmented {
581
+ display: inline-flex;
582
+ background: #f1f5f9;
583
+ border: 1px solid var(--border-subtle);
584
+ border-radius: var(--radius-sm);
585
+ padding: 2px;
586
+ gap: 1px;
587
+ }
588
+ .segmented button {
589
+ border: none;
590
+ background: transparent;
591
+ padding: 4px 10px;
592
+ border-radius: 4px;
593
+ color: var(--text-muted);
594
+ font-size: 12px;
595
+ font-weight: 500;
596
+ transition: all 0.12s ease;
597
+ }
598
+ .segmented button:hover {
599
+ color: var(--text-main);
600
+ }
601
+ .segmented button.active {
602
+ background: var(--bg-surface);
603
+ color: var(--text-main);
604
+ font-weight: 600;
605
+ box-shadow: var(--shadow-xs);
606
+ }
607
+
608
+ .controls {
609
+ display: flex;
610
+ gap: 10px;
611
+ flex-wrap: wrap;
612
+ align-items: center;
613
+ margin-bottom: 14px;
614
+ }
615
+ .search-wrapper {
616
+ position: relative;
617
+ flex: 1;
618
+ min-width: 220px;
619
+ max-width: 320px;
620
+ }
621
+ .search-wrapper svg {
622
+ position: absolute;
623
+ left: 10px;
624
+ top: 50%;
625
+ transform: translateY(-50%);
626
+ width: 14px;
627
+ height: 14px;
628
+ color: var(--text-muted);
629
+ pointer-events: none;
630
+ }
631
+ .controls input, .controls select, .form input, .form select {
632
+ border: 1px solid var(--border-default);
633
+ border-radius: var(--radius-sm);
634
+ background: var(--bg-surface);
635
+ padding: 6px 10px;
636
+ color: var(--text-main);
637
+ font-size: 12.5px;
638
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
639
+ }
640
+ .controls input:focus, .controls select:focus, .form input:focus, .form select:focus {
641
+ outline: none;
642
+ border-color: var(--brand-primary);
643
+ box-shadow: 0 0 0 3px rgba(13, 118, 110, 0.12);
644
+ }
645
+ .search-wrapper input {
646
+ width: 100%;
647
+ padding-left: 30px;
648
+ }
649
+
650
+ .table-wrap {
651
+ overflow-x: auto;
652
+ background: var(--bg-surface);
653
+ border: 1px solid var(--border-subtle);
654
+ border-radius: var(--radius-md);
655
+ box-shadow: var(--shadow-card);
656
+ }
657
+ table.data {
658
+ width: 100%;
659
+ border-collapse: collapse;
660
+ min-width: 760px;
661
+ font-size: 12.5px;
662
+ }
663
+ table.data th, table.data td {
664
+ padding: 10px 14px;
665
+ border-bottom: 1px solid var(--border-subtle);
666
+ text-align: left;
667
+ vertical-align: middle;
668
+ }
669
+ table.data th {
670
+ position: sticky;
671
+ top: 0;
672
+ background: #fafbfc;
673
+ color: var(--text-muted);
674
+ font-size: 11px;
675
+ font-weight: 650;
676
+ text-transform: uppercase;
677
+ letter-spacing: 0.04em;
678
+ border-bottom: 1px solid var(--border-subtle);
679
+ z-index: 1;
680
+ }
681
+ table.data tr:last-child td { border-bottom: 0; }
682
+ table.data tr:hover td { background: #fafbfc; }
683
+ .name { font-weight: 600; color: var(--text-main); }
684
+ .subtle { color: var(--text-muted); font-size: 11.5px; margin-top: 2px; }
685
+
686
+ .provider-grid {
687
+ display: grid;
688
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
689
+ gap: 18px;
690
+ margin-bottom: 20px;
691
+ }
692
+ .section {
693
+ padding: 20px;
694
+ }
695
+ .section-head {
696
+ display: flex;
697
+ align-items: center;
698
+ gap: 8px;
699
+ margin-bottom: 16px;
700
+ }
701
+ .section h2 {
702
+ font-size: 15px;
703
+ font-weight: 650;
704
+ color: var(--text-main);
705
+ margin: 0;
706
+ }
707
+ .form {
708
+ display: grid;
709
+ gap: 14px;
710
+ }
711
+ .field {
712
+ display: grid;
713
+ gap: 6px;
714
+ }
715
+ .field label {
716
+ font-size: 12px;
717
+ font-weight: 550;
718
+ color: var(--text-secondary);
719
+ }
720
+ .feedback {
721
+ font-size: 12px;
722
+ color: var(--brand-primary);
723
+ min-height: 18px;
724
+ font-weight: 500;
725
+ }
726
+
727
+ .cost-metrics {
728
+ display: grid;
729
+ grid-template-columns: repeat(3, minmax(0, 1fr));
730
+ gap: 14px;
731
+ margin: 16px 0;
732
+ }
733
+ .metric {
734
+ padding: 16px 18px;
735
+ border: 1px solid var(--border-subtle);
736
+ background: #fafbfc;
737
+ border-radius: var(--radius-md);
738
+ transition: all 0.15s ease;
739
+ }
740
+ .metric:hover {
741
+ background: var(--bg-surface);
742
+ border-color: var(--border-default);
743
+ box-shadow: var(--shadow-sm);
744
+ }
745
+ .metric small {
746
+ display: block;
747
+ color: var(--text-muted);
748
+ font-size: 11.5px;
749
+ font-weight: 550;
750
+ margin-bottom: 6px;
751
+ }
752
+ .metric strong {
753
+ font-size: 22px;
754
+ font-weight: 700;
755
+ font-family: var(--font-mono);
756
+ color: var(--text-main);
757
+ letter-spacing: -0.02em;
758
+ }
759
+ .cost-note {
760
+ font-size: 11.5px;
761
+ color: var(--text-muted);
762
+ margin: 0 0 14px;
763
+ }
764
+
765
+ .empty {
766
+ display: grid;
767
+ place-items: center;
768
+ min-height: 280px;
769
+ color: var(--text-muted);
770
+ font-size: 12.5px;
771
+ text-align: center;
772
+ }
773
+ .hidden { display: none !important; }
774
+
775
+ /* Pipeline Stepper */
776
+ .pipeline-stepper {
777
+ display: flex;
778
+ align-items: center;
779
+ gap: 6px;
780
+ background: var(--bg-surface);
781
+ border: 1px solid var(--border-subtle);
782
+ border-radius: var(--radius-md);
783
+ padding: 10px 14px;
784
+ margin-bottom: 18px;
785
+ overflow-x: auto;
786
+ box-shadow: var(--shadow-sm);
787
+ }
788
+ .stepper-label {
789
+ font-size: 11px;
790
+ font-weight: 700;
791
+ color: var(--text-tertiary);
792
+ text-transform: uppercase;
793
+ letter-spacing: 0.05em;
794
+ margin-right: 6px;
795
+ white-space: nowrap;
796
+ }
797
+ .step-item {
798
+ display: inline-flex;
799
+ align-items: center;
800
+ gap: 6px;
801
+ padding: 5px 10px;
802
+ border-radius: var(--radius-sm);
803
+ font-size: 12px;
804
+ font-weight: 550;
805
+ color: var(--text-secondary);
806
+ background: var(--bg-surface-subtle);
807
+ border: 1px solid var(--border-subtle);
808
+ cursor: pointer;
809
+ transition: all 0.15s ease;
810
+ white-space: nowrap;
811
+ }
812
+ .step-item:hover {
813
+ background: var(--bg-surface-hover);
814
+ color: var(--text-main);
815
+ border-color: var(--border-default);
816
+ }
817
+ .step-item.active {
818
+ background: var(--brand-primary-light);
819
+ color: var(--brand-primary);
820
+ border-color: var(--brand-primary-border);
821
+ font-weight: 600;
822
+ }
823
+ .step-arrow {
824
+ color: var(--text-tertiary);
825
+ font-size: 11px;
826
+ }
827
+
828
+ /* Topbar Quick Commands */
829
+ .topbar-cmds {
830
+ display: flex;
831
+ align-items: center;
832
+ gap: 6px;
833
+ }
834
+ .topbar-btn {
835
+ display: inline-flex;
836
+ align-items: center;
837
+ gap: 5px;
838
+ padding: 4px 9px;
839
+ font-size: 12px;
840
+ font-weight: 550;
841
+ border-radius: var(--radius-sm);
842
+ background: var(--bg-surface-subtle);
843
+ color: var(--text-secondary);
844
+ border: 1px solid var(--border-subtle);
845
+ cursor: pointer;
846
+ transition: all 0.15s ease;
847
+ }
848
+ .topbar-btn:hover {
849
+ background: var(--bg-surface-hover);
850
+ color: var(--text-main);
851
+ border-color: var(--border-default);
852
+ }
853
+ .topbar-btn.primary {
854
+ background: var(--brand-primary);
855
+ color: #fff;
856
+ border-color: var(--brand-primary);
857
+ }
858
+ .topbar-btn.primary:hover {
859
+ background: var(--brand-primary-hover);
860
+ }
861
+
862
+ /* Interactive Web Console Drawer */
863
+ .console-fab {
864
+ position: fixed;
865
+ bottom: 24px;
866
+ right: 28px;
867
+ z-index: 100;
868
+ display: flex;
869
+ align-items: center;
870
+ gap: 8px;
871
+ padding: 10px 18px;
872
+ background: #0f172a;
873
+ color: #f8fafc;
874
+ border: 1px solid #334155;
875
+ border-radius: 999px;
876
+ font-size: 13px;
877
+ font-weight: 600;
878
+ cursor: pointer;
879
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
880
+ transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
881
+ }
882
+ .console-fab:hover {
883
+ background: #1e293b;
884
+ transform: translateY(-2px);
885
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.35);
886
+ }
887
+ .console-drawer {
888
+ position: fixed;
889
+ bottom: 0;
890
+ right: 28px;
891
+ width: 480px;
892
+ max-width: calc(100vw - 40px);
893
+ height: 560px;
894
+ max-height: calc(100vh - 80px);
895
+ background: #0f172a;
896
+ color: #e2e8f0;
897
+ border: 1px solid #334155;
898
+ border-bottom: 0;
899
+ border-radius: 12px 12px 0 0;
900
+ box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.4);
901
+ display: flex;
902
+ flex-direction: column;
903
+ z-index: 1000;
904
+ transform: translateY(105%);
905
+ transition: transform 0.28s cubic-bezier(0.16, 1, 0.3, 1);
906
+ }
907
+ .console-drawer.open {
908
+ transform: translateY(0);
909
+ }
910
+ .console-header {
911
+ display: flex;
912
+ align-items: center;
913
+ justify-content: space-between;
914
+ padding: 12px 16px;
915
+ border-bottom: 1px solid #1e293b;
916
+ background: #0b1120;
917
+ border-radius: 12px 12px 0 0;
918
+ }
919
+ .console-title {
920
+ font-size: 13px;
921
+ font-weight: 650;
922
+ color: #f8fafc;
923
+ display: flex;
924
+ align-items: center;
925
+ gap: 8px;
926
+ }
927
+ .console-status-tag {
928
+ font-size: 10px;
929
+ font-weight: 600;
930
+ padding: 1px 6px;
931
+ border-radius: 999px;
932
+ background: #064e3b;
933
+ color: #34d399;
934
+ border: 1px solid #047857;
935
+ }
936
+ .console-actions {
937
+ display: flex;
938
+ align-items: center;
939
+ gap: 8px;
940
+ }
941
+ .console-btn-icon {
942
+ background: transparent;
943
+ border: none;
944
+ color: #94a3b8;
945
+ cursor: pointer;
946
+ padding: 4px;
947
+ border-radius: 4px;
948
+ display: flex;
949
+ }
950
+ .console-btn-icon:hover {
951
+ color: #f8fafc;
952
+ background: #1e293b;
953
+ }
954
+ .console-quick-chips {
955
+ display: flex;
956
+ align-items: center;
957
+ gap: 6px;
958
+ padding: 8px 14px;
959
+ background: #141e33;
960
+ border-bottom: 1px solid #1e293b;
961
+ overflow-x: auto;
962
+ }
963
+ .chip {
964
+ font-family: var(--font-mono);
965
+ font-size: 11px;
966
+ background: #1e293b;
967
+ color: #38bdf8;
968
+ border: 1px solid #334155;
969
+ padding: 3px 8px;
970
+ border-radius: 4px;
971
+ cursor: pointer;
972
+ white-space: nowrap;
973
+ transition: all 0.12s ease;
974
+ }
975
+ .chip:hover {
976
+ background: #334155;
977
+ color: #fff;
978
+ }
979
+ .console-log {
980
+ flex: 1;
981
+ overflow-y: auto;
982
+ padding: 14px 16px;
983
+ font-family: var(--font-mono);
984
+ font-size: 12px;
985
+ line-height: 1.6;
986
+ display: flex;
987
+ flex-direction: column;
988
+ gap: 12px;
989
+ }
990
+ .log-entry {
991
+ display: flex;
992
+ flex-direction: column;
993
+ gap: 4px;
994
+ }
995
+ .log-user {
996
+ color: #38bdf8;
997
+ font-weight: 600;
998
+ }
999
+ .log-agent {
1000
+ color: #e2e8f0;
1001
+ background: #1e293b;
1002
+ padding: 10px 12px;
1003
+ border-radius: 6px;
1004
+ border-left: 3px solid #2dd4bf;
1005
+ white-space: pre-wrap;
1006
+ word-break: break-word;
1007
+ }
1008
+ .log-sys {
1009
+ color: #94a3b8;
1010
+ font-style: italic;
1011
+ font-size: 11px;
1012
+ }
1013
+ .console-input-box {
1014
+ padding: 12px;
1015
+ background: #0b1120;
1016
+ border-top: 1px solid #1e293b;
1017
+ display: flex;
1018
+ gap: 8px;
1019
+ }
1020
+ .console-input {
1021
+ flex: 1;
1022
+ background: #1e293b;
1023
+ border: 1px solid #334155;
1024
+ color: #f8fafc;
1025
+ padding: 8px 12px;
1026
+ font-size: 12.5px;
1027
+ border-radius: 6px;
1028
+ outline: none;
1029
+ font-family: var(--font-mono);
1030
+ }
1031
+ .console-input:focus {
1032
+ border-color: #38bdf8;
1033
+ }
1034
+ .console-send-btn {
1035
+ background: var(--brand-primary);
1036
+ color: #fff;
1037
+ border: none;
1038
+ padding: 8px 14px;
1039
+ border-radius: 6px;
1040
+ font-size: 12.5px;
1041
+ font-weight: 600;
1042
+ cursor: pointer;
1043
+ display: flex;
1044
+ align-items: center;
1045
+ gap: 4px;
1046
+ }
1047
+ .console-send-btn:hover {
1048
+ background: var(--brand-primary-hover);
1049
+ }
1050
+
1051
+ /* Command Palette Modal */
1052
+ .modal-overlay {
1053
+ position: fixed;
1054
+ inset: 0;
1055
+ background: rgba(15, 23, 42, 0.6);
1056
+ backdrop-filter: blur(3px);
1057
+ display: flex;
1058
+ align-items: center;
1059
+ justify-content: center;
1060
+ z-index: 2000;
1061
+ padding: 20px;
1062
+ }
1063
+ .modal-box {
1064
+ width: 560px;
1065
+ max-width: 100%;
1066
+ background: var(--bg-surface);
1067
+ border: 1px solid var(--border-subtle);
1068
+ border-radius: var(--radius-lg);
1069
+ box-shadow: var(--shadow-modal);
1070
+ overflow: hidden;
1071
+ animation: modalPop 0.18s cubic-bezier(0.16, 1, 0.3, 1);
1072
+ }
1073
+ @keyframes modalPop {
1074
+ from { transform: scale(0.96); opacity: 0; }
1075
+ to { transform: scale(1); opacity: 1; }
1076
+ }
1077
+ .modal-head {
1078
+ display: flex;
1079
+ align-items: center;
1080
+ justify-content: space-between;
1081
+ padding: 16px 20px;
1082
+ border-bottom: 1px solid var(--border-subtle);
1083
+ }
1084
+ .modal-head h3 {
1085
+ font-size: 15px;
1086
+ font-weight: 650;
1087
+ margin: 0;
1088
+ color: var(--text-main);
1089
+ }
1090
+ .modal-body {
1091
+ padding: 20px;
1092
+ display: grid;
1093
+ gap: 16px;
1094
+ }
1095
+ .modal-foot {
1096
+ padding: 12px 20px;
1097
+ background: #f8fafc;
1098
+ border-top: 1px solid var(--border-subtle);
1099
+ display: flex;
1100
+ justify-content: flex-end;
1101
+ gap: 8px;
1102
+ }
1103
+
1104
+ @media (max-width: 860px) {
1105
+ .layout { grid-template-columns: 1fr; }
1106
+ .nav {
1107
+ flex-direction: row;
1108
+ border-right: 0;
1109
+ border-bottom: 1px solid var(--border-subtle);
1110
+ padding: 8px 12px;
1111
+ overflow-x: auto;
1112
+ }
1113
+ .nav-group { flex-direction: row; }
1114
+ .nav-label, .nav-footer { display: none; }
1115
+ .nav button { width: auto; white-space: nowrap; }
1116
+ .main { padding: 16px; }
1117
+ .workspace { grid-template-columns: 1fr; height: auto; }
1118
+ .tree { max-height: 260px; border-right: 0; border-bottom: 1px solid var(--border-subtle); }
1119
+ .viewer { min-height: 480px; }
1120
+ .provider-grid, .cost-metrics { grid-template-columns: 1fr; }
1121
+ .topbar { padding: 0 14px; }
1122
+ .console-drawer { width: 100vw; right: 0; }
1123
+ }
12
1124
  </style>
13
1125
  </head>
14
1126
  <body>
15
1127
  <div class="shell">
16
- <header class="topbar"><div class="brand"><span class="brand-mark">ψ</span>PsyClaw Panel</div><div class="local">Local only</div></header>
1128
+ <!-- 顶栏 Header -->
1129
+ <header class="topbar">
1130
+ <div class="brand">
1131
+ <span class="brand-mark">ψ</span>
1132
+ <div class="brand-title">
1133
+ PsyClaw Panel
1134
+ <span class="brand-badge">Academic Studio</span>
1135
+ </div>
1136
+ </div>
1137
+ <div class="topbar-meta">
1138
+ <div class="topbar-cmds">
1139
+ <button id="cmd-init-btn" class="topbar-btn primary" title="初始化或配置社科研究工作区 (/init)">
1140
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M2 12h20"/></svg>
1141
+ /init 项目
1142
+ </button>
1143
+ <button id="cmd-plan-btn" class="topbar-btn" title="查看或确认统计分析方案 (/plan)">
1144
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
1145
+ /plan 方案
1146
+ </button>
1147
+ <button id="cmd-verify-btn" class="topbar-btn" title="关键字段人机核查清单 (/verify)">
1148
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
1149
+ /verify 核查
1150
+ </button>
1151
+ </div>
1152
+ <div class="local-badge">
1153
+ <span class="pulse-dot"></span>
1154
+ 安全沙盒 · 本地隔离
1155
+ </div>
1156
+ </div>
1157
+ </header>
1158
+
1159
+ <!-- 主体工作区 Layout -->
17
1160
  <div class="layout">
1161
+ <!-- 侧边导航栏 Sidebar -->
18
1162
  <nav class="nav" aria-label="Panel 导航">
19
- <button class="active" data-view="project">项目文件</button>
20
- <button data-view="ecosystem">Skill / Plugin / MCP / Agent</button>
21
- <button data-view="provider">Provider 与成本</button>
1163
+ <div class="nav-group">
1164
+ <div class="nav-label">工作台导航</div>
1165
+ <button class="active" data-view="project">
1166
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
1167
+ 项目文件
1168
+ </button>
1169
+ <button data-view="ecosystem">
1170
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
1171
+ 能力生态
1172
+ </button>
1173
+ <button data-view="provider">
1174
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
1175
+ 模型与成本
1176
+ </button>
1177
+ <button data-view="checklist">
1178
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
1179
+ 核对清单
1180
+ </button>
1181
+ <button data-view="chat">
1182
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
1183
+ 对话
1184
+ </button>
1185
+ </div>
1186
+ <div class="nav-footer">
1187
+ <div style="font-weight:600;margin-bottom:2px;color:var(--text-secondary)">PsyClaw 运行提示</div>
1188
+ 终端 <code>Shift+Tab</code> 切换模式;Thinking 用 <code>Ctrl+Shift+T</code>。
1189
+ </div>
22
1190
  </nav>
1191
+
1192
+ <!-- 主面板内容区 Main -->
23
1193
  <main class="main">
1194
+ <!-- 视图 1:项目文件 -->
24
1195
  <section id="view-project" class="view active">
25
- <div class="page-head"><div><h1>项目文件</h1><p>查看研究文档与 Export 导出结果;内容只读。</p></div><button id="refresh-project" class="button">刷新</button></div>
1196
+ <!-- 科研全流程导航条 Pipeline Stepper -->
1197
+ <div class="pipeline-stepper" id="pipeline-stepper">
1198
+ <span class="stepper-label">ARS 科研流水线:</span>
1199
+ <div class="step-item" data-stage="intake" title="探索性学术追问与目标澄清 (notes/goal.md)">
1200
+ <span>① 选题与入口</span>
1201
+ </div>
1202
+ <span class="step-arrow">➔</span>
1203
+ <div class="step-item" data-stage="evidence" title="证据溯源与文献账本 (notes/evidence.jsonl, claims.jsonl)">
1204
+ <span>② 证据账本</span>
1205
+ </div>
1206
+ <span class="step-arrow">➔</span>
1207
+ <div class="step-item" data-stage="plan" title="统计分析方案与 EDA (analysis/plans/)">
1208
+ <span>③ 统计方案</span>
1209
+ </div>
1210
+ <span class="step-arrow">➔</span>
1211
+ <div class="step-item" data-stage="manuscript" title="论文撰写与图表编排 (paper/, outputs/)">
1212
+ <span>④ 手稿编排</span>
1213
+ </div>
1214
+ <span class="step-arrow">➔</span>
1215
+ <div class="step-item" data-stage="review" title="多角色同行盲审与交叉核验 (notes/review.md)">
1216
+ <span>⑤ 严苛审稿</span>
1217
+ </div>
1218
+ </div>
1219
+
1220
+ <div class="page-head">
1221
+ <div>
1222
+ <h1>项目文件浏览器</h1>
1223
+ <p>只读浏览核心研究文档、Claim 证据账本与 Outputs 导出产物。</p>
1224
+ </div>
1225
+ <button id="refresh-project" class="button">
1226
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/></svg>
1227
+ 刷新文档
1228
+ </button>
1229
+ </div>
26
1230
  <div class="panel workspace">
27
- <aside id="file-tree" class="tree"><div class="empty">读取中...</div></aside>
1231
+ <aside id="file-tree" class="tree">
1232
+ <div class="empty">读取项目文档中...</div>
1233
+ </aside>
28
1234
  <section class="viewer">
29
- <div class="viewer-head"><div id="viewer-path" class="viewer-path">选择左侧文件</div><span id="viewer-kind" class="tag">只读</span></div>
30
- <div id="viewer-body" class="viewer-body empty">选择研究文档或 Export 结果查看</div>
1235
+ <div class="viewer-head">
1236
+ <div class="viewer-path-box">
1237
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--text-muted)"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
1238
+ <div id="viewer-path" class="viewer-path">选择左侧文件</div>
1239
+ </div>
1240
+ <div class="viewer-actions">
1241
+ <span id="viewer-kind" class="tag info">只读</span>
1242
+ <button id="viewer-copy-btn" class="button small" title="复制当前文档内容">
1243
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
1244
+ 复制
1245
+ </button>
1246
+ </div>
1247
+ </div>
1248
+ <div id="viewer-body" class="viewer-body empty">
1249
+ 请在左侧文件树选择研究文档、证据卡片或导出结果查看
1250
+ </div>
31
1251
  </section>
32
1252
  </div>
33
1253
  </section>
34
1254
 
1255
+ <!-- 视图 2:能力生态 -->
35
1256
  <section id="view-ecosystem" class="view">
36
- <div class="page-head"><div><h1>Skill / Plugin / MCP / Agent 生态</h1><p>在同一页面管理研究 Skill、Pi Plugin、MCP 与 Subagent(对齐 Claude Code)。配置变更后使用 <code>/reload</code>。</p></div><button id="refresh-ecosystem" class="button">刷新</button></div>
37
- <div class="controls" style="margin-bottom:12px"><input id="eco-search" placeholder="搜索名称或描述"><div class="segmented" aria-label="能力类型"><button class="active" data-eco-kind="all">全部</button><button data-eco-kind="skill">Skill</button><button data-eco-kind="plugin">Plugin</button><button data-eco-kind="mcp">MCP</button><button data-eco-kind="agent">Agent</button></div><select id="eco-kind" aria-label="更多类型"><option value="all">全部来源</option><option value="local">用户 Skill</option><option value="external">外部工具</option></select><select id="eco-status"><option value="all">全部状态</option><option value="enabled">已启用</option><option value="available">未启用</option></select></div>
38
- <div class="table-wrap"><table class="data"><thead><tr><th>名称</th><th>类型</th><th>来源</th><th>状态</th><th>操作</th></tr></thead><tbody id="ecosystem-body"><tr><td colspan="5">读取中...</td></tr></tbody></table></div>
1257
+ <div class="page-head">
1258
+ <div>
1259
+ <h1>Skill / Plugin / MCP / Agent 生态</h1>
1260
+ <p>集中发现并受控管理科研 Skill、Pi Plugin、MCP 服务与 Subagent。修改后在终端输入 <code>/reload</code> 即可热更新。</p>
1261
+ </div>
1262
+ <button id="refresh-ecosystem" class="button">
1263
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/></svg>
1264
+ 刷新生态
1265
+ </button>
1266
+ </div>
1267
+
1268
+ <div class="controls">
1269
+ <div class="search-wrapper">
1270
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
1271
+ <input id="eco-search" placeholder="搜索能力名称、描述或指令...">
1272
+ </div>
1273
+ <div class="segmented" aria-label="能力类型">
1274
+ <button class="active" data-eco-kind="all">全部类型</button>
1275
+ <button data-eco-kind="skill">Skill</button>
1276
+ <button data-eco-kind="plugin">Plugin</button>
1277
+ <button data-eco-kind="mcp">MCP</button>
1278
+ <button data-eco-kind="agent">Agent</button>
1279
+ </div>
1280
+ <select id="eco-kind" aria-label="更多类型">
1281
+ <option value="all">全部来源</option>
1282
+ <option value="local">用户 Skill</option>
1283
+ <option value="external">外部工具</option>
1284
+ </select>
1285
+ <select id="eco-status" aria-label="状态筛选">
1286
+ <option value="all">全部状态</option>
1287
+ <option value="enabled">已启用</option>
1288
+ <option value="available">未启用</option>
1289
+ </select>
1290
+ </div>
1291
+
1292
+ <div class="table-wrap">
1293
+ <table class="data">
1294
+ <thead>
1295
+ <tr>
1296
+ <th style="width:38%">能力名称与描述</th>
1297
+ <th style="width:14%">类型</th>
1298
+ <th style="width:18%">来源引用</th>
1299
+ <th style="width:15%">运行状态</th>
1300
+ <th style="width:15%;text-align:right">操作</th>
1301
+ </tr>
1302
+ </thead>
1303
+ <tbody id="ecosystem-body">
1304
+ <tr><td colspan="5" class="empty" style="min-height:120px">正在读取生态清单...</td></tr>
1305
+ </tbody>
1306
+ </table>
1307
+ </div>
39
1308
  </section>
40
1309
 
1310
+ <!-- 视图 3:Provider 与成本 -->
41
1311
  <section id="view-provider" class="view">
42
- <div class="page-head"><div><h1>Provider 与成本</h1><p>管理默认模型;成本统计来自全部本地 PsyClaw 会话,不会上传。</p></div><button id="refresh-provider" class="button">刷新</button></div>
1312
+ <div class="page-head">
1313
+ <div>
1314
+ <h1>Provider 与成本看板</h1>
1315
+ <p>配置默认模型通道;用量统计自动从全部本地 PsyClaw 会话中聚合,数据完全离线不上传。</p>
1316
+ </div>
1317
+ <button id="refresh-provider" class="button">
1318
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/></svg>
1319
+ 刷新看板
1320
+ </button>
1321
+ </div>
1322
+
43
1323
  <div class="provider-grid">
44
- <div class="panel section"><h2>当前 Provider</h2><div class="form"><div class="field"><label for="provider-select">Provider</label><select id="provider-select"></select></div><div class="field"><label for="model-select">Model</label><select id="model-select"></select></div><button id="save-provider" class="button primary">设为默认模型</button><div id="provider-feedback" class="feedback"></div></div></div>
45
- <div class="panel section"><h2>配置 API Key</h2><div class="form"><div class="field"><label for="provider-key">API Key(留空不会覆盖已有凭据)</label><input id="provider-key" type="password" autocomplete="off" placeholder="输入当前 Provider 的 Key"></div><button id="configure-provider" class="button">保存当前 Provider 配置</button><div id="config-feedback" class="feedback"></div></div></div>
1324
+ <!-- 默认模型选择 -->
1325
+ <div class="panel section">
1326
+ <div class="section-head">
1327
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--brand-primary)"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
1328
+ <h2>默认模型选择</h2>
1329
+ </div>
1330
+ <div class="form">
1331
+ <div class="field">
1332
+ <label for="provider-select">模型供应商 (Provider)</label>
1333
+ <select id="provider-select"></select>
1334
+ </div>
1335
+ <div class="field">
1336
+ <label for="model-select">模型型号 (Model)</label>
1337
+ <select id="model-select"></select>
1338
+ </div>
1339
+ <button id="save-provider" class="button primary" style="margin-top:4px">
1340
+ 设为默认模型
1341
+ </button>
1342
+ <div id="provider-feedback" class="feedback"></div>
1343
+ </div>
1344
+ </div>
1345
+
1346
+ <!-- API Key 配置 -->
1347
+ <div class="panel section">
1348
+ <div class="section-head">
1349
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--brand-primary)"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
1350
+ <h2>配置凭据 (API Key)</h2>
1351
+ </div>
1352
+ <div class="form">
1353
+ <div class="field">
1354
+ <label for="provider-key">API Key(留空不覆盖已有凭据)</label>
1355
+ <input id="provider-key" type="password" autocomplete="off" placeholder="输入当前 Provider 的专属 Key">
1356
+ </div>
1357
+ <p class="cost-note" style="margin:0">凭据委托 Pi 官方加密存储,不会在日志中明文打印。</p>
1358
+ <button id="configure-provider" class="button" style="margin-top:4px">
1359
+ 保存当前 Provider 配置
1360
+ </button>
1361
+ <div id="config-feedback" class="feedback"></div>
1362
+ </div>
1363
+ </div>
1364
+ </div>
1365
+
1366
+ <!-- 成本统计面板 -->
1367
+ <div class="cost panel section">
1368
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px">
1369
+ <div class="section-head" style="margin:0">
1370
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--brand-primary)"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
1371
+ <h2>Token 用量与成本统计</h2>
1372
+ </div>
1373
+ <div class="segmented">
1374
+ <button class="active" data-currency="cny">CNY (¥)</button>
1375
+ <button data-currency="usd">USD ($)</button>
1376
+ </div>
1377
+ </div>
1378
+
1379
+ <div id="cost-content">
1380
+ <div class="empty" style="min-height:160px">正在统计 Token 用量与成本...</div>
1381
+ </div>
1382
+ </div>
1383
+ </section>
1384
+
1385
+ <section id="view-checklist" class="view">
1386
+ <div class="page-head">
1387
+ <div>
1388
+ <h1>交叉核验清单</h1>
1389
+ <p>分析前/后由模型提出项,人在此勾选。可跳过,但会标注「未经核对」。本地写入 <code>.psyclaw/verify-checklist.json</code>。</p>
1390
+ </div>
1391
+ <div style="display:flex;gap:8px">
1392
+ <button id="refresh-checklist" class="button">刷新</button>
1393
+ <button id="skip-checklist" class="button" title="其余未核项标为跳过/未经核对">跳过未核项</button>
1394
+ </div>
1395
+ </div>
1396
+ <div class="panel section">
1397
+ <table>
1398
+ <thead>
1399
+ <tr><th>状态</th><th>ID</th><th>类别</th><th>阶段</th><th>说明</th><th>操作</th></tr>
1400
+ </thead>
1401
+ <tbody id="checklist-body">
1402
+ <tr><td colspan="6" class="empty">正在读取核对清单...</td></tr>
1403
+ </tbody>
1404
+ </table>
1405
+ </div>
1406
+ </section>
1407
+
1408
+ <section id="view-chat" class="view">
1409
+ <div class="page-head">
1410
+ <div>
1411
+ <h1>交互对话</h1>
1412
+ <p>整页对话视图(不再只依赖右下角抽屉)。打开本页时会把控制台嵌到此处。</p>
1413
+ </div>
46
1414
  </div>
47
- <div class="cost panel section"><div style="display:flex;justify-content:space-between;align-items:center;gap:12px"><h2>Token 与成本</h2><div class="segmented"><button class="active" data-currency="cny">CNY</button><button data-currency="usd">USD</button></div></div><div id="cost-content">读取中...</div></div>
1415
+ <div id="chat-console-host" class="panel section" style="min-height:62vh;padding:0;overflow:hidden;display:flex;flex-direction:column"></div>
48
1416
  </section>
49
1417
  </main>
50
1418
  </div>
51
1419
  </div>
1420
+
1421
+ <!-- 悬浮控制台入口 Floating Action Button -->
1422
+ <button id="console-fab" class="console-fab" title="打开/折叠 PsyClaw 交互控制台">
1423
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
1424
+ <span>PsyClaw 控制台</span>
1425
+ </button>
1426
+
1427
+ <!-- 抽屉式交互控制台 Console Drawer -->
1428
+ <div id="console-drawer" class="console-drawer">
1429
+ <div class="console-header">
1430
+ <div class="console-title">
1431
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:#38bdf8"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
1432
+ <span>科研会话终端</span>
1433
+ <span id="console-status-badge" class="console-status-tag">连线中</span>
1434
+ </div>
1435
+ <div class="console-actions">
1436
+ <button id="console-clear-btn" class="console-btn-icon" title="清空终端历史">
1437
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
1438
+ </button>
1439
+ <button id="console-close-btn" class="console-btn-icon" title="最小化">
1440
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
1441
+ </button>
1442
+ </div>
1443
+ </div>
1444
+
1445
+ <!-- 快捷指令快速贴入栏 -->
1446
+ <div class="console-quick-chips">
1447
+ <span class="chip" data-cmd="/plan status">/plan status</span>
1448
+ <span class="chip" data-cmd="/verify list">/verify list</span>
1449
+ <span class="chip" data-cmd="/grill">/grill 压力追问</span>
1450
+ <span class="chip" data-cmd="/review">/review 同行评审</span>
1451
+ <span class="chip" data-cmd="/agents">/agents 角色</span>
1452
+ </div>
1453
+
1454
+ <!-- 终端流式日志输出区域 -->
1455
+ <div id="console-log" class="console-log">
1456
+ <div class="log-entry">
1457
+ <div class="log-sys">ψ PsyClaw 交互控制台已就绪。支持直接提问或发送斜杠指令 (/init, /plan, /grill, /review, /verify 等)。</div>
1458
+ </div>
1459
+ </div>
1460
+
1461
+ <!-- 输入与触发栏 -->
1462
+ <div class="console-input-box">
1463
+ <input type="text" id="console-input" class="console-input" placeholder="输入科研指令或研究问题 (Enter 发送)..." autocomplete="off" />
1464
+ <button id="console-send-btn" class="console-send-btn">
1465
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
1466
+ 发送
1467
+ </button>
1468
+ </div>
1469
+ </div>
1470
+
1471
+ <!-- /init 项目向导模态框 -->
1472
+ <div id="init-modal" class="modal-overlay hidden">
1473
+ <div class="modal-box">
1474
+ <div class="modal-head">
1475
+ <h3>🚀 初始化研究项目 (/init)</h3>
1476
+ <button id="init-modal-close" class="console-btn-icon" style="color:var(--text-muted)">
1477
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
1478
+ </button>
1479
+ </div>
1480
+ <div class="modal-body">
1481
+ <div class="field">
1482
+ <label for="init-paradigm">研究范式 (Paradigm)</label>
1483
+ <select id="init-paradigm" class="select">
1484
+ <option value="survey-observational">survey-observational (问卷与实证观察研究)</option>
1485
+ <option value="experimental">experimental (随机受控实验 / 心理学实验室)</option>
1486
+ <option value="quasi-experimental">quasi-experimental (准实验 / 双重差分 / 断点回归)</option>
1487
+ <option value="qualitative-thematic">qualitative-thematic (质性扎根理论与主题分析)</option>
1488
+ <option value="meta-analysis">meta-analysis (元分析 / 效应量合并)</option>
1489
+ <option value="longitudinal-panel">longitudinal-panel (追踪面板数据)</option>
1490
+ <option value="mixed-methods">mixed-methods (混合研究方法)</option>
1491
+ <option value="policy-legal">policy-legal (政策制度与法学文本分析)</option>
1492
+ </select>
1493
+ </div>
1494
+ <div class="field">
1495
+ <label for="init-goal">研究问题与目标 (Research Goal)</label>
1496
+ <textarea id="init-goal" class="textarea" rows="3" placeholder="例如:探究社交媒体主动使用与大学生学业倦怠之间的中介效应机制..."></textarea>
1497
+ </div>
1498
+ <div id="init-preview-cmd" style="font-family:var(--font-mono);font-size:11.5px;background:var(--bg-surface-subtle);padding:8px 10px;border-radius:4px;border:1px solid var(--border-subtle);color:var(--text-secondary)">
1499
+ /init --paradigm survey-observational
1500
+ </div>
1501
+ </div>
1502
+ <div class="modal-foot">
1503
+ <button id="init-cancel-btn" class="button">取消</button>
1504
+ <button id="init-submit-btn" class="button primary">提交并生成指令</button>
1505
+ </div>
1506
+ </div>
1507
+ </div>
1508
+
52
1509
  <script>
53
- const state={view:"project",files:[],eco:[],ecoKind:"all",models:[],active:{},usage:null,currency:"cny"};
54
- const $=id=>document.getElementById(id);
55
- const esc=value=>String(value??"").replace(/[&<>"']/g,ch=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[ch]));
56
- async function json(url,options){const response=await fetch(url,options);const body=await response.json().catch(()=>({error:"请求失败"}));if(!response.ok)throw new Error(body.error||"请求失败");return body}
57
- function setView(view){state.view=view;document.querySelectorAll(".nav button").forEach(button=>button.classList.toggle("active",button.dataset.view===view));document.querySelectorAll(".view").forEach(section=>section.classList.toggle("active",section.id===`view-${view}`));if(view==="ecosystem")loadEcosystem();if(view==="provider")loadProvider()}
58
- document.querySelectorAll(".nav button").forEach(button=>button.addEventListener("click",()=>setView(button.dataset.view)));
59
-
60
- function inlineMarkdown(text){let value=esc(text);value=value.replace(/`([^`]+)`/g,"<code>$1</code>").replace(/\*\*([^*]+)\*\*/g,"<strong>$1</strong>").replace(/\*([^*]+)\*/g,"<em>$1</em>");value=value.replace(/\[([^\]]+)\]\(([^)]+)\)/g,(_,label,target)=>/^https?:\/\//i.test(target)?`<a href="${esc(target)}" target="_blank" rel="noreferrer">${label}</a>`:label);return value}
61
- function renderMarkdown(markdown){const lines=String(markdown).replace(/\r\n/g,"\n").split("\n"),out=[];let i=0,inCode=false,code=[];while(i<lines.length){const line=lines[i],trim=line.trim();if(trim.startsWith("```")){if(inCode){out.push(`<pre><code>${esc(code.join("\n"))}</code></pre>`);code=[];inCode=false}else inCode=true;i++;continue}if(inCode){code.push(line);i++;continue}const heading=trim.match(/^(#{1,3})\s+(.+)$/);if(heading){const level=heading[1].length;out.push(`<h${level}>${inlineMarkdown(heading[2])}</h${level}>`);i++;continue}if(trim.startsWith(">")){out.push(`<blockquote>${inlineMarkdown(trim.replace(/^>\s?/,""))}</blockquote>`);i++;continue}if(/^[-*+]\s+/.test(trim)){const items=[];while(i<lines.length&&/^[-*+]\s+/.test(lines[i].trim()))items.push(lines[i++].trim().replace(/^[-*+]\s+/,""));out.push(`<ul>${items.map(item=>`<li>${inlineMarkdown(item)}</li>`).join("")}</ul>`);continue}if(/^\d+\.\s+/.test(trim)){const items=[];while(i<lines.length&&/^\d+\.\s+/.test(lines[i].trim()))items.push(lines[i++].trim().replace(/^\d+\.\s+/,""));out.push(`<ol>${items.map(item=>`<li>${inlineMarkdown(item)}</li>`).join("")}</ol>`);continue}if(trim.startsWith("|")&&i+1<lines.length&&/^\|?[\s:|-]+\|/.test(lines[i+1].trim())){const cells=value=>value.replace(/^\||\|$/g,"").split("|").map(cell=>cell.trim());const head=cells(trim);i+=2;const rows=[];while(i<lines.length&&lines[i].trim().startsWith("|"))rows.push(cells(lines[i++]));out.push(`<table><thead><tr>${head.map(cell=>`<th>${inlineMarkdown(cell)}</th>`).join("")}</tr></thead><tbody>${rows.map(row=>`<tr>${row.map(cell=>`<td>${inlineMarkdown(cell)}</td>`).join("")}</tr>`).join("")}</tbody></table>`);continue}if(!trim){i++;continue}const para=[trim];i++;while(i<lines.length&&lines[i].trim()&&!/^(#{1,3})\s+/.test(lines[i].trim())&&!/^[-*+]\s+/.test(lines[i].trim())&&!/^\d+\.\s+/.test(lines[i].trim())&&!lines[i].trim().startsWith(">")&&!lines[i].trim().startsWith("```"))para.push(lines[i++].trim());out.push(`<p>${inlineMarkdown(para.join(" "))}</p>`)}if(inCode)out.push(`<pre><code>${esc(code.join("\n"))}</code></pre>`);return out.join("")}
62
- function groupFiles(files){const priority=["notes/goal.md","notes/research-spec.md","notes/plan.md","notes/decisions.md","notes/HANDOFF.md"];const ordered=[...files].sort((a,b)=>{const ai=priority.indexOf(a),bi=priority.indexOf(b);if(ai>=0||bi>=0)return(ai<0?99:ai)-(bi<0?99:bi);return a.localeCompare(b,"zh-CN")});const groups={"核心文档":ordered.filter(path=>priority.includes(path)),"Export 导出结果":ordered.filter(path=>path.startsWith("outputs/")),"论文与报告":ordered.filter(path=>/^(paper|docs)\//.test(path)&&!priority.includes(path)),"研究记录":ordered.filter(path=>/^(notes|logs)\//.test(path)&&!priority.includes(path))};return groups}
63
- function renderTree(){const tree=$("file-tree"),groups=groupFiles(state.files);let html="";for(const[name,files]of Object.entries(groups)){if(!files.length)continue;html+=`<div class="tree-section">${esc(name)}</div>${files.map(path=>`<button class="tree-item" data-file="${esc(path)}"><span>◇</span><span>${esc(path.split("/").at(-1))}</span></button>`).join("")}`}tree.innerHTML=html||'<div class="empty">暂无项目文件或 Export 结果</div>';tree.querySelectorAll("[data-file]").forEach(button=>button.addEventListener("click",()=>openFile(button.dataset.file,button)))}
64
- function selectTree(button){document.querySelectorAll(".tree-item").forEach(item=>item.classList.toggle("active",item===button))}
65
- async function openFile(path,button){selectTree(button);$("viewer-path").textContent=path;$("viewer-kind").textContent="读取中";$("viewer-body").className="viewer-body empty";$("viewer-body").textContent="读取中...";try{const file=await json(`/api/project-file?path=${encodeURIComponent(path)}`);$("viewer-kind").textContent=file.format;$("viewer-body").className="viewer-body";$("viewer-body").innerHTML=file.format==="markdown"?`<article class="markdown">${renderMarkdown(file.content)}</article>`:`<pre class="raw">${esc(file.format==="json"?JSON.stringify(JSON.parse(file.content),null,2):file.content)}</pre>`}catch(error){$("viewer-body").className="viewer-body empty";$("viewer-body").textContent=error.message}}
66
- async function loadProject(){const files=await json("/api/project-files");state.files=files.files||[];renderTree();const goal=document.querySelector('[data-file="notes/goal.md"]')||document.querySelector("[data-file]");if(goal)goal.click()}
67
-
68
- async function loadEcosystem(){try{const[skills,plugins,mcps,agents,enabled]=await Promise.all([json("/api/recommended-skills"),json("/api/recommended-plugins"),json("/api/recommended-mcps"),json("/api/recommended-agents"),json("/api/enabled-capabilities")]);const skillSet=new Set((enabled.skills||[]).map(item=>item.id)),mcpSet=new Set((enabled.mcp||[]).map(item=>item.id));state.eco=[...(enabled.coreSkills||[]).map(item=>({...item,kind:"core",description:item.description||"PsyClaw 内置 Skill",sourceRef:"bundled",enabled:item.enabled,installed:true})),...(skills.items||[]).map(item=>({...item,kind:"skill",enabled:skillSet.has(item.id),installed:skillSet.has(item.id)})),...(plugins.items||[]).map(item=>({...item,kind:"plugin",enabled:Boolean(item.enabled),installed:Boolean(item.installed)})),...(enabled.localSkills||[]).map(item=>({...item,kind:"local",sourceRef:item.path||"用户目录",installed:true})),...(mcps.items||[]).map(item=>({...item,kind:"mcp",enabled:mcpSet.has(item.id),installed:mcpSet.has(item.id)})),...(agents.items||[]).map(item=>({...item,kind:"agent",enabled:Boolean(item.enabled),installed:Boolean(item.installed),locked:Boolean(item.locked)})),...(skills.externalTools||[]).map(item=>({...item,kind:"external",enabled:false,installed:false}))];renderEcosystem()}catch(error){$("ecosystem-body").innerHTML=`<tr><td colspan="5">${esc(error.message)}</td></tr>`}}
69
- function renderEcosystem(){const query=$("eco-search").value.trim().toLowerCase(),sourceKind=$("eco-kind").value,status=$("eco-status").value,primaryKind=state.ecoKind;const primaryMatches=item=>primaryKind==="all"||(primaryKind==="skill"?(item.kind==="skill"||item.kind==="core"||item.kind==="local"):item.kind===primaryKind);const items=state.eco.filter(item=>primaryMatches(item)&&(sourceKind==="all"||item.kind===sourceKind)&&(status==="all"||(status==="enabled"?item.enabled:!item.enabled))&&`${item.name} ${item.description||""}`.toLowerCase().includes(query));$("ecosystem-body").innerHTML=items.map(item=>`<tr><td><div class="name">${esc(item.name)}</div><div class="subtle">${esc(item.description||"")}${item.kind==="plugin"&&item.scope?` · ${item.scope==="project"?"当前项目":"所有项目"}${item.filtered?" · 已限定资源范围":""}`:""}${item.kind==="agent"&&item.slashCommand?` · ${esc(item.slashCommand)}`:""}</div></td><td>${esc(item.kind==="core"?"Core Skill":item.kind==="local"?"用户 Skill":item.kind==="agent"?"Agent":item.kind.toUpperCase())}</td><td>${/^https?:/.test(item.sourceRef||"")?`<a href="${esc(item.sourceRef)}" target="_blank" rel="noreferrer">来源</a>`:esc(item.sourceRef||"bundled")}</td><td><span class="tag ${item.enabled?"ok":item.configured?"warn":""}">${item.locked?"内置可用":item.enabled?(item.kind==="plugin"?"已安装":"已启用"):item.kind==="plugin"&&item.configured?"配置存在,文件缺失":item.kind==="external"?"可安装外部工具":"未启用"}</span></td><td>${item.locked||item.kind==="agent"?"—":item.kind==="plugin"&&item.local?"—":item.kind==="plugin"?`<button class="button small ${item.installed?"":"primary"}" onclick="installCapability('plugin','${esc(item.id)}')">${item.installed?"重新安装":"安装"}</button>`:item.kind==="external"?`<button class="button small primary" onclick="installCapability('external','${esc(item.id)}')">安装</button>`:item.enabled?`<button class="button small" onclick="toggleCapability('${item.kind}','${esc(item.id)}',false)">停用</button>`:item.kind==="local"?`<button class="button small primary" onclick="toggleCapability('local','${esc(item.id)}',true)">启用</button>`:`<button class="button small primary" onclick="installCapability('${item.kind}','${esc(item.id)}')">${item.kind==="core"?"启用":"安装"}</button>`}</td></tr>`).join("")||'<tr><td colspan="5">没有匹配项</td></tr>'}
70
- async function toggleCapability(kind,id,enabled){await json("/api/recommendation-state",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({kind:kind==="core"?"core-skill":kind==="skill"?"skills":kind==="local"?"local-skill":kind,id,enabled})});await loadEcosystem()}
71
- async function installCapability(kind,id){if(kind==="core"){await toggleCapability(kind,id,true);return}const scope=kind==="skill"||kind==="plugin"?(confirm("安装到系统目录?\n确定:所有项目使用;取消:仅当前项目使用。")?"user":"project"):undefined;if(!confirm(`确认安装 ${id}?${kind==="external"?"模型将按官方来源安装并验证。":"安装完成后需要 /reload。"}`))return;const result=await json("/api/install/execute",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({kind,id,scope,approved:true,actor:"researcher"})});alert(result.message||(result.ok?"安装任务已处理。":"安装未完成"));await loadEcosystem()}
72
- [$("eco-search"),$("eco-kind"),$("eco-status")].forEach(control=>control.addEventListener("input",renderEcosystem));
73
- document.querySelectorAll("[data-eco-kind]").forEach(button=>button.addEventListener("click",()=>{state.ecoKind=button.dataset.ecoKind;document.querySelectorAll("[data-eco-kind]").forEach(item=>item.classList.toggle("active",item===button));renderEcosystem()}));
74
-
75
- function providerModels(provider){return state.models.filter(model=>model.provider===provider)}
76
- function updateModels(){const models=providerModels($("provider-select").value);$("model-select").innerHTML=models.map(model=>`<option value="${esc(model.version||model.id.split("/").slice(1).join("/"))}">${esc(model.name)}</option>`).join("");if(state.active.provider===$("provider-select").value&&state.active.model)$("model-select").value=state.active.model}
77
- async function loadProvider(){try{const[catalog,active,usage]=await Promise.all([json("/api/catalog"),json("/api/active-provider"),json("/api/token-usage")]);state.models=catalog.models||[];state.active=active||{};state.usage=usage;const providers=[...new Set(state.models.map(model=>model.provider))].sort();$("provider-select").innerHTML=providers.map(provider=>`<option value="${esc(provider)}">${esc(provider)}</option>`).join("");if(active.provider&&providers.includes(active.provider))$("provider-select").value=active.provider;updateModels();renderCost()}catch(error){$("provider-feedback").textContent=error.message}}
78
- function currentModel(){return providerModels($("provider-select").value).find(model=>(model.version||model.id.split("/").slice(1).join("/"))===$("model-select").value)}
79
- $("provider-select").addEventListener("change",updateModels);
80
- $("save-provider").addEventListener("click",async()=>{const feedback=$("provider-feedback");feedback.textContent="保存中...";try{await json("/api/active-provider",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({provider:$("provider-select").value,model:$("model-select").value})});feedback.textContent="已保存,重启会话后生效。"}catch(error){feedback.textContent=error.message}});
81
- $("configure-provider").addEventListener("click",async()=>{const feedback=$("config-feedback"),model=currentModel(),key=$("provider-key").value;if(!model){feedback.textContent="请选择模型";return}feedback.textContent="保存中...";try{await json("/api/provider-config",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({id:model.provider,name:model.provider,baseUrl:model.endpoint,api:model.provider==="anthropic"?"anthropic-messages":"openai-completions",apiKeyEnv:model.apiKeyEnv,modelId:$("model-select").value,...(key?{apiKey:key}:{})})});$("provider-key").value="";feedback.textContent="配置已保存,重启会话后生效。"}catch(error){feedback.textContent=error.message}});
82
- function money(usd,cny){return state.currency==="usd"?`$${Number(usd||0).toFixed(4)}`:`¥${Number(cny||0).toFixed(4)}`}
83
- function renderCost(){const usage=state.usage||{},totals=usage.totals||{},rows=usage.byModel||[];$("cost-content").innerHTML=`<div class="cost-metrics"><div class="metric"><small>总 Token</small><strong>${Number(totals.tokens||0).toLocaleString()}</strong></div><div class="metric"><small>实际费用</small><strong>${money(totals.cost,totals.costCny)}</strong></div><div class="metric"><small>预估费用</small><strong>${money(totals.estimatedCost,totals.estimatedCostCny)}</strong></div></div><p class="cost-note">统计范围:全部本地 PsyClaw 会话。实际账单以 Provider 为准。</p><div class="table-wrap"><table class="data"><thead><tr><th>模型</th><th>输入</th><th>输出</th><th>缓存</th><th>Token</th><th>预估费用</th></tr></thead><tbody>${rows.map(row=>`<tr><td><div class="name">${esc(row.model)}</div><div class="subtle">${esc(row.provider)}</div></td><td>${Number(row.input||0).toLocaleString()}</td><td>${Number(row.output||0).toLocaleString()}</td><td>${Number((row.cacheRead||0)+(row.cacheWrite||0)).toLocaleString()}</td><td>${Number(row.tokens||0).toLocaleString()}</td><td>${money(row.estimatedCost,row.estimatedCostCny)}</td></tr>`).join("")||'<tr><td colspan="6">暂无用量记录</td></tr>'}</tbody></table></div>`}
84
- document.querySelectorAll("[data-currency]").forEach(button=>button.addEventListener("click",()=>{state.currency=button.dataset.currency;document.querySelectorAll("[data-currency]").forEach(item=>item.classList.toggle("active",item===button));renderCost()}));
85
- $("refresh-project").addEventListener("click",loadProject);$("refresh-ecosystem").addEventListener("click",loadEcosystem);$("refresh-provider").addEventListener("click",loadProvider);
86
- loadProject().catch(error=>{$("viewer-body").textContent=error.message});
1510
+ const state = {
1511
+ view: "project",
1512
+ files: [],
1513
+ eco: [],
1514
+ ecoKind: "all",
1515
+ models: [],
1516
+ active: {},
1517
+ usage: null,
1518
+ currency: "cny",
1519
+ currentContent: ""
1520
+ };
1521
+
1522
+ const $ = id => document.getElementById(id);
1523
+ const esc = value => String(value ?? "").replace(/[&<>"']/g, ch => ({
1524
+ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"
1525
+ }[ch]));
1526
+
1527
+ async function json(url, options) {
1528
+ const response = await fetch(url, options);
1529
+ const body = await response.json().catch(() => ({ error: "请求解析失败" }));
1530
+ if (!response.ok) throw new Error(body.error || `请求失败 (${response.status})`);
1531
+ return body;
1532
+ }
1533
+
1534
+ function setView(view) {
1535
+ state.view = view;
1536
+ document.querySelectorAll(".nav button").forEach(button => {
1537
+ button.classList.toggle("active", button.dataset.view === view);
1538
+ });
1539
+ document.querySelectorAll(".view").forEach(section => {
1540
+ section.classList.toggle("active", section.id === `view-${view}`);
1541
+ });
1542
+ if (view === "ecosystem") loadEcosystem();
1543
+ if (view === "provider") loadProvider();
1544
+ if (view === "checklist") loadChecklist();
1545
+ if (view === "chat") mountConsoleInChat();
1546
+ else restoreConsoleDrawer();
1547
+ }
1548
+
1549
+ function mountConsoleInChat() {
1550
+ const host = $("chat-console-host");
1551
+ const drawer = $("console-drawer");
1552
+ if (!host || !drawer) return;
1553
+ host.appendChild(drawer);
1554
+ drawer.classList.add("open");
1555
+ drawer.style.position = "relative";
1556
+ drawer.style.inset = "auto";
1557
+ drawer.style.width = "100%";
1558
+ drawer.style.height = "100%";
1559
+ drawer.style.minHeight = "58vh";
1560
+ drawer.style.boxShadow = "none";
1561
+ drawer.style.borderRadius = "0";
1562
+ }
1563
+
1564
+ function restoreConsoleDrawer() {
1565
+ const drawer = $("console-drawer");
1566
+ if (!drawer || drawer.parentElement === document.body) return;
1567
+ document.body.appendChild(drawer);
1568
+ drawer.style.position = "";
1569
+ drawer.style.inset = "";
1570
+ drawer.style.width = "";
1571
+ drawer.style.height = "";
1572
+ drawer.style.minHeight = "";
1573
+ drawer.style.boxShadow = "";
1574
+ drawer.style.borderRadius = "";
1575
+ }
1576
+
1577
+ async function loadChecklist() {
1578
+ const body = $("checklist-body");
1579
+ if (!body) return;
1580
+ body.innerHTML = `<tr><td colspan="6" class="empty">正在读取核对清单...</td></tr>`;
1581
+ try {
1582
+ const data = await json("/api/crosscheck");
1583
+ const items = data.checklist?.items || [];
1584
+ body.innerHTML = items.map(item => {
1585
+ const status = item.status || "unverified";
1586
+ return `<tr>
1587
+ <td>${esc(status)}</td>
1588
+ <td><code>${esc(item.id)}</code></td>
1589
+ <td>${esc(item.kind || "general")}</td>
1590
+ <td>${esc(item.phase || "general")}</td>
1591
+ <td>${esc(item.label)}${item.notes ? `<div style="color:var(--text-muted);font-size:11px">${esc(item.notes)}</div>` : ""}</td>
1592
+ <td style="white-space:nowrap">
1593
+ <button class="button" data-check-id="${esc(item.id)}" data-check-status="verified">核实</button>
1594
+ <button class="button" data-check-id="${esc(item.id)}" data-check-status="flagged">存疑</button>
1595
+ <button class="button" data-check-id="${esc(item.id)}" data-check-status="skipped">跳过</button>
1596
+ </td>
1597
+ </tr>`;
1598
+ }).join("") || `<tr><td colspan="6" class="empty">清单为空</td></tr>`;
1599
+ body.querySelectorAll("[data-check-id]").forEach(btn => {
1600
+ btn.addEventListener("click", async () => {
1601
+ await json("/api/crosscheck", {
1602
+ method: "POST",
1603
+ headers: { "content-type": "application/json" },
1604
+ body: JSON.stringify({ id: btn.dataset.checkId, status: btn.dataset.checkStatus }),
1605
+ });
1606
+ await loadChecklist();
1607
+ });
1608
+ });
1609
+ } catch (error) {
1610
+ body.innerHTML = `<tr><td colspan="6" class="empty">加载失败: ${esc(error.message)}</td></tr>`;
1611
+ }
1612
+ }
1613
+
1614
+ $("refresh-checklist")?.addEventListener("click", loadChecklist);
1615
+ $("skip-checklist")?.addEventListener("click", async () => {
1616
+ if (!confirm("将把所有未核项标为跳过,并写入「未经核对」。继续?")) return;
1617
+ await json("/api/crosscheck", {
1618
+ method: "POST",
1619
+ headers: { "content-type": "application/json" },
1620
+ body: JSON.stringify({ action: "skip" }),
1621
+ });
1622
+ await loadChecklist();
1623
+ });
1624
+
1625
+ document.querySelectorAll(".nav button").forEach(button => {
1626
+ button.addEventListener("click", () => setView(button.dataset.view));
1627
+ });
1628
+
1629
+ function inlineMarkdown(text) {
1630
+ let value = esc(text);
1631
+ value = value.replace(/`([^`]+)`/g, "<code>$1</code>")
1632
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
1633
+ .replace(/\*([^*]+)\*/g, "<em>$1</em>");
1634
+ value = value.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, target) =>
1635
+ /^https?:\/\//i.test(target)
1636
+ ? `<a href="${esc(target)}" target="_blank" rel="noreferrer" style="color:var(--brand-accent);text-decoration:none;border-bottom:1px dashed currentColor">${label}</a>`
1637
+ : label
1638
+ );
1639
+ return value;
1640
+ }
1641
+
1642
+ function renderMarkdown(markdown) {
1643
+ const lines = String(markdown).replace(/\r\n/g, "\n").split("\n"), out = [];
1644
+ let i = 0, inCode = false, code = [];
1645
+ while (i < lines.length) {
1646
+ const line = lines[i], trim = line.trim();
1647
+ if (trim.startsWith("```")) {
1648
+ if (inCode) {
1649
+ out.push(`<pre><code>${esc(code.join("\n"))}</code></pre>`);
1650
+ code = [];
1651
+ inCode = false;
1652
+ } else {
1653
+ inCode = true;
1654
+ }
1655
+ i++;
1656
+ continue;
1657
+ }
1658
+ if (inCode) {
1659
+ code.push(line);
1660
+ i++;
1661
+ continue;
1662
+ }
1663
+ const heading = trim.match(/^(#{1,3})\s+(.+)$/);
1664
+ if (heading) {
1665
+ const level = heading[1].length;
1666
+ out.push(`<h${level}>${inlineMarkdown(heading[2])}</h${level}>`);
1667
+ i++;
1668
+ continue;
1669
+ }
1670
+ if (trim.startsWith(">")) {
1671
+ out.push(`<blockquote><p>${inlineMarkdown(trim.replace(/^>\s?/, ""))}</p></blockquote>`);
1672
+ i++;
1673
+ continue;
1674
+ }
1675
+ if (/^[-*+]\s+/.test(trim)) {
1676
+ const items = [];
1677
+ while (i < lines.length && /^[-*+]\s+/.test(lines[i].trim())) {
1678
+ items.push(lines[i++].trim().replace(/^[-*+]\s+/, ""));
1679
+ }
1680
+ out.push(`<ul>${items.map(item => `<li>${inlineMarkdown(item)}</li>`).join("")}</ul>`);
1681
+ continue;
1682
+ }
1683
+ if (/^\d+\.\s+/.test(trim)) {
1684
+ const items = [];
1685
+ while (i < lines.length && /^\d+\.\s+/.test(lines[i].trim())) {
1686
+ items.push(lines[i++].trim().replace(/^\d+\.\s+/,""));
1687
+ }
1688
+ out.push(`<ol>${items.map(item => `<li>${inlineMarkdown(item)}</li>`).join("")}</ol>`);
1689
+ continue;
1690
+ }
1691
+ if (trim.startsWith("|") && i + 1 < lines.length && /^\|?[\s:|-]+\|/.test(lines[i + 1].trim())) {
1692
+ const cells = value => value.replace(/^\||\|$/g, "").split("|").map(cell => cell.trim());
1693
+ const head = cells(trim);
1694
+ i += 2;
1695
+ const rows = [];
1696
+ while (i < lines.length && lines[i].trim().startsWith("|")) {
1697
+ rows.push(cells(lines[i++]));
1698
+ }
1699
+ out.push(`<table><thead><tr>${head.map(cell => `<th>${inlineMarkdown(cell)}</th>`).join("")}</tr></thead><tbody>${rows.map(row => `<tr>${row.map(cell => `<td>${inlineMarkdown(cell)}</td>`).join("")}</tr>`).join("")}</tbody></table>`);
1700
+ continue;
1701
+ }
1702
+ if (!trim) {
1703
+ i++;
1704
+ continue;
1705
+ }
1706
+ const para = [trim];
1707
+ i++;
1708
+ while (i < lines.length && lines[i].trim() && !/^(#{1,3})\s+/.test(lines[i].trim()) && !/^[-*+]\s+/.test(lines[i].trim()) && !/^\d+\.\s+/.test(lines[i].trim()) && !lines[i].trim().startsWith(">") && !lines[i].trim().startsWith("```")) {
1709
+ para.push(lines[i++].trim());
1710
+ }
1711
+ out.push(`<p>${inlineMarkdown(para.join(" "))}</p>`);
1712
+ }
1713
+ if (inCode) out.push(`<pre><code>${esc(code.join("\n"))}</code></pre>`);
1714
+ return out.join("");
1715
+ }
1716
+
1717
+ function getFileIcon(path) {
1718
+ if (path.endsWith(".md")) {
1719
+ return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`;
1720
+ }
1721
+ if (path.endsWith(".json")) {
1722
+ return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>`;
1723
+ }
1724
+ if (path.startsWith("outputs/")) {
1725
+ return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 14 14"/></svg>`;
1726
+ }
1727
+ return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>`;
1728
+ }
1729
+
1730
+ function groupFiles(files) {
1731
+ const priority = ["notes/goal.md", "notes/research-spec.md", "notes/plan.md", "notes/decisions.md", "notes/HANDOFF.md"];
1732
+ const ordered = [...files].sort((a, b) => {
1733
+ const ai = priority.indexOf(a), bi = priority.indexOf(b);
1734
+ if (ai >= 0 || bi >= 0) return (ai < 0 ? 99 : ai) - (bi < 0 ? 99 : bi);
1735
+ return a.localeCompare(b, "zh-CN");
1736
+ });
1737
+ return {
1738
+ "核心文档": ordered.filter(path => priority.includes(path)),
1739
+ "Export 导出结果": ordered.filter(path => path.startsWith("outputs/")),
1740
+ "论文与报告": ordered.filter(path => /^(paper|docs)\//.test(path) && !priority.includes(path)),
1741
+ "研究记录": ordered.filter(path => /^(notes|logs)\//.test(path) && !priority.includes(path))
1742
+ };
1743
+ }
1744
+
1745
+ function renderTree() {
1746
+ const tree = $("file-tree"), groups = groupFiles(state.files);
1747
+ let html = "";
1748
+ for (const [name, files] of Object.entries(groups)) {
1749
+ if (!files.length) continue;
1750
+ html += `<div class="tree-section">${esc(name)}</div>${files.map(path => `
1751
+ <button class="tree-item" data-file="${esc(path)}">
1752
+ ${getFileIcon(path)}
1753
+ <span>${esc(path.split("/").at(-1))}</span>
1754
+ </button>`).join("")}`;
1755
+ }
1756
+ tree.innerHTML = html || '<div class="empty">暂无项目文件或 Export 导出结果</div>';
1757
+ tree.querySelectorAll("[data-file]").forEach(button => {
1758
+ button.addEventListener("click", () => openFile(button.dataset.file, button));
1759
+ });
1760
+ }
1761
+
1762
+ function selectTree(button) {
1763
+ document.querySelectorAll(".tree-item").forEach(item => item.classList.toggle("active", item === button));
1764
+ }
1765
+
1766
+ async function openFile(path, button) {
1767
+ selectTree(button);
1768
+ $("viewer-path").textContent = path;
1769
+ $("viewer-kind").textContent = "读取中";
1770
+ $("viewer-body").className = "viewer-body empty";
1771
+ $("viewer-body").textContent = "正在加载文件内容...";
1772
+ try {
1773
+ const file = await json(`/api/project-file?path=${encodeURIComponent(path)}`);
1774
+ state.currentContent = file.content;
1775
+ $("viewer-kind").textContent = file.format ? file.format.toUpperCase() : "READONLY";
1776
+ $("viewer-body").className = "viewer-body";
1777
+ $("viewer-body").innerHTML = file.format === "markdown"
1778
+ ? `<article class="markdown">${renderMarkdown(file.content)}</article>`
1779
+ : `<pre class="raw">${esc(file.format === "json" ? JSON.stringify(JSON.parse(file.content), null, 2) : file.content)}</pre>`;
1780
+ } catch (error) {
1781
+ state.currentContent = "";
1782
+ $("viewer-body").className = "viewer-body empty";
1783
+ $("viewer-body").textContent = `读取失败: ${error.message}`;
1784
+ }
1785
+ }
1786
+
1787
+ // 复制文件内容按钮
1788
+ $("viewer-copy-btn").addEventListener("click", async () => {
1789
+ if (!state.currentContent) return;
1790
+ try {
1791
+ await navigator.clipboard.writeText(state.currentContent);
1792
+ const originalText = $("viewer-copy-btn").innerHTML;
1793
+ $("viewer-copy-btn").innerHTML = `
1794
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="color:var(--ok-text)"><polyline points="20 6 9 17 4 12"/></svg>
1795
+ 已复制
1796
+ `;
1797
+ setTimeout(() => { $("viewer-copy-btn").innerHTML = originalText; }, 1800);
1798
+ } catch {
1799
+ alert("无法写入剪贴板,请手动复制。");
1800
+ }
1801
+ });
1802
+
1803
+ async function loadProject() {
1804
+ const files = await json("/api/project-files");
1805
+ state.files = files.files || [];
1806
+ renderTree();
1807
+ const goal = document.querySelector('[data-file="notes/goal.md"]') || document.querySelector("[data-file]");
1808
+ if (goal) goal.click();
1809
+ }
1810
+
1811
+ async function loadEcosystem() {
1812
+ try {
1813
+ const [skills, plugins, mcps, agents, enabled] = await Promise.all([
1814
+ json("/api/recommended-skills"),
1815
+ json("/api/recommended-plugins"),
1816
+ json("/api/recommended-mcps"),
1817
+ json("/api/recommended-agents"),
1818
+ json("/api/enabled-capabilities")
1819
+ ]);
1820
+ const skillSet = new Set((enabled.skills || []).map(item => item.id));
1821
+ const mcpSet = new Set((enabled.mcp || []).map(item => item.id));
1822
+ state.eco = [
1823
+ ...(enabled.coreSkills || []).map(item => ({ ...item, kind: "core", description: item.description || "PsyClaw 内置科研核心 Skill", sourceRef: "bundled", enabled: item.enabled, installed: true })),
1824
+ ...(skills.items || []).map(item => ({ ...item, kind: "skill", enabled: skillSet.has(item.id), installed: skillSet.has(item.id) })),
1825
+ ...(plugins.items || []).map(item => ({ ...item, kind: "plugin", enabled: Boolean(item.enabled), installed: Boolean(item.installed) })),
1826
+ ...(enabled.localSkills || []).map(item => ({ ...item, kind: "local", sourceRef: item.path || "用户配置目录", installed: true })),
1827
+ ...(mcps.items || []).map(item => ({ ...item, kind: "mcp", enabled: mcpSet.has(item.id), installed: mcpSet.has(item.id) })),
1828
+ ...(agents.items || []).map(item => ({ ...item, kind: "agent", enabled: Boolean(item.enabled), installed: Boolean(item.installed), locked: Boolean(item.locked) })),
1829
+ ...(skills.externalTools || []).map(item => ({ ...item, kind: "external", enabled: false, installed: false }))
1830
+ ];
1831
+ renderEcosystem();
1832
+ } catch (error) {
1833
+ $("ecosystem-body").innerHTML = `<tr><td colspan="5" class="empty" style="min-height:100px;color:var(--bad-text)">加载能力生态失败: ${esc(error.message)}</td></tr>`;
1834
+ }
1835
+ }
1836
+
1837
+ function renderEcosystem() {
1838
+ const query = $("eco-search").value.trim().toLowerCase();
1839
+ const sourceKind = $("eco-kind").value;
1840
+ const status = $("eco-status").value;
1841
+ const primaryKind = state.ecoKind;
1842
+ const primaryMatches = item => primaryKind === "all" || (primaryKind === "skill" ? (item.kind === "skill" || item.kind === "core" || item.kind === "local") : item.kind === primaryKind);
1843
+ const items = state.eco.filter(item =>
1844
+ primaryMatches(item) &&
1845
+ (sourceKind === "all" || item.kind === sourceKind) &&
1846
+ (status === "all" || (status === "enabled" ? item.enabled : !item.enabled)) &&
1847
+ `${item.name} ${item.description || ""}`.toLowerCase().includes(query)
1848
+ );
1849
+
1850
+ $("ecosystem-body").innerHTML = items.map(item => `
1851
+ <tr>
1852
+ <td>
1853
+ <div class="name">${esc(item.name)}</div>
1854
+ <div class="subtle">
1855
+ ${esc(item.description || "")}
1856
+ ${item.kind === "plugin" && item.scope ? ` · ${item.scope === "project" ? "当前项目" : "所有项目"}${item.filtered ? " · 已限定资源" : ""}` : ""}
1857
+ ${item.kind === "agent" && item.slashCommand ? ` · <code style="font-family:var(--font-mono);font-size:11px">${esc(item.slashCommand)}</code>` : ""}
1858
+ </div>
1859
+ </td>
1860
+ <td>
1861
+ <span class="tag" style="background:#f1f5f9;font-weight:600">
1862
+ ${esc(item.kind === "core" ? "Core Skill" : item.kind === "local" ? "用户 Skill" : item.kind === "agent" ? "Subagent" : item.kind.toUpperCase())}
1863
+ </span>
1864
+ </td>
1865
+ <td>
1866
+ <span style="font-family:var(--font-mono);font-size:11.5px;color:var(--text-muted)">
1867
+ ${/^https?:/.test(item.sourceRef || "") ? `<a href="${esc(item.sourceRef)}" target="_blank" rel="noreferrer" style="color:var(--brand-accent);text-decoration:none">来源链接</a>` : esc(item.sourceRef || "bundled")}
1868
+ </span>
1869
+ </td>
1870
+ <td>
1871
+ <span class="tag ${item.enabled ? "ok" : item.configured ? "warn" : ""}">
1872
+ ${item.locked ? "内置可用" : item.enabled ? (item.kind === "plugin" ? "已安装" : "已启用") : item.kind === "plugin" && item.configured ? "配置存在,文件缺失" : item.kind === "external" ? "可安装外部工具" : "未启用"}
1873
+ </span>
1874
+ </td>
1875
+ <td style="text-align:right">
1876
+ ${item.locked || item.kind === "agent" ? "—" :
1877
+ item.kind === "plugin" && item.local ? "—" :
1878
+ item.kind === "plugin" ? `<button class="button small ${item.installed ? "" : "primary"}" onclick="installCapability('plugin','${esc(item.id)}')">${item.installed ? "重新安装" : "安装"}</button>` :
1879
+ item.kind === "external" ? `<button class="button small primary" onclick="installCapability('external','${esc(item.id)}')">安装</button>` :
1880
+ item.enabled ? `<button class="button small" onclick="toggleCapability('${item.kind}','${esc(item.id)}',false)">停用</button>` :
1881
+ item.kind === "local" ? `<button class="button small primary" onclick="toggleCapability('local','${esc(item.id)}',true)">启用</button>` :
1882
+ `<button class="button small primary" onclick="installCapability('${item.kind}','${esc(item.id)}')">${item.kind === "core" ? "启用" : "安装"}</button>`
1883
+ }
1884
+ </td>
1885
+ </tr>
1886
+ `).join("") || '<tr><td colspan="5" class="empty" style="min-height:100px">未找到匹配的能力项</td></tr>';
1887
+ }
1888
+
1889
+ async function toggleCapability(kind, id, enabled) {
1890
+ await json("/api/recommendation-state", {
1891
+ method: "POST",
1892
+ headers: { "content-type": "application/json" },
1893
+ body: JSON.stringify({
1894
+ kind: kind === "core" ? "core-skill" : kind === "skill" ? "skills" : kind === "local" ? "local-skill" : kind,
1895
+ id,
1896
+ enabled
1897
+ })
1898
+ });
1899
+ await loadEcosystem();
1900
+ }
1901
+
1902
+ async function installCapability(kind, id) {
1903
+ if (kind === "core") {
1904
+ await toggleCapability(kind, id, true);
1905
+ return;
1906
+ }
1907
+ const scope = kind === "skill" || kind === "plugin"
1908
+ ? (confirm("安装到系统全局还是当前项目?\n确定:全局所有项目可用;取消:仅当前项目。") ? "user" : "project")
1909
+ : undefined;
1910
+ if (!confirm(`确认安装 ${id}?${kind === "external" ? "系统将按官方可信来源安装并进行完整性验证。" : "安装完成后需要执行 /reload 生效。"}`)) return;
1911
+ const result = await json("/api/install/execute", {
1912
+ method: "POST",
1913
+ headers: { "content-type": "application/json" },
1914
+ body: JSON.stringify({ kind, id, scope, approved: true, actor: "researcher" })
1915
+ });
1916
+ alert(result.message || (result.ok ? "安装任务已提交并处理。" : "安装未能完成。"));
1917
+ await loadEcosystem();
1918
+ }
1919
+
1920
+ [$("eco-search"), $("eco-kind"), $("eco-status")].forEach(control => {
1921
+ control.addEventListener("input", renderEcosystem);
1922
+ });
1923
+ document.querySelectorAll("[data-eco-kind]").forEach(button => {
1924
+ button.addEventListener("click", () => {
1925
+ state.ecoKind = button.dataset.ecoKind;
1926
+ document.querySelectorAll("[data-eco-kind]").forEach(item => item.classList.toggle("active", item === button));
1927
+ renderEcosystem();
1928
+ });
1929
+ });
1930
+
1931
+ function providerModels(provider) {
1932
+ return state.models.filter(model => model.provider === provider);
1933
+ }
1934
+
1935
+ function updateModels() {
1936
+ const models = providerModels($("provider-select").value);
1937
+ $("model-select").innerHTML = models.map(model => `
1938
+ <option value="${esc(model.version || model.id.split("/").slice(1).join("/"))}">${esc(model.name)}</option>
1939
+ `).join("");
1940
+ if (state.active.provider === $("provider-select").value && state.active.model) {
1941
+ $("model-select").value = state.active.model;
1942
+ }
1943
+ }
1944
+
1945
+ async function loadProvider() {
1946
+ try {
1947
+ const [catalog, active, usage] = await Promise.all([
1948
+ json("/api/catalog"),
1949
+ json("/api/active-provider"),
1950
+ json("/api/token-usage")
1951
+ ]);
1952
+ state.models = catalog.models || [];
1953
+ state.active = active || {};
1954
+ state.usage = usage;
1955
+ const providers = [...new Set(state.models.map(model => model.provider))].sort();
1956
+ $("provider-select").innerHTML = providers.map(provider => `
1957
+ <option value="${esc(provider)}">${esc(provider)}</option>
1958
+ `).join("");
1959
+ if (active.provider && providers.includes(active.provider)) {
1960
+ $("provider-select").value = active.provider;
1961
+ }
1962
+ updateModels();
1963
+ renderCost();
1964
+ } catch (error) {
1965
+ $("provider-feedback").textContent = `加载配置失败: ${error.message}`;
1966
+ }
1967
+ }
1968
+
1969
+ function currentModel() {
1970
+ return providerModels($("provider-select").value).find(
1971
+ model => (model.version || model.id.split("/").slice(1).join("/")) === $("model-select").value
1972
+ );
1973
+ }
1974
+
1975
+ $("provider-select").addEventListener("change", updateModels);
1976
+
1977
+ $("save-provider").addEventListener("click", async () => {
1978
+ const feedback = $("provider-feedback");
1979
+ feedback.textContent = "保存设置中...";
1980
+ try {
1981
+ await json("/api/active-provider", {
1982
+ method: "POST",
1983
+ headers: { "content-type": "application/json" },
1984
+ body: JSON.stringify({ provider: $("provider-select").value, model: $("model-select").value })
1985
+ });
1986
+ feedback.textContent = "✓ 默认模型设置已保存,重启会话后生效。";
1987
+ setTimeout(() => { feedback.textContent = ""; }, 3000);
1988
+ } catch (error) {
1989
+ feedback.textContent = `保存失败: ${error.message}`;
1990
+ }
1991
+ });
1992
+
1993
+ $("configure-provider").addEventListener("click", async () => {
1994
+ const feedback = $("config-feedback"), model = currentModel(), key = $("provider-key").value;
1995
+ if (!model) {
1996
+ feedback.textContent = "请选择模型";
1997
+ return;
1998
+ }
1999
+ feedback.textContent = "正在保存凭据...";
2000
+ try {
2001
+ await json("/api/provider-config", {
2002
+ method: "POST",
2003
+ headers: { "content-type": "application/json" },
2004
+ body: JSON.stringify({
2005
+ id: model.provider,
2006
+ name: model.provider,
2007
+ baseUrl: model.endpoint,
2008
+ api: model.provider === "anthropic" ? "anthropic-messages" : "openai-completions",
2009
+ apiKeyEnv: model.apiKeyEnv,
2010
+ modelId: $("model-select").value,
2011
+ ...(key ? { apiKey: key } : {})
2012
+ })
2013
+ });
2014
+ $("provider-key").value = "";
2015
+ feedback.textContent = "✓ 凭据配置已安全保存,重启会话后生效。";
2016
+ setTimeout(() => { feedback.textContent = ""; }, 3000);
2017
+ } catch (error) {
2018
+ feedback.textContent = `保存失败: ${error.message}`;
2019
+ }
2020
+ });
2021
+
2022
+ function money(usd, cny) {
2023
+ return state.currency === "usd" ? `$${Number(usd || 0).toFixed(4)}` : `¥${Number(cny || 0).toFixed(4)}`;
2024
+ }
2025
+
2026
+ function renderCost() {
2027
+ const usage = state.usage || {}, totals = usage.totals || {}, rows = usage.byModel || [];
2028
+ $("cost-content").innerHTML = `
2029
+ <div class="cost-metrics">
2030
+ <div class="metric">
2031
+ <small>消耗总 TOKEN</small>
2032
+ <strong>${Number(totals.tokens || 0).toLocaleString()}</strong>
2033
+ </div>
2034
+ <div class="metric">
2035
+ <small>已结算实际费用</small>
2036
+ <strong>${money(totals.cost, totals.costCny)}</strong>
2037
+ </div>
2038
+ <div class="metric">
2039
+ <small>全量预估费用</small>
2040
+ <strong>${money(totals.estimatedCost, totals.estimatedCostCny)}</strong>
2041
+ </div>
2042
+ </div>
2043
+ <p class="cost-note">统计范围:全部本地 PsyClaw 会话。实际账单请以相应模型 Provider 控制台为准。</p>
2044
+ <div class="table-wrap">
2045
+ <table class="data">
2046
+ <thead>
2047
+ <tr>
2048
+ <th style="width:28%">模型型号</th>
2049
+ <th style="width:14%;text-align:right">输入 Token</th>
2050
+ <th style="width:14%;text-align:right">输出 Token</th>
2051
+ <th style="width:14%;text-align:right">缓存读写</th>
2052
+ <th style="width:15%;text-align:right">总 Token</th>
2053
+ <th style="width:15%;text-align:right">预估费用</th>
2054
+ </tr>
2055
+ </thead>
2056
+ <tbody>
2057
+ ${rows.map(row => `
2058
+ <tr>
2059
+ <td>
2060
+ <div class="name">${esc(row.model)}</div>
2061
+ <div class="subtle">${esc(row.provider)}</div>
2062
+ </td>
2063
+ <td style="text-align:right;font-family:var(--font-mono)">${Number(row.input || 0).toLocaleString()}</td>
2064
+ <td style="text-align:right;font-family:var(--font-mono)">${Number(row.output || 0).toLocaleString()}</td>
2065
+ <td style="text-align:right;font-family:var(--font-mono)">${Number((row.cacheRead || 0) + (row.cacheWrite || 0)).toLocaleString()}</td>
2066
+ <td style="text-align:right;font-family:var(--font-mono);font-weight:600">${Number(row.tokens || 0).toLocaleString()}</td>
2067
+ <td style="text-align:right;font-family:var(--font-mono);font-weight:600;color:var(--brand-primary)">${money(row.estimatedCost, row.estimatedCostCny)}</td>
2068
+ </tr>
2069
+ `).join("") || '<tr><td colspan="6" class="empty" style="min-height:90px">暂无任何本地会话 Token 用量记录</td></tr>'}
2070
+ </tbody>
2071
+ </table>
2072
+ </div>
2073
+ `;
2074
+ }
2075
+
2076
+ document.querySelectorAll("[data-currency]").forEach(button => {
2077
+ button.addEventListener("click", () => {
2078
+ state.currency = button.dataset.currency;
2079
+ document.querySelectorAll("[data-currency]").forEach(item => item.classList.toggle("active", item === button));
2080
+ renderCost();
2081
+ });
2082
+ });
2083
+
2084
+ $("refresh-project").addEventListener("click", loadProject);
2085
+ $("refresh-ecosystem").addEventListener("click", loadEcosystem);
2086
+ $("refresh-provider").addEventListener("click", loadProvider);
2087
+
2088
+ /* --- 科研全流程导引 Pipeline Stepper 逻辑 --- */
2089
+ const PIPELINE_STAGE_FILES = {
2090
+ intake: ["notes/goal.md", "psyclaw.md", "notes/research_questions.md"],
2091
+ evidence: [".psyclaw/evidence.jsonl", ".psyclaw/claims.jsonl", "notes/literature.md", "notes/evidence_ledger.md"],
2092
+ plan: ["analysis/plans/active.json", "analysis/HANDOFF.md", "analysis/scripts/"],
2093
+ manuscript: ["paper/draft.md", "paper/manuscript.md", "outputs/figures/", "outputs/tables/"],
2094
+ review: ["notes/review.md", "notes/decision_request.md", "notes/audit.jsonl"]
2095
+ };
2096
+
2097
+ document.querySelectorAll(".step-item").forEach(item => {
2098
+ item.addEventListener("click", () => {
2099
+ document.querySelectorAll(".step-item").forEach(el => el.classList.remove("active"));
2100
+ item.classList.add("active");
2101
+ const stage = item.dataset.stage;
2102
+ const candidates = PIPELINE_STAGE_FILES[stage] || [];
2103
+ const found = state.files.find(f => candidates.some(c => f.path.includes(c) || c.includes(f.path)));
2104
+ if (found) {
2105
+ const btn = document.querySelector(`[data-file="${found.path}"]`);
2106
+ openFile(found.path, btn);
2107
+ } else {
2108
+ openConsoleWithMessage(`当前处于 [${item.textContent.trim()}] 阶段。您可以在此向智能体提问或输入指令推进。`);
2109
+ }
2110
+ });
2111
+ });
2112
+
2113
+ /* --- 交互控制台 Console Drawer 交互 --- */
2114
+ const consoleDrawer = $("console-drawer");
2115
+ const consoleFab = $("console-fab");
2116
+ const consoleLog = $("console-log");
2117
+ const consoleInput = $("console-input");
2118
+
2119
+ function openConsole() {
2120
+ consoleDrawer.classList.add("open");
2121
+ consoleInput.focus();
2122
+ }
2123
+
2124
+ function closeConsole() {
2125
+ consoleDrawer.classList.remove("open");
2126
+ }
2127
+
2128
+ function openConsoleWithMessage(text) {
2129
+ openConsole();
2130
+ appendLog("sys", text);
2131
+ }
2132
+
2133
+ function appendLog(role, text) {
2134
+ const entry = document.createElement("div");
2135
+ entry.className = "log-entry";
2136
+ if (role === "user") {
2137
+ entry.innerHTML = `<div class="log-user">❯ ${esc(text)}</div>`;
2138
+ } else if (role === "agent") {
2139
+ entry.innerHTML = `<div class="log-agent">${renderMarkdown(text)}</div>`;
2140
+ } else {
2141
+ entry.innerHTML = `<div class="log-sys">ℹ ${esc(text)}</div>`;
2142
+ }
2143
+ consoleLog.appendChild(entry);
2144
+ consoleLog.scrollTop = consoleLog.scrollHeight;
2145
+ }
2146
+
2147
+ consoleFab.addEventListener("click", () => {
2148
+ if (consoleDrawer.classList.contains("open")) closeConsole();
2149
+ else openConsole();
2150
+ });
2151
+ $("console-close-btn").addEventListener("click", closeConsole);
2152
+ $("console-clear-btn").addEventListener("click", () => {
2153
+ consoleLog.innerHTML = `<div class="log-entry"><div class="log-sys">ψ 终端历史已清空。输入 /help 或提问即可开始。</div></div>`;
2154
+ });
2155
+
2156
+ async function sendConsoleMessage(msg) {
2157
+ const text = (msg || consoleInput.value).trim();
2158
+ if (!text) return;
2159
+ consoleInput.value = "";
2160
+ appendLog("user", text);
2161
+
2162
+ try {
2163
+ const res = await json("/api/assistant", {
2164
+ method: "POST",
2165
+ headers: { "content-type": "application/json" },
2166
+ body: JSON.stringify({ message: text })
2167
+ });
2168
+ appendLog("agent", res.text || res.message || "✓ 执行成功");
2169
+ } catch (err) {
2170
+ appendLog("sys", `[提示] ${err.message}`);
2171
+ }
2172
+ }
2173
+
2174
+ $("console-send-btn").addEventListener("click", () => sendConsoleMessage());
2175
+ consoleInput.addEventListener("keydown", (e) => {
2176
+ if (e.key === "Enter" && !e.shiftKey) {
2177
+ e.preventDefault();
2178
+ sendConsoleMessage();
2179
+ }
2180
+ });
2181
+
2182
+ document.querySelectorAll(".chip").forEach(chip => {
2183
+ chip.addEventListener("click", () => {
2184
+ const cmd = chip.dataset.cmd;
2185
+ if (cmd) {
2186
+ consoleInput.value = cmd;
2187
+ sendConsoleMessage(cmd);
2188
+ }
2189
+ });
2190
+ });
2191
+
2192
+ /* --- 顶栏快捷命令 Quick Actions --- */
2193
+ const initModal = $("init-modal");
2194
+ const initParadigm = $("init-paradigm");
2195
+ const initGoal = $("init-goal");
2196
+ const initPreviewCmd = $("init-preview-cmd");
2197
+
2198
+ function updateInitPreview() {
2199
+ const paradigm = initParadigm.value;
2200
+ const goal = initGoal.value.trim();
2201
+ initPreviewCmd.textContent = `/init --paradigm ${paradigm}${goal ? ` "${goal}"` : ""}`;
2202
+ }
2203
+
2204
+ initParadigm.addEventListener("change", updateInitPreview);
2205
+ initGoal.addEventListener("input", updateInitPreview);
2206
+
2207
+ $("cmd-init-btn").addEventListener("click", () => {
2208
+ updateInitPreview();
2209
+ initModal.classList.remove("hidden");
2210
+ });
2211
+ $("init-modal-close").addEventListener("click", () => initModal.classList.add("hidden"));
2212
+ $("init-cancel-btn").addEventListener("click", () => initModal.classList.add("hidden"));
2213
+
2214
+ $("init-submit-btn").addEventListener("click", async () => {
2215
+ const cmd = initPreviewCmd.textContent.trim();
2216
+ initModal.classList.add("hidden");
2217
+ openConsole();
2218
+ await sendConsoleMessage(cmd);
2219
+ loadProject();
2220
+ });
2221
+
2222
+ $("cmd-plan-btn").addEventListener("click", async () => {
2223
+ openConsole();
2224
+ await sendConsoleMessage("/plan status");
2225
+ });
2226
+
2227
+ $("cmd-verify-btn").addEventListener("click", async () => {
2228
+ setView("checklist");
2229
+ await loadChecklist();
2230
+ openConsole();
2231
+ await sendConsoleMessage("/crosscheck list");
2232
+ });
2233
+
2234
+ /* --- 加载项目状态并更新 Stepper 状态 --- */
2235
+ async function loadProjectStatus() {
2236
+ try {
2237
+ const status = await json("/api/project-status");
2238
+ if (!status.initialized) {
2239
+ $("cmd-init-btn").classList.add("primary");
2240
+ } else {
2241
+ $("cmd-init-btn").classList.remove("primary");
2242
+ if (status.activePlan) {
2243
+ document.querySelectorAll(".step-item").forEach(item => {
2244
+ if (item.dataset.stage === "plan") item.classList.add("active");
2245
+ });
2246
+ }
2247
+ }
2248
+ } catch { /* ignore */ }
2249
+ }
2250
+
2251
+ loadProject().catch(error => {
2252
+ $("viewer-body").textContent = `加载失败: ${error.message}`;
2253
+ });
2254
+ loadProjectStatus();
87
2255
  </script>
88
2256
  </body>
89
2257
  </html>