skyloom 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +142 -46
  3. package/config/default.yaml +43 -47
  4. package/config/models.yaml +155 -155
  5. package/config/providers.yaml +39 -39
  6. package/config/skills/api_integrator/SKILL.md +15 -15
  7. package/config/skills/arch_designer/SKILL.md +13 -13
  8. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  9. package/config/skills/code_analysis/SKILL.md +13 -13
  10. package/config/skills/code_generator/SKILL.md +12 -12
  11. package/config/skills/code_reviewer/SKILL.md +13 -13
  12. package/config/skills/content_writer/SKILL.md +14 -14
  13. package/config/skills/data_transformer/SKILL.md +15 -15
  14. package/config/skills/document_analysis/SKILL.md +13 -13
  15. package/config/skills/emotional_companion/SKILL.md +15 -15
  16. package/config/skills/performance_checker/SKILL.md +14 -14
  17. package/config/skills/security_auditor/SKILL.md +14 -14
  18. package/config/skills/self_evolve/SKILL.md +13 -13
  19. package/config/skills/sys_operator/SKILL.md +15 -15
  20. package/config/skills/task_planner/SKILL.md +14 -14
  21. package/config/skills/web_research/SKILL.md +14 -14
  22. package/config/skills/workflow_designer/SKILL.md +13 -13
  23. package/dist/agents/dew.js +52 -52
  24. package/dist/agents/fair.js +84 -84
  25. package/dist/agents/fog.js +30 -30
  26. package/dist/agents/frost.js +32 -32
  27. package/dist/agents/rain.js +32 -32
  28. package/dist/agents/snow.js +68 -68
  29. package/dist/cli/main.js +103 -51
  30. package/dist/cli/main.js.map +1 -1
  31. package/dist/cli/tui.d.ts.map +1 -1
  32. package/dist/cli/tui.js +8 -1
  33. package/dist/cli/tui.js.map +1 -1
  34. package/dist/core/agent/task.d.ts +58 -0
  35. package/dist/core/agent/task.d.ts.map +1 -0
  36. package/dist/core/agent/task.js +83 -0
  37. package/dist/core/agent/task.js.map +1 -0
  38. package/dist/core/agent.d.ts +2 -45
  39. package/dist/core/agent.d.ts.map +1 -1
  40. package/dist/core/agent.js +61 -145
  41. package/dist/core/agent.js.map +1 -1
  42. package/dist/core/agent_helpers.d.ts +10 -0
  43. package/dist/core/agent_helpers.d.ts.map +1 -1
  44. package/dist/core/agent_helpers.js +39 -0
  45. package/dist/core/agent_helpers.js.map +1 -1
  46. package/dist/core/catalog.d.ts +71 -0
  47. package/dist/core/catalog.d.ts.map +1 -0
  48. package/dist/core/catalog.js +176 -0
  49. package/dist/core/catalog.js.map +1 -0
  50. package/dist/core/config.d.ts +8 -0
  51. package/dist/core/config.d.ts.map +1 -1
  52. package/dist/core/config.js +12 -4
  53. package/dist/core/config.js.map +1 -1
  54. package/dist/core/factory.js +16 -16
  55. package/dist/core/llm.d.ts +7 -0
  56. package/dist/core/llm.d.ts.map +1 -1
  57. package/dist/core/llm.js +139 -7
  58. package/dist/core/llm.js.map +1 -1
  59. package/dist/core/longdoc.js +5 -5
  60. package/dist/core/memory.d.ts.map +1 -1
  61. package/dist/core/memory.js +69 -62
  62. package/dist/core/memory.js.map +1 -1
  63. package/dist/core/theme.d.ts +46 -0
  64. package/dist/core/theme.d.ts.map +1 -0
  65. package/dist/core/theme.js +42 -0
  66. package/dist/core/theme.js.map +1 -0
  67. package/dist/web/server.js +542 -519
  68. package/dist/web/server.js.map +1 -1
  69. package/docs/AESTHETIC_DESIGN.md +144 -0
  70. package/docs/OPTIMIZATION_PLAN.md +178 -0
  71. package/package.json +60 -60
  72. package/scripts/install.js +48 -48
  73. package/scripts/link.js +10 -10
  74. package/setup.bat +79 -79
  75. package/skill-test-ty2fOA/test.md +10 -10
  76. package/src/agents/dew.ts +70 -70
  77. package/src/agents/fair.ts +102 -102
  78. package/src/agents/fog.ts +48 -48
  79. package/src/agents/frost.ts +50 -50
  80. package/src/agents/rain.ts +50 -50
  81. package/src/agents/snow.ts +239 -239
  82. package/src/cli/main.ts +425 -372
  83. package/src/cli/mode.ts +58 -58
  84. package/src/cli/tui.ts +272 -269
  85. package/src/core/agent/task.ts +100 -0
  86. package/src/core/agent.ts +1446 -1549
  87. package/src/core/agent_helpers.ts +496 -461
  88. package/src/core/arbitrate.ts +162 -162
  89. package/src/core/catalog.ts +178 -0
  90. package/src/core/checkpoint.ts +94 -94
  91. package/src/core/config.ts +20 -4
  92. package/src/core/estimate.ts +104 -104
  93. package/src/core/evolve.ts +191 -191
  94. package/src/core/factory.ts +627 -627
  95. package/src/core/filter.ts +103 -103
  96. package/src/core/graph.ts +156 -156
  97. package/src/core/icons.ts +53 -53
  98. package/src/core/index.ts +37 -37
  99. package/src/core/learn.ts +146 -146
  100. package/src/core/llm.ts +108 -5
  101. package/src/core/longdoc.ts +155 -155
  102. package/src/core/mcp_server.ts +176 -176
  103. package/src/core/memory.ts +1178 -1171
  104. package/src/core/profile.ts +255 -255
  105. package/src/core/router.ts +124 -124
  106. package/src/core/sandbox.ts +142 -142
  107. package/src/core/security.ts +243 -243
  108. package/src/core/skill.ts +342 -342
  109. package/src/core/theme.ts +65 -0
  110. package/src/core/tool_router.ts +193 -193
  111. package/src/core/vector.ts +152 -152
  112. package/src/core/workspace.ts +150 -150
  113. package/src/plugins/loader.ts +66 -66
  114. package/src/skills/loader.ts +46 -46
  115. package/src/sql.js.d.ts +29 -29
  116. package/src/tools/builtin.ts +380 -380
  117. package/src/tools/computer.ts +269 -269
  118. package/src/tools/delegate.ts +49 -49
  119. package/src/web/server.ts +660 -634
  120. package/src/web/tts.ts +93 -93
  121. package/tests/agent_helpers.test.ts +48 -0
  122. package/tests/bus.test.ts +121 -121
  123. package/tests/catalog.test.ts +86 -0
  124. package/tests/config.test.ts +41 -0
  125. package/tests/icons.test.ts +45 -45
  126. package/tests/memory.test.ts +147 -0
  127. package/tests/router.test.ts +86 -86
  128. package/tests/schemas.test.ts +51 -51
  129. package/tests/semantic.test.ts +83 -83
  130. package/tests/setup.ts +10 -10
  131. package/tests/skill.test.ts +172 -172
  132. package/tests/task.test.ts +60 -0
  133. package/tests/tool.test.ts +108 -108
  134. package/tests/tool_router.test.ts +71 -71
  135. package/vitest.config.ts +17 -17
@@ -13,7 +13,7 @@ const PIGMENTS = {
13
13
  fog: {
14
14
  color: "#4a4a44", nameZH: "松烟墨", kanji: "霧",
15
15
  poem: "山色有无中",
16
- ambient: `background:radial-gradient(ellipse at 30% 60%,rgba(180,175,165,.18) 0%,transparent 70%),
16
+ ambient: `background:radial-gradient(ellipse at 30% 60%,rgba(180,175,165,.18) 0%,transparent 70%),
17
17
  radial-gradient(ellipse at 70% 30%,rgba(190,185,175,.10) 0%,transparent 55%);`,
18
18
  msgStyle: `border-style:dashed;border-color:rgba(140,135,125,.25);`,
19
19
  dotPulse: "fog-pulse", kanjiAnim: "fog-drift",
@@ -21,7 +21,7 @@ const PIGMENTS = {
21
21
  rain: {
22
22
  color: "#2a5c8a", nameZH: "石青", kanji: "雨",
23
23
  poem: "一蓑烟雨任平生",
24
- ambient: `background:radial-gradient(ellipse at 50% 20%,rgba(42,92,138,.10) 0%,transparent 50%),
24
+ ambient: `background:radial-gradient(ellipse at 50% 20%,rgba(42,92,138,.10) 0%,transparent 50%),
25
25
  repeating-linear-gradient(175deg,transparent,transparent 3px,rgba(42,92,138,.02) 3px,rgba(42,92,138,.02) 6px);`,
26
26
  msgStyle: `border-style:solid;border-color:rgba(42,92,138,.18);box-shadow:inset 0 -1px 0 rgba(42,92,138,.06);`,
27
27
  dotPulse: "rain-ripple", kanjiAnim: "rain-fall",
@@ -29,7 +29,7 @@ const PIGMENTS = {
29
29
  frost: {
30
30
  color: "#3a7a6e", nameZH: "石绿", kanji: "霜",
31
31
  poem: "月落乌啼霜满天",
32
- ambient: `background:radial-gradient(ellipse at 60% 40%,rgba(58,122,110,.09) 0%,transparent 55%),
32
+ ambient: `background:radial-gradient(ellipse at 60% 40%,rgba(58,122,110,.09) 0%,transparent 55%),
33
33
  conic-gradient(from 0deg at 75% 25%,transparent 0deg,rgba(58,122,110,.03) 2deg,transparent 4deg,transparent 180deg);`,
34
34
  msgStyle: `border-style:solid;border-color:rgba(58,122,110,.20);border-radius:2px 8px 8px 2px;`,
35
35
  dotPulse: "frost-glint", kanjiAnim: "frost-sparkle",
@@ -37,7 +37,7 @@ const PIGMENTS = {
37
37
  snow: {
38
38
  color: "#8a8a82", nameZH: "铅白", kanji: "雪",
39
39
  poem: "千树万树梨花开",
40
- ambient: `background:radial-gradient(circle at 25% 70%,rgba(210,208,200,.10) 0%,transparent 50%),
40
+ ambient: `background:radial-gradient(circle at 25% 70%,rgba(210,208,200,.10) 0%,transparent 50%),
41
41
  radial-gradient(circle at 80% 35%,rgba(220,218,210,.07) 0%,transparent 45%);`,
42
42
  msgStyle: `border-style:solid;border-color:rgba(180,178,170,.15);border-radius:8px 8px 8px 2px;`,
43
43
  dotPulse: "snow-float", kanjiAnim: "snow-fall",
@@ -45,7 +45,7 @@ const PIGMENTS = {
45
45
  dew: {
46
46
  color: "#8b6914", nameZH: "赭石", kanji: "露",
47
47
  poem: "金风玉露一相逢",
48
- ambient: `background:radial-gradient(ellipse at 50% 90%,rgba(139,105,20,.08) 0%,transparent 55%),
48
+ ambient: `background:radial-gradient(ellipse at 50% 90%,rgba(139,105,20,.08) 0%,transparent 55%),
49
49
  radial-gradient(ellipse at 80% 95%,rgba(139,105,20,.04) 0%,transparent 40%);`,
50
50
  msgStyle: `border-style:solid;border-color:rgba(139,105,20,.22);border-width:0 0 2px 2px;`,
51
51
  dotPulse: "dew-bead", kanjiAnim: "dew-still",
@@ -53,7 +53,7 @@ const PIGMENTS = {
53
53
  fair: {
54
54
  color: "#b3342d", nameZH: "朱砂", kanji: "晴",
55
55
  poem: "道是无晴却有晴",
56
- ambient: `background:radial-gradient(ellipse at 80% 15%,rgba(179,52,45,.12) 0%,transparent 60%),
56
+ ambient: `background:radial-gradient(ellipse at 80% 15%,rgba(179,52,45,.12) 0%,transparent 60%),
57
57
  radial-gradient(ellipse at 30% 40%,rgba(200,100,50,.05) 0%,transparent 50%);`,
58
58
  msgStyle: `border-style:solid;border-color:rgba(179,52,45,.20);box-shadow:0 0 1px rgba(179,52,45,.04);`,
59
59
  dotPulse: "fair-glow", kanjiAnim: "fair-warm",
@@ -107,12 +107,23 @@ async function handleChat(req, res, ctx) {
107
107
  return;
108
108
  }
109
109
  await agent.init();
110
+ // Real streaming over SSE — tokens, reasoning, and tool events as they happen.
111
+ res.writeHead(200, {
112
+ "Content-Type": "text/event-stream; charset=utf-8",
113
+ "Cache-Control": "no-cache, no-transform",
114
+ "Connection": "keep-alive",
115
+ "X-Accel-Buffering": "no",
116
+ });
117
+ const send = (ev) => res.write(`data: ${JSON.stringify(ev)}\n\n`);
110
118
  try {
111
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ response: await agent.chat(message) }));
119
+ for await (const ev of agent.chatStream(message))
120
+ send(ev);
112
121
  }
113
122
  catch (e) {
114
- res.writeHead(500, { "Content-Type": "application/json" }).end(JSON.stringify({ error: String(e) }));
123
+ send({ type: "error", text: String(e) });
115
124
  }
125
+ send({ type: "end" });
126
+ res.end();
116
127
  }
117
128
  function handleAgents(res, ctx) {
118
129
  res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({
@@ -129,517 +140,529 @@ function handleStatus(res, ctx) {
129
140
  水墨气象台 · Ink Wash Weather Station
130
141
  ────────────────────────────────────────────── */
131
142
  function serveInkWashUI(res) {
132
- const html = `<!DOCTYPE html>
133
- <html lang="zh">
134
- <head>
135
- <meta charset="UTF-8">
136
- <meta name="viewport" content="width=device-width,initial-scale=1.0">
137
- <title>水墨气象台 · Skyloom</title>
138
- <link rel="preconnect" href="https://fonts.googleapis.com">
139
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
140
- <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&family=Noto+Serif:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
141
- <style>
142
- /* ═══════════════════════════════════
143
- 水墨气象台 · 全局
144
- ═══════════════════════════════════ */
145
- :root{
146
- --paper:#f8f4ec;
147
- --paper-warm:#f3ede2;
148
- --paper-edge:rgba(180,160,130,.06);
149
- --ink-deep:#1a1614;
150
- --ink-mid:#3d3833;
151
- --ink-light:#8c8680;
152
- --ink-faint:#c4bfb8;
153
- --pigment:#4a4a44;
154
- --pigment-soft:rgba(74,74,68,.12);
155
- --pigment-seal:#4a4a44;
156
- --dot-anim:fog-pulse;
157
- --kanji-anim:fog-drift;
158
- --gutter:clamp(24px,5vw,56px);
159
- --sidebar-w:200px;
160
- }
161
- *,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
162
- html,body{height:100%;overflow:hidden}
163
- body{
164
- font-family:'Noto Serif SC','Noto Serif',Georgia,serif;
165
- background:var(--paper);
166
- color:var(--ink-deep);
167
- display:flex;height:100vh;font-weight:400;font-size:16px;line-height:1.8;
168
- -webkit-font-smoothing:antialiased;
169
- }
170
-
171
- /* ── Aged paper texture with vignette edges ── */
172
- #paper-grain{
173
- position:fixed;inset:0;z-index:0;pointer-events:none;opacity:.28;
174
- background:
175
- repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(139,119,90,.02) 2px,rgba(139,119,90,.02) 4px),
176
- repeating-linear-gradient(90deg,transparent,transparent 3px,rgba(139,119,90,.012) 3px,rgba(139,119,90,.012) 6px),
177
- linear-gradient(135deg,rgba(80,60,30,.04) 0%,transparent 15%,transparent 85%,rgba(80,60,30,.04) 100%),
178
- linear-gradient(225deg,rgba(80,60,30,.03) 0%,transparent 10%,transparent 90%,rgba(80,60,30,.03) 100%);
179
- }
180
- /* Ink wash mountain silhouette at top */
181
- #paper-grain::before{
182
- content:'';position:fixed;top:0;left:0;right:0;height:clamp(80px,12vh,160px);z-index:0;pointer-events:none;opacity:.12;
183
- background:
184
- radial-gradient(ellipse 120% 100% at 25% 100%,rgba(60,55,50,.5) 0%,transparent 45%),
185
- radial-gradient(ellipse 80% 70% at 55% 100%,rgba(50,45,40,.4) 0%,transparent 55%),
186
- radial-gradient(ellipse 100% 60% at 70% 100%,rgba(70,65,60,.3) 0%,transparent 60%),
187
- radial-gradient(ellipse 60% 80% at 40% 100%,rgba(55,50,45,.25) 0%,transparent 50%),
188
- radial-gradient(ellipse 140% 90% at 60% 100%,rgba(65,60,55,.15) 0%,transparent 70%);
189
- mask:linear-gradient(0deg,transparent 0%,#000 100%);
190
- -webkit-mask:linear-gradient(0deg,transparent 0%,#000 100%);
191
- }
192
-
193
- /* ── Ambient layer — agent-specific atmosphere ── */
194
- #ambient-layer{position:fixed;inset:0;z-index:0;pointer-events:none;opacity:.7;transition:background .8s ease;background:var(--ambient-bg)}
195
-
196
- /* ═══════════════════════════════════
197
- 雾 Fog · 松烟墨 · mist particles
198
- ═══════════════════════════════════ */
199
- .mist-particles{position:absolute;inset:0;overflow:hidden;pointer-events:none}
200
- .mist-particles i{
201
- position:absolute;border-radius:50%;background:rgba(160,155,145,.10);
202
- animation:fog-drift var(--dur,8s) linear infinite;
203
- animation-delay:var(--delay,0s);
204
- width:var(--w,120px);height:var(--h,40px);left:var(--x,10%);top:var(--y,30%);
205
- filter:blur(20px);
206
- }
207
- @keyframes fog-drift{0%{transform:translateX(-30px);opacity:.3}50%{transform:translateX(40px);opacity:.7}100%{transform:translateX(-30px);opacity:.3}}
208
-
209
- /* ═══════════════════════════════════
210
- 雨 Rain · 石青 · falling streaks
211
- ═══════════════════════════════════ */
212
- .rain-streaks{position:absolute;inset:0;overflow:hidden;pointer-events:none}
213
- .rain-streaks i{
214
- position:absolute;width:1px;height:var(--h,40px);
215
- background:linear-gradient(0deg,rgba(42,92,138,.12),transparent);
216
- left:var(--x,15%);top:-10%;
217
- animation:rain-fall var(--dur,1.2s) linear infinite;
218
- animation-delay:var(--delay,0s);
219
- }
220
- @keyframes rain-fall{0%{transform:translateY(-10vh)}100%{transform:translateY(110vh)}}
221
-
222
- /* ═══════════════════════════════════
223
- 霜 Frost · 石绿 · crystalline sparks
224
- ═══════════════════════════════════ */
225
- .frost-crystals{position:absolute;inset:0;overflow:hidden;pointer-events:none}
226
- .frost-crystals i{
227
- position:absolute;
228
- width:var(--w,4px);height:var(--h,4px);
229
- background:rgba(58,122,110,.15);
230
- left:var(--x,20%);top:var(--y,30%);
231
- clip-path:polygon(50% 0%,100% 50%,50% 100%,0% 50%);
232
- animation:frost-glint var(--dur,3s) ease-in-out infinite;
233
- animation-delay:var(--delay,0s);
234
- }
235
- @keyframes frost-glint{0%,100%{opacity:.1;transform:scale(.8) rotate(0deg)}50%{opacity:.6;transform:scale(1.4) rotate(45deg)}}
236
-
237
- /* ═══════════════════════════════════
238
- 雪 Snow · 铅白 · gentle snowfall
239
- ═══════════════════════════════════ */
240
- .snow-particles{position:absolute;inset:0;overflow:hidden;pointer-events:none}
241
- .snow-particles i{
242
- position:absolute;border-radius:50%;background:rgba(190,188,180,.14);
243
- width:var(--w,6px);height:var(--w,6px);
244
- left:var(--x,20%);top:-5%;
245
- animation:snow-fall var(--dur,10s) linear infinite;
246
- animation-delay:var(--delay,0s);
247
- }
248
- @keyframes snow-fall{0%{transform:translateY(-5vh) translateX(0)}25%{transform:translateY(25vh) translateX(15px)}50%{transform:translateY(50vh) translateX(-10px)}75%{transform:translateY(75vh) translateX(8px)}100%{transform:translateY(110vh) translateX(-5px)}}
249
-
250
- /* ═══════════════════════════════════
251
- 露 Dew · 赭石 · morning dew beads
252
- ═══════════════════════════════════ */
253
- .dew-beads{position:absolute;inset:0;overflow:hidden;pointer-events:none}
254
- .dew-beads i{
255
- position:absolute;border-radius:50%;
256
- background:radial-gradient(circle at 40% 35%,rgba(200,170,100,.10),rgba(139,105,20,.06));
257
- width:var(--w,8px);height:var(--w,8px);
258
- left:var(--x,25%);bottom:var(--y,15%);
259
- animation:dew-bead var(--dur,4s) ease-in-out infinite;
260
- animation-delay:var(--delay,0s);
261
- }
262
- @keyframes dew-bead{0%,100%{opacity:.3;transform:scale(1)}50%{opacity:.7;transform:scale(1.5)}}
263
-
264
- /* ═══════════════════════════════════
265
- 晴 Fair · 朱砂 · sun motes rising
266
- ═══════════════════════════════════ */
267
- .sun-motes{position:absolute;inset:0;overflow:hidden;pointer-events:none}
268
- .sun-motes i{
269
- position:absolute;border-radius:50%;
270
- background:rgba(200,120,50,.06);
271
- width:var(--w,3px);height:var(--w,3px);
272
- left:var(--x,50%);bottom:-5%;
273
- animation:fair-warm var(--dur,7s) ease-in infinite;
274
- animation-delay:var(--delay,0s);
275
- }
276
- @keyframes fair-warm{0%{transform:translateY(0) translateX(0);opacity:0}20%{opacity:.8}80%{opacity:.4}100%{transform:translateY(-100vh) translateX(var(--drift,20px));opacity:0}}
277
-
278
- /* ═══════════════════════════════════
279
- Layout — scroll aesthetic
280
- ═══════════════════════════════════ */
281
- #sidebar{
282
- width:var(--sidebar-w);flex-shrink:0;position:relative;z-index:1;
283
- padding:var(--gutter) clamp(20px,3vw,32px);
284
- display:flex;flex-direction:column;gap:0;
285
- background:linear-gradient(90deg,rgba(0,0,0,.015),transparent 40%);
286
- }
287
-
288
- /* Logo: vertical seal style */
289
- #logo-block{margin-bottom:clamp(28px,7vh,56px);text-align:center}
290
- #logo{
291
- font-size:clamp(1.5rem,2.5vw,2rem);font-weight:700;
292
- letter-spacing:.2em;color:var(--ink-deep);line-height:1.2;
293
- writing-mode:horizontal-tb;
294
- }
295
- #logo small{
296
- display:block;font-weight:300;font-size:.7rem;color:var(--ink-light);
297
- letter-spacing:.2em;margin-top:4px;
298
- }
299
-
300
- #agents-list{display:flex;flex-direction:column;gap:2px;flex:1;overflow-y:auto}
301
-
302
- .agent-item{
303
- display:flex;align-items:center;gap:12px;
304
- padding:11px 14px;cursor:pointer;
305
- border-radius:4px;position:relative;transition:all .4s ease;
306
- border-left:2px solid transparent;
307
- }
308
- .agent-item:hover{background:var(--paper-edge)}
309
- .agent-item.active{background:var(--pigment-soft);border-left-color:var(--pigment)}
310
-
311
- /* Seal stamp effect for active agent */
312
- .agent-item.active::after{
313
- content:attr(data-seal);
314
- position:absolute;right:10px;top:50%;transform:translateY(-50%);
315
- font-size:1.3rem;font-weight:700;color:var(--pigment);
316
- opacity:.55;font-family:'Noto Serif SC',serif;
317
- letter-spacing:0;
318
- }
319
-
320
- .agent-dot{display:none}
321
-
322
- .agent-info{display:flex;flex-direction:column;gap:1px}
323
- .agent-label{font-size:.92rem;font-weight:600;color:var(--ink-mid);letter-spacing:.04em;transition:color .35s;line-height:1.3}
324
- .agent-item.active .agent-label{color:var(--ink-deep)}
325
- .agent-sublabel{font-size:.7rem;color:var(--ink-light);font-weight:300;letter-spacing:.04em}
326
- .agent-item.active .agent-sublabel{color:var(--pigment)}
327
-
328
- #sidebar-verse{margin-top:auto;padding-top:20px;text-align:center}
329
- #sidebar-verse p{font-size:.72rem;color:var(--ink-light);font-style:italic;line-height:2.2;letter-spacing:.05em;font-weight:300}
330
-
331
- /* ── Main chat ── */
332
- #main{flex:1;display:flex;flex-direction:column;position:relative;z-index:1;min-width:0}
333
- #chat-strip{
334
- display:flex;align-items:center;gap:10px;
335
- padding:clamp(12px,3vh,18px) var(--gutter);
336
- }
337
- .strip-dot{
338
- width:6px;height:6px;border-radius:50%;background:var(--pigment);
339
- flex-shrink:0;animation:var(--dot-anim) 2s ease-in-out infinite;
340
- transition:background .5s;
341
- }
342
- #strip-name{font-weight:600;font-size:.95rem;color:var(--ink-deep);letter-spacing:.06em}
343
- #strip-pigment{font-size:.75rem;color:var(--ink-light);font-weight:300}
344
-
345
- /* ── Divider line with brush feel ── */
346
- #chat-strip::after{
347
- content:'';position:absolute;bottom:0;left:var(--gutter);right:var(--gutter);
348
- height:1px;background:linear-gradient(90deg,transparent,var(--ink-faint) 20%,var(--ink-faint) 80%,transparent);
349
- }
350
-
351
- /* ── Messages ── */
352
- #messages{
353
- flex:1;overflow-y:auto;padding:var(--gutter);
354
- display:flex;flex-direction:column;gap:clamp(18px,3.5vh,32px);
355
- scroll-behavior:smooth;
356
- }
357
- #messages::-webkit-scrollbar{width:4px}
358
- #messages::-webkit-scrollbar-track{background:transparent}
359
- #messages::-webkit-scrollbar-thumb{background:var(--ink-faint);border-radius:2px}
360
-
361
- .msg{max-width:66%;line-height:1.85;animation:msg-in .5s ease both;position:relative}
362
- @keyframes msg-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
363
-
364
- .msg.user{align-self:flex-end;margin-right:clamp(0px,5vw,60px)}
365
- .msg.user .msg-body{padding-right:18px;border-right:2px solid var(--pigment);text-align:right;color:var(--ink-deep);transition:border-color .5s}
366
-
367
- .msg.assistant{align-self:flex-start;margin-left:clamp(0px,3vw,36px)}
368
- .msg.assistant .msg-body{
369
- padding:14px 18px;color:var(--ink-mid);
370
- background:linear-gradient(135deg,rgba(255,255,255,.35),rgba(255,255,255,.15));
371
- border-left:3px solid var(--pigment);
372
- border-radius:0 6px 6px 0;
373
- transition:border-color .5s,background .5s;
374
- box-shadow:1px 1px 3px rgba(0,0,0,.03);
375
- }
376
-
377
- .msg.system{align-self:center;max-width:88%}
378
- .msg.system .msg-body{color:var(--ink-light);font-size:.76rem;text-align:center;font-style:italic;border:none;padding:0}
379
-
380
- .msg-time{font-size:.65rem;color:var(--ink-faint);margin-top:5px;letter-spacing:.08em;display:block}
381
- .msg.typing .msg-body{animation:ink-bleed 2s infinite}
382
- @keyframes ink-bleed{0%,100%{opacity:.28}50%{opacity:.6}}
383
-
384
- /* ── Input ── */
385
- #input-area{
386
- padding:14px var(--gutter) clamp(18px,3.5vh,28px);
387
- background:linear-gradient(0deg,var(--paper-warm),transparent 40%);
388
- }
389
- #input-wrap{
390
- display:flex;gap:0;align-items:stretch;
391
- border-bottom:1.5px solid var(--ink-faint);
392
- transition:border-color .4s ease;padding-bottom:4px;
393
- }
394
- #input-wrap:focus-within{border-bottom-color:var(--pigment)}
395
- #input-wrap textarea{
396
- flex:1;background:transparent;border:none;outline:none;color:var(--ink-deep);
397
- font-family:inherit;font-size:.95rem;font-weight:300;padding:8px 0;
398
- resize:none;min-height:24px;max-height:140px;line-height:1.7;
399
- }
400
- #input-wrap textarea::placeholder{color:var(--ink-faint);font-style:italic}
401
- #send-btn{
402
- background:none;border:none;color:var(--ink-light);cursor:pointer;
403
- padding:8px 14px;font-size:1.1rem;transition:all .3s ease;
404
- font-family:inherit;display:flex;align-items:center;opacity:.45;
405
- }
406
- #send-btn:hover{opacity:1;color:var(--pigment)}
407
- #send-btn:disabled{opacity:.12}
408
- #input-hint{font-size:.66rem;color:var(--ink-faint);margin-top:8px;letter-spacing:.06em;text-align:right}
409
-
410
- /* ── Kanji seal stamp ── */
411
- #kanji-decoration{position:fixed;right:clamp(20px,5vw,56px);bottom:clamp(80px,14vh,140px);z-index:0;pointer-events:none
412
- ;font-size:clamp(2.5rem,6vw,4.5rem);font-weight:700;font-family:'Noto Serif SC',serif;user-select:none;
413
- color:var(--pigment);opacity:.05;animation:var(--kanji-anim) 8s ease-in-out infinite;
414
- transition:color .8s ease;
415
- border:2px solid var(--pigment);border-radius:4px;padding:clamp(4px,1vw,10px) clamp(6px,1.5vw,16px);
416
- writing-mode:vertical-rl;letter-spacing:.1em;
417
- }
418
-
419
- /* Dot animations for header indicator */
420
- @keyframes fog-pulse{0%,100%{opacity:.4;transform:scale(1)}50%{opacity:1;transform:scale(1.8)}}
421
- @keyframes rain-ripple{0%,100%{opacity:.5;transform:scale(1)}30%{opacity:1;transform:scale(1.6)}60%{opacity:.5;transform:scale(1)}90%{opacity:1;transform:scale(1.4)}}
422
- @keyframes frost-glint{0%,100%{opacity:1;transform:scale(1)}45%{opacity:.2;transform:scale(.5)}50%{opacity:1;transform:scale(1.7)}55%{opacity:.2;transform:scale(.5)}}
423
- @keyframes snow-float{0%,100%{transform:translateY(0);opacity:.6}50%{transform:translateY(-4px);opacity:1}}
424
- @keyframes dew-bead{0%,100%{transform:scale(1);opacity:.5}50%{transform:scale(1.6);opacity:1}}
425
- @keyframes fair-glow{0%,100%{opacity:.5;transform:scale(1)}50%{opacity:1;transform:scale(1.5)}}
426
-
427
- .agent-label{font-size:.95rem;font-weight:600;color:var(--ink-mid);letter-spacing:.02em;transition:color .35s}
428
- .agent-item.active .agent-label{color:var(--ink-deep)}
429
- .agent-sublabel{font-size:.72rem;color:var(--ink-light);font-weight:300;letter-spacing:.04em}
430
- .agent-item.active .agent-sublabel{color:var(--pigment)}
431
-
432
- #sidebar-verse{margin-top:auto;padding-top:24px}
433
- #sidebar-verse p{font-size:.75rem;color:var(--ink-light);font-style:italic;line-height:2;letter-spacing:.03em;font-weight:300}
434
-
435
- /* ── Main chat ── */
436
- #main{flex:1;display:flex;flex-direction:column;position:relative;z-index:1;min-width:0}
437
- #chat-strip{display:flex;align-items:center;gap:12px;padding:clamp(12px,3vh,20px) var(--gutter);border-bottom:1px solid var(--ink-faint)}
438
- .strip-dot{width:7px;height:7px;border-radius:50%;background:var(--pigment);flex-shrink:0;animation:var(--dot-anim) 2s ease-in-out infinite;transition:background .5s}
439
- #strip-name{font-weight:600;font-size:1rem;color:var(--ink-deep);letter-spacing:.04em}
440
- #strip-pigment{font-size:.78rem;color:var(--ink-light);font-weight:300}
441
-
442
- /* ── Messages ── */
443
- #messages{flex:1;overflow-y:auto;padding:var(--gutter);display:flex;flex-direction:column;gap:clamp(16px,3vh,28px);scroll-behavior:smooth}
444
- #messages::-webkit-scrollbar{width:3px}
445
- #messages::-webkit-scrollbar-track{background:transparent}
446
- #messages::-webkit-scrollbar-thumb{background:var(--ink-faint);border-radius:2px}
447
-
448
- .msg{max-width:68%;line-height:1.8;animation:msg-in .5s ease both;position:relative}
449
- @keyframes msg-in{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}
450
-
451
- .msg.user{align-self:flex-end;margin-right:clamp(0px,4vw,40px)}
452
- .msg.user .msg-body{padding-right:16px;border-right:2px solid var(--pigment);text-align:right;color:var(--ink-deep);transition:border-color .5s}
453
-
454
- .msg.assistant{align-self:flex-start;margin-left:clamp(0px,2vw,24px)}
455
- .msg.assistant .msg-body{
456
- padding:12px 16px;color:var(--ink-mid);
457
- border:1px solid var(--ink-faint);border-left:3px solid var(--pigment);
458
- background:rgba(255,255,255,.25);
459
- transition:border-color .5s,background .5s;
460
- }
461
-
462
- .msg.system{align-self:center;max-width:90%}
463
- .msg.system .msg-body{color:var(--ink-light);font-size:.78rem;text-align:center;font-style:italic;border:none;padding:0}
464
-
465
- .msg-time{font-size:.67rem;color:var(--ink-faint);margin-top:6px;letter-spacing:.06em;display:block}
466
- .msg.typing .msg-body{animation:ink-bleed 2s infinite}
467
- @keyframes ink-bleed{0%,100%{opacity:.3}50%{opacity:.65}}
468
-
469
- /* ── Input ── */
470
- #input-area{padding:16px var(--gutter) clamp(16px,3vh,28px);border-top:1px solid var(--ink-faint);background:linear-gradient(0deg,var(--paper-warm),var(--paper))}
471
- #input-wrap{display:flex;gap:0;align-items:stretch;border-bottom:1px solid var(--ink-faint);transition:border-color .4s ease;padding-bottom:0}
472
- #input-wrap:focus-within{border-bottom-color:var(--pigment)}
473
- #input-wrap textarea{
474
- flex:1;background:transparent;border:none;outline:none;color:var(--ink-deep);
475
- font-family:inherit;font-size:.95rem;font-weight:300;padding:10px 0;
476
- resize:none;min-height:26px;max-height:140px;line-height:1.7;
477
- }
478
- #input-wrap textarea::placeholder{color:var(--ink-faint);font-style:italic}
479
- #send-btn{background:none;border:none;color:var(--ink-light);cursor:pointer;padding:8px 12px;font-size:1.2rem;transition:all .3s ease;font-family:inherit;display:flex;align-items:center;opacity:.5}
480
- #send-btn:hover{opacity:1;color:var(--pigment)}
481
- #send-btn:disabled{opacity:.15}
482
- #input-hint{font-size:.68rem;color:var(--ink-faint);margin-top:6px;letter-spacing:.04em}
483
-
484
- /* ── Kanji decoration ── */
485
- #kanji-decoration{
486
- position:fixed;right:clamp(16px,4vw,48px);bottom:clamp(80px,15vh,140px);
487
- font-size:clamp(3.5rem,8vw,6rem);z-index:0;pointer-events:none;
488
- color:var(--pigment);opacity:.06;font-weight:700;
489
- font-family:'Noto Serif SC',serif;user-select:none;
490
- animation:var(--kanji-anim) 8s ease-in-out infinite;
491
- transition:color .8s ease,animation .8s ease;
492
- }
493
-
494
- @keyframes fog-drift{0%,100%{opacity:.04;transform:translateX(0)}50%{opacity:.09;transform:translateX(-8px)}}
495
- @keyframes rain-fall{0%,100%{opacity:.04;transform:translateY(0)}50%{opacity:.1;transform:translateY(5px)}}
496
- @keyframes frost-sparkle{0%{opacity:.04;transform:scale(1) rotate(0deg)}45%{opacity:.12;transform:scale(1.04) rotate(1.5deg)}50%{opacity:.04;transform:scale(1) rotate(0deg)}100%{opacity:.04;transform:scale(1) rotate(0deg)}}
497
- @keyframes snow-fall{0%,100%{opacity:.04;transform:translateY(0) rotate(0deg)}50%{opacity:.08;transform:translateY(6px) rotate(.5deg)}}
498
- @keyframes dew-still{0%,100%{opacity:.05;transform:scale(1)}}
499
- @keyframes fair-warm{0%,100%{opacity:.04;transform:scale(1)}50%{opacity:.1;transform:scale(1.02)}}
500
-
501
- /* ── Mobile ── */
502
- @media(max-width:720px){
503
- body{flex-direction:column}
504
- #sidebar{width:100%;flex-direction:row;align-items:center;gap:4px;padding:8px 12px;overflow-x:auto;flex-shrink:0;background:var(--paper-warm)}
505
- #logo-block{display:none}#sidebar-verse{display:none}
506
- .agent-item{border-left:none;border-bottom:2px solid transparent;border-radius:0;padding:8px 12px;white-space:nowrap}
507
- .agent-item.active{border-bottom-color:var(--pigment);border-left-color:transparent;background:transparent}
508
- .agent-item.active::after{display:none}
509
- .agent-sublabel{display:none}
510
- .msg{max-width:90%}
511
- #kanji-decoration{right:8px;bottom:40px;font-size:1.8rem;border-width:1px;padding:4px 6px;opacity:.06}
512
- #messages{padding:16px 12px}#chat-strip{padding:8px 12px}#input-area{padding:10px 12px}
513
- }
514
- </style>
515
- </head>
516
- <body>
517
-
518
- <div id="paper-grain"></div>
519
- <div id="ambient-layer"><div class="mist-particles"></div></div>
520
- <div id="kanji-decoration">霧</div>
521
-
522
- <div id="sidebar">
523
- <div id="logo-block"><div id="logo">气象台<small>skyloom</small></div></div>
524
- <div id="agents-list"></div>
525
- <div id="sidebar-verse"><p id="verse-text">山色有无中</p></div>
526
- </div>
527
-
528
- <div id="main">
529
- <div id="chat-strip">
530
- <div class="strip-dot"></div>
531
- <span id="strip-name">雾 Fog</span>
532
- <span id="strip-pigment">· 松烟墨</span>
533
- </div>
534
- <div id="messages"></div>
535
- <div id="input-area">
536
- <div id="input-wrap">
537
- <textarea id="chat-input" rows="1" placeholder="写下你想说的话…" autofocus></textarea>
538
- <button id="send-btn" aria-label="发送">→</button>
539
- </div>
540
- <div id="input-hint">enter 发送 · shift+enter 换行 · ⌘1-6 切换灵</div>
541
- </div>
542
- </div>
543
-
544
- <script>
545
- const PIGMENTS = ${JSON.stringify(PIGMENTS)};
546
- const AGENTS = [
547
- {name:'fog', label:'雾 Fog', sub:'松烟墨 · 探索洞察', pigment:PIGMENTS.fog, particles:'mist-particles', count:8, nameZH:'松烟墨',kanji:'霧'},
548
- {name:'rain', label:'雨 Rain', sub:'石青 · 创造产出', pigment:PIGMENTS.rain, particles:'rain-streaks', count:20, nameZH:'石青',kanji:'雨'},
549
- {name:'frost',label:'霜 Frost', sub:'石绿 · 精炼品质', pigment:PIGMENTS.frost, particles:'frost-crystals', count:14, nameZH:'石绿',kanji:'霜'},
550
- {name:'snow', label:'雪 Snow', sub:'铅白 · 架构规划', pigment:PIGMENTS.snow, particles:'snow-particles', count:12, nameZH:'铅白',kanji:'雪'},
551
- {name:'dew', label:'露 Dew', sub:'赭石 · 可靠守护', pigment:PIGMENTS.dew, particles:'dew-beads', count:10, nameZH:'赭石',kanji:'露'},
552
- {name:'fair', label:'晴 Fair', sub:'朱砂 · 情感陪伴', pigment:PIGMENTS.fair, particles:'sun-motes', count:16, nameZH:'朱砂',kanji:'晴'},
553
- ];
554
-
555
- const root=document.documentElement;
556
- const ambientLayer=document.getElementById('ambient-layer');
557
- const kanjiEl=document.getElementById('kanji-decoration');
558
- let currentAgent=AGENTS[0],isStreaming=false;
559
-
560
- /* Build ambient particles */
561
- const particleMap=new Map();
562
- AGENTS.forEach(a=>{
563
- const container=document.createElement('div');container.className=a.particles;
564
- for(let i=0;i<a.count;i++){
565
- const el=document.createElement('i');
566
- const seed=Math.random();
567
- el.style.cssText='--x:'+(8+seed*84)+'%;--y:'+(5+seed*90)+'%;--dur:'+(2+seed*8)+'s;--delay:'+(seed*-6)+'s;--w:'+(4+seed*10)+'px;--h:'+(3+seed*8)+'px;--drift:'+((seed-.5)*40)+'px';
568
- container.appendChild(el);
569
- }
570
- ambientLayer.appendChild(container);
571
- });
572
-
573
- function applyTheme(agent){
574
- if(currentAgent===agent)return;currentAgent=agent;
575
- const p=agent.pigment;
576
- root.style.setProperty('--pigment',p.color);
577
- root.style.setProperty('--ambient-bg',p.ambient);
578
- root.style.setProperty('--msg-border',p.msgStyle);
579
- root.style.setProperty('--dot-anim',p.dotPulse);
580
- root.style.setProperty('--kanji-anim',p.kanjiAnim);
581
-
582
- /* Show only this agent's particles */
583
- ambientLayer.querySelectorAll('div').forEach(d=>d.style.display=d.classList.contains(agent.particles)?'block':'none');
584
- kanjiEl.textContent=agent.kanji;
585
- kanjiEl.style.animation='none';void kanjiEl.offsetWidth;kanjiEl.style.animation=p.kanjiAnim+' 8s ease-in-out infinite';
586
-
587
- document.querySelectorAll('.agent-item').forEach(e=>e.classList.remove('active'));
588
- const card=document.querySelector('[data-agent="'+agent.name+'"]');if(card)card.classList.add('active');
589
- document.getElementById('strip-name').textContent=agent.label;
590
- document.getElementById('strip-pigment').textContent='· '+agent.nameZH;
591
- document.getElementById('verse-text').textContent=agent.pigment.poem;
592
- }
593
-
594
- /* Build sidebar */
595
- const list=document.getElementById('agents-list');
596
- AGENTS.forEach((a,i)=>{
597
- const el=document.createElement('div');el.className='agent-item';el.dataset.agent=a.name;el.dataset.seal=a.kanji;
598
- el.innerHTML='<span class="agent-info"><span class="agent-label">'+a.label+'</span><span class="agent-sublabel">'+a.sub+'</span></span>';
599
- el.addEventListener('click',()=>applyTheme(a));
600
- list.appendChild(el);
601
- });
602
- applyTheme(AGENTS[0]);
603
-
604
- /* ── Chat ── */
605
- const msgsEl=document.getElementById('messages'),input=document.getElementById('chat-input'),sendBtn=document.getElementById('send-btn');
606
-
607
- function addMsg(role,text){
608
- const w=document.createElement('div');w.className='msg '+role;
609
- w.innerHTML='<div class="msg-body">'+text+'</div><span class="msg-time">'+new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})+'</span>';
610
- msgsEl.appendChild(w);scrollBottom();return w;
611
- }
612
- function addSys(t){addMsg('system',t)}
613
- function scrollBottom(){msgsEl.scrollTop=msgsEl.scrollHeight}
614
-
615
- function typewriter(el,text){
616
- return new Promise(resolve=>{
617
- let i=0;el.innerHTML='';
618
- function tick(){if(i<text.length){el.innerHTML+=text[i]==='\\n'?'<br>':text[i];i++;scrollBottom();setTimeout(tick,text[i-1]==='。'||text[i-1]===','?70:text[i-1]===' '?18:22+Math.random()*14)}else resolve()}
619
- tick();
620
- });
621
- }
622
-
623
- async function sendMessage(){
624
- const text=input.value.trim();if(!text||isStreaming)return;
625
- input.value='';input.style.height='auto';isStreaming=true;sendBtn.disabled=true;
626
- addMsg('user',esc(text));const thinking=addMsg('assistant','<span class="typing">…</span>');
627
- try{
628
- const resp=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,agent:currentAgent.name})});
629
- const data=await resp.json();thinking.remove();
630
- if(data.error){addSys('× '+esc(data.error))}else{const el=addMsg('assistant','');await typewriter(el,data.response)}
631
- }catch(e){thinking.remove();addSys('× 连接中断')}
632
- isStreaming=false;sendBtn.disabled=false;input.focus();
633
- }
634
- function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
635
-
636
- input.addEventListener('input',()=>{input.style.height='auto';input.style.height=Math.min(input.scrollHeight,140)+'px'});
637
- input.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage()}});
638
- sendBtn.addEventListener('click',sendMessage);
639
- document.addEventListener('keydown',e=>{if(e.ctrlKey||e.metaKey){const n=parseInt(e.key);if(n>=1&&n<=6){e.preventDefault();applyTheme(AGENTS[n-1])}}});
640
- addSys('气象台已就绪 · ⌘1-6 唤灵 · enter 传讯');
641
- </script>
642
- </body>
143
+ const html = `<!DOCTYPE html>
144
+ <html lang="zh">
145
+ <head>
146
+ <meta charset="UTF-8">
147
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
148
+ <title>水墨气象台 · Skyloom</title>
149
+ <link rel="preconnect" href="https://fonts.googleapis.com">
150
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
151
+ <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&family=Noto+Serif:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
152
+ <style>
153
+ /* ═══════════════════════════════════
154
+ 水墨气象台 · 全局
155
+ ═══════════════════════════════════ */
156
+ :root{
157
+ --paper:#f8f4ec;
158
+ --paper-warm:#f3ede2;
159
+ --paper-edge:rgba(180,160,130,.06);
160
+ --ink-deep:#1a1614;
161
+ --ink-mid:#3d3833;
162
+ --ink-light:#8c8680;
163
+ --ink-faint:#c4bfb8;
164
+ --pigment:#4a4a44;
165
+ --pigment-soft:rgba(74,74,68,.12);
166
+ --pigment-seal:#4a4a44;
167
+ --dot-anim:fog-pulse;
168
+ --kanji-anim:fog-drift;
169
+ --gutter:clamp(24px,5vw,56px);
170
+ --sidebar-w:200px;
171
+ }
172
+ *,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
173
+ html,body{height:100%;overflow:hidden}
174
+ body{
175
+ font-family:'Noto Serif SC','Noto Serif',Georgia,serif;
176
+ background:var(--paper);
177
+ color:var(--ink-deep);
178
+ display:flex;height:100vh;font-weight:400;font-size:16px;line-height:1.8;
179
+ -webkit-font-smoothing:antialiased;
180
+ }
181
+
182
+ /* ── Aged paper texture with vignette edges ── */
183
+ #paper-grain{
184
+ position:fixed;inset:0;z-index:0;pointer-events:none;opacity:.28;
185
+ background:
186
+ repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(139,119,90,.02) 2px,rgba(139,119,90,.02) 4px),
187
+ repeating-linear-gradient(90deg,transparent,transparent 3px,rgba(139,119,90,.012) 3px,rgba(139,119,90,.012) 6px),
188
+ linear-gradient(135deg,rgba(80,60,30,.04) 0%,transparent 15%,transparent 85%,rgba(80,60,30,.04) 100%),
189
+ linear-gradient(225deg,rgba(80,60,30,.03) 0%,transparent 10%,transparent 90%,rgba(80,60,30,.03) 100%);
190
+ }
191
+ /* Ink wash mountain silhouette at top */
192
+ #paper-grain::before{
193
+ content:'';position:fixed;top:0;left:0;right:0;height:clamp(80px,12vh,160px);z-index:0;pointer-events:none;opacity:.12;
194
+ background:
195
+ radial-gradient(ellipse 120% 100% at 25% 100%,rgba(60,55,50,.5) 0%,transparent 45%),
196
+ radial-gradient(ellipse 80% 70% at 55% 100%,rgba(50,45,40,.4) 0%,transparent 55%),
197
+ radial-gradient(ellipse 100% 60% at 70% 100%,rgba(70,65,60,.3) 0%,transparent 60%),
198
+ radial-gradient(ellipse 60% 80% at 40% 100%,rgba(55,50,45,.25) 0%,transparent 50%),
199
+ radial-gradient(ellipse 140% 90% at 60% 100%,rgba(65,60,55,.15) 0%,transparent 70%);
200
+ mask:linear-gradient(0deg,transparent 0%,#000 100%);
201
+ -webkit-mask:linear-gradient(0deg,transparent 0%,#000 100%);
202
+ }
203
+
204
+ /* ── Ambient layer — agent-specific atmosphere ── */
205
+ #ambient-layer{position:fixed;inset:0;z-index:0;pointer-events:none;opacity:.7;transition:background .8s ease;background:var(--ambient-bg)}
206
+
207
+ /* ═══════════════════════════════════
208
+ 雾 Fog · 松烟墨 · mist particles
209
+ ═══════════════════════════════════ */
210
+ .mist-particles{position:absolute;inset:0;overflow:hidden;pointer-events:none}
211
+ .mist-particles i{
212
+ position:absolute;border-radius:50%;background:rgba(160,155,145,.10);
213
+ animation:fog-drift var(--dur,8s) linear infinite;
214
+ animation-delay:var(--delay,0s);
215
+ width:var(--w,120px);height:var(--h,40px);left:var(--x,10%);top:var(--y,30%);
216
+ filter:blur(20px);
217
+ }
218
+ @keyframes fog-drift{0%{transform:translateX(-30px);opacity:.3}50%{transform:translateX(40px);opacity:.7}100%{transform:translateX(-30px);opacity:.3}}
219
+
220
+ /* ═══════════════════════════════════
221
+ 雨 Rain · 石青 · falling streaks
222
+ ═══════════════════════════════════ */
223
+ .rain-streaks{position:absolute;inset:0;overflow:hidden;pointer-events:none}
224
+ .rain-streaks i{
225
+ position:absolute;width:1px;height:var(--h,40px);
226
+ background:linear-gradient(0deg,rgba(42,92,138,.12),transparent);
227
+ left:var(--x,15%);top:-10%;
228
+ animation:rain-fall var(--dur,1.2s) linear infinite;
229
+ animation-delay:var(--delay,0s);
230
+ }
231
+ @keyframes rain-fall{0%{transform:translateY(-10vh)}100%{transform:translateY(110vh)}}
232
+
233
+ /* ═══════════════════════════════════
234
+ 霜 Frost · 石绿 · crystalline sparks
235
+ ═══════════════════════════════════ */
236
+ .frost-crystals{position:absolute;inset:0;overflow:hidden;pointer-events:none}
237
+ .frost-crystals i{
238
+ position:absolute;
239
+ width:var(--w,4px);height:var(--h,4px);
240
+ background:rgba(58,122,110,.15);
241
+ left:var(--x,20%);top:var(--y,30%);
242
+ clip-path:polygon(50% 0%,100% 50%,50% 100%,0% 50%);
243
+ animation:frost-glint var(--dur,3s) ease-in-out infinite;
244
+ animation-delay:var(--delay,0s);
245
+ }
246
+ @keyframes frost-glint{0%,100%{opacity:.1;transform:scale(.8) rotate(0deg)}50%{opacity:.6;transform:scale(1.4) rotate(45deg)}}
247
+
248
+ /* ═══════════════════════════════════
249
+ 雪 Snow · 铅白 · gentle snowfall
250
+ ═══════════════════════════════════ */
251
+ .snow-particles{position:absolute;inset:0;overflow:hidden;pointer-events:none}
252
+ .snow-particles i{
253
+ position:absolute;border-radius:50%;background:rgba(190,188,180,.14);
254
+ width:var(--w,6px);height:var(--w,6px);
255
+ left:var(--x,20%);top:-5%;
256
+ animation:snow-fall var(--dur,10s) linear infinite;
257
+ animation-delay:var(--delay,0s);
258
+ }
259
+ @keyframes snow-fall{0%{transform:translateY(-5vh) translateX(0)}25%{transform:translateY(25vh) translateX(15px)}50%{transform:translateY(50vh) translateX(-10px)}75%{transform:translateY(75vh) translateX(8px)}100%{transform:translateY(110vh) translateX(-5px)}}
260
+
261
+ /* ═══════════════════════════════════
262
+ 露 Dew · 赭石 · morning dew beads
263
+ ═══════════════════════════════════ */
264
+ .dew-beads{position:absolute;inset:0;overflow:hidden;pointer-events:none}
265
+ .dew-beads i{
266
+ position:absolute;border-radius:50%;
267
+ background:radial-gradient(circle at 40% 35%,rgba(200,170,100,.10),rgba(139,105,20,.06));
268
+ width:var(--w,8px);height:var(--w,8px);
269
+ left:var(--x,25%);bottom:var(--y,15%);
270
+ animation:dew-bead var(--dur,4s) ease-in-out infinite;
271
+ animation-delay:var(--delay,0s);
272
+ }
273
+ @keyframes dew-bead{0%,100%{opacity:.3;transform:scale(1)}50%{opacity:.7;transform:scale(1.5)}}
274
+
275
+ /* ═══════════════════════════════════
276
+ 晴 Fair · 朱砂 · sun motes rising
277
+ ═══════════════════════════════════ */
278
+ .sun-motes{position:absolute;inset:0;overflow:hidden;pointer-events:none}
279
+ .sun-motes i{
280
+ position:absolute;border-radius:50%;
281
+ background:rgba(200,120,50,.06);
282
+ width:var(--w,3px);height:var(--w,3px);
283
+ left:var(--x,50%);bottom:-5%;
284
+ animation:fair-warm var(--dur,7s) ease-in infinite;
285
+ animation-delay:var(--delay,0s);
286
+ }
287
+ @keyframes fair-warm{0%{transform:translateY(0) translateX(0);opacity:0}20%{opacity:.8}80%{opacity:.4}100%{transform:translateY(-100vh) translateX(var(--drift,20px));opacity:0}}
288
+
289
+ /* ═══════════════════════════════════
290
+ Layout — scroll aesthetic
291
+ ═══════════════════════════════════ */
292
+ #sidebar{
293
+ width:var(--sidebar-w);flex-shrink:0;position:relative;z-index:1;
294
+ padding:var(--gutter) clamp(20px,3vw,32px);
295
+ display:flex;flex-direction:column;gap:0;
296
+ background:linear-gradient(90deg,rgba(0,0,0,.015),transparent 40%);
297
+ }
298
+
299
+ /* Logo: vertical seal style */
300
+ #logo-block{margin-bottom:clamp(28px,7vh,56px);text-align:center}
301
+ #logo{
302
+ font-size:clamp(1.5rem,2.5vw,2rem);font-weight:700;
303
+ letter-spacing:.2em;color:var(--ink-deep);line-height:1.2;
304
+ writing-mode:horizontal-tb;
305
+ }
306
+ #logo small{
307
+ display:block;font-weight:300;font-size:.7rem;color:var(--ink-light);
308
+ letter-spacing:.2em;margin-top:4px;
309
+ }
310
+
311
+ #agents-list{display:flex;flex-direction:column;gap:2px;flex:1;overflow-y:auto}
312
+
313
+ .agent-item{
314
+ display:flex;align-items:center;gap:12px;
315
+ padding:11px 14px;cursor:pointer;
316
+ border-radius:4px;position:relative;transition:all .4s ease;
317
+ border-left:2px solid transparent;
318
+ }
319
+ .agent-item:hover{background:var(--paper-edge)}
320
+ .agent-item.active{background:var(--pigment-soft);border-left-color:var(--pigment)}
321
+
322
+ /* Seal stamp effect for active agent */
323
+ .agent-item.active::after{
324
+ content:attr(data-seal);
325
+ position:absolute;right:10px;top:50%;transform:translateY(-50%);
326
+ font-size:1.3rem;font-weight:700;color:var(--pigment);
327
+ opacity:.55;font-family:'Noto Serif SC',serif;
328
+ letter-spacing:0;
329
+ }
330
+
331
+ .agent-dot{display:none}
332
+
333
+ .agent-info{display:flex;flex-direction:column;gap:1px}
334
+ .agent-label{font-size:.92rem;font-weight:600;color:var(--ink-mid);letter-spacing:.04em;transition:color .35s;line-height:1.3}
335
+ .agent-item.active .agent-label{color:var(--ink-deep)}
336
+ .agent-sublabel{font-size:.7rem;color:var(--ink-light);font-weight:300;letter-spacing:.04em}
337
+ .agent-item.active .agent-sublabel{color:var(--pigment)}
338
+
339
+ #sidebar-verse{margin-top:auto;padding-top:20px;text-align:center}
340
+ #sidebar-verse p{font-size:.72rem;color:var(--ink-light);font-style:italic;line-height:2.2;letter-spacing:.05em;font-weight:300}
341
+
342
+ /* ── Main chat ── */
343
+ #main{flex:1;display:flex;flex-direction:column;position:relative;z-index:1;min-width:0}
344
+ #chat-strip{
345
+ display:flex;align-items:center;gap:10px;
346
+ padding:clamp(12px,3vh,18px) var(--gutter);
347
+ }
348
+ .strip-dot{
349
+ width:6px;height:6px;border-radius:50%;background:var(--pigment);
350
+ flex-shrink:0;animation:var(--dot-anim) 2s ease-in-out infinite;
351
+ transition:background .5s;
352
+ }
353
+ #strip-name{font-weight:600;font-size:.95rem;color:var(--ink-deep);letter-spacing:.06em}
354
+ #strip-pigment{font-size:.75rem;color:var(--ink-light);font-weight:300}
355
+
356
+ /* ── Divider line with brush feel ── */
357
+ #chat-strip::after{
358
+ content:'';position:absolute;bottom:0;left:var(--gutter);right:var(--gutter);
359
+ height:1px;background:linear-gradient(90deg,transparent,var(--ink-faint) 20%,var(--ink-faint) 80%,transparent);
360
+ }
361
+
362
+ /* ── Messages ── */
363
+ #messages{
364
+ flex:1;overflow-y:auto;padding:var(--gutter);
365
+ display:flex;flex-direction:column;gap:clamp(18px,3.5vh,32px);
366
+ scroll-behavior:smooth;
367
+ }
368
+ #messages::-webkit-scrollbar{width:4px}
369
+ #messages::-webkit-scrollbar-track{background:transparent}
370
+ #messages::-webkit-scrollbar-thumb{background:var(--ink-faint);border-radius:2px}
371
+
372
+ .msg{max-width:66%;line-height:1.85;animation:msg-in .5s ease both;position:relative}
373
+ @keyframes msg-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
374
+
375
+ .msg.user{align-self:flex-end;margin-right:clamp(0px,5vw,60px)}
376
+ .msg.user .msg-body{padding-right:18px;border-right:2px solid var(--pigment);text-align:right;color:var(--ink-deep);transition:border-color .5s}
377
+
378
+ .msg.assistant{align-self:flex-start;margin-left:clamp(0px,3vw,36px)}
379
+ .msg.assistant .msg-body{
380
+ padding:14px 18px;color:var(--ink-mid);
381
+ background:linear-gradient(135deg,rgba(255,255,255,.35),rgba(255,255,255,.15));
382
+ border-left:3px solid var(--pigment);
383
+ border-radius:0 6px 6px 0;
384
+ transition:border-color .5s,background .5s;
385
+ box-shadow:1px 1px 3px rgba(0,0,0,.03);
386
+ }
387
+
388
+ .msg.system{align-self:center;max-width:88%}
389
+ .msg.system .msg-body{color:var(--ink-light);font-size:.76rem;text-align:center;font-style:italic;border:none;padding:0}
390
+
391
+ .msg-time{font-size:.65rem;color:var(--ink-faint);margin-top:5px;letter-spacing:.08em;display:block}
392
+ .msg.typing .msg-body{animation:ink-bleed 2s infinite}
393
+ @keyframes ink-bleed{0%,100%{opacity:.28}50%{opacity:.6}}
394
+
395
+ /* ── Input ── */
396
+ #input-area{
397
+ padding:14px var(--gutter) clamp(18px,3.5vh,28px);
398
+ background:linear-gradient(0deg,var(--paper-warm),transparent 40%);
399
+ }
400
+ #input-wrap{
401
+ display:flex;gap:0;align-items:stretch;
402
+ border-bottom:1.5px solid var(--ink-faint);
403
+ transition:border-color .4s ease;padding-bottom:4px;
404
+ }
405
+ #input-wrap:focus-within{border-bottom-color:var(--pigment)}
406
+ #input-wrap textarea{
407
+ flex:1;background:transparent;border:none;outline:none;color:var(--ink-deep);
408
+ font-family:inherit;font-size:.95rem;font-weight:300;padding:8px 0;
409
+ resize:none;min-height:24px;max-height:140px;line-height:1.7;
410
+ }
411
+ #input-wrap textarea::placeholder{color:var(--ink-faint);font-style:italic}
412
+ #send-btn{
413
+ background:none;border:none;color:var(--ink-light);cursor:pointer;
414
+ padding:8px 14px;font-size:1.1rem;transition:all .3s ease;
415
+ font-family:inherit;display:flex;align-items:center;opacity:.45;
416
+ }
417
+ #send-btn:hover{opacity:1;color:var(--pigment)}
418
+ #send-btn:disabled{opacity:.12}
419
+ #input-hint{font-size:.66rem;color:var(--ink-faint);margin-top:8px;letter-spacing:.06em;text-align:right}
420
+
421
+ /* ── Kanji seal stamp ── */
422
+ #kanji-decoration{position:fixed;right:clamp(20px,5vw,56px);bottom:clamp(80px,14vh,140px);z-index:0;pointer-events:none
423
+ ;font-size:clamp(2.5rem,6vw,4.5rem);font-weight:700;font-family:'Noto Serif SC',serif;user-select:none;
424
+ color:var(--pigment);opacity:.05;animation:var(--kanji-anim) 8s ease-in-out infinite;
425
+ transition:color .8s ease;
426
+ border:2px solid var(--pigment);border-radius:4px;padding:clamp(4px,1vw,10px) clamp(6px,1.5vw,16px);
427
+ writing-mode:vertical-rl;letter-spacing:.1em;
428
+ }
429
+
430
+ /* Dot animations for header indicator */
431
+ @keyframes fog-pulse{0%,100%{opacity:.4;transform:scale(1)}50%{opacity:1;transform:scale(1.8)}}
432
+ @keyframes rain-ripple{0%,100%{opacity:.5;transform:scale(1)}30%{opacity:1;transform:scale(1.6)}60%{opacity:.5;transform:scale(1)}90%{opacity:1;transform:scale(1.4)}}
433
+ @keyframes frost-glint{0%,100%{opacity:1;transform:scale(1)}45%{opacity:.2;transform:scale(.5)}50%{opacity:1;transform:scale(1.7)}55%{opacity:.2;transform:scale(.5)}}
434
+ @keyframes snow-float{0%,100%{transform:translateY(0);opacity:.6}50%{transform:translateY(-4px);opacity:1}}
435
+ @keyframes dew-bead{0%,100%{transform:scale(1);opacity:.5}50%{transform:scale(1.6);opacity:1}}
436
+ @keyframes fair-glow{0%,100%{opacity:.5;transform:scale(1)}50%{opacity:1;transform:scale(1.5)}}
437
+
438
+ .agent-label{font-size:.95rem;font-weight:600;color:var(--ink-mid);letter-spacing:.02em;transition:color .35s}
439
+ .agent-item.active .agent-label{color:var(--ink-deep)}
440
+ .agent-sublabel{font-size:.72rem;color:var(--ink-light);font-weight:300;letter-spacing:.04em}
441
+ .agent-item.active .agent-sublabel{color:var(--pigment)}
442
+
443
+ #sidebar-verse{margin-top:auto;padding-top:24px}
444
+ #sidebar-verse p{font-size:.75rem;color:var(--ink-light);font-style:italic;line-height:2;letter-spacing:.03em;font-weight:300}
445
+
446
+ /* ── Main chat ── */
447
+ #main{flex:1;display:flex;flex-direction:column;position:relative;z-index:1;min-width:0}
448
+ #chat-strip{display:flex;align-items:center;gap:12px;padding:clamp(12px,3vh,20px) var(--gutter);border-bottom:1px solid var(--ink-faint)}
449
+ .strip-dot{width:7px;height:7px;border-radius:50%;background:var(--pigment);flex-shrink:0;animation:var(--dot-anim) 2s ease-in-out infinite;transition:background .5s}
450
+ #strip-name{font-weight:600;font-size:1rem;color:var(--ink-deep);letter-spacing:.04em}
451
+ #strip-pigment{font-size:.78rem;color:var(--ink-light);font-weight:300}
452
+
453
+ /* ── Messages ── */
454
+ #messages{flex:1;overflow-y:auto;padding:var(--gutter);display:flex;flex-direction:column;gap:clamp(16px,3vh,28px);scroll-behavior:smooth}
455
+ #messages::-webkit-scrollbar{width:3px}
456
+ #messages::-webkit-scrollbar-track{background:transparent}
457
+ #messages::-webkit-scrollbar-thumb{background:var(--ink-faint);border-radius:2px}
458
+
459
+ .msg{max-width:68%;line-height:1.8;animation:msg-in .5s ease both;position:relative}
460
+ @keyframes msg-in{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}
461
+
462
+ .msg.user{align-self:flex-end;margin-right:clamp(0px,4vw,40px)}
463
+ .msg.user .msg-body{padding-right:16px;border-right:2px solid var(--pigment);text-align:right;color:var(--ink-deep);transition:border-color .5s}
464
+
465
+ .msg.assistant{align-self:flex-start;margin-left:clamp(0px,2vw,24px)}
466
+ .msg.assistant .msg-body{
467
+ padding:12px 16px;color:var(--ink-mid);
468
+ border:1px solid var(--ink-faint);border-left:3px solid var(--pigment);
469
+ background:rgba(255,255,255,.25);
470
+ transition:border-color .5s,background .5s;
471
+ }
472
+
473
+ .msg.system{align-self:center;max-width:90%}
474
+ .msg.system .msg-body{color:var(--ink-light);font-size:.78rem;text-align:center;font-style:italic;border:none;padding:0}
475
+
476
+ .msg-time{font-size:.67rem;color:var(--ink-faint);margin-top:6px;letter-spacing:.06em;display:block}
477
+ .msg.typing .msg-body{animation:ink-bleed 2s infinite}
478
+ @keyframes ink-bleed{0%,100%{opacity:.3}50%{opacity:.65}}
479
+
480
+ /* ── Input ── */
481
+ #input-area{padding:16px var(--gutter) clamp(16px,3vh,28px);border-top:1px solid var(--ink-faint);background:linear-gradient(0deg,var(--paper-warm),var(--paper))}
482
+ #input-wrap{display:flex;gap:0;align-items:stretch;border-bottom:1px solid var(--ink-faint);transition:border-color .4s ease;padding-bottom:0}
483
+ #input-wrap:focus-within{border-bottom-color:var(--pigment)}
484
+ #input-wrap textarea{
485
+ flex:1;background:transparent;border:none;outline:none;color:var(--ink-deep);
486
+ font-family:inherit;font-size:.95rem;font-weight:300;padding:10px 0;
487
+ resize:none;min-height:26px;max-height:140px;line-height:1.7;
488
+ }
489
+ #input-wrap textarea::placeholder{color:var(--ink-faint);font-style:italic}
490
+ #send-btn{background:none;border:none;color:var(--ink-light);cursor:pointer;padding:8px 12px;font-size:1.2rem;transition:all .3s ease;font-family:inherit;display:flex;align-items:center;opacity:.5}
491
+ #send-btn:hover{opacity:1;color:var(--pigment)}
492
+ #send-btn:disabled{opacity:.15}
493
+ #input-hint{font-size:.68rem;color:var(--ink-faint);margin-top:6px;letter-spacing:.04em}
494
+
495
+ /* ── Kanji decoration ── */
496
+ #kanji-decoration{
497
+ position:fixed;right:clamp(16px,4vw,48px);bottom:clamp(80px,15vh,140px);
498
+ font-size:clamp(3.5rem,8vw,6rem);z-index:0;pointer-events:none;
499
+ color:var(--pigment);opacity:.06;font-weight:700;
500
+ font-family:'Noto Serif SC',serif;user-select:none;
501
+ animation:var(--kanji-anim) 8s ease-in-out infinite;
502
+ transition:color .8s ease,animation .8s ease;
503
+ }
504
+
505
+ @keyframes fog-drift{0%,100%{opacity:.04;transform:translateX(0)}50%{opacity:.09;transform:translateX(-8px)}}
506
+ @keyframes rain-fall{0%,100%{opacity:.04;transform:translateY(0)}50%{opacity:.1;transform:translateY(5px)}}
507
+ @keyframes frost-sparkle{0%{opacity:.04;transform:scale(1) rotate(0deg)}45%{opacity:.12;transform:scale(1.04) rotate(1.5deg)}50%{opacity:.04;transform:scale(1) rotate(0deg)}100%{opacity:.04;transform:scale(1) rotate(0deg)}}
508
+ @keyframes snow-fall{0%,100%{opacity:.04;transform:translateY(0) rotate(0deg)}50%{opacity:.08;transform:translateY(6px) rotate(.5deg)}}
509
+ @keyframes dew-still{0%,100%{opacity:.05;transform:scale(1)}}
510
+ @keyframes fair-warm{0%,100%{opacity:.04;transform:scale(1)}50%{opacity:.1;transform:scale(1.02)}}
511
+
512
+ /* ── Mobile ── */
513
+ @media(max-width:720px){
514
+ body{flex-direction:column}
515
+ #sidebar{width:100%;flex-direction:row;align-items:center;gap:4px;padding:8px 12px;overflow-x:auto;flex-shrink:0;background:var(--paper-warm)}
516
+ #logo-block{display:none}#sidebar-verse{display:none}
517
+ .agent-item{border-left:none;border-bottom:2px solid transparent;border-radius:0;padding:8px 12px;white-space:nowrap}
518
+ .agent-item.active{border-bottom-color:var(--pigment);border-left-color:transparent;background:transparent}
519
+ .agent-item.active::after{display:none}
520
+ .agent-sublabel{display:none}
521
+ .msg{max-width:90%}
522
+ #kanji-decoration{right:8px;bottom:40px;font-size:1.8rem;border-width:1px;padding:4px 6px;opacity:.06}
523
+ #messages{padding:16px 12px}#chat-strip{padding:8px 12px}#input-area{padding:10px 12px}
524
+ }
525
+ </style>
526
+ </head>
527
+ <body>
528
+
529
+ <div id="paper-grain"></div>
530
+ <div id="ambient-layer"><div class="mist-particles"></div></div>
531
+ <div id="kanji-decoration">霧</div>
532
+
533
+ <div id="sidebar">
534
+ <div id="logo-block"><div id="logo">气象台<small>skyloom</small></div></div>
535
+ <div id="agents-list"></div>
536
+ <div id="sidebar-verse"><p id="verse-text">山色有无中</p></div>
537
+ </div>
538
+
539
+ <div id="main">
540
+ <div id="chat-strip">
541
+ <div class="strip-dot"></div>
542
+ <span id="strip-name">雾 Fog</span>
543
+ <span id="strip-pigment">· 松烟墨</span>
544
+ </div>
545
+ <div id="messages"></div>
546
+ <div id="input-area">
547
+ <div id="input-wrap">
548
+ <textarea id="chat-input" rows="1" placeholder="写下你想说的话…" autofocus></textarea>
549
+ <button id="send-btn" aria-label="发送">→</button>
550
+ </div>
551
+ <div id="input-hint">enter 发送 · shift+enter 换行 · ⌘1-6 切换灵</div>
552
+ </div>
553
+ </div>
554
+
555
+ <script>
556
+ const PIGMENTS = ${JSON.stringify(PIGMENTS)};
557
+ const AGENTS = [
558
+ {name:'fog', label:'雾 Fog', sub:'松烟墨 · 探索洞察', pigment:PIGMENTS.fog, particles:'mist-particles', count:8, nameZH:'松烟墨',kanji:'霧'},
559
+ {name:'rain', label:'雨 Rain', sub:'石青 · 创造产出', pigment:PIGMENTS.rain, particles:'rain-streaks', count:20, nameZH:'石青',kanji:'雨'},
560
+ {name:'frost',label:'霜 Frost', sub:'石绿 · 精炼品质', pigment:PIGMENTS.frost, particles:'frost-crystals', count:14, nameZH:'石绿',kanji:'霜'},
561
+ {name:'snow', label:'雪 Snow', sub:'铅白 · 架构规划', pigment:PIGMENTS.snow, particles:'snow-particles', count:12, nameZH:'铅白',kanji:'雪'},
562
+ {name:'dew', label:'露 Dew', sub:'赭石 · 可靠守护', pigment:PIGMENTS.dew, particles:'dew-beads', count:10, nameZH:'赭石',kanji:'露'},
563
+ {name:'fair', label:'晴 Fair', sub:'朱砂 · 情感陪伴', pigment:PIGMENTS.fair, particles:'sun-motes', count:16, nameZH:'朱砂',kanji:'晴'},
564
+ ];
565
+
566
+ const root=document.documentElement;
567
+ const ambientLayer=document.getElementById('ambient-layer');
568
+ const kanjiEl=document.getElementById('kanji-decoration');
569
+ let currentAgent=AGENTS[0],isStreaming=false;
570
+
571
+ /* Build ambient particles */
572
+ const particleMap=new Map();
573
+ AGENTS.forEach(a=>{
574
+ const container=document.createElement('div');container.className=a.particles;
575
+ for(let i=0;i<a.count;i++){
576
+ const el=document.createElement('i');
577
+ const seed=Math.random();
578
+ el.style.cssText='--x:'+(8+seed*84)+'%;--y:'+(5+seed*90)+'%;--dur:'+(2+seed*8)+'s;--delay:'+(seed*-6)+'s;--w:'+(4+seed*10)+'px;--h:'+(3+seed*8)+'px;--drift:'+((seed-.5)*40)+'px';
579
+ container.appendChild(el);
580
+ }
581
+ ambientLayer.appendChild(container);
582
+ });
583
+
584
+ function applyTheme(agent){
585
+ if(currentAgent===agent)return;currentAgent=agent;
586
+ const p=agent.pigment;
587
+ root.style.setProperty('--pigment',p.color);
588
+ root.style.setProperty('--ambient-bg',p.ambient);
589
+ root.style.setProperty('--msg-border',p.msgStyle);
590
+ root.style.setProperty('--dot-anim',p.dotPulse);
591
+ root.style.setProperty('--kanji-anim',p.kanjiAnim);
592
+
593
+ /* Show only this agent's particles */
594
+ ambientLayer.querySelectorAll('div').forEach(d=>d.style.display=d.classList.contains(agent.particles)?'block':'none');
595
+ kanjiEl.textContent=agent.kanji;
596
+ kanjiEl.style.animation='none';void kanjiEl.offsetWidth;kanjiEl.style.animation=p.kanjiAnim+' 8s ease-in-out infinite';
597
+
598
+ document.querySelectorAll('.agent-item').forEach(e=>e.classList.remove('active'));
599
+ const card=document.querySelector('[data-agent="'+agent.name+'"]');if(card)card.classList.add('active');
600
+ document.getElementById('strip-name').textContent=agent.label;
601
+ document.getElementById('strip-pigment').textContent='· '+agent.nameZH;
602
+ document.getElementById('verse-text').textContent=agent.pigment.poem;
603
+ }
604
+
605
+ /* Build sidebar */
606
+ const list=document.getElementById('agents-list');
607
+ AGENTS.forEach((a,i)=>{
608
+ const el=document.createElement('div');el.className='agent-item';el.dataset.agent=a.name;el.dataset.seal=a.kanji;
609
+ el.innerHTML='<span class="agent-info"><span class="agent-label">'+a.label+'</span><span class="agent-sublabel">'+a.sub+'</span></span>';
610
+ el.addEventListener('click',()=>applyTheme(a));
611
+ list.appendChild(el);
612
+ });
613
+ applyTheme(AGENTS[0]);
614
+
615
+ /* ── Chat ── */
616
+ const msgsEl=document.getElementById('messages'),input=document.getElementById('chat-input'),sendBtn=document.getElementById('send-btn');
617
+
618
+ function addMsg(role,text){
619
+ const w=document.createElement('div');w.className='msg '+role;
620
+ w.innerHTML='<div class="msg-body">'+text+'</div><span class="msg-time">'+new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})+'</span>';
621
+ msgsEl.appendChild(w);scrollBottom();return w;
622
+ }
623
+ function addSys(t){addMsg('system',t)}
624
+ function scrollBottom(){msgsEl.scrollTop=msgsEl.scrollHeight}
625
+
626
+ function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
627
+ function render(s){return esc(s).replace(/\\n/g,'<br>')}
628
+
629
+ /* Consume the SSE stream: tokens render live, tool calls appear as weather events. */
630
+ async function sendMessage(){
631
+ const text=input.value.trim();if(!text||isStreaming)return;
632
+ input.value='';input.style.height='auto';isStreaming=true;sendBtn.disabled=true;
633
+ addMsg('user',esc(text));
634
+ const el=addMsg('assistant','<span class="typing">…</span>');
635
+ const body=el.querySelector('.msg-body');
636
+ let content='',started=false;
637
+ try{
638
+ const resp=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,agent:currentAgent.name})});
639
+ const reader=resp.body.getReader(),dec=new TextDecoder();let buf='';
640
+ while(true){
641
+ const {done,value}=await reader.read();if(done)break;
642
+ buf+=dec.decode(value,{stream:true});
643
+ const parts=buf.split('\\n\\n');buf=parts.pop();
644
+ for(const p of parts){
645
+ const line=p.replace(/^data: /,'').trim();if(!line)continue;
646
+ let ev;try{ev=JSON.parse(line)}catch{continue}
647
+ if(ev.type==='content'){if(!started){started=true;body.innerHTML=''}content+=ev.text;body.innerHTML=render(content);scrollBottom()}
648
+ else if(ev.type==='tool_status'){addSys(currentAgent.kanji+' '+esc(ev.tool_name)+' …')}
649
+ else if(ev.type==='tool_done'){addSys((ev.success?'':'× ')+esc(ev.tool_name))}
650
+ else if(ev.type==='error'){addSys( '+esc(ev.text||'出错了'))}
651
+ else if(ev.type==='truncated'){addSys(' '+esc(ev.reason||'截断'))}
652
+ }
653
+ }
654
+ if(!content.trim())body.innerHTML='<span style="opacity:.5">(无回复)</span>';
655
+ }catch(e){body.innerHTML='';addSys('× 连接中断')}
656
+ isStreaming=false;sendBtn.disabled=false;input.focus();
657
+ }
658
+
659
+ input.addEventListener('input',()=>{input.style.height='auto';input.style.height=Math.min(input.scrollHeight,140)+'px'});
660
+ input.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage()}});
661
+ sendBtn.addEventListener('click',sendMessage);
662
+ document.addEventListener('keydown',e=>{if(e.ctrlKey||e.metaKey){const n=parseInt(e.key);if(n>=1&&n<=6){e.preventDefault();applyTheme(AGENTS[n-1])}}});
663
+ addSys('气象台已就绪 · ⌘1-6 唤灵 · enter 传讯');
664
+ </script>
665
+ </body>
643
666
  </html>`;
644
667
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
645
668
  res.end(html);