universal-llm-client 4.5.0 → 4.5.1

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 (174) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -0
  3. package/dist/ai-model.d.ts +0 -1
  4. package/dist/ai-model.js +0 -1
  5. package/dist/auditor.d.ts +0 -1
  6. package/dist/auditor.js +0 -1
  7. package/dist/client.d.ts +0 -1
  8. package/dist/client.js +0 -1
  9. package/dist/gemma-channel.d.ts +0 -1
  10. package/dist/gemma-channel.js +0 -1
  11. package/dist/gemma-diffusion.d.ts +0 -1
  12. package/dist/gemma-diffusion.js +0 -1
  13. package/dist/http.d.ts +0 -1
  14. package/dist/http.js +0 -1
  15. package/dist/index.d.ts +0 -1
  16. package/dist/index.js +0 -1
  17. package/dist/interfaces.d.ts +0 -1
  18. package/dist/interfaces.js +0 -1
  19. package/dist/mcp.d.ts +0 -1
  20. package/dist/mcp.js +0 -1
  21. package/dist/providers/anthropic.d.ts +0 -1
  22. package/dist/providers/anthropic.js +0 -1
  23. package/dist/providers/google.d.ts +0 -1
  24. package/dist/providers/google.js +0 -1
  25. package/dist/providers/index.d.ts +0 -1
  26. package/dist/providers/index.js +0 -1
  27. package/dist/providers/ollama.d.ts +0 -1
  28. package/dist/providers/ollama.js +0 -1
  29. package/dist/providers/openai.d.ts +2 -1
  30. package/dist/providers/openai.js +303 -74
  31. package/dist/router.d.ts +0 -1
  32. package/dist/router.js +0 -1
  33. package/dist/stream-decoder.d.ts +0 -1
  34. package/dist/stream-decoder.js +0 -1
  35. package/dist/structured-output.d.ts +0 -1
  36. package/dist/structured-output.js +0 -1
  37. package/dist/thinking.d.ts +0 -1
  38. package/dist/thinking.js +0 -1
  39. package/dist/tools.d.ts +0 -1
  40. package/dist/tools.js +0 -1
  41. package/dist/zod-adapter.d.ts +0 -1
  42. package/dist/zod-adapter.js +0 -1
  43. package/package.json +1 -2
  44. package/dist/ai-model.d.ts.map +0 -1
  45. package/dist/ai-model.js.map +0 -1
  46. package/dist/auditor.d.ts.map +0 -1
  47. package/dist/auditor.js.map +0 -1
  48. package/dist/client.d.ts.map +0 -1
  49. package/dist/client.js.map +0 -1
  50. package/dist/gemma-channel.d.ts.map +0 -1
  51. package/dist/gemma-channel.js.map +0 -1
  52. package/dist/gemma-diffusion.d.ts.map +0 -1
  53. package/dist/gemma-diffusion.js.map +0 -1
  54. package/dist/http.d.ts.map +0 -1
  55. package/dist/http.js.map +0 -1
  56. package/dist/index.d.ts.map +0 -1
  57. package/dist/index.js.map +0 -1
  58. package/dist/interfaces.d.ts.map +0 -1
  59. package/dist/interfaces.js.map +0 -1
  60. package/dist/mcp.d.ts.map +0 -1
  61. package/dist/mcp.js.map +0 -1
  62. package/dist/providers/anthropic.d.ts.map +0 -1
  63. package/dist/providers/anthropic.js.map +0 -1
  64. package/dist/providers/google.d.ts.map +0 -1
  65. package/dist/providers/google.js.map +0 -1
  66. package/dist/providers/index.d.ts.map +0 -1
  67. package/dist/providers/index.js.map +0 -1
  68. package/dist/providers/ollama.d.ts.map +0 -1
  69. package/dist/providers/ollama.js.map +0 -1
  70. package/dist/providers/openai.d.ts.map +0 -1
  71. package/dist/providers/openai.js.map +0 -1
  72. package/dist/router.d.ts.map +0 -1
  73. package/dist/router.js.map +0 -1
  74. package/dist/stream-decoder.d.ts.map +0 -1
  75. package/dist/stream-decoder.js.map +0 -1
  76. package/dist/structured-output.d.ts.map +0 -1
  77. package/dist/structured-output.js.map +0 -1
  78. package/dist/thinking.d.ts.map +0 -1
  79. package/dist/thinking.js.map +0 -1
  80. package/dist/tools.d.ts.map +0 -1
  81. package/dist/tools.js.map +0 -1
  82. package/dist/zod-adapter.d.ts.map +0 -1
  83. package/dist/zod-adapter.js.map +0 -1
  84. package/src/ai-model.ts +0 -400
  85. package/src/auditor.ts +0 -213
  86. package/src/client.ts +0 -402
  87. package/src/debug/debug-google-streaming.ts +0 -97
  88. package/src/debug/debug-tool-execution.ts +0 -86
  89. package/src/debug/test-lmstudio-tools.ts +0 -155
  90. package/src/demos/README.md +0 -47
  91. package/src/demos/basic/universal-llm-examples.ts +0 -161
  92. package/src/demos/diffusion-gemma/.env +0 -29
  93. package/src/demos/diffusion-gemma/.env.example +0 -27
  94. package/src/demos/diffusion-gemma/CLAUDE.md +0 -95
  95. package/src/demos/diffusion-gemma/README.md +0 -59
  96. package/src/demos/diffusion-gemma/canvas.ts +0 -1606
  97. package/src/demos/diffusion-gemma/docker-compose.yml +0 -29
  98. package/src/demos/diffusion-gemma/probe-stream.ts +0 -51
  99. package/src/demos/diffusion-gemma/probe-tools.ts +0 -55
  100. package/src/demos/diffusion-gemma/server.ts +0 -1205
  101. package/src/demos/diffusion-gemma/start-vllm.sh +0 -98
  102. package/src/demos/mcp/astrid-memory-demo.ts +0 -295
  103. package/src/demos/mcp/astrid-persona-memory.ts +0 -357
  104. package/src/demos/mcp/mcp-mongodb-demo.ts +0 -275
  105. package/src/demos/mcp/simple-astrid-memory.ts +0 -148
  106. package/src/demos/mcp/simple-mcp-demo.ts +0 -68
  107. package/src/demos/mcp/working-mcp-demo.ts +0 -62
  108. package/src/demos/model-alias-demo.ts +0 -0
  109. package/src/demos/tools/RAG_MEMORY_INTEGRATION.md +0 -267
  110. package/src/demos/tools/astrid-memory-demo.ts +0 -270
  111. package/src/demos/tools/astrid-production-memory-clean.ts +0 -785
  112. package/src/demos/tools/astrid-production-memory.ts +0 -558
  113. package/src/demos/tools/basic-translation-test.ts +0 -66
  114. package/src/demos/tools/chromadb-similarity-tuning.ts +0 -390
  115. package/src/demos/tools/clean-multilingual-conversation.ts +0 -209
  116. package/src/demos/tools/clean-translation-test.ts +0 -119
  117. package/src/demos/tools/clean-universal-multilingual-test.ts +0 -131
  118. package/src/demos/tools/complete-rag-demo.ts +0 -369
  119. package/src/demos/tools/complete-tool-demo.ts +0 -132
  120. package/src/demos/tools/demo-tool-calling.ts +0 -124
  121. package/src/demos/tools/dynamic-language-switching-test.ts +0 -251
  122. package/src/demos/tools/hybrid-thinking-test.ts +0 -154
  123. package/src/demos/tools/memory-integration-test.ts +0 -420
  124. package/src/demos/tools/multilingual-memory-system.ts +0 -802
  125. package/src/demos/tools/ondemand-translation-demo.ts +0 -655
  126. package/src/demos/tools/production-tool-demo.ts +0 -245
  127. package/src/demos/tools/revolutionary-multilingual-test.ts +0 -151
  128. package/src/demos/tools/rigorous-language-analysis.ts +0 -218
  129. package/src/demos/tools/test-universal-memory-system.ts +0 -126
  130. package/src/demos/tools/translation-integration-guide.ts +0 -346
  131. package/src/demos/tools/universal-memory-system.ts +0 -560
  132. package/src/gemma-channel.ts +0 -47
  133. package/src/gemma-diffusion.ts +0 -167
  134. package/src/http.ts +0 -261
  135. package/src/index.ts +0 -180
  136. package/src/interfaces.ts +0 -843
  137. package/src/mcp.ts +0 -345
  138. package/src/providers/anthropic.ts +0 -796
  139. package/src/providers/google.ts +0 -840
  140. package/src/providers/index.ts +0 -8
  141. package/src/providers/ollama.ts +0 -503
  142. package/src/providers/openai.ts +0 -587
  143. package/src/router.ts +0 -785
  144. package/src/stream-decoder.ts +0 -535
  145. package/src/structured-output.ts +0 -759
  146. package/src/test-scripts/test-advanced-tools.ts +0 -310
  147. package/src/test-scripts/test-google-deep-research.ts +0 -33
  148. package/src/test-scripts/test-google-streaming-enhanced.ts +0 -147
  149. package/src/test-scripts/test-google-streaming.ts +0 -63
  150. package/src/test-scripts/test-google-system-prompt-comprehensive.ts +0 -189
  151. package/src/test-scripts/test-google-thinking.ts +0 -46
  152. package/src/test-scripts/test-mcp-config.ts +0 -28
  153. package/src/test-scripts/test-mcp-connection.ts +0 -29
  154. package/src/test-scripts/test-system-message-positions.ts +0 -163
  155. package/src/test-scripts/test-system-prompt-improvement-demo.ts +0 -83
  156. package/src/test-scripts/test-tool-calling.ts +0 -231
  157. package/src/test-scripts/test-vllm-qwen36.ts +0 -256
  158. package/src/tests/ai-model.test.ts +0 -1614
  159. package/src/tests/auditor.test.ts +0 -224
  160. package/src/tests/gemma-diffusion.test.ts +0 -115
  161. package/src/tests/http.test.ts +0 -200
  162. package/src/tests/interfaces.test.ts +0 -117
  163. package/src/tests/providers/anthropic.test.ts +0 -118
  164. package/src/tests/providers/google.test.ts +0 -841
  165. package/src/tests/providers/ollama.test.ts +0 -1034
  166. package/src/tests/providers/openai.test.ts +0 -1511
  167. package/src/tests/router.test.ts +0 -254
  168. package/src/tests/stream-decoder.test.ts +0 -263
  169. package/src/tests/structured-output.test.ts +0 -1450
  170. package/src/tests/thinking.test.ts +0 -65
  171. package/src/tests/tools.test.ts +0 -175
  172. package/src/thinking.ts +0 -73
  173. package/src/tools.ts +0 -246
  174. package/src/zod-adapter.ts +0 -72
@@ -1,1606 +0,0 @@
1
- /**
2
- * DiffusionGemma — "Signal from Noise" chat experience.
3
- *
4
- * DiffusionGemma drafts whole 256-token blocks at once and denoises them
5
- * iteratively. The vLLM OpenAI-compatible stream collapses that process into
6
- * one big SSE burst per finished block (~1KB every ~1s, measured), so true
7
- * per-step state is not observable. This UI is an honest dramatization built
8
- * on the real signals we DO have:
9
- *
10
- * - chunk boundaries = real 256-token block boundaries
11
- * - chunk timing = real per-block compute duration
12
- * - chunk text = real final text
13
- *
14
- * Each reply renders instantly as flickering noise glyphs in its final layout
15
- * (monospace ⇒ zero layout shift), then resolves in waves while the NEXT
16
- * block is genuinely being computed server-side. A deterministic, seekable
17
- * lock schedule makes every materialization replayable and scrubbable.
18
- *
19
- * NOTE for editors: this file is a TS template literal. Backslash escapes in
20
- * the inner <script> WOULD be eaten by the outer literal (the old version's
21
- * /\S+/ silently became /S+/). The inner script therefore uses NO backslashes:
22
- * newlines via String.fromCharCode(10), tokenizing via charCode scanning.
23
- */
24
-
25
- export const CANVAS_HTML = /*html*/ `<!DOCTYPE html>
26
- <html lang="en">
27
- <head>
28
- <meta charset="UTF-8">
29
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
30
- <title>DiffusionGemma ⁄ Signal from Noise</title>
31
- <link rel="preconnect" href="https://fonts.googleapis.com">
32
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
33
- <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet">
34
- <link rel="icon" href="data:,">
35
- <style>
36
- :root {
37
- --ink: #06090b;
38
- --ink-2: #0a0f13;
39
- --panel: #0c1216;
40
- --line: #1a2730;
41
- --line-soft: #142028;
42
- --bone: #f2eee1;
43
- --bone-dim: #c6c1b2;
44
- --dim: #7790a1;
45
- --sans: 'IBM Plex Sans', -apple-system, sans-serif;
46
- --faint: #364854;
47
- --noise: #415866;
48
- --signal: #c8f24e;
49
- --signal-soft: rgba(200, 242, 78, 0.14);
50
- --amber: #e8b34b;
51
- --red: #ff6258;
52
- --mono: 'IBM Plex Mono', ui-monospace, monospace;
53
- --serif: 'Instrument Serif', Georgia, serif;
54
- }
55
-
56
- * { margin: 0; padding: 0; box-sizing: border-box; }
57
-
58
- /* Everything is rem-based: scale the root with the viewport so the UI
59
- stays readable on large displays and in screen recordings. */
60
- html { font-size: clamp(15px, 9px + 0.55vw, 22px); }
61
-
62
- html, body { height: 100%; }
63
-
64
- body {
65
- font-family: var(--mono);
66
- background: var(--ink);
67
- color: var(--bone);
68
- overflow: hidden;
69
- }
70
-
71
- /* ── Atmosphere: deep-field gradient, scanlines, vignette, grain ── */
72
- body::before {
73
- content: '';
74
- position: fixed; inset: 0;
75
- background:
76
- radial-gradient(ellipse 70% 55% at 18% -5%, rgba(200,242,78,0.045), transparent 60%),
77
- radial-gradient(ellipse 55% 45% at 95% 100%, rgba(105,160,190,0.05), transparent 65%),
78
- radial-gradient(ellipse 120% 100% at 50% 50%, transparent 55%, rgba(0,0,0,0.55) 100%);
79
- pointer-events: none; z-index: 0;
80
- }
81
-
82
- .scan {
83
- position: fixed; inset: 0; z-index: 1; pointer-events: none;
84
- background: repeating-linear-gradient(0deg,
85
- rgba(255,255,255,0.022) 0px, rgba(255,255,255,0.022) 1px,
86
- transparent 1px, transparent 3px);
87
- mix-blend-mode: overlay;
88
- }
89
-
90
- #grain {
91
- position: fixed; inset: 0; width: 100vw; height: 100vh;
92
- z-index: 1; pointer-events: none;
93
- image-rendering: pixelated;
94
- mix-blend-mode: screen;
95
- opacity: 0;
96
- transition: opacity 0.6s ease;
97
- }
98
- #grain.on { opacity: 0.055; }
99
-
100
- /* ── Frame ── */
101
- .frame {
102
- position: relative; z-index: 2;
103
- height: 100vh;
104
- display: flex; flex-direction: column;
105
- }
106
-
107
- header {
108
- flex: 0 0 auto;
109
- display: flex; align-items: center; justify-content: space-between;
110
- padding: 0.85rem 1.4rem;
111
- border-bottom: 1px solid var(--line-soft);
112
- background: rgba(6,9,11,0.7);
113
- backdrop-filter: blur(6px);
114
- }
115
-
116
- .brand { display: flex; align-items: baseline; gap: 0.9rem; }
117
-
118
- .lamp {
119
- width: 8px; height: 8px; border-radius: 50%;
120
- background: var(--faint);
121
- align-self: center;
122
- transition: all 0.4s;
123
- }
124
- .lamp.live { background: var(--signal); box-shadow: 0 0 10px rgba(200,242,78,0.7); }
125
- .lamp.dead { background: var(--red); box-shadow: 0 0 8px rgba(255,98,88,0.6); }
126
- .lamp.warm { background: var(--amber); box-shadow: 0 0 10px rgba(232,179,75,0.7); animation: breathe 1.2s ease-in-out infinite; }
127
-
128
- .brand h1 {
129
- font-size: 0.95rem; font-weight: 700;
130
- letter-spacing: 0.22em;
131
- color: var(--bone);
132
- }
133
- .brand h1 em { color: var(--signal); font-style: normal; }
134
-
135
- .tagline {
136
- font-family: var(--serif); font-style: italic;
137
- font-size: 1.02rem; color: var(--dim);
138
- letter-spacing: 0.01em;
139
- }
140
-
141
- .head-right { display: flex; align-items: center; gap: 0.75rem; }
142
-
143
- .model-chip {
144
- font-size: 0.62rem; color: var(--dim);
145
- letter-spacing: 0.06em;
146
- padding: 0.3rem 0.65rem;
147
- border: 1px solid var(--line);
148
- border-radius: 3px;
149
- max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
150
- }
151
-
152
- .harness-link {
153
- font-size: 0.62rem; color: var(--dim); text-decoration: none;
154
- letter-spacing: 0.1em;
155
- padding: 0.3rem 0.65rem;
156
- border: 1px solid var(--line); border-radius: 3px;
157
- transition: all 0.2s;
158
- }
159
- .harness-link:hover { color: var(--signal); border-color: var(--signal); }
160
-
161
- /* ── Stage: feed + rail ── */
162
- .stage { flex: 1 1 auto; display: flex; min-height: 0; }
163
-
164
- .feed-wrap {
165
- flex: 1 1 auto; min-width: 0;
166
- display: flex; flex-direction: column;
167
- }
168
-
169
- #feed {
170
- flex: 1 1 auto;
171
- overflow-y: auto;
172
- padding: 2rem 2rem 1rem;
173
- scroll-behavior: auto;
174
- }
175
- #feed::-webkit-scrollbar { width: 4px; }
176
- #feed::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
177
-
178
- .feed-inner { max-width: 54rem; margin: 0 auto; }
179
-
180
- /* ── Empty state ── */
181
- .empty {
182
- padding: 14vh 1rem 0;
183
- text-align: center;
184
- }
185
- .empty .big {
186
- font-family: var(--serif); font-style: italic;
187
- font-size: clamp(2.2rem, 5vw, 3.4rem);
188
- color: var(--bone);
189
- line-height: 1.1;
190
- }
191
- .empty .big .lit { color: var(--signal); }
192
- .empty .sub {
193
- margin: 1.1rem auto 0; max-width: 470px;
194
- font-size: 0.72rem; line-height: 1.8; color: var(--dim);
195
- letter-spacing: 0.03em;
196
- }
197
- .empty .chips { margin-top: 2rem; display: flex; gap: 0.6rem; justify-content: center; flex-wrap: wrap; }
198
- .chip {
199
- font-family: var(--mono); font-size: 0.78rem; color: var(--bone-dim);
200
- background: transparent;
201
- border: 1px solid var(--line); border-radius: 3px;
202
- padding: 0.5rem 0.85rem; cursor: pointer;
203
- letter-spacing: 0.04em;
204
- transition: all 0.2s;
205
- }
206
- .chip:hover { border-color: var(--signal); color: var(--signal); background: var(--signal-soft); }
207
-
208
- /* ── Messages ── */
209
- .msg { margin-bottom: 2.1rem; animation: rise 0.35s ease-out; }
210
- @keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
211
-
212
- .msg .who {
213
- display: flex; align-items: center; gap: 0.6rem;
214
- font-size: 0.72rem; letter-spacing: 0.2em; color: var(--faint);
215
- margin-bottom: 0.6rem;
216
- }
217
- .msg.user .who { color: var(--dim); justify-content: flex-end; }
218
- .msg.ai .who .name { color: var(--signal); }
219
-
220
- .who .phase { color: var(--dim); letter-spacing: 0.08em; }
221
- .who .phase.working { color: var(--amber); animation: breathe 1.4s ease-in-out infinite; }
222
- @keyframes breathe { 50% { opacity: 0.45; } }
223
-
224
- .who .thought-chip {
225
- color: var(--amber);
226
- border: 1px solid rgba(232,179,75,0.35); border-radius: 3px;
227
- padding: 0.1rem 0.4rem;
228
- letter-spacing: 0.1em;
229
- font-size: 0.56rem;
230
- }
231
-
232
- .msg.user .body {
233
- margin-left: auto;
234
- max-width: 78%;
235
- width: fit-content;
236
- font-size: 0.95rem; line-height: 1.7;
237
- color: var(--bone-dim);
238
- background: var(--ink-2);
239
- border: 1px solid var(--line-soft);
240
- border-right: 2px solid var(--faint);
241
- border-radius: 6px 2px 2px 6px;
242
- padding: 0.7rem 0.95rem;
243
- white-space: pre-wrap; word-break: break-word;
244
- }
245
-
246
- .msg.ai .body {
247
- border-left: 1px solid var(--line-soft);
248
- padding-left: 1.2rem;
249
- }
250
-
251
- .answer-body {
252
- font-size: 1.05rem; line-height: 1.95;
253
- white-space: pre-wrap; word-break: break-word;
254
- }
255
-
256
- /* ── Rendered markdown view: fades in when the reply settles. The mono
257
- token surface is the animation; this is the reading layer. ── */
258
- .md-body {
259
- display: none;
260
- font-family: var(--sans);
261
- font-size: 1.08rem; line-height: 1.75;
262
- color: var(--bone);
263
- word-break: break-word;
264
- animation: mdin 0.6s ease;
265
- }
266
- .md-body.show { display: block; }
267
- @keyframes mdin { from { opacity: 0; filter: blur(3px); } to { opacity: 1; filter: none; } }
268
-
269
- .md-body p { margin: 0 0 0.85em; }
270
- .md-body h1, .md-body h2, .md-body h3, .md-body h4 {
271
- font-family: var(--serif); font-weight: 400;
272
- color: var(--bone);
273
- margin: 1.1em 0 0.45em; line-height: 1.25;
274
- }
275
- .md-body h1 { font-size: 1.7rem; }
276
- .md-body h2 { font-size: 1.45rem; }
277
- .md-body h3 { font-size: 1.22rem; font-style: italic; }
278
- .md-body h4 { font-size: 1.08rem; font-style: italic; color: var(--bone-dim); }
279
- .md-body ul, .md-body ol { margin: 0 0 0.85em; padding-left: 1.5em; }
280
- .md-body li { margin-bottom: 0.3em; }
281
- .md-body strong { font-weight: 600; color: #fff; }
282
- .md-body em { font-style: italic; }
283
- .md-body a { color: var(--signal); text-decoration: none; border-bottom: 1px solid rgba(200,242,78,0.35); }
284
- .md-body blockquote {
285
- border-left: 2px solid rgba(232,179,75,0.5);
286
- padding: 0.1em 0 0.1em 0.9em; margin: 0 0 0.85em;
287
- color: var(--bone-dim);
288
- }
289
- .md-body code {
290
- font-family: var(--mono); font-size: 0.88em;
291
- background: rgba(200,242,78,0.08);
292
- border: 1px solid rgba(200,242,78,0.12);
293
- border-radius: 3px; padding: 0.08em 0.35em;
294
- color: #dff3a8;
295
- }
296
- .md-body pre {
297
- background: var(--ink-2);
298
- border: 1px solid var(--line);
299
- border-left: 2px solid rgba(200,242,78,0.4);
300
- border-radius: 6px;
301
- padding: 0.85em 1em; margin: 0 0 0.9em;
302
- overflow-x: auto;
303
- }
304
- .md-body pre code {
305
- background: none; border: none; padding: 0;
306
- color: var(--bone); font-size: 0.85rem; line-height: 1.6;
307
- white-space: pre;
308
- }
309
- .md-body hr { border: none; border-top: 1px solid var(--line); margin: 1.2em 0; }
310
-
311
- .cap-warn { color: var(--amber); }
312
-
313
- /* ── Reasoning channel: visually subordinate, collapsible ── */
314
- .think-wrap {
315
- display: none;
316
- margin-bottom: 0.9rem;
317
- border-left: 2px solid rgba(232,179,75,0.45);
318
- background: rgba(232,179,75,0.04);
319
- border-radius: 0 6px 6px 0;
320
- padding: 0.55rem 0.8rem 0.65rem;
321
- }
322
- .think-wrap.used { display: block; }
323
-
324
- .think-head {
325
- display: flex; align-items: center; gap: 0.6rem;
326
- font-size: 0.66rem; letter-spacing: 0.18em;
327
- color: var(--amber);
328
- cursor: pointer; user-select: none;
329
- }
330
- .think-head .chev { transition: transform 0.25s; font-size: 0.6rem; }
331
- .think-wrap.collapsed .chev { transform: rotate(-90deg); }
332
- .think-head .tmeta { color: rgba(232,179,75,0.55); letter-spacing: 0.06em; }
333
-
334
- .think-body {
335
- margin-top: 0.5rem;
336
- font-size: 0.85rem; line-height: 1.75;
337
- white-space: pre-wrap; word-break: break-word;
338
- opacity: 0.8;
339
- }
340
- .think-wrap.collapsed .think-body { display: none; }
341
-
342
- .think-body .tk.lock { color: #c9bb96; }
343
- .think-body .tk.lock.just {
344
- color: var(--amber);
345
- text-shadow: 0 0 14px rgba(232,179,75,0.5);
346
- }
347
-
348
- /* token spans: the diffusion surface */
349
- .tk { transition: color 0.55s ease, text-shadow 0.55s ease, opacity 0.4s ease; }
350
- .tk.noise { color: var(--noise); opacity: 0.55; }
351
- .tk.lock { color: var(--bone); opacity: 1; }
352
- .tk.lock.just {
353
- color: var(--signal);
354
- text-shadow: 0 0 16px rgba(200,242,78,0.55), 0 0 3px rgba(200,242,78,0.4);
355
- transition: none;
356
- }
357
-
358
- /* block wrapper: hover reveals structure */
359
- .blk { border-bottom: 1px solid transparent; transition: background 0.25s; }
360
- .blk:hover { background: rgba(200,242,78,0.035); }
361
-
362
- /* footer under a finished reply */
363
- .msg .foot {
364
- display: flex; align-items: center; gap: 0.9rem; flex-wrap: wrap;
365
- margin-top: 0.8rem; padding-left: 1.2rem;
366
- font-size: 0.7rem; color: var(--faint); letter-spacing: 0.08em;
367
- opacity: 0; transition: opacity 0.5s;
368
- }
369
- .msg .foot.show { opacity: 1; }
370
- .foot .stat b { color: var(--dim); font-weight: 500; }
371
-
372
- .foot .replay-ctl { display: flex; align-items: center; gap: 0.45rem; }
373
- .foot button {
374
- font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.1em;
375
- color: var(--dim); background: transparent;
376
- border: 1px solid var(--line); border-radius: 3px;
377
- padding: 0.22rem 0.5rem; cursor: pointer;
378
- transition: all 0.2s;
379
- }
380
- .foot button:hover { color: var(--signal); border-color: var(--signal); }
381
- .foot input[type=range] {
382
- -webkit-appearance: none; appearance: none;
383
- width: 130px; height: 2px;
384
- background: var(--line); border-radius: 2px;
385
- outline: none; cursor: pointer;
386
- }
387
- .foot input[type=range]::-webkit-slider-thumb {
388
- -webkit-appearance: none; appearance: none;
389
- width: 9px; height: 9px; border-radius: 50%;
390
- background: var(--signal);
391
- box-shadow: 0 0 6px rgba(200,242,78,0.6);
392
- }
393
-
394
- .msg.error .body { color: var(--red); border-left-color: var(--red); }
395
-
396
- /* ── Composer ── */
397
- .composer {
398
- flex: 0 0 auto;
399
- border-top: 1px solid var(--line-soft);
400
- background: rgba(8,12,15,0.85);
401
- backdrop-filter: blur(8px);
402
- padding: 0.9rem 2rem 1.1rem;
403
- }
404
- .composer-inner { max-width: 54rem; margin: 0 auto; display: flex; gap: 0.6rem; align-items: flex-end; }
405
-
406
- .composer textarea {
407
- flex: 1;
408
- background: var(--ink-2);
409
- border: 1px solid var(--line);
410
- border-radius: 4px;
411
- color: var(--bone);
412
- font-family: var(--mono); font-size: 0.95rem; line-height: 1.5;
413
- padding: 0.65rem 0.8rem;
414
- resize: none; outline: none;
415
- min-height: 44px; max-height: 140px;
416
- transition: border-color 0.2s, box-shadow 0.2s;
417
- }
418
- .composer textarea:focus {
419
- border-color: var(--signal);
420
- box-shadow: 0 0 0 1px rgba(200,242,78,0.25), 0 0 22px rgba(200,242,78,0.07);
421
- }
422
- .composer textarea::placeholder { color: var(--faint); }
423
-
424
- .tx-btn {
425
- font-family: var(--mono); font-size: 0.68rem; font-weight: 600;
426
- letter-spacing: 0.18em;
427
- color: var(--ink);
428
- background: var(--signal);
429
- border: 1px solid var(--signal);
430
- border-radius: 4px;
431
- padding: 0.78rem 1.1rem;
432
- cursor: pointer;
433
- transition: all 0.2s;
434
- white-space: nowrap;
435
- }
436
- .tx-btn:hover:not(:disabled) { box-shadow: 0 0 24px rgba(200,242,78,0.35); transform: translateY(-1px); }
437
- .tx-btn:disabled { opacity: 0.45; cursor: not-allowed; }
438
- .tx-btn.halt { background: transparent; color: var(--red); border-color: var(--red); }
439
- .tx-btn.halt:hover { box-shadow: 0 0 18px rgba(255,98,88,0.3); }
440
-
441
- .mt-sel {
442
- font-family: var(--mono); font-size: 0.62rem;
443
- color: var(--dim); background: var(--ink-2);
444
- border: 1px solid var(--line); border-radius: 4px;
445
- padding: 0.78rem 0.4rem; outline: none; cursor: pointer;
446
- }
447
-
448
- /* ── Telemetry rail ── */
449
- .rail {
450
- flex: 0 0 18rem;
451
- border-left: 1px solid var(--line-soft);
452
- background: rgba(9,13,16,0.6);
453
- padding: 1.1rem 1rem;
454
- overflow-y: auto;
455
- display: flex; flex-direction: column; gap: 1.2rem;
456
- }
457
- .rail::-webkit-scrollbar { width: 3px; }
458
- .rail::-webkit-scrollbar-thumb { background: var(--line); }
459
-
460
- .rail h2 {
461
- font-size: 0.58rem; font-weight: 600;
462
- letter-spacing: 0.3em; color: var(--faint);
463
- }
464
-
465
- .dials { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
466
- .dial {
467
- border: 1px solid var(--line-soft); border-radius: 4px;
468
- padding: 0.55rem 0.6rem;
469
- background: var(--ink-2);
470
- }
471
- .dial .v {
472
- font-size: 1.45rem; font-weight: 700;
473
- color: var(--signal);
474
- font-variant-numeric: tabular-nums;
475
- letter-spacing: 0.02em;
476
- }
477
- .dial .k { font-size: 0.52rem; letter-spacing: 0.16em; color: var(--faint); margin-top: 0.2rem; }
478
- .dial.wide { grid-column: span 2; }
479
-
480
- .blocks { display: flex; flex-direction: column; gap: 0.45rem; }
481
- .brow {
482
- display: flex; align-items: center; gap: 0.5rem;
483
- font-size: 0.58rem; color: var(--dim);
484
- font-variant-numeric: tabular-nums;
485
- animation: rise 0.3s ease-out;
486
- }
487
- .brow .bid { color: var(--signal); font-weight: 600; min-width: 26px; letter-spacing: 0.05em; }
488
- .brow .bbar { flex: 1; height: 3px; background: var(--line-soft); border-radius: 2px; overflow: hidden; }
489
- .brow .bbar i {
490
- display: block; height: 100%; border-radius: 2px;
491
- background: linear-gradient(90deg, rgba(200,242,78,0.4), var(--signal));
492
- }
493
- .brow .bt { min-width: 42px; text-align: right; color: var(--faint); }
494
- .brow.computing .bid { color: var(--amber); }
495
- .brow.computing .bbar i {
496
- width: 40%;
497
- background: linear-gradient(90deg, transparent, rgba(232,179,75,0.8), transparent);
498
- animation: sweep 1.1s linear infinite;
499
- }
500
- @keyframes sweep { from { transform: translateX(-100%); } to { transform: translateX(350%); } }
501
-
502
- .race { display: flex; flex-direction: column; gap: 0.5rem; }
503
- .rrow { font-size: 0.56rem; letter-spacing: 0.08em; color: var(--dim); }
504
- .rrow .rbar { margin-top: 0.25rem; height: 4px; background: var(--line-soft); border-radius: 2px; overflow: hidden; }
505
- .rrow .rbar i { display: block; height: 100%; border-radius: 2px; width: 0%; transition: width 0.8s ease; }
506
- .rrow.diff .rbar i { background: var(--signal); box-shadow: 0 0 8px rgba(200,242,78,0.5); }
507
- .rrow.ar .rbar i { background: var(--faint); }
508
- .race .verdict {
509
- font-family: var(--serif); font-style: italic;
510
- font-size: 0.95rem; color: var(--signal);
511
- margin-top: 0.1rem;
512
- }
513
-
514
- .rail .note {
515
- font-size: 0.56rem; line-height: 1.7; color: var(--faint);
516
- border-top: 1px solid var(--line-soft);
517
- padding-top: 0.8rem;
518
- letter-spacing: 0.03em;
519
- }
520
- .rail .note em { color: var(--dim); font-style: normal; }
521
-
522
- @media (max-width: 940px) { .rail { display: none; } }
523
- @media (max-width: 640px) {
524
- #feed { padding: 1.2rem 1rem 0.5rem; }
525
- .composer { padding: 0.7rem 1rem 0.9rem; }
526
- .tagline { display: none; }
527
- }
528
- </style>
529
- </head>
530
- <body>
531
- <canvas id="grain" width="420" height="260"></canvas>
532
- <div class="scan"></div>
533
-
534
- <div class="frame">
535
- <header>
536
- <div class="brand">
537
- <div class="lamp" id="lamp"></div>
538
- <h1>DIFFUSION<em>⁄</em>GEMMA</h1>
539
- <div class="tagline">language, condensed from static</div>
540
- </div>
541
- <div class="head-right">
542
- <div class="model-chip" id="model-chip">linking…</div>
543
- <a class="harness-link" href="/">HARNESS ↗</a>
544
- </div>
545
- </header>
546
-
547
- <div class="stage">
548
- <div class="feed-wrap">
549
- <div id="feed">
550
- <div class="feed-inner" id="feed-inner">
551
- <div class="empty" id="empty">
552
- <div class="big">The static <span class="lit">speaks</span>.</div>
553
- <div class="sub">
554
- Every reply is drafted as a whole — 256 tokens at a time —
555
- then denoised into language over a handful of passes.
556
- While one block resolves on your screen, the next is already
557
- being computed. Ask something and watch.
558
- </div>
559
- <div class="chips">
560
- <button class="chip" data-p="Write a short poem about the stars at night.">✶ a poem about stars</button>
561
- <button class="chip" data-p="Explain how diffusion language models differ from autoregressive ones, in three short paragraphs.">⌬ explain yourself</button>
562
- <button class="chip" data-p="Write a TypeScript function that debounces another function. Include types and a short usage example.">␥ debounce in TypeScript</button>
563
- </div>
564
- </div>
565
- </div>
566
- </div>
567
- <div class="composer">
568
- <div class="composer-inner">
569
- <textarea id="input" rows="1" placeholder="transmit a prompt…"></textarea>
570
- <select class="mt-sel" id="max-tok" title="max tokens">
571
- <option value="1024">1k</option>
572
- <option value="2048">2k</option>
573
- <option value="4096" selected>4k</option>
574
- <option value="8192">8k</option>
575
- <option value="16384">16k</option>
576
- </select>
577
- <button class="tx-btn" id="tx">TRANSMIT ▸</button>
578
- </div>
579
- </div>
580
- </div>
581
-
582
- <aside class="rail">
583
- <div>
584
- <h2>TELEMETRY</h2>
585
- <div class="dials" style="margin-top:0.7rem">
586
- <div class="dial"><div class="v" id="d-tokps">—</div><div class="k">≈ TOK/SEC</div></div>
587
- <div class="dial"><div class="v" id="d-blocks">0</div><div class="k">BLOCKS</div></div>
588
- <div class="dial wide"><div class="v" id="d-time">0.00s</div><div class="k">WALL TIME</div></div>
589
- </div>
590
- </div>
591
- <div>
592
- <h2>BLOCK LEDGER</h2>
593
- <div class="blocks" id="blocks" style="margin-top:0.7rem"></div>
594
- </div>
595
- <div>
596
- <h2>SAMPLER</h2>
597
- <div style="margin-top:0.7rem; display:flex; gap:0.45rem; align-items:center">
598
- <select class="mt-sel" id="entropy-sel" style="flex:1; padding:0.45rem 0.4rem">
599
- <option value="0.05">entropy 0.05 · precise</option>
600
- <option value="0.1" selected>entropy 0.10 · default</option>
601
- <option value="0.2">entropy 0.20 · faster</option>
602
- <option value="0.4">entropy 0.40 · reckless</option>
603
- </select>
604
- <button class="foot-like" id="entropy-apply" style="font-family:var(--mono);font-size:0.62rem;letter-spacing:0.1em;color:var(--dim);background:transparent;border:1px solid var(--line);border-radius:3px;padding:0.45rem 0.6rem;cursor:pointer">APPLY</button>
605
- </div>
606
- <div id="engine-status" style="margin-top:0.5rem;font-size:0.6rem;letter-spacing:0.08em;color:var(--faint);line-height:1.6">
607
- tokens accepted per denoise pass — higher is faster, looser.
608
- applying reloads the engine (~2–4 min).
609
- </div>
610
- </div>
611
- <div>
612
- <h2>VS AUTOREGRESSIVE</h2>
613
- <div class="race" style="margin-top:0.7rem" id="race">
614
- <div class="rrow diff">THIS REPLY <span id="r-diff-t" style="float:right"></span><div class="rbar"><i id="r-diff"></i></div></div>
615
- <div class="rrow ar">TYPICAL 55 TOK/S STREAM <span id="r-ar-t" style="float:right"></span><div class="rbar"><i id="r-ar"></i></div></div>
616
- <div class="verdict" id="verdict"></div>
617
- </div>
618
- </div>
619
- <div class="note">
620
- Each ledger row is one <em>real</em> 256-token diffusion block —
621
- its duration is the true server compute time. The glyph
622
- resolution order is staged; the text, blocks and timing are not.
623
- </div>
624
- </aside>
625
- </div>
626
- </div>
627
-
628
- <script>
629
- // ─────────────────────────────────────────────────────────────
630
- // constants & helpers (NO backslash escapes in this script —
631
- // see the note at the top of canvas.ts)
632
- // ─────────────────────────────────────────────────────────────
633
- var NL = String.fromCharCode(10);
634
- var GLYPHS = '·:;+*#%@&=?!~^░▒▓';
635
- var feed = document.getElementById('feed');
636
- var feedInner = document.getElementById('feed-inner');
637
- var inputEl = document.getElementById('input');
638
- var txBtn = document.getElementById('tx');
639
- var grain = document.getElementById('grain');
640
- var gctx = grain.getContext('2d');
641
-
642
- var convo = []; // window.history is unshadowable — do not name this 'history'
643
- var messages = []; // engine objects
644
- var live = null; // currently generating message
645
- var aborter = null;
646
-
647
- function mulberry(seed) {
648
- return function () {
649
- seed |= 0; seed = (seed + 1831565813) | 0;
650
- var t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
651
- t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
652
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
653
- };
654
- }
655
-
656
- function isWs(c) { return c.charCodeAt(0) <= 32; }
657
-
658
- // split text into tokens { txt, ws } — word + its trailing whitespace
659
- function tokenize(text) {
660
- var out = [], i = 0, n = text.length;
661
- var lead = '';
662
- while (i < n && isWs(text[i])) { lead += text[i]; i++; }
663
- if (lead) out.push({ txt: '', ws: lead });
664
- while (i < n) {
665
- var w = '';
666
- while (i < n && !isWs(text[i])) { w += text[i]; i++; }
667
- var s = '';
668
- while (i < n && isWs(text[i])) { s += text[i]; i++; }
669
- out.push({ txt: w, ws: s });
670
- }
671
- return out;
672
- }
673
-
674
- function noiseFor(token, rng) {
675
- var s = '';
676
- for (var i = 0; i < token.txt.length; i++) {
677
- s += GLYPHS[(rng() * GLYPHS.length) | 0];
678
- }
679
- return s + token.ws;
680
- }
681
-
682
- function fmtS(ms) { return (ms / 1000).toFixed(2) + 's'; }
683
- function estTok(chars) { return Math.max(1, Math.round(chars / 4)); }
684
-
685
- // ─────────────────────────────────────────────────────────────
686
- // health check
687
- // ─────────────────────────────────────────────────────────────
688
- fetch('/api/health').then(function (r) { return r.json(); }).then(function (d) {
689
- var lamp = document.getElementById('lamp');
690
- var chip = document.getElementById('model-chip');
691
- if (d.status === 'ok') {
692
- lamp.className = 'lamp live';
693
- chip.textContent = (d.models && d.models[0]) ? d.models[0] : 'model online';
694
- } else {
695
- lamp.className = 'lamp dead';
696
- chip.textContent = 'vLLM offline';
697
- }
698
- }).catch(function () {
699
- document.getElementById('lamp').className = 'lamp dead';
700
- document.getElementById('model-chip').textContent = 'vLLM unreachable';
701
- });
702
-
703
- // ─────────────────────────────────────────────────────────────
704
- // message construction
705
- // ─────────────────────────────────────────────────────────────
706
- function clearEmpty() {
707
- var e = document.getElementById('empty');
708
- if (e) e.remove();
709
- }
710
-
711
- function addUserMsg(text) {
712
- clearEmpty();
713
- var div = document.createElement('div');
714
- div.className = 'msg user';
715
- var who = document.createElement('div');
716
- who.className = 'who';
717
- who.textContent = 'YOU ▸';
718
- var body = document.createElement('div');
719
- body.className = 'body';
720
- body.textContent = text;
721
- div.appendChild(who); div.appendChild(body);
722
- feedInner.appendChild(div);
723
- scrollFeed(true);
724
- }
725
-
726
- function newAiMsg() {
727
- clearEmpty();
728
- var div = document.createElement('div');
729
- div.className = 'msg ai';
730
- div.innerHTML =
731
- '<div class="who"><span class="name">GEMMA</span>' +
732
- '<span class="phase working">▚ listening for first block…</span></div>' +
733
- '<div class="body">' +
734
- ' <div class="think-wrap collapsed">' +
735
- ' <div class="think-head"><span class="chev">▼</span> ⌬ REASONING CHANNEL <span class="tmeta"></span></div>' +
736
- ' <div class="think-body"></div>' +
737
- ' </div>' +
738
- ' <div class="answer-body"></div>' +
739
- ' <div class="md-body"></div>' +
740
- '</div>' +
741
- '<div class="foot"></div>';
742
- feedInner.appendChild(div);
743
- var thinkWrap = div.querySelector('.think-wrap');
744
- thinkWrap.querySelector('.think-head').addEventListener('click', function () {
745
- thinkWrap.classList.toggle('collapsed');
746
- });
747
- var m = {
748
- el: div,
749
- bodyEl: div.querySelector('.answer-body'),
750
- mdEl: div.querySelector('.md-body'),
751
- mdShown: false,
752
- capped: false,
753
- thinkWrapEl: thinkWrap,
754
- thinkBodyEl: div.querySelector('.think-body'),
755
- thinkMetaEl: div.querySelector('.tmeta'),
756
- phaseEl: div.querySelector('.phase'),
757
- footEl: div.querySelector('.foot'),
758
- whoEl: div.querySelector('.who'),
759
- mode: 'detect', // channel splitter state: detect | think | answer
760
- carry: '', // partial marker held back between chunks
761
- thinkChars: 0,
762
- tokens: [], // { txt, ws, lockAt, el, locked, justUntil }
763
- blocks: [], // { chars, start, dur, rowEl }
764
- t0: performance.now(),
765
- seed: ((Math.random() * 1e9) | 0) || 7,
766
- rng: null,
767
- fullText: '',
768
- thought: false,
769
- done: false,
770
- settled: false,
771
- totalMs: 0,
772
- streamMs: 0,
773
- replay: null, // { playing, speed, T, wall }
774
- lastNoise: 0,
775
- };
776
- m.rng = mulberry(m.seed);
777
- messages.push(m);
778
- // pin the viewport to the top of the reply: the surface grows downward
779
- // and resolves in place — yanking the scroll would fight the reader
780
- feed.scrollTop = Math.max(0, div.offsetTop - 70);
781
- return m;
782
- }
783
-
784
- // ─────────────────────────────────────────────────────────────
785
- // channel splitter: <|channel>thought ... <channel|> → reasoning,
786
- // everything after → answer. Markers can split across chunks, so a
787
- // partial-marker tail is carried into the next chunk.
788
- // ─────────────────────────────────────────────────────────────
789
- var OPEN_T = '<|channel>thought';
790
- var CLOSE_T = '<channel|>';
791
- var TURN_T = '<turn|>';
792
- var MARKERS = [OPEN_T, CLOSE_T, TURN_T];
793
-
794
- function partialHold(s) {
795
- var max = 0;
796
- for (var k = 0; k < MARKERS.length; k++) {
797
- var tok = MARKERS[k];
798
- var lim = Math.min(tok.length - 1, s.length);
799
- for (var L = lim; L > max; L--) {
800
- if (tok.indexOf(s.slice(s.length - L)) === 0) { max = L; break; }
801
- }
802
- }
803
- return max;
804
- }
805
-
806
- // returns [{ tgt: 't'|'a', txt }]
807
- function processPiece(m, piece) {
808
- var s = m.carry + piece;
809
- m.carry = '';
810
- var out = [];
811
-
812
- function emit(tgt, txt) {
813
- if (txt) out.push({ tgt: tgt, txt: txt });
814
- }
815
-
816
- while (s.length > 0) {
817
- if (m.mode === 'detect') {
818
- if (s.length < OPEN_T.length) {
819
- if (OPEN_T.indexOf(s) === 0) { m.carry = s; return out; }
820
- m.mode = 'answer';
821
- continue;
822
- }
823
- if (s.indexOf(OPEN_T) === 0) {
824
- m.mode = 'think';
825
- s = s.slice(OPEN_T.length);
826
- if (s[0] === NL) s = s.slice(1);
827
- } else {
828
- m.mode = 'answer';
829
- }
830
- continue;
831
- }
832
-
833
- if (m.mode === 'think') {
834
- var ci = s.indexOf(CLOSE_T);
835
- if (ci !== -1) {
836
- emit('t', s.slice(0, ci));
837
- s = s.slice(ci + CLOSE_T.length);
838
- m.mode = 'answer';
839
- continue;
840
- }
841
- var hold = partialHold(s);
842
- if (hold) { m.carry = s.slice(s.length - hold); s = s.slice(0, s.length - hold); }
843
- emit('t', s);
844
- return out;
845
- }
846
-
847
- // answer mode: drop stray markers, switch back on a reopened channel
848
- var best = -1, bestTok = '';
849
- for (var mi = 0; mi < MARKERS.length; mi++) {
850
- var idx = s.indexOf(MARKERS[mi]);
851
- if (idx !== -1 && (best === -1 || idx < best)) { best = idx; bestTok = MARKERS[mi]; }
852
- }
853
- if (best !== -1) {
854
- emit('a', s.slice(0, best));
855
- s = s.slice(best + bestTok.length);
856
- if (bestTok === OPEN_T) {
857
- m.mode = 'think';
858
- if (s[0] === NL) s = s.slice(1);
859
- }
860
- continue;
861
- }
862
- var hold2 = partialHold(s);
863
- if (hold2) { m.carry = s.slice(s.length - hold2); s = s.slice(0, s.length - hold2); }
864
- emit('a', s);
865
- return out;
866
- }
867
- return out;
868
- }
869
-
870
- // ─────────────────────────────────────────────────────────────
871
- // block scheduling: assign deterministic lock times in waves
872
- // ─────────────────────────────────────────────────────────────
873
- function scheduleBlock(m, segs, tStart, estDur) {
874
- var toks = [];
875
- var bIdx = m.blocks.length;
876
- var totalChars = 0;
877
-
878
- for (var si = 0; si < segs.length; si++) {
879
- var seg = segs[si];
880
- var segToks = tokenize(seg.txt);
881
- if (segToks.length === 0) continue;
882
- totalChars += seg.txt.length;
883
-
884
- var target = seg.tgt === 't' ? m.thinkBodyEl : m.bodyEl;
885
- if (seg.tgt === 't') {
886
- m.thinkWrapEl.classList.add('used');
887
- m.thinkChars += seg.txt.length;
888
- m.thinkMetaEl.textContent = '· ' + m.thinkChars + ' chars';
889
- }
890
- var blkSpan = document.createElement('span');
891
- blkSpan.className = 'blk';
892
- blkSpan.title = 'block ' + (bIdx + 1);
893
- target.appendChild(blkSpan);
894
- for (var ti = 0; ti < segToks.length; ti++) {
895
- segToks[ti].seg = blkSpan;
896
- segToks[ti].ch = seg.tgt;
897
- toks.push(segToks[ti]);
898
- }
899
- }
900
- if (toks.length === 0) return false;
901
-
902
- // weight: mostly random, biased so short words surface first
903
- var order = [];
904
- for (var i = 0; i < toks.length; i++) {
905
- var t = toks[i];
906
- var w = m.rng() * 0.7 + Math.min(t.txt.length, 12) / 12 * 0.3;
907
- order.push({ i: i, w: w });
908
- }
909
- order.sort(function (a, b) { return a.w - b.w; });
910
-
911
- // waves: visible denoising passes across the block window
912
- var waves = Math.max(4, Math.min(9, Math.round(estDur / 170)));
913
- var perWave = Math.ceil(order.length / waves);
914
-
915
- for (var k = 0; k < order.length; k++) {
916
- var wave = (k / perWave) | 0;
917
- // ease-in-out pacing: slow start, fast middle, slow end
918
- var p = (wave + 1) / waves;
919
- var eased = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
920
- var lockAt = tStart + eased * estDur + (m.rng() - 0.5) * 90;
921
- var tok = toks[order[k].i];
922
- tok.lockAt = Math.max(tStart + 30, lockAt);
923
- tok.locked = false;
924
- tok.justUntil = 0;
925
- }
926
-
927
- // build spans in document order, each into its segment's container
928
- for (var j = 0; j < toks.length; j++) {
929
- var tk = toks[j];
930
- var span = document.createElement('span');
931
- span.className = 'tk noise';
932
- span.textContent = noiseFor(tk, m.rng);
933
- tk.seg.appendChild(span);
934
- tk.el = span;
935
- m.tokens.push(tk);
936
- }
937
-
938
- m.blocks.push({ chars: totalChars, start: tStart, dur: estDur, rowEl: null });
939
- railAddBlock(m, bIdx, totalChars);
940
- return true;
941
- }
942
-
943
- // compress: pull every unlocked token's lockAt into [now, now+span]
944
- function compressPending(m, nowRel, span) {
945
- var pend = [];
946
- for (var i = 0; i < m.tokens.length; i++) {
947
- var t = m.tokens[i];
948
- if (!t.locked && t.lockAt > nowRel + span) pend.push(t);
949
- }
950
- pend.sort(function (a, b) { return a.lockAt - b.lockAt; });
951
- for (var k = 0; k < pend.length; k++) {
952
- pend[k].lockAt = nowRel + (k / Math.max(1, pend.length)) * span;
953
- }
954
- }
955
-
956
- // ─────────────────────────────────────────────────────────────
957
- // telemetry rail
958
- // ─────────────────────────────────────────────────────────────
959
- var blocksEl = document.getElementById('blocks');
960
- var computingRow = null;
961
-
962
- function railReset() {
963
- blocksEl.innerHTML = '';
964
- computingRow = null;
965
- document.getElementById('d-tokps').textContent = '—';
966
- document.getElementById('d-blocks').textContent = '0';
967
- document.getElementById('d-time').textContent = '0.00s';
968
- document.getElementById('r-diff').style.width = '0%';
969
- document.getElementById('r-ar').style.width = '0%';
970
- document.getElementById('r-diff-t').textContent = '';
971
- document.getElementById('r-ar-t').textContent = '';
972
- document.getElementById('verdict').textContent = '';
973
- }
974
-
975
- function railAddBlock(m, idx, chars) {
976
- if (computingRow) { computingRow.remove(); computingRow = null; }
977
- // a block's compute time is the gap BEFORE its arrival
978
- // (block 1's window includes prompt prefill)
979
- var prevStart = idx > 0 ? m.blocks[idx - 1].start : 0;
980
- var realDur = m.blocks[idx].start - prevStart;
981
- m.blocks[idx].realDur = realDur;
982
-
983
- var row = document.createElement('div');
984
- row.className = 'brow';
985
- row.innerHTML =
986
- '<span class="bid">B' + String(idx + 1).padStart(2, '0') + '</span>' +
987
- '<span class="bbar"><i style="width:8%"></i></span>' +
988
- '<span class="bt">' + fmtS(realDur) + '</span>';
989
- blocksEl.appendChild(row);
990
- m.blocks[idx].rowEl = row;
991
-
992
- rescaleBars(m);
993
- railComputing();
994
- blocksEl.scrollTop = blocksEl.scrollHeight;
995
- }
996
-
997
- function rescaleBars(m) {
998
- var maxDur = 1;
999
- for (var i = 0; i < m.blocks.length; i++) {
1000
- if (m.blocks[i].realDur && m.blocks[i].realDur > maxDur) maxDur = m.blocks[i].realDur;
1001
- }
1002
- for (var k = 0; k < m.blocks.length; k++) {
1003
- var bb = m.blocks[k];
1004
- if (bb.realDur && bb.rowEl) {
1005
- bb.rowEl.querySelector('i').style.width = Math.max(8, bb.realDur / maxDur * 100) + '%';
1006
- }
1007
- }
1008
- }
1009
-
1010
- function railComputing() {
1011
- computingRow = document.createElement('div');
1012
- computingRow.className = 'brow computing';
1013
- computingRow.innerHTML =
1014
- '<span class="bid">B' + String((live ? live.blocks.length : 0) + 1).padStart(2, '0') + '</span>' +
1015
- '<span class="bbar"><i></i></span>' +
1016
- '<span class="bt">denoise</span>';
1017
- blocksEl.appendChild(computingRow);
1018
- }
1019
-
1020
- function railFinish(m) {
1021
- if (computingRow) { computingRow.remove(); computingRow = null; }
1022
- // race
1023
- var chars = m.fullText.length;
1024
- var toks = estTok(chars);
1025
- var diffS = m.streamMs / 1000;
1026
- var arS = toks / 55;
1027
- var maxS = Math.max(diffS, arS, 0.001);
1028
- document.getElementById('r-diff').style.width = (diffS / maxS * 100) + '%';
1029
- document.getElementById('r-ar').style.width = (arS / maxS * 100) + '%';
1030
- document.getElementById('r-diff-t').textContent = diffS.toFixed(2) + 's';
1031
- document.getElementById('r-ar-t').textContent = arS.toFixed(1) + 's';
1032
- if (arS > diffS && diffS > 0) {
1033
- document.getElementById('verdict').textContent = '× ' + (arS / diffS).toFixed(1) + ' faster';
1034
- }
1035
- if (diffS > 0) {
1036
- document.getElementById('d-tokps').textContent = String(Math.round(toks / diffS));
1037
- document.getElementById('d-time').textContent = fmtS(m.streamMs);
1038
- }
1039
- }
1040
-
1041
- // ─────────────────────────────────────────────────────────────
1042
- // markdown: minimal zero-dep renderer for the settled reading view.
1043
- // All input is HTML-escaped first; we only emit our own tags.
1044
- // (Backtick literals via charCode — see the template-literal note.)
1045
- // ─────────────────────────────────────────────────────────────
1046
- var BT = String.fromCharCode(96);
1047
- var FENCE = BT + BT + BT;
1048
-
1049
- function escMd(s) {
1050
- return s.split('&').join('&amp;').split('<').join('&lt;').split('>').join('&gt;');
1051
- }
1052
-
1053
- // wrap delimiter pairs: pairWrap('a **b** c','**','<strong>','</strong>')
1054
- function pairWrap(s, delim, tagO, tagC) {
1055
- var out = '', rest = s;
1056
- while (true) {
1057
- var a = rest.indexOf(delim);
1058
- if (a === -1) break;
1059
- var b = rest.indexOf(delim, a + delim.length);
1060
- if (b === -1) break;
1061
- var inner = rest.slice(a + delim.length, b);
1062
- if (inner.length === 0 || inner.length > 400) { out += rest.slice(0, b); rest = rest.slice(b); continue; }
1063
- out += rest.slice(0, a) + tagO + inner + tagC;
1064
- rest = rest.slice(b + delim.length);
1065
- }
1066
- return out + rest;
1067
- }
1068
-
1069
- function linkify(s) {
1070
- var out = '', rest = s;
1071
- while (true) {
1072
- var a = rest.indexOf('[');
1073
- if (a === -1) break;
1074
- var b = rest.indexOf('](', a);
1075
- if (b === -1) break;
1076
- var c = rest.indexOf(')', b + 2);
1077
- if (c === -1) break;
1078
- var label = rest.slice(a + 1, b);
1079
- var url = rest.slice(b + 2, c);
1080
- if (url.indexOf('http') === 0 && label.indexOf('[') === -1) {
1081
- out += rest.slice(0, a) + '<a href="' + url.split('"').join('') + '" target="_blank" rel="noopener">' + label + '</a>';
1082
- rest = rest.slice(c + 1);
1083
- } else {
1084
- out += rest.slice(0, a + 1);
1085
- rest = rest.slice(a + 1);
1086
- }
1087
- }
1088
- return out + rest;
1089
- }
1090
-
1091
- function inlineMd(s) {
1092
- s = escMd(s);
1093
- s = pairWrap(s, BT, '<code>', '</code>');
1094
- s = linkify(s);
1095
- s = pairWrap(s, '**', '<strong>', '</strong>');
1096
- s = pairWrap(s, '*', '<em>', '</em>');
1097
- return s;
1098
- }
1099
-
1100
- function mdRender(text) {
1101
- var lines = text.split(NL);
1102
- var html = '', para = [], i = 0;
1103
-
1104
- function flushPara() {
1105
- if (para.length === 0) return;
1106
- html += '<p>' + para.map(inlineMd).join('<br>') + '</p>';
1107
- para = [];
1108
- }
1109
-
1110
- while (i < lines.length) {
1111
- var line = lines[i];
1112
- var t = line.trim();
1113
-
1114
- if (t.indexOf(FENCE) === 0) { // fenced code
1115
- flushPara();
1116
- var code = [];
1117
- i++;
1118
- while (i < lines.length && lines[i].trim().indexOf(FENCE) !== 0) { code.push(lines[i]); i++; }
1119
- i++; // closing fence
1120
- html += '<pre><code>' + escMd(code.join(NL)) + '</code></pre>';
1121
- continue;
1122
- }
1123
-
1124
- if (t === '') { flushPara(); i++; continue; }
1125
-
1126
- if (t === '---' || t === '***' || t === '___') { flushPara(); html += '<hr>'; i++; continue; }
1127
-
1128
- var h = 0;
1129
- while (h < 4 && t[h] === '#') h++;
1130
- if (h > 0 && t[h] === ' ') {
1131
- flushPara();
1132
- html += '<h' + h + '>' + inlineMd(t.slice(h + 1)) + '</h' + h + '>';
1133
- i++; continue;
1134
- }
1135
-
1136
- if (t[0] === '>') {
1137
- flushPara();
1138
- var quote = [];
1139
- while (i < lines.length && lines[i].trim()[0] === '>') {
1140
- quote.push(lines[i].trim().slice(1).trim());
1141
- i++;
1142
- }
1143
- html += '<blockquote>' + quote.map(inlineMd).join('<br>') + '</blockquote>';
1144
- continue;
1145
- }
1146
-
1147
- var isUl = (t.indexOf('- ') === 0 || t.indexOf('* ') === 0 || t.indexOf('+ ') === 0);
1148
- var olLen = 0;
1149
- while (olLen < 3 && t.charCodeAt(olLen) >= 48 && t.charCodeAt(olLen) <= 57) olLen++;
1150
- var isOl = olLen > 0 && t.slice(olLen, olLen + 2) === '. ';
1151
- if (isUl || isOl) {
1152
- flushPara();
1153
- var tag = isUl ? 'ul' : 'ol';
1154
- html += '<' + tag + '>';
1155
- while (i < lines.length) {
1156
- var lt = lines[i].trim();
1157
- var ul2 = (lt.indexOf('- ') === 0 || lt.indexOf('* ') === 0 || lt.indexOf('+ ') === 0);
1158
- var on = 0;
1159
- while (on < 3 && lt.charCodeAt(on) >= 48 && lt.charCodeAt(on) <= 57) on++;
1160
- var ol2 = on > 0 && lt.slice(on, on + 2) === '. ';
1161
- if (isUl ? !ul2 : !ol2) break;
1162
- html += '<li>' + inlineMd(isUl ? lt.slice(2) : lt.slice(on + 2)) + '</li>';
1163
- i++;
1164
- }
1165
- html += '</' + tag + '>';
1166
- continue;
1167
- }
1168
-
1169
- para.push(line);
1170
- i++;
1171
- }
1172
- flushPara();
1173
- return html;
1174
- }
1175
-
1176
- function answerText(m) {
1177
- var s = '';
1178
- for (var i = 0; i < m.tokens.length; i++) {
1179
- var t = m.tokens[i];
1180
- if (t.ch !== 't') s += t.txt + t.ws;
1181
- }
1182
- return s;
1183
- }
1184
-
1185
- function showMd(m) {
1186
- if (m.mdShown) return;
1187
- var txt = answerText(m).trim();
1188
- if (!txt) return;
1189
- m.mdEl.innerHTML = mdRender(txt);
1190
- m.mdEl.classList.add('show');
1191
- m.bodyEl.style.display = 'none';
1192
- m.mdShown = true;
1193
- }
1194
-
1195
- function hideMd(m) {
1196
- if (!m.mdShown) return;
1197
- m.mdEl.classList.remove('show');
1198
- m.bodyEl.style.display = '';
1199
- m.mdShown = false;
1200
- }
1201
-
1202
- // ─────────────────────────────────────────────────────────────
1203
- // the engine: one pure render pass, driven by T per message
1204
- // ─────────────────────────────────────────────────────────────
1205
- var noiseRng = mulberry(42);
1206
-
1207
- function msgTime(m, now) {
1208
- if (m.replay) {
1209
- if (m.replay.playing) {
1210
- var t = m.replay.T + (now - m.replay.wall) * m.replay.speed;
1211
- if (t >= m.totalMs) { m.replay = null; return m.totalMs; }
1212
- return t;
1213
- }
1214
- return m.replay.T; // paused via scrubber
1215
- }
1216
- if (!m.done) return now - m.t0;
1217
- return m.totalMs;
1218
- }
1219
-
1220
- function renderMsg(m, now) {
1221
- var T = msgTime(m, now);
1222
- var doNoise = now - m.lastNoise > 70;
1223
- if (doNoise) m.lastNoise = now;
1224
- var unlockedBudget = 700;
1225
- var unsettled = 0;
1226
-
1227
- for (var i = 0; i < m.tokens.length; i++) {
1228
- var t = m.tokens[i];
1229
- if (t.lockAt <= T) {
1230
- if (!t.locked) {
1231
- t.locked = true;
1232
- t.el.textContent = t.txt + t.ws;
1233
- t.el.className = 'tk lock just';
1234
- t.justUntil = now + 380;
1235
- unsettled++;
1236
- } else if (t.justUntil) {
1237
- if (now > t.justUntil) {
1238
- t.el.className = 'tk lock';
1239
- t.justUntil = 0;
1240
- } else {
1241
- unsettled++;
1242
- }
1243
- }
1244
- } else {
1245
- unsettled++;
1246
- if (t.locked) { // scrubbed backwards
1247
- t.locked = false;
1248
- t.el.className = 'tk noise';
1249
- t.el.textContent = noiseFor(t, noiseRng);
1250
- } else if (doNoise && unlockedBudget > 0) {
1251
- t.el.textContent = noiseFor(t, noiseRng);
1252
- unlockedBudget--;
1253
- }
1254
- }
1255
- }
1256
-
1257
- // scrub position
1258
- if (m.scrubEl && m.totalMs > 0) {
1259
- m.scrubEl.value = String(Math.round(T / m.totalMs * 1000));
1260
- }
1261
- return unsettled;
1262
- }
1263
-
1264
- function anyActive() {
1265
- if (live) return true;
1266
- for (var i = 0; i < messages.length; i++) {
1267
- var m = messages[i];
1268
- if (m.replay && m.replay.playing) return true;
1269
- for (var k = 0; k < m.tokens.length; k++) {
1270
- if (!m.tokens[k].locked) return true;
1271
- }
1272
- if (m.replay) return true;
1273
- }
1274
- return false;
1275
- }
1276
-
1277
- var lastGrain = 0;
1278
- function drawGrain(now) {
1279
- if (now - lastGrain < 90) return;
1280
- lastGrain = now;
1281
- var w = grain.width, h = grain.height;
1282
- var img = gctx.createImageData(w, h);
1283
- var d = img.data;
1284
- for (var i = 0; i < 4200; i++) {
1285
- var p = ((Math.random() * w * h) | 0) * 4;
1286
- var v = 90 + ((Math.random() * 120) | 0);
1287
- d[p] = v; d[p + 1] = v; d[p + 2] = v; d[p + 3] = 255;
1288
- }
1289
- gctx.putImageData(img, 0, 0);
1290
- }
1291
-
1292
- function loop() {
1293
- var now = performance.now();
1294
- var active = false;
1295
-
1296
- for (var i = 0; i < messages.length; i++) {
1297
- var m = messages[i];
1298
- // a message stays animated until every token is locked AND its
1299
- // lock-flash has faded — m.done alone is not enough (fast streams
1300
- // finish before the tail of the schedule plays out)
1301
- if (m.settled && !m.replay) { showMd(m); continue; }
1302
- active = true;
1303
- var unsettled = renderMsg(m, now);
1304
- m.settled = (m.done && !m.replay && unsettled === 0);
1305
- }
1306
-
1307
- if (live) {
1308
- active = true;
1309
- var T = now - live.t0;
1310
- // HUD
1311
- var lockedChars = 0;
1312
- for (var k = 0; k < live.tokens.length; k++) {
1313
- if (live.tokens[k].locked) lockedChars += live.tokens[k].txt.length + 1;
1314
- }
1315
- document.getElementById('d-time').textContent = fmtS(T);
1316
- document.getElementById('d-blocks').textContent = String(live.blocks.length);
1317
- if (T > 200) {
1318
- document.getElementById('d-tokps').textContent =
1319
- String(Math.round(estTok(live.fullText.length) / (live.streamMs ? live.streamMs / 1000 : T / 1000)));
1320
- }
1321
- }
1322
-
1323
- if (active) {
1324
- grain.classList.add('on');
1325
- drawGrain(now);
1326
- } else {
1327
- grain.classList.remove('on');
1328
- }
1329
- requestAnimationFrame(loop);
1330
- }
1331
- requestAnimationFrame(loop);
1332
-
1333
- function scrollFeed(force) {
1334
- var nearBottom = feed.scrollHeight - feed.scrollTop - feed.clientHeight < 140;
1335
- if (force || nearBottom) feed.scrollTop = feed.scrollHeight;
1336
- }
1337
-
1338
- // ─────────────────────────────────────────────────────────────
1339
- // streaming
1340
- // ─────────────────────────────────────────────────────────────
1341
- async function transmit() {
1342
- var text = inputEl.value.trim();
1343
- if (!text || live || engineReloading) return;
1344
- inputEl.value = '';
1345
- inputEl.style.height = 'auto';
1346
-
1347
- // older messages may be mid-replay or scrubbed — snap them back to settled
1348
- for (var oi = 0; oi < messages.length; oi++) {
1349
- if (messages[oi].done) { messages[oi].replay = null; messages[oi].settled = false; }
1350
- }
1351
-
1352
- convo.push({ role: 'user', content: text });
1353
- addUserMsg(text);
1354
-
1355
- var m = newAiMsg();
1356
- live = m;
1357
- railReset();
1358
- railComputing();
1359
- txBtn.textContent = 'HALT ■';
1360
- txBtn.className = 'tx-btn halt';
1361
- aborter = new AbortController();
1362
-
1363
- var defaultGap = 1100;
1364
- var lastArrival = 0;
1365
-
1366
- try {
1367
- var res = await fetch('/api/stream-raw', {
1368
- method: 'POST',
1369
- headers: { 'Content-Type': 'application/json' },
1370
- body: JSON.stringify({
1371
- messages: convo,
1372
- maxTokens: parseInt(document.getElementById('max-tok').value, 10) || 1024,
1373
- }),
1374
- signal: aborter.signal,
1375
- });
1376
- if (!res.ok) throw new Error('upstream ' + res.status);
1377
-
1378
- var reader = res.body.getReader();
1379
- var decoder = new TextDecoder();
1380
- var buffer = '';
1381
-
1382
- while (true) {
1383
- var r = await reader.read();
1384
- if (r.done) break;
1385
- buffer += decoder.decode(r.value, { stream: true });
1386
- var lines = buffer.split(NL);
1387
- buffer = lines.pop() || '';
1388
-
1389
- for (var li = 0; li < lines.length; li++) {
1390
- var line = lines[li];
1391
- if (line.indexOf('data: ') !== 0 || line === 'data: [DONE]') continue;
1392
- var chunk;
1393
- try { chunk = JSON.parse(line.slice(6)); } catch (e) { continue; }
1394
- var choice0 = chunk.choices && chunk.choices[0];
1395
- var delta = choice0 && choice0.delta;
1396
- if (choice0 && choice0.finish_reason === 'length') m.capped = true;
1397
- if (!delta) continue;
1398
- var piece = delta.reasoning_content || delta.content || '';
1399
- if (!piece) continue;
1400
-
1401
- var rel = performance.now() - m.t0;
1402
- m.fullText += piece; // raw with markers — the chat template strips thinking itself
1403
-
1404
- var segs = processPiece(m, piece);
1405
- if (segs.length === 0) continue; // chunk was only markers / carried tail
1406
-
1407
- // estimate this block's denoise window from the previous gap
1408
- var gap = lastArrival > 0 ? rel - lastArrival : defaultGap;
1409
- lastArrival = rel;
1410
- var estDur = Math.max(450, Math.min(2400, gap)) * 0.92;
1411
-
1412
- // earlier blocks must not lag behind: compress leftovers
1413
- compressPending(m, rel, 220);
1414
- if (scheduleBlock(m, segs, rel, estDur)) {
1415
- m.phaseEl.textContent = '▚ block ' + m.blocks.length + ' resolving · next one computing…';
1416
- }
1417
- }
1418
- }
1419
- m.streamMs = performance.now() - m.t0;
1420
- } catch (err) {
1421
- if (err && err.name === 'AbortError') {
1422
- m.streamMs = performance.now() - m.t0;
1423
- m.phaseEl.textContent = '■ halted';
1424
- } else {
1425
- m.el.classList.add('error');
1426
- m.bodyEl.textContent = '✕ ' + (err && err.message ? err.message : String(err));
1427
- m.phaseEl.textContent = '✕ link error';
1428
- }
1429
- }
1430
-
1431
- // flush any held-back partial-marker tail as literal text
1432
- if (m.carry) {
1433
- var tailSegs = [{ tgt: m.mode === 'think' ? 't' : 'a', txt: m.carry }];
1434
- m.carry = '';
1435
- scheduleBlock(m, tailSegs, performance.now() - m.t0, 300);
1436
- }
1437
-
1438
- // settle: lock whatever remains over a short tail
1439
- var nowRel = performance.now() - m.t0;
1440
- compressPending(m, nowRel, 600);
1441
- if (!m.streamMs) m.streamMs = nowRel;
1442
-
1443
- var maxLock = 0;
1444
- for (var i = 0; i < m.tokens.length; i++) {
1445
- if (m.tokens[i].lockAt > maxLock) maxLock = m.tokens[i].lockAt;
1446
- }
1447
- m.totalMs = maxLock + 200;
1448
- m.done = true;
1449
-
1450
- if (m.fullText) convo.push({ role: 'assistant', content: m.fullText });
1451
- if (!m.el.classList.contains('error')) buildFoot(m);
1452
- railFinish(m);
1453
- m.phaseEl.className = 'phase';
1454
- if (m.phaseEl.textContent.indexOf('■') !== 0 && m.phaseEl.textContent.indexOf('✕') !== 0) {
1455
- m.phaseEl.textContent = m.capped ? '▰ settled · ⚠ capped' : '▰ settled';
1456
- if (m.capped) m.phaseEl.className = 'phase cap-warn';
1457
- }
1458
-
1459
- live = null;
1460
- aborter = null;
1461
- txBtn.textContent = 'TRANSMIT ▸';
1462
- txBtn.className = 'tx-btn';
1463
- inputEl.focus();
1464
- }
1465
-
1466
- // ─────────────────────────────────────────────────────────────
1467
- // per-message footer: stats + replay + scrubber
1468
- // ─────────────────────────────────────────────────────────────
1469
- function buildFoot(m) {
1470
- var toks = estTok(m.fullText.length);
1471
- var tokps = m.streamMs > 0 ? Math.round(toks / (m.streamMs / 1000)) : 0;
1472
- var stats = document.createElement('span');
1473
- stats.className = 'stat';
1474
- stats.innerHTML =
1475
- '<b>' + m.blocks.length + '</b> blocks · <b>≈' + toks + '</b> tok · ' +
1476
- '<b>' + fmtS(m.streamMs) + '</b> · <b>' + tokps + '</b> tok/s' +
1477
- (m.capped ? ' · <b class="cap-warn">⚠ hit the token cap — raise MAX TOKENS</b>' : '');
1478
- m.footEl.appendChild(stats);
1479
-
1480
- var ctl = document.createElement('span');
1481
- ctl.className = 'replay-ctl';
1482
-
1483
- var btn = document.createElement('button');
1484
- btn.textContent = '⟲ REPLAY';
1485
- var speeds = [0.35, 0.7, 1];
1486
- var speedNames = ['⅓×', '⅔×', '1×'];
1487
- var si = 0;
1488
- var spd = document.createElement('button');
1489
- spd.textContent = speedNames[si];
1490
- var scrub = document.createElement('input');
1491
- scrub.type = 'range';
1492
- scrub.min = '0'; scrub.max = '1000'; scrub.value = '1000';
1493
- m.scrubEl = scrub;
1494
-
1495
- btn.addEventListener('click', function () {
1496
- hideMd(m);
1497
- m.replay = { playing: true, speed: speeds[si], T: 0, wall: performance.now() };
1498
- });
1499
- spd.addEventListener('click', function () {
1500
- si = (si + 1) % speeds.length;
1501
- spd.textContent = speedNames[si];
1502
- if (m.replay) {
1503
- var now = performance.now();
1504
- m.replay.T = msgTime(m, now);
1505
- m.replay.wall = now;
1506
- m.replay.speed = speeds[si];
1507
- }
1508
- });
1509
- scrub.addEventListener('input', function () {
1510
- hideMd(m);
1511
- var frac = parseInt(scrub.value, 10) / 1000;
1512
- m.replay = { playing: false, speed: speeds[si], T: frac * m.totalMs, wall: performance.now() };
1513
- });
1514
-
1515
- ctl.appendChild(btn);
1516
- ctl.appendChild(spd);
1517
- ctl.appendChild(scrub);
1518
- m.footEl.appendChild(ctl);
1519
- m.footEl.classList.add('show');
1520
- }
1521
-
1522
- // ─────────────────────────────────────────────────────────────
1523
- // engine config: entropy is engine-level (vLLM hf_overrides), so
1524
- // applying a new value reloads the container (~2–4 min)
1525
- // ─────────────────────────────────────────────────────────────
1526
- var engineReloading = false;
1527
- var entropySel = document.getElementById('entropy-sel');
1528
- var entropyApply = document.getElementById('entropy-apply');
1529
- var engineStatus = document.getElementById('engine-status');
1530
- var lampEl = document.getElementById('lamp');
1531
-
1532
- fetch('/api/engine-config').then(function (r) { return r.json(); }).then(function (d) {
1533
- if (d && d.entropy != null) {
1534
- var v = String(d.entropy);
1535
- for (var i = 0; i < entropySel.options.length; i++) {
1536
- if (entropySel.options[i].value === v) { entropySel.value = v; break; }
1537
- }
1538
- }
1539
- }).catch(function () {});
1540
-
1541
- entropyApply.addEventListener('click', function () {
1542
- if (engineReloading || live) return;
1543
- var v = parseFloat(entropySel.value);
1544
- engineReloading = true;
1545
- entropyApply.disabled = true;
1546
- txBtn.disabled = true;
1547
- lampEl.className = 'lamp warm';
1548
- engineStatus.textContent = '⟳ requesting engine reload…';
1549
- var t0 = Date.now();
1550
-
1551
- fetch('/api/engine-config', {
1552
- method: 'POST',
1553
- headers: { 'Content-Type': 'application/json' },
1554
- body: JSON.stringify({ entropy: v }),
1555
- }).then(function (r) {
1556
- if (!r.ok) return r.json().then(function (e) { throw new Error(e.error || ('http ' + r.status)); });
1557
- var timer = setInterval(function () {
1558
- var secs = Math.round((Date.now() - t0) / 1000);
1559
- engineStatus.textContent = '⟳ engine reloading · entropy ' + v + ' · ' + secs + 's (typ. 2–4 min)';
1560
- fetch('/api/health').then(function (hr) { return hr.json(); }).then(function (h) {
1561
- if (h.status === 'ok') {
1562
- clearInterval(timer);
1563
- engineReloading = false;
1564
- entropyApply.disabled = false;
1565
- txBtn.disabled = false;
1566
- lampEl.className = 'lamp live';
1567
- engineStatus.textContent = '▰ engine live · entropy ' + v + ' · reloaded in ' + secs + 's';
1568
- }
1569
- }).catch(function () {});
1570
- }, 5000);
1571
- }).catch(function (e) {
1572
- engineReloading = false;
1573
- entropyApply.disabled = false;
1574
- txBtn.disabled = false;
1575
- lampEl.className = 'lamp dead';
1576
- engineStatus.textContent = '✕ ' + e.message;
1577
- });
1578
- });
1579
-
1580
- // ─────────────────────────────────────────────────────────────
1581
- // input wiring
1582
- // ─────────────────────────────────────────────────────────────
1583
- inputEl.addEventListener('input', function () {
1584
- inputEl.style.height = 'auto';
1585
- inputEl.style.height = Math.min(inputEl.scrollHeight, 140) + 'px';
1586
- });
1587
- inputEl.addEventListener('keydown', function (e) {
1588
- if (e.key === 'Enter' && !e.shiftKey) {
1589
- e.preventDefault();
1590
- transmit();
1591
- }
1592
- });
1593
- txBtn.addEventListener('click', function () {
1594
- if (live && aborter) { aborter.abort(); return; }
1595
- transmit();
1596
- });
1597
- document.querySelectorAll('.chip').forEach(function (c) {
1598
- c.addEventListener('click', function () {
1599
- inputEl.value = c.getAttribute('data-p') || '';
1600
- transmit();
1601
- });
1602
- });
1603
- inputEl.focus();
1604
- </script>
1605
- </body>
1606
- </html>`;