otterly 0.3.1 → 0.3.3
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.
- package/dist/cli.js +1 -1
- package/dist/engine.js +133 -2
- package/dist/server/index.js +1 -1
- package/dist/server/playground.js +1075 -676
- package/dist/server/routes-native.js +1 -1
- package/dist/server/swagger.js +3 -3
- package/package.json +1 -1
|
@@ -1,540 +1,795 @@
|
|
|
1
1
|
// Interactive API playground — self-contained HTML served at GET /playground.
|
|
2
|
-
// Zero external dependencies. Dark mode.
|
|
2
|
+
// Zero external dependencies. Dark mode. Modern dev-tool aesthetic.
|
|
3
3
|
function playgroundCSS() {
|
|
4
4
|
return `
|
|
5
5
|
:root {
|
|
6
|
-
--bg: #
|
|
7
|
-
--
|
|
8
|
-
--
|
|
9
|
-
--
|
|
10
|
-
--
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
6
|
+
--bg-0: #09090b;
|
|
7
|
+
--bg-1: #0f0f12;
|
|
8
|
+
--bg-2: #18181b;
|
|
9
|
+
--bg-3: #232328;
|
|
10
|
+
--border: #27272a;
|
|
11
|
+
--border-subtle: #1e1e22;
|
|
12
|
+
--text-0: #fafafa;
|
|
13
|
+
--text-1: #a1a1aa;
|
|
14
|
+
--text-2: #63636e;
|
|
15
|
+
--accent: #5b9dff;
|
|
16
|
+
--accent-soft: rgba(91, 157, 255, 0.1);
|
|
17
|
+
--accent-border: rgba(91, 157, 255, 0.25);
|
|
18
|
+
--green: #4ade80;
|
|
19
|
+
--green-soft: rgba(74, 222, 128, 0.1);
|
|
20
|
+
--green-border: rgba(74, 222, 128, 0.25);
|
|
21
|
+
--red: #f87171;
|
|
22
|
+
--red-soft: rgba(248, 113, 113, 0.1);
|
|
23
|
+
--amber: #fbbf24;
|
|
24
|
+
--amber-soft: rgba(251, 191, 36, 0.1);
|
|
25
|
+
--font-sans: -apple-system, 'Inter', system-ui, 'Segoe UI', sans-serif;
|
|
26
|
+
--font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
|
27
|
+
--radius: 8px;
|
|
28
|
+
--radius-sm: 6px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
23
32
|
|
|
24
33
|
body {
|
|
25
34
|
font-family: var(--font-sans);
|
|
26
|
-
background: var(--bg);
|
|
27
|
-
color: var(--text);
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
background: var(--bg-0);
|
|
36
|
+
color: var(--text-0);
|
|
37
|
+
height: 100vh;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
-webkit-font-smoothing: antialiased;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
|
-
|
|
42
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
43
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
44
|
+
::-webkit-scrollbar-thumb { background: var(--bg-3); border-radius: 3px; }
|
|
45
|
+
::-webkit-scrollbar-thumb:hover { background: var(--text-2); }
|
|
46
|
+
|
|
47
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 2px; }
|
|
48
|
+
|
|
49
|
+
/* ── App layout ── */
|
|
50
|
+
.app {
|
|
51
|
+
display: grid;
|
|
52
|
+
grid-template-rows: 52px 1fr;
|
|
53
|
+
height: 100vh;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ── Header ── */
|
|
33
57
|
.header {
|
|
34
|
-
display:
|
|
58
|
+
display: grid;
|
|
59
|
+
grid-template-columns: 1fr auto 1fr;
|
|
35
60
|
align-items: center;
|
|
36
|
-
|
|
37
|
-
|
|
61
|
+
padding: 0 20px;
|
|
62
|
+
background: var(--bg-1);
|
|
38
63
|
border-bottom: 1px solid var(--border);
|
|
39
|
-
|
|
64
|
+
z-index: 10;
|
|
40
65
|
}
|
|
41
66
|
.header-left {
|
|
42
67
|
display: flex;
|
|
43
68
|
align-items: center;
|
|
44
69
|
gap: 10px;
|
|
45
|
-
|
|
70
|
+
}
|
|
71
|
+
.logo {
|
|
72
|
+
font-size: 18px;
|
|
73
|
+
line-height: 1;
|
|
74
|
+
}
|
|
75
|
+
.logo-text {
|
|
76
|
+
font-size: 14px;
|
|
46
77
|
font-weight: 600;
|
|
78
|
+
color: var(--text-0);
|
|
79
|
+
letter-spacing: -0.3px;
|
|
80
|
+
}
|
|
81
|
+
.version-badge {
|
|
82
|
+
font-size: 11px;
|
|
83
|
+
font-family: var(--font-mono);
|
|
84
|
+
color: var(--text-2);
|
|
85
|
+
background: var(--bg-3);
|
|
86
|
+
padding: 2px 7px;
|
|
87
|
+
border-radius: 4px;
|
|
88
|
+
}
|
|
89
|
+
.header-center {
|
|
90
|
+
display: flex;
|
|
91
|
+
justify-content: center;
|
|
47
92
|
}
|
|
48
93
|
.header-right {
|
|
49
94
|
display: flex;
|
|
95
|
+
justify-content: flex-end;
|
|
50
96
|
align-items: center;
|
|
51
|
-
gap: 8px;
|
|
52
97
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
98
|
+
|
|
99
|
+
/* ── Nav (segmented control) ── */
|
|
100
|
+
.nav {
|
|
101
|
+
display: inline-flex;
|
|
102
|
+
gap: 1px;
|
|
103
|
+
background: var(--bg-2);
|
|
56
104
|
border-radius: var(--radius);
|
|
57
|
-
|
|
58
|
-
|
|
105
|
+
padding: 3px;
|
|
106
|
+
}
|
|
107
|
+
.nav-item {
|
|
108
|
+
padding: 6px 16px;
|
|
109
|
+
border-radius: var(--radius-sm);
|
|
59
110
|
font-size: 13px;
|
|
60
|
-
font-
|
|
61
|
-
|
|
111
|
+
font-weight: 500;
|
|
112
|
+
color: var(--text-1);
|
|
113
|
+
background: transparent;
|
|
114
|
+
border: none;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
transition: color 0.15s, background 0.15s;
|
|
117
|
+
font-family: var(--font-sans);
|
|
118
|
+
white-space: nowrap;
|
|
119
|
+
}
|
|
120
|
+
.nav-item:hover { color: var(--text-0); background: var(--bg-3); }
|
|
121
|
+
.nav-item.active {
|
|
122
|
+
color: var(--text-0);
|
|
123
|
+
background: var(--bg-3);
|
|
124
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.04);
|
|
62
125
|
}
|
|
63
|
-
.api-key-input:focus { outline: none; border-color: var(--accent); }
|
|
64
126
|
|
|
65
|
-
/*
|
|
66
|
-
.
|
|
127
|
+
/* ── API key input ── */
|
|
128
|
+
.key-wrapper {
|
|
67
129
|
display: flex;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
background: var(--
|
|
71
|
-
|
|
72
|
-
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: 8px;
|
|
132
|
+
background: var(--bg-2);
|
|
133
|
+
border: 1px solid var(--border);
|
|
134
|
+
border-radius: var(--radius-sm);
|
|
135
|
+
padding: 0 10px;
|
|
136
|
+
height: 32px;
|
|
137
|
+
transition: border-color 0.15s;
|
|
73
138
|
}
|
|
74
|
-
.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
color: var(--text-muted);
|
|
78
|
-
font-size: 13px;
|
|
79
|
-
font-weight: 500;
|
|
80
|
-
border-bottom: 2px solid transparent;
|
|
81
|
-
transition: color 0.15s, border-color 0.15s;
|
|
82
|
-
white-space: nowrap;
|
|
139
|
+
.key-wrapper:focus-within { border-color: var(--accent); }
|
|
140
|
+
.key-wrapper svg { flex-shrink: 0; color: var(--text-2); }
|
|
141
|
+
.key-input {
|
|
83
142
|
background: none;
|
|
84
|
-
border
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
font-family: var(--font-
|
|
143
|
+
border: none;
|
|
144
|
+
color: var(--text-0);
|
|
145
|
+
font-size: 12px;
|
|
146
|
+
font-family: var(--font-mono);
|
|
147
|
+
width: 160px;
|
|
148
|
+
outline: none;
|
|
88
149
|
}
|
|
89
|
-
.
|
|
90
|
-
.
|
|
91
|
-
|
|
92
|
-
|
|
150
|
+
.key-input::placeholder { color: var(--text-2); }
|
|
151
|
+
.key-input:focus { outline: none; }
|
|
152
|
+
|
|
153
|
+
/* ── Content ── */
|
|
154
|
+
.content {
|
|
155
|
+
overflow: hidden;
|
|
156
|
+
position: relative;
|
|
93
157
|
}
|
|
158
|
+
.panel { display: none; height: 100%; }
|
|
159
|
+
.panel.active { display: flex; }
|
|
94
160
|
|
|
95
|
-
/*
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
161
|
+
/* ── Split pane ── */
|
|
162
|
+
.split-pane {
|
|
163
|
+
display: grid;
|
|
164
|
+
grid-template-columns: 1fr 1fr;
|
|
165
|
+
width: 100%;
|
|
166
|
+
height: 100%;
|
|
167
|
+
}
|
|
168
|
+
.split-pane > .pane {
|
|
169
|
+
display: flex;
|
|
170
|
+
flex-direction: column;
|
|
171
|
+
overflow: hidden;
|
|
172
|
+
}
|
|
173
|
+
.split-pane > .pane:first-child {
|
|
174
|
+
border-right: 1px solid var(--border);
|
|
175
|
+
}
|
|
176
|
+
.pane-head {
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
gap: 10px;
|
|
180
|
+
padding: 12px 20px;
|
|
181
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
182
|
+
flex-shrink: 0;
|
|
183
|
+
min-height: 44px;
|
|
184
|
+
}
|
|
185
|
+
.pane-head-right {
|
|
186
|
+
margin-left: auto;
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
gap: 6px;
|
|
190
|
+
}
|
|
191
|
+
.pane-body {
|
|
192
|
+
flex: 1;
|
|
193
|
+
overflow-y: auto;
|
|
194
|
+
padding: 16px 20px;
|
|
195
|
+
display: flex;
|
|
196
|
+
flex-direction: column;
|
|
100
197
|
}
|
|
101
|
-
.panel { display: none; }
|
|
102
|
-
.panel.active { display: block; }
|
|
103
198
|
|
|
104
|
-
/*
|
|
105
|
-
.
|
|
199
|
+
/* ── Method badges ── */
|
|
200
|
+
.method-badge {
|
|
201
|
+
display: inline-flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
padding: 3px 8px;
|
|
204
|
+
border-radius: 4px;
|
|
205
|
+
font-size: 11px;
|
|
206
|
+
font-weight: 700;
|
|
106
207
|
font-family: var(--font-mono);
|
|
107
|
-
|
|
108
|
-
color: var(--accent);
|
|
109
|
-
margin-bottom: 16px;
|
|
208
|
+
letter-spacing: 0.5px;
|
|
110
209
|
}
|
|
111
|
-
.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
font-
|
|
117
|
-
|
|
118
|
-
margin-right: 8px;
|
|
210
|
+
.method-get { color: var(--green); background: var(--green-soft); }
|
|
211
|
+
.method-post { color: var(--accent); background: var(--accent-soft); }
|
|
212
|
+
.method-ws { color: var(--amber); background: var(--amber-soft); }
|
|
213
|
+
.endpoint-path {
|
|
214
|
+
font-family: var(--font-mono);
|
|
215
|
+
font-size: 13px;
|
|
216
|
+
color: var(--text-1);
|
|
119
217
|
}
|
|
120
218
|
|
|
121
|
-
|
|
122
|
-
|
|
219
|
+
/* ── Form elements ── */
|
|
220
|
+
.field-label {
|
|
221
|
+
font-size: 11px;
|
|
123
222
|
font-weight: 600;
|
|
124
223
|
text-transform: uppercase;
|
|
125
224
|
letter-spacing: 0.5px;
|
|
126
|
-
color: var(--text-
|
|
127
|
-
margin-bottom:
|
|
128
|
-
cursor: pointer;
|
|
129
|
-
user-select: none;
|
|
225
|
+
color: var(--text-2);
|
|
226
|
+
margin-bottom: 6px;
|
|
130
227
|
}
|
|
131
|
-
.
|
|
132
|
-
content: '\\25B6';
|
|
133
|
-
display: inline-block;
|
|
134
|
-
margin-right: 6px;
|
|
135
|
-
font-size: 10px;
|
|
136
|
-
transition: transform 0.15s;
|
|
137
|
-
}
|
|
138
|
-
.section-header.open::before {
|
|
139
|
-
transform: rotate(90deg);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
.collapsible { display: none; margin-bottom: 16px; }
|
|
143
|
-
.collapsible.open { display: block; }
|
|
144
|
-
|
|
145
|
-
.header-row {
|
|
228
|
+
.field-row {
|
|
146
229
|
display: flex;
|
|
230
|
+
align-items: center;
|
|
147
231
|
gap: 8px;
|
|
148
232
|
margin-bottom: 8px;
|
|
149
233
|
}
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
background: var(--bg);
|
|
153
|
-
border: 1px solid var(--border);
|
|
154
|
-
border-radius: var(--radius);
|
|
155
|
-
color: var(--text);
|
|
156
|
-
padding: 6px 10px;
|
|
157
|
-
font-size: 13px;
|
|
234
|
+
.field-row .label-inline {
|
|
235
|
+
font-size: 12px;
|
|
158
236
|
font-family: var(--font-mono);
|
|
237
|
+
color: var(--text-2);
|
|
238
|
+
min-width: 90px;
|
|
159
239
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
240
|
+
input[type="text"] {
|
|
241
|
+
flex: 1;
|
|
242
|
+
background: var(--bg-0);
|
|
243
|
+
border: 1px solid var(--border);
|
|
244
|
+
border-radius: var(--radius-sm);
|
|
245
|
+
color: var(--text-0);
|
|
246
|
+
padding: 7px 10px;
|
|
163
247
|
font-size: 12px;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
align-items: center;
|
|
248
|
+
font-family: var(--font-mono);
|
|
249
|
+
transition: border-color 0.15s;
|
|
167
250
|
}
|
|
251
|
+
input[type="text"]:focus { outline: none; border-color: var(--accent); }
|
|
252
|
+
input[type="text"]::placeholder { color: var(--text-2); }
|
|
168
253
|
|
|
169
254
|
textarea {
|
|
170
|
-
|
|
171
|
-
min-height:
|
|
172
|
-
background: var(--bg);
|
|
255
|
+
flex: 1;
|
|
256
|
+
min-height: 120px;
|
|
257
|
+
background: var(--bg-0);
|
|
173
258
|
border: 1px solid var(--border);
|
|
174
|
-
border-radius: var(--radius);
|
|
175
|
-
color: var(--text);
|
|
176
|
-
padding: 12px;
|
|
259
|
+
border-radius: var(--radius-sm);
|
|
260
|
+
color: var(--text-0);
|
|
261
|
+
padding: 12px 14px;
|
|
177
262
|
font-size: 13px;
|
|
178
263
|
font-family: var(--font-mono);
|
|
179
|
-
line-height: 1.
|
|
180
|
-
resize:
|
|
264
|
+
line-height: 1.6;
|
|
265
|
+
resize: none;
|
|
181
266
|
tab-size: 2;
|
|
267
|
+
transition: border-color 0.15s;
|
|
182
268
|
}
|
|
183
269
|
textarea:focus { outline: none; border-color: var(--accent); }
|
|
270
|
+
textarea::placeholder { color: var(--text-2); }
|
|
184
271
|
|
|
185
|
-
|
|
272
|
+
/* ── Collapsible sections ── */
|
|
273
|
+
.section-toggle {
|
|
274
|
+
display: flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
gap: 6px;
|
|
277
|
+
font-size: 11px;
|
|
278
|
+
font-weight: 600;
|
|
279
|
+
text-transform: uppercase;
|
|
280
|
+
letter-spacing: 0.5px;
|
|
281
|
+
color: var(--text-2);
|
|
282
|
+
cursor: pointer;
|
|
283
|
+
user-select: none;
|
|
284
|
+
padding: 6px 0;
|
|
285
|
+
margin-bottom: 4px;
|
|
286
|
+
background: none;
|
|
287
|
+
border: none;
|
|
288
|
+
font-family: var(--font-sans);
|
|
289
|
+
}
|
|
290
|
+
.section-toggle:hover { color: var(--text-1); }
|
|
291
|
+
.section-toggle svg {
|
|
292
|
+
transition: transform 0.15s;
|
|
293
|
+
}
|
|
294
|
+
.section-toggle.open svg {
|
|
295
|
+
transform: rotate(90deg);
|
|
296
|
+
}
|
|
297
|
+
.collapsible { display: none; margin-bottom: 12px; }
|
|
298
|
+
.collapsible.open { display: block; }
|
|
299
|
+
|
|
300
|
+
/* ── Buttons ── */
|
|
301
|
+
.actions {
|
|
186
302
|
display: flex;
|
|
187
303
|
gap: 8px;
|
|
188
304
|
margin-top: 12px;
|
|
305
|
+
flex-shrink: 0;
|
|
189
306
|
}
|
|
190
307
|
.btn {
|
|
191
|
-
|
|
308
|
+
display: inline-flex;
|
|
309
|
+
align-items: center;
|
|
310
|
+
justify-content: center;
|
|
311
|
+
gap: 6px;
|
|
312
|
+
height: 32px;
|
|
313
|
+
padding: 0 14px;
|
|
192
314
|
border: none;
|
|
193
|
-
border-radius: var(--radius);
|
|
315
|
+
border-radius: var(--radius-sm);
|
|
194
316
|
font-size: 13px;
|
|
195
|
-
font-weight:
|
|
317
|
+
font-weight: 500;
|
|
196
318
|
cursor: pointer;
|
|
197
319
|
font-family: var(--font-sans);
|
|
198
|
-
transition:
|
|
320
|
+
transition: all 0.15s;
|
|
199
321
|
}
|
|
200
|
-
.btn:
|
|
201
|
-
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
322
|
+
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
202
323
|
.btn-primary {
|
|
203
324
|
background: var(--accent);
|
|
204
|
-
color:
|
|
325
|
+
color: #fff;
|
|
205
326
|
}
|
|
206
|
-
.btn-
|
|
207
|
-
|
|
208
|
-
|
|
327
|
+
.btn-primary:hover:not(:disabled) { background: #4a8df0; }
|
|
328
|
+
.btn-ghost {
|
|
329
|
+
background: transparent;
|
|
330
|
+
color: var(--text-1);
|
|
209
331
|
border: 1px solid var(--border);
|
|
210
332
|
}
|
|
333
|
+
.btn-ghost:hover:not(:disabled) { background: var(--bg-2); color: var(--text-0); }
|
|
334
|
+
.btn-sm {
|
|
335
|
+
height: 26px;
|
|
336
|
+
padding: 0 10px;
|
|
337
|
+
font-size: 12px;
|
|
338
|
+
}
|
|
211
339
|
|
|
212
|
-
/* Response */
|
|
213
|
-
.
|
|
340
|
+
/* ── Response area ── */
|
|
341
|
+
.res-status {
|
|
214
342
|
display: flex;
|
|
215
343
|
align-items: center;
|
|
216
|
-
gap:
|
|
217
|
-
margin-top: 24px;
|
|
218
|
-
margin-bottom: 8px;
|
|
344
|
+
gap: 10px;
|
|
219
345
|
font-size: 13px;
|
|
220
346
|
}
|
|
221
|
-
.
|
|
347
|
+
.status-pill {
|
|
348
|
+
display: inline-flex;
|
|
349
|
+
align-items: center;
|
|
222
350
|
padding: 2px 8px;
|
|
223
|
-
border-radius:
|
|
224
|
-
font-weight: 600;
|
|
351
|
+
border-radius: 4px;
|
|
225
352
|
font-size: 12px;
|
|
353
|
+
font-weight: 600;
|
|
226
354
|
font-family: var(--font-mono);
|
|
227
355
|
}
|
|
228
|
-
.
|
|
229
|
-
.
|
|
230
|
-
.
|
|
231
|
-
|
|
232
|
-
.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
356
|
+
.status-pill.ok { color: var(--green); background: var(--green-soft); }
|
|
357
|
+
.status-pill.err { color: var(--red); background: var(--red-soft); }
|
|
358
|
+
.status-pill.pending { color: var(--amber); background: var(--amber-soft); }
|
|
359
|
+
.status-pill.cancelled { color: var(--text-2); background: var(--bg-3); }
|
|
360
|
+
.res-duration { color: var(--text-2); font-size: 12px; font-family: var(--font-mono); }
|
|
361
|
+
|
|
362
|
+
.mode-toggle {
|
|
363
|
+
display: inline-flex;
|
|
364
|
+
background: var(--bg-2);
|
|
365
|
+
border-radius: 5px;
|
|
366
|
+
padding: 2px;
|
|
367
|
+
gap: 1px;
|
|
236
368
|
}
|
|
237
|
-
.
|
|
238
|
-
padding:
|
|
369
|
+
.mode-toggle button {
|
|
370
|
+
padding: 3px 10px;
|
|
371
|
+
border-radius: 4px;
|
|
372
|
+
font-size: 11px;
|
|
373
|
+
font-weight: 500;
|
|
374
|
+
color: var(--text-2);
|
|
375
|
+
background: transparent;
|
|
376
|
+
border: none;
|
|
239
377
|
cursor: pointer;
|
|
240
|
-
font-size: 12px;
|
|
241
|
-
color: var(--text-muted);
|
|
242
|
-
background: var(--surface);
|
|
243
|
-
border: 1px solid var(--border);
|
|
244
|
-
border-bottom: none;
|
|
245
|
-
border-radius: var(--radius) var(--radius) 0 0;
|
|
246
378
|
font-family: var(--font-sans);
|
|
379
|
+
transition: all 0.15s;
|
|
247
380
|
}
|
|
248
|
-
.
|
|
249
|
-
|
|
250
|
-
background: var(--bg);
|
|
251
|
-
}
|
|
381
|
+
.mode-toggle button.active { color: var(--text-0); background: var(--bg-3); }
|
|
382
|
+
.mode-toggle button:hover:not(.active) { color: var(--text-1); }
|
|
252
383
|
|
|
253
|
-
.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
384
|
+
.code-output {
|
|
385
|
+
flex: 1;
|
|
386
|
+
overflow-y: auto;
|
|
387
|
+
background: var(--bg-0);
|
|
388
|
+
border: 1px solid var(--border-subtle);
|
|
389
|
+
border-radius: var(--radius-sm);
|
|
390
|
+
padding: 14px 16px;
|
|
258
391
|
font-family: var(--font-mono);
|
|
259
392
|
font-size: 13px;
|
|
393
|
+
line-height: 1.6;
|
|
260
394
|
white-space: pre-wrap;
|
|
261
395
|
word-break: break-word;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
396
|
+
margin-top: 12px;
|
|
397
|
+
}
|
|
398
|
+
.code-output.empty-state {
|
|
399
|
+
display: flex;
|
|
400
|
+
align-items: center;
|
|
401
|
+
justify-content: center;
|
|
402
|
+
color: var(--text-2);
|
|
403
|
+
font-family: var(--font-sans);
|
|
404
|
+
font-size: 13px;
|
|
265
405
|
}
|
|
266
406
|
|
|
267
|
-
/*
|
|
268
|
-
.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
407
|
+
/* ── JSON syntax highlighting ── */
|
|
408
|
+
.json-key { color: #c4c4cc; }
|
|
409
|
+
.json-string { color: #7dd3a8; }
|
|
410
|
+
.json-number { color: #8cb4ff; }
|
|
411
|
+
.json-boolean { color: #fbbf24; }
|
|
412
|
+
.json-null { color: #63636e; }
|
|
413
|
+
|
|
414
|
+
/* ── Streaming ── */
|
|
415
|
+
.stream-area {
|
|
416
|
+
flex: 1;
|
|
417
|
+
overflow-y: auto;
|
|
418
|
+
background: var(--bg-0);
|
|
419
|
+
border: 1px solid var(--border-subtle);
|
|
420
|
+
border-radius: var(--radius-sm);
|
|
421
|
+
padding: 14px 16px;
|
|
273
422
|
font-family: var(--font-mono);
|
|
274
423
|
font-size: 13px;
|
|
424
|
+
line-height: 1.6;
|
|
275
425
|
white-space: pre-wrap;
|
|
276
426
|
word-break: break-word;
|
|
277
|
-
|
|
278
|
-
overflow-y: auto;
|
|
279
|
-
margin-top: 16px;
|
|
427
|
+
margin-top: 12px;
|
|
280
428
|
position: relative;
|
|
281
429
|
}
|
|
282
|
-
|
|
283
430
|
.cursor-blink {
|
|
284
431
|
display: inline-block;
|
|
285
|
-
width:
|
|
286
|
-
height:
|
|
432
|
+
width: 2px;
|
|
433
|
+
height: 15px;
|
|
287
434
|
background: var(--accent);
|
|
288
435
|
animation: blink 1s step-end infinite;
|
|
289
436
|
vertical-align: text-bottom;
|
|
437
|
+
margin-left: 1px;
|
|
438
|
+
border-radius: 1px;
|
|
290
439
|
}
|
|
291
|
-
@keyframes blink {
|
|
292
|
-
50% { opacity: 0; }
|
|
293
|
-
}
|
|
440
|
+
@keyframes blink { 50% { opacity: 0; } }
|
|
294
441
|
|
|
295
|
-
.tool-
|
|
296
|
-
border-left:
|
|
442
|
+
.tool-block {
|
|
443
|
+
border-left: 2px solid var(--amber);
|
|
297
444
|
margin: 8px 0;
|
|
298
445
|
padding: 8px 12px;
|
|
299
|
-
background: var(--
|
|
300
|
-
border-radius: 0 var(--radius) var(--radius) 0;
|
|
446
|
+
background: var(--bg-2);
|
|
447
|
+
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
|
301
448
|
}
|
|
302
|
-
.tool-
|
|
449
|
+
.tool-block-head {
|
|
303
450
|
font-weight: 600;
|
|
304
451
|
font-size: 12px;
|
|
305
|
-
color: var(--
|
|
452
|
+
color: var(--amber);
|
|
306
453
|
cursor: pointer;
|
|
307
454
|
user-select: none;
|
|
455
|
+
display: flex;
|
|
456
|
+
align-items: center;
|
|
457
|
+
gap: 6px;
|
|
308
458
|
}
|
|
309
|
-
.tool-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
.tool-call-block.open .tool-call-header::before {
|
|
314
|
-
content: '\\25BC ';
|
|
315
|
-
}
|
|
316
|
-
.tool-call-body {
|
|
459
|
+
.tool-block-head svg { transition: transform 0.15s; }
|
|
460
|
+
.tool-block.open .tool-block-head svg { transform: rotate(90deg); }
|
|
461
|
+
.tool-block-body {
|
|
317
462
|
display: none;
|
|
318
463
|
margin-top: 6px;
|
|
319
464
|
font-size: 12px;
|
|
320
|
-
color: var(--text-
|
|
465
|
+
color: var(--text-1);
|
|
321
466
|
}
|
|
322
|
-
.tool-
|
|
323
|
-
|
|
324
|
-
.tool-result-ok {
|
|
325
|
-
.tool-result-err { border-left-color: var(--
|
|
467
|
+
.tool-block.open .tool-block-body { display: block; }
|
|
468
|
+
.tool-block.result-ok { border-left-color: var(--green); }
|
|
469
|
+
.tool-block.result-ok .tool-block-head { color: var(--green); }
|
|
470
|
+
.tool-block.result-err { border-left-color: var(--red); }
|
|
471
|
+
.tool-block.result-err .tool-block-head { color: var(--red); }
|
|
326
472
|
|
|
327
473
|
.summary-bar {
|
|
328
474
|
display: flex;
|
|
329
|
-
gap:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
border-radius: var(--radius);
|
|
475
|
+
gap: 20px;
|
|
476
|
+
padding: 10px 14px;
|
|
477
|
+
background: var(--bg-2);
|
|
478
|
+
border-radius: var(--radius-sm);
|
|
334
479
|
font-size: 12px;
|
|
335
|
-
color: var(--text-
|
|
480
|
+
color: var(--text-1);
|
|
481
|
+
margin-top: 10px;
|
|
482
|
+
flex-shrink: 0;
|
|
336
483
|
}
|
|
337
484
|
.summary-bar span { font-family: var(--font-mono); }
|
|
485
|
+
.summary-bar .label { color: var(--text-2); margin-right: 4px; }
|
|
338
486
|
|
|
339
|
-
.jump-
|
|
487
|
+
.jump-btn {
|
|
340
488
|
position: sticky;
|
|
341
489
|
bottom: 8px;
|
|
342
490
|
float: right;
|
|
343
|
-
padding: 4px
|
|
491
|
+
padding: 4px 10px;
|
|
344
492
|
font-size: 11px;
|
|
345
|
-
background: var(--
|
|
346
|
-
color: var(--
|
|
347
|
-
border:
|
|
348
|
-
border-radius: var(--radius);
|
|
493
|
+
background: var(--bg-3);
|
|
494
|
+
color: var(--text-1);
|
|
495
|
+
border: 1px solid var(--border);
|
|
496
|
+
border-radius: var(--radius-sm);
|
|
349
497
|
cursor: pointer;
|
|
350
498
|
display: none;
|
|
499
|
+
font-family: var(--font-sans);
|
|
351
500
|
}
|
|
501
|
+
.jump-btn:hover { background: var(--bg-2); }
|
|
352
502
|
|
|
353
|
-
/* Status dashboard */
|
|
354
|
-
.status-
|
|
355
|
-
|
|
356
|
-
|
|
503
|
+
/* ── Status dashboard ── */
|
|
504
|
+
.status-panel {
|
|
505
|
+
width: 100%;
|
|
506
|
+
height: 100%;
|
|
507
|
+
overflow-y: auto;
|
|
508
|
+
padding: 32px;
|
|
509
|
+
}
|
|
510
|
+
.status-inner {
|
|
511
|
+
max-width: 720px;
|
|
512
|
+
margin: 0 auto;
|
|
513
|
+
}
|
|
514
|
+
.status-hero {
|
|
515
|
+
display: flex;
|
|
516
|
+
align-items: center;
|
|
357
517
|
gap: 12px;
|
|
518
|
+
margin-bottom: 28px;
|
|
519
|
+
}
|
|
520
|
+
.status-dot {
|
|
521
|
+
width: 10px;
|
|
522
|
+
height: 10px;
|
|
523
|
+
border-radius: 50%;
|
|
524
|
+
flex-shrink: 0;
|
|
525
|
+
}
|
|
526
|
+
.status-dot.green { background: var(--green); box-shadow: 0 0 8px rgba(74, 222, 128, 0.4); }
|
|
527
|
+
.status-dot.red { background: var(--red); box-shadow: 0 0 8px rgba(248, 113, 113, 0.4); }
|
|
528
|
+
.status-headline {
|
|
529
|
+
font-size: 16px;
|
|
530
|
+
font-weight: 600;
|
|
531
|
+
color: var(--text-0);
|
|
358
532
|
}
|
|
359
|
-
.status-
|
|
360
|
-
|
|
533
|
+
.status-sub {
|
|
534
|
+
font-size: 12px;
|
|
535
|
+
color: var(--text-2);
|
|
536
|
+
margin-top: 1px;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.metrics-row {
|
|
540
|
+
display: grid;
|
|
541
|
+
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
542
|
+
gap: 1px;
|
|
543
|
+
background: var(--border-subtle);
|
|
361
544
|
border: 1px solid var(--border);
|
|
362
545
|
border-radius: var(--radius);
|
|
363
|
-
|
|
546
|
+
overflow: hidden;
|
|
364
547
|
}
|
|
365
|
-
.
|
|
366
|
-
|
|
548
|
+
.metric {
|
|
549
|
+
background: var(--bg-1);
|
|
550
|
+
padding: 16px 18px;
|
|
551
|
+
}
|
|
552
|
+
.metric-value {
|
|
553
|
+
font-size: 20px;
|
|
554
|
+
font-weight: 600;
|
|
555
|
+
font-family: var(--font-mono);
|
|
556
|
+
color: var(--text-0);
|
|
557
|
+
line-height: 1.2;
|
|
558
|
+
}
|
|
559
|
+
.metric-label {
|
|
560
|
+
font-size: 11px;
|
|
561
|
+
font-weight: 500;
|
|
367
562
|
text-transform: uppercase;
|
|
368
563
|
letter-spacing: 0.5px;
|
|
369
|
-
color: var(--text-
|
|
370
|
-
margin-
|
|
564
|
+
color: var(--text-2);
|
|
565
|
+
margin-top: 4px;
|
|
371
566
|
}
|
|
372
|
-
.
|
|
373
|
-
font-size:
|
|
374
|
-
|
|
567
|
+
.metric-sub {
|
|
568
|
+
font-size: 11px;
|
|
569
|
+
color: var(--text-2);
|
|
375
570
|
font-family: var(--font-mono);
|
|
571
|
+
margin-top: 2px;
|
|
376
572
|
}
|
|
377
|
-
.
|
|
573
|
+
.metric-value.green { color: var(--green); }
|
|
574
|
+
.metric-value.red { color: var(--red); }
|
|
575
|
+
.metric-value.amber { color: var(--amber); }
|
|
576
|
+
|
|
577
|
+
.queue-section {
|
|
578
|
+
margin-top: 20px;
|
|
579
|
+
}
|
|
580
|
+
.queue-label {
|
|
581
|
+
font-size: 11px;
|
|
582
|
+
font-weight: 600;
|
|
583
|
+
text-transform: uppercase;
|
|
584
|
+
letter-spacing: 0.5px;
|
|
585
|
+
color: var(--text-2);
|
|
586
|
+
margin-bottom: 8px;
|
|
587
|
+
}
|
|
588
|
+
.queue-bar-wrap {
|
|
589
|
+
display: flex;
|
|
590
|
+
align-items: center;
|
|
591
|
+
gap: 12px;
|
|
592
|
+
margin-bottom: 8px;
|
|
593
|
+
}
|
|
594
|
+
.queue-bar {
|
|
595
|
+
flex: 1;
|
|
596
|
+
height: 4px;
|
|
597
|
+
background: var(--bg-3);
|
|
598
|
+
border-radius: 2px;
|
|
599
|
+
overflow: hidden;
|
|
600
|
+
}
|
|
601
|
+
.queue-bar-fill {
|
|
602
|
+
height: 100%;
|
|
603
|
+
border-radius: 2px;
|
|
604
|
+
background: var(--accent);
|
|
605
|
+
transition: width 0.3s;
|
|
606
|
+
}
|
|
607
|
+
.queue-bar-fill.warn { background: var(--amber); }
|
|
608
|
+
.queue-bar-fill.crit { background: var(--red); }
|
|
609
|
+
.queue-text {
|
|
378
610
|
font-size: 12px;
|
|
379
|
-
|
|
380
|
-
|
|
611
|
+
font-family: var(--font-mono);
|
|
612
|
+
color: var(--text-1);
|
|
613
|
+
min-width: 70px;
|
|
614
|
+
text-align: right;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.status-footer {
|
|
618
|
+
margin-top: 20px;
|
|
619
|
+
font-size: 11px;
|
|
620
|
+
color: var(--text-2);
|
|
381
621
|
}
|
|
382
|
-
.cb-closed { color: var(--success); }
|
|
383
|
-
.cb-open { color: var(--error); }
|
|
384
|
-
.cb-half-open { color: var(--warning); }
|
|
385
622
|
|
|
386
|
-
|
|
623
|
+
.status-error {
|
|
624
|
+
color: var(--red);
|
|
625
|
+
padding: 20px;
|
|
626
|
+
font-size: 14px;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/* ── WebSocket ── */
|
|
387
630
|
.ws-controls {
|
|
388
631
|
display: flex;
|
|
389
|
-
gap: 8px;
|
|
390
|
-
margin-bottom: 12px;
|
|
391
632
|
align-items: center;
|
|
633
|
+
gap: 10px;
|
|
634
|
+
margin-bottom: 12px;
|
|
392
635
|
}
|
|
393
|
-
.ws-
|
|
394
|
-
width:
|
|
395
|
-
height:
|
|
636
|
+
.ws-dot {
|
|
637
|
+
width: 8px;
|
|
638
|
+
height: 8px;
|
|
396
639
|
border-radius: 50%;
|
|
397
|
-
background: var(--
|
|
398
|
-
|
|
640
|
+
background: var(--text-2);
|
|
641
|
+
flex-shrink: 0;
|
|
642
|
+
transition: background 0.2s, box-shadow 0.2s;
|
|
643
|
+
}
|
|
644
|
+
.ws-dot.connected { background: var(--green); box-shadow: 0 0 6px rgba(74,222,128,0.4); }
|
|
645
|
+
.ws-dot.connecting { background: var(--amber); box-shadow: 0 0 6px rgba(251,191,36,0.3); }
|
|
646
|
+
.ws-label {
|
|
647
|
+
font-size: 12px;
|
|
648
|
+
color: var(--text-2);
|
|
649
|
+
font-family: var(--font-mono);
|
|
399
650
|
}
|
|
400
|
-
.ws-status.connected { background: var(--success); }
|
|
401
|
-
.ws-status.connecting { background: var(--warning); }
|
|
402
651
|
|
|
403
652
|
.ws-log {
|
|
404
|
-
|
|
405
|
-
border: 1px solid var(--border);
|
|
406
|
-
border-radius: var(--radius);
|
|
407
|
-
padding: 12px;
|
|
408
|
-
font-family: var(--font-mono);
|
|
409
|
-
font-size: 13px;
|
|
410
|
-
max-height: 400px;
|
|
653
|
+
flex: 1;
|
|
411
654
|
overflow-y: auto;
|
|
412
|
-
|
|
655
|
+
background: var(--bg-0);
|
|
656
|
+
border: 1px solid var(--border-subtle);
|
|
657
|
+
border-radius: var(--radius-sm);
|
|
658
|
+
padding: 12px 14px;
|
|
659
|
+
font-family: var(--font-mono);
|
|
660
|
+
font-size: 12px;
|
|
661
|
+
line-height: 1.6;
|
|
413
662
|
}
|
|
414
|
-
.ws-msg { margin-bottom:
|
|
663
|
+
.ws-msg { margin-bottom: 2px; }
|
|
415
664
|
.ws-msg.sent { color: var(--accent); }
|
|
416
|
-
.ws-msg.received { color: var(--
|
|
417
|
-
.ws-msg.system { color: var(--text-
|
|
665
|
+
.ws-msg.received { color: var(--green); }
|
|
666
|
+
.ws-msg.system { color: var(--text-2); }
|
|
667
|
+
.ws-msg .ws-prefix {
|
|
668
|
+
display: inline-block;
|
|
669
|
+
width: 16px;
|
|
670
|
+
text-align: center;
|
|
671
|
+
margin-right: 6px;
|
|
672
|
+
opacity: 0.6;
|
|
673
|
+
}
|
|
418
674
|
|
|
419
|
-
.ws-input-
|
|
675
|
+
.ws-input-wrap {
|
|
420
676
|
display: flex;
|
|
421
677
|
gap: 8px;
|
|
678
|
+
margin-top: 12px;
|
|
679
|
+
flex-shrink: 0;
|
|
680
|
+
}
|
|
681
|
+
.ws-input-wrap textarea {
|
|
682
|
+
min-height: 50px;
|
|
683
|
+
flex: 1;
|
|
422
684
|
}
|
|
423
|
-
.ws-input-
|
|
424
|
-
|
|
685
|
+
.ws-input-wrap .btn {
|
|
686
|
+
align-self: flex-end;
|
|
425
687
|
}
|
|
426
688
|
|
|
427
|
-
/* Utilities */
|
|
689
|
+
/* ── Utilities ── */
|
|
428
690
|
.hidden { display: none !important; }
|
|
429
|
-
.
|
|
430
|
-
.
|
|
691
|
+
.gap-8 { gap: 8px; }
|
|
692
|
+
.mt-12 { margin-top: 12px; }
|
|
693
|
+
.mb-6 { margin-bottom: 6px; }
|
|
694
|
+
|
|
695
|
+
/* ── Skeleton loading ── */
|
|
696
|
+
@keyframes shimmer {
|
|
697
|
+
0% { opacity: 0.5; }
|
|
698
|
+
50% { opacity: 0.2; }
|
|
699
|
+
100% { opacity: 0.5; }
|
|
700
|
+
}
|
|
701
|
+
.skeleton {
|
|
702
|
+
background: var(--bg-3);
|
|
703
|
+
border-radius: 4px;
|
|
704
|
+
animation: shimmer 1.5s ease-in-out infinite;
|
|
705
|
+
}
|
|
431
706
|
`;
|
|
432
707
|
}
|
|
433
708
|
function playgroundJS(port) {
|
|
434
709
|
return `
|
|
435
710
|
(function() {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
// Request templates per tab
|
|
441
|
-
const TEMPLATES = {
|
|
442
|
-
run: JSON.stringify({ prompt: "What is 2 + 2?", options: { maxTurns: 1 } }, null, 2),
|
|
443
|
-
stream: JSON.stringify({ prompt: "Write a haiku about coding", options: { maxTurns: 1 } }, null, 2),
|
|
444
|
-
chat: JSON.stringify({
|
|
445
|
-
model: "claude-sonnet-4-20250514",
|
|
446
|
-
messages: [{ role: "user", content: "Hello! What can you do?" }],
|
|
447
|
-
stream: false
|
|
448
|
-
}, null, 2),
|
|
449
|
-
ws: JSON.stringify({ type: "start", prompt: "Hello from WebSocket" }, null, 2)
|
|
450
|
-
};
|
|
711
|
+
var PORT = ${port};
|
|
712
|
+
var BASE = location.origin;
|
|
713
|
+
var WS_BASE = BASE.replace(/^http/, 'ws');
|
|
451
714
|
|
|
452
|
-
|
|
453
|
-
const ENDPOINTS = {
|
|
454
|
-
status: { method: 'GET', path: '/api/status' },
|
|
715
|
+
var ENDPOINTS = {
|
|
455
716
|
run: { method: 'POST', path: '/api/run' },
|
|
456
717
|
stream: { method: 'POST', path: '/api/stream' },
|
|
457
718
|
chat: { method: 'POST', path: '/v1/chat/completions' },
|
|
458
719
|
};
|
|
459
720
|
|
|
460
|
-
|
|
461
|
-
const state = {
|
|
721
|
+
var state = {
|
|
462
722
|
activeTab: 'status',
|
|
463
723
|
apiKey: localStorage.getItem('otterly_api_key') || '',
|
|
464
|
-
sessionId: '',
|
|
465
724
|
ws: null,
|
|
466
725
|
wsConnected: false,
|
|
467
726
|
activeAbort: null,
|
|
468
727
|
statusInterval: null,
|
|
469
728
|
autoScroll: true,
|
|
470
|
-
responseMode: 'formatted', // 'formatted' | 'raw'
|
|
471
729
|
history: [],
|
|
472
730
|
};
|
|
473
731
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
732
|
+
var $ = function(sel) { return document.querySelector(sel); };
|
|
733
|
+
var $$ = function(sel) { return document.querySelectorAll(sel); };
|
|
734
|
+
|
|
735
|
+
var chevronSvg = '<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M4.5 3L7.5 6L4.5 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
477
736
|
|
|
478
|
-
// Init
|
|
479
|
-
document.addEventListener('DOMContentLoaded', ()
|
|
480
|
-
|
|
481
|
-
const keyInput = $('#api-key');
|
|
737
|
+
// ── Init ──
|
|
738
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
739
|
+
var keyInput = $('#api-key');
|
|
482
740
|
keyInput.value = state.apiKey;
|
|
483
|
-
keyInput.addEventListener('input', (e)
|
|
741
|
+
keyInput.addEventListener('input', function(e) {
|
|
484
742
|
state.apiKey = e.target.value;
|
|
485
743
|
localStorage.setItem('otterly_api_key', state.apiKey);
|
|
486
744
|
});
|
|
487
745
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
tab.addEventListener('click', () => switchTab(tab.dataset.tab));
|
|
746
|
+
$$('.nav-item').forEach(function(btn) {
|
|
747
|
+
btn.addEventListener('click', function() { switchTab(btn.dataset.tab); });
|
|
491
748
|
});
|
|
492
749
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
h.addEventListener('click', () => {
|
|
750
|
+
$$('.section-toggle').forEach(function(h) {
|
|
751
|
+
h.addEventListener('click', function() {
|
|
496
752
|
h.classList.toggle('open');
|
|
497
|
-
|
|
753
|
+
var target = h.nextElementSibling;
|
|
498
754
|
if (target) target.classList.toggle('open');
|
|
499
755
|
});
|
|
500
756
|
});
|
|
501
757
|
|
|
502
|
-
|
|
503
|
-
$('#send-
|
|
504
|
-
$('#send-
|
|
505
|
-
$('#send-chat')?.addEventListener('click', () => sendChat());
|
|
758
|
+
$('#send-run').addEventListener('click', function() { sendRequest('run'); });
|
|
759
|
+
$('#send-stream').addEventListener('click', function() { sendStream(); });
|
|
760
|
+
$('#send-chat').addEventListener('click', function() { sendChat(); });
|
|
506
761
|
|
|
507
|
-
|
|
508
|
-
$$('.btn-cancel').forEach((btn) => {
|
|
762
|
+
$$('.btn-cancel').forEach(function(btn) {
|
|
509
763
|
btn.addEventListener('click', cancelRequest);
|
|
510
764
|
});
|
|
511
765
|
|
|
512
|
-
//
|
|
513
|
-
$$('.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
766
|
+
// Mode toggles (formatted/raw)
|
|
767
|
+
$$('.mode-toggle').forEach(function(group) {
|
|
768
|
+
group.querySelectorAll('button').forEach(function(btn) {
|
|
769
|
+
btn.addEventListener('click', function() {
|
|
770
|
+
group.querySelectorAll('button').forEach(function(b) { b.classList.remove('active'); });
|
|
771
|
+
btn.classList.add('active');
|
|
772
|
+
var panel = group.closest('.pane') || group.closest('.panel');
|
|
773
|
+
var formatted = panel.querySelector('.response-formatted');
|
|
774
|
+
var raw = panel.querySelector('.response-raw');
|
|
775
|
+
if (formatted && raw) {
|
|
776
|
+
var mode = btn.dataset.mode;
|
|
777
|
+
formatted.classList.toggle('hidden', mode !== 'formatted');
|
|
778
|
+
raw.classList.toggle('hidden', mode !== 'raw');
|
|
779
|
+
}
|
|
780
|
+
});
|
|
526
781
|
});
|
|
527
782
|
});
|
|
528
783
|
|
|
529
|
-
// WebSocket
|
|
530
|
-
$('#ws-connect')
|
|
531
|
-
$('#ws-disconnect')
|
|
532
|
-
$('#ws-send')
|
|
784
|
+
// WebSocket
|
|
785
|
+
$('#ws-connect').addEventListener('click', wsConnect);
|
|
786
|
+
$('#ws-disconnect').addEventListener('click', wsDisconnect);
|
|
787
|
+
$('#ws-send').addEventListener('click', wsSend);
|
|
533
788
|
|
|
534
|
-
// Jump
|
|
535
|
-
$$('.jump-
|
|
536
|
-
btn.addEventListener('click', ()
|
|
537
|
-
|
|
789
|
+
// Jump buttons
|
|
790
|
+
$$('.jump-btn').forEach(function(btn) {
|
|
791
|
+
btn.addEventListener('click', function() {
|
|
792
|
+
var container = btn.closest('.stream-area') || btn.closest('.ws-log');
|
|
538
793
|
if (container) {
|
|
539
794
|
container.scrollTop = container.scrollHeight;
|
|
540
795
|
state.autoScroll = true;
|
|
@@ -543,26 +798,23 @@ function playgroundJS(port) {
|
|
|
543
798
|
});
|
|
544
799
|
});
|
|
545
800
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 30;
|
|
801
|
+
$$('.stream-area, .ws-log').forEach(function(el) {
|
|
802
|
+
el.addEventListener('scroll', function() {
|
|
803
|
+
var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 30;
|
|
550
804
|
state.autoScroll = atBottom;
|
|
551
|
-
|
|
805
|
+
var jumpBtn = el.querySelector('.jump-btn');
|
|
552
806
|
if (jumpBtn) jumpBtn.style.display = atBottom ? 'none' : 'block';
|
|
553
807
|
});
|
|
554
808
|
});
|
|
555
809
|
|
|
556
|
-
// Start with status tab
|
|
557
810
|
switchTab('status');
|
|
558
811
|
});
|
|
559
812
|
|
|
560
813
|
function switchTab(tab) {
|
|
561
814
|
state.activeTab = tab;
|
|
562
|
-
$$('.
|
|
563
|
-
$$('.panel').forEach((p)
|
|
815
|
+
$$('.nav-item').forEach(function(t) { t.classList.toggle('active', t.dataset.tab === tab); });
|
|
816
|
+
$$('.panel').forEach(function(p) { p.classList.toggle('active', p.id === 'panel-' + tab); });
|
|
564
817
|
|
|
565
|
-
// Start/stop status polling
|
|
566
818
|
if (tab === 'status') {
|
|
567
819
|
fetchStatus();
|
|
568
820
|
state.statusInterval = setInterval(fetchStatus, 5000);
|
|
@@ -573,97 +825,148 @@ function playgroundJS(port) {
|
|
|
573
825
|
}
|
|
574
826
|
|
|
575
827
|
function getHeaders() {
|
|
576
|
-
|
|
828
|
+
var h = { 'Content-Type': 'application/json' };
|
|
577
829
|
if (state.apiKey) h['Authorization'] = 'Bearer ' + state.apiKey;
|
|
578
|
-
|
|
830
|
+
var sid = $('#session-id-' + state.activeTab);
|
|
579
831
|
if (sid && sid.value) h['X-Session-Id'] = sid.value;
|
|
580
832
|
return h;
|
|
581
833
|
}
|
|
582
834
|
|
|
835
|
+
// ── JSON syntax highlighting ──
|
|
836
|
+
function syntaxHighlight(json) {
|
|
837
|
+
if (typeof json !== 'string') json = JSON.stringify(json, null, 2);
|
|
838
|
+
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
839
|
+
return json.replace(
|
|
840
|
+
/("(\\\\u[a-zA-Z0-9]{4}|\\\\[^u]|[^\\\\"])*"(\\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)/g,
|
|
841
|
+
function(match) {
|
|
842
|
+
var cls = 'json-number';
|
|
843
|
+
if (/^"/.test(match)) {
|
|
844
|
+
if (/:$/.test(match)) cls = 'json-key';
|
|
845
|
+
else cls = 'json-string';
|
|
846
|
+
} else if (/true|false/.test(match)) {
|
|
847
|
+
cls = 'json-boolean';
|
|
848
|
+
} else if (/null/.test(match)) {
|
|
849
|
+
cls = 'json-null';
|
|
850
|
+
}
|
|
851
|
+
return '<span class="' + cls + '">' + match + '</span>';
|
|
852
|
+
}
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
|
|
583
856
|
// ── Status ──
|
|
857
|
+
var lastStatusData = null;
|
|
584
858
|
async function fetchStatus() {
|
|
585
859
|
try {
|
|
586
|
-
|
|
587
|
-
|
|
860
|
+
var res = await fetch(BASE + '/api/status');
|
|
861
|
+
var data = await res.json();
|
|
862
|
+
lastStatusData = data;
|
|
588
863
|
renderStatus(data);
|
|
589
864
|
} catch (err) {
|
|
590
|
-
$('#status-content').innerHTML = '<div
|
|
865
|
+
$('#status-content').innerHTML = '<div class="status-error">Failed to connect: ' + escHtml(err.message) + '</div>';
|
|
591
866
|
}
|
|
592
867
|
}
|
|
593
868
|
|
|
594
|
-
function renderStatus(
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
869
|
+
function renderStatus(d) {
|
|
870
|
+
var isOk = d.status === 'ok';
|
|
871
|
+
var cb = d.circuitBreaker || 'closed';
|
|
872
|
+
var cbClass = cb === 'closed' ? 'green' : cb === 'open' ? 'red' : 'amber';
|
|
873
|
+
var q = d.queue || {};
|
|
874
|
+
var activeP = q.maxConcurrent ? Math.round((q.active || 0) / q.maxConcurrent * 100) : 0;
|
|
875
|
+
var queuedP = q.maxQueueSize ? Math.round((q.queued || 0) / q.maxQueueSize * 100) : 0;
|
|
876
|
+
var activeBarClass = activeP > 80 ? 'crit' : activeP > 50 ? 'warn' : '';
|
|
877
|
+
var queuedBarClass = queuedP > 80 ? 'crit' : queuedP > 50 ? 'warn' : '';
|
|
878
|
+
|
|
598
879
|
$('#status-content').innerHTML =
|
|
599
|
-
'<div class="status-
|
|
600
|
-
'<div class="status-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
'
|
|
606
|
-
'
|
|
607
|
-
|
|
880
|
+
'<div class="status-inner">' +
|
|
881
|
+
'<div class="status-hero">' +
|
|
882
|
+
'<div class="status-dot ' + (isOk ? 'green' : 'red') + '"></div>' +
|
|
883
|
+
'<div>' +
|
|
884
|
+
'<div class="status-headline">' + (isOk ? 'All systems operational' : 'System issue detected') + '</div>' +
|
|
885
|
+
'<div class="status-sub">v' + escHtml(d.version || '?') + '</div>' +
|
|
886
|
+
'</div>' +
|
|
887
|
+
'</div>' +
|
|
888
|
+
'<div class="metrics-row">' +
|
|
889
|
+
'<div class="metric"><div class="metric-value">' + escHtml(d.version || '?') + '</div><div class="metric-label">Version</div></div>' +
|
|
890
|
+
'<div class="metric"><div class="metric-value">' + (d.activeSessions || 0) + '</div><div class="metric-label">Sessions</div></div>' +
|
|
891
|
+
'<div class="metric"><div class="metric-value ' + cbClass + '">' + escHtml(cb) + '</div><div class="metric-label">Circuit</div></div>' +
|
|
892
|
+
'<div class="metric"><div class="metric-value">' + (q.active || 0) + '</div><div class="metric-label">Active</div><div class="metric-sub">/ ' + (q.maxConcurrent || '?') + ' max</div></div>' +
|
|
893
|
+
'<div class="metric"><div class="metric-value">' + (q.queued || 0) + '</div><div class="metric-label">Queued</div><div class="metric-sub">/ ' + (q.maxQueueSize || '?') + ' max</div></div>' +
|
|
894
|
+
'</div>' +
|
|
895
|
+
'<div class="queue-section">' +
|
|
896
|
+
'<div class="queue-label">Queue utilization</div>' +
|
|
897
|
+
'<div class="queue-bar-wrap">' +
|
|
898
|
+
'<div style="font-size:12px;color:var(--text-2);min-width:50px">Active</div>' +
|
|
899
|
+
'<div class="queue-bar"><div class="queue-bar-fill ' + activeBarClass + '" style="width:' + Math.max(activeP, 1) + '%"></div></div>' +
|
|
900
|
+
'<div class="queue-text">' + (q.active || 0) + ' / ' + (q.maxConcurrent || '?') + '</div>' +
|
|
901
|
+
'</div>' +
|
|
902
|
+
'<div class="queue-bar-wrap">' +
|
|
903
|
+
'<div style="font-size:12px;color:var(--text-2);min-width:50px">Waiting</div>' +
|
|
904
|
+
'<div class="queue-bar"><div class="queue-bar-fill ' + queuedBarClass + '" style="width:' + Math.max(queuedP, 1) + '%"></div></div>' +
|
|
905
|
+
'<div class="queue-text">' + (q.queued || 0) + ' / ' + (q.maxQueueSize || '?') + '</div>' +
|
|
906
|
+
'</div>' +
|
|
907
|
+
'</div>' +
|
|
908
|
+
'<div class="status-footer">Auto-refreshing every 5s</div>' +
|
|
608
909
|
'</div>';
|
|
609
910
|
}
|
|
610
911
|
|
|
611
912
|
// ── Run (one-shot) ──
|
|
612
913
|
async function sendRequest(tab) {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
resBar.innerHTML = '<span class="status-badge err">JSON Error</span> <span>' + escHtml(e.message) + '</span>';
|
|
914
|
+
var bodyEl = $('#body-' + tab);
|
|
915
|
+
var resStatus = $('#res-status-' + tab);
|
|
916
|
+
var resFormatted = $('#res-formatted-' + tab);
|
|
917
|
+
var resRaw = $('#res-raw-' + tab);
|
|
918
|
+
var sendBtn = $('#send-' + tab);
|
|
919
|
+
var emptyEl = $('#res-empty-' + tab);
|
|
920
|
+
|
|
921
|
+
var body;
|
|
922
|
+
try { body = JSON.parse(bodyEl.value); } catch (e) {
|
|
923
|
+
resStatus.innerHTML = '<span class="status-pill err">JSON Error</span> <span style="color:var(--text-1);font-size:12px">' + escHtml(e.message) + '</span>';
|
|
624
924
|
return;
|
|
625
925
|
}
|
|
626
926
|
|
|
627
927
|
sendBtn.disabled = true;
|
|
628
|
-
|
|
629
|
-
|
|
928
|
+
if (emptyEl) emptyEl.classList.add('hidden');
|
|
929
|
+
resStatus.innerHTML = '<span class="status-pill pending">Pending</span>';
|
|
930
|
+
resFormatted.innerHTML = '';
|
|
630
931
|
resRaw.textContent = '';
|
|
631
932
|
|
|
632
|
-
|
|
633
|
-
|
|
933
|
+
var startTime = Date.now();
|
|
934
|
+
var controller = new AbortController();
|
|
634
935
|
state.activeAbort = controller;
|
|
635
936
|
|
|
636
937
|
try {
|
|
637
|
-
|
|
638
|
-
|
|
938
|
+
var ep = ENDPOINTS[tab];
|
|
939
|
+
var res = await fetch(BASE + ep.path, {
|
|
639
940
|
method: ep.method,
|
|
640
941
|
headers: getHeaders(),
|
|
641
942
|
body: JSON.stringify(body),
|
|
642
943
|
signal: controller.signal,
|
|
643
944
|
});
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
let parsed;
|
|
945
|
+
var duration = Date.now() - startTime;
|
|
946
|
+
var raw = await res.text();
|
|
947
|
+
var parsed;
|
|
648
948
|
try { parsed = JSON.parse(raw); } catch { parsed = raw; }
|
|
649
949
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
'<span class="status-
|
|
653
|
-
'<span class="duration">' + duration + 'ms</span>';
|
|
950
|
+
var ok = res.status >= 200 && res.status < 300;
|
|
951
|
+
resStatus.innerHTML =
|
|
952
|
+
'<span class="status-pill ' + (ok ? 'ok' : 'err') + '">' + res.status + '</span>' +
|
|
953
|
+
'<span class="res-duration">' + duration + 'ms</span>';
|
|
654
954
|
|
|
655
|
-
|
|
955
|
+
if (typeof parsed === 'object') {
|
|
956
|
+
resFormatted.innerHTML = syntaxHighlight(parsed);
|
|
957
|
+
} else {
|
|
958
|
+
resFormatted.textContent = raw;
|
|
959
|
+
}
|
|
656
960
|
resRaw.textContent = raw;
|
|
657
961
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const sidInput = $('#session-id-' + tab);
|
|
962
|
+
var newSid = res.headers.get('X-Session-Id');
|
|
963
|
+
var sidInput = $('#session-id-' + tab);
|
|
661
964
|
if (newSid && sidInput) sidInput.value = newSid;
|
|
662
965
|
} catch (err) {
|
|
663
966
|
if (err.name === 'AbortError') {
|
|
664
|
-
|
|
967
|
+
resStatus.innerHTML = '<span class="status-pill cancelled">Cancelled</span>';
|
|
665
968
|
} else {
|
|
666
|
-
|
|
969
|
+
resStatus.innerHTML = '<span class="status-pill err">Error</span> <span style="color:var(--text-1);font-size:12px">' + escHtml(err.message) + '</span>';
|
|
667
970
|
}
|
|
668
971
|
} finally {
|
|
669
972
|
sendBtn.disabled = false;
|
|
@@ -673,18 +976,16 @@ function playgroundJS(port) {
|
|
|
673
976
|
|
|
674
977
|
// ── Stream (NDJSON) ──
|
|
675
978
|
async function sendStream() {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
try {
|
|
686
|
-
body = JSON.parse(bodyEl.value);
|
|
687
|
-
} catch (e) {
|
|
979
|
+
var bodyEl = $('#body-stream');
|
|
980
|
+
var output = $('#stream-output');
|
|
981
|
+
var textArea = $('#stream-text');
|
|
982
|
+
var rawArea = $('#stream-raw');
|
|
983
|
+
var summary = $('#stream-summary');
|
|
984
|
+
var sendBtn = $('#send-stream');
|
|
985
|
+
var rawToggle = $('#stream-raw-toggle');
|
|
986
|
+
|
|
987
|
+
var body;
|
|
988
|
+
try { body = JSON.parse(bodyEl.value); } catch (e) {
|
|
688
989
|
output.classList.remove('hidden');
|
|
689
990
|
textArea.textContent = 'JSON parse error: ' + e.message;
|
|
690
991
|
return;
|
|
@@ -697,11 +998,11 @@ function playgroundJS(port) {
|
|
|
697
998
|
summary.classList.add('hidden');
|
|
698
999
|
state.autoScroll = true;
|
|
699
1000
|
|
|
700
|
-
|
|
1001
|
+
var controller = new AbortController();
|
|
701
1002
|
state.activeAbort = controller;
|
|
702
|
-
|
|
1003
|
+
var showRaw = false;
|
|
703
1004
|
rawToggle.textContent = 'Raw';
|
|
704
|
-
rawToggle.onclick = ()
|
|
1005
|
+
rawToggle.onclick = function() {
|
|
705
1006
|
showRaw = !showRaw;
|
|
706
1007
|
rawToggle.textContent = showRaw ? 'Formatted' : 'Raw';
|
|
707
1008
|
textArea.classList.toggle('hidden', showRaw);
|
|
@@ -709,71 +1010,59 @@ function playgroundJS(port) {
|
|
|
709
1010
|
};
|
|
710
1011
|
rawArea.classList.add('hidden');
|
|
711
1012
|
|
|
712
|
-
|
|
1013
|
+
var fullText = '';
|
|
713
1014
|
|
|
714
1015
|
try {
|
|
715
|
-
|
|
1016
|
+
var res = await fetch(BASE + '/api/stream', {
|
|
716
1017
|
method: 'POST',
|
|
717
1018
|
headers: getHeaders(),
|
|
718
1019
|
body: JSON.stringify(body),
|
|
719
1020
|
signal: controller.signal,
|
|
720
1021
|
});
|
|
721
1022
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1023
|
+
var reader = res.body.getReader();
|
|
1024
|
+
var decoder = new TextDecoder();
|
|
1025
|
+
var buffer = '';
|
|
725
1026
|
|
|
726
1027
|
while (true) {
|
|
727
|
-
|
|
728
|
-
if (done) break;
|
|
1028
|
+
var chunk = await reader.read();
|
|
1029
|
+
if (chunk.done) break;
|
|
729
1030
|
|
|
730
|
-
buffer += decoder.decode(value, { stream: true });
|
|
731
|
-
|
|
732
|
-
buffer = lines.pop();
|
|
1031
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
1032
|
+
var lines = buffer.split('\\n');
|
|
1033
|
+
buffer = lines.pop();
|
|
733
1034
|
|
|
734
|
-
for (
|
|
1035
|
+
for (var i = 0; i < lines.length; i++) {
|
|
1036
|
+
var line = lines[i];
|
|
735
1037
|
if (!line.trim()) continue;
|
|
736
1038
|
rawArea.textContent += line + '\\n';
|
|
737
1039
|
|
|
738
|
-
|
|
1040
|
+
var event;
|
|
739
1041
|
try { event = JSON.parse(line); } catch { continue; }
|
|
740
1042
|
|
|
741
1043
|
if (event.type === 'text_delta') {
|
|
742
1044
|
fullText += event.text || '';
|
|
743
1045
|
textArea.innerHTML = escHtml(fullText) + '<span class="cursor-blink"></span>';
|
|
744
1046
|
} else if (event.type === 'tool_use') {
|
|
745
|
-
|
|
746
|
-
block.className = 'tool-call-block';
|
|
747
|
-
block.innerHTML =
|
|
748
|
-
'<div class="tool-call-header">' + escHtml(event.name || 'tool_use') + '</div>' +
|
|
749
|
-
'<div class="tool-call-body"><pre>' + escHtml(JSON.stringify(event.input || {}, null, 2)) + '</pre></div>';
|
|
750
|
-
block.querySelector('.tool-call-header').onclick = () => block.classList.toggle('open');
|
|
1047
|
+
var block = makeToolBlock(event.name || 'tool_use', JSON.stringify(event.input || {}, null, 2));
|
|
751
1048
|
textArea.insertBefore(block, textArea.querySelector('.cursor-blink'));
|
|
752
1049
|
} else if (event.type === 'tool_result') {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
'<div class="tool-call-header">Result' + (event.isError ? ' (error)' : '') + '</div>' +
|
|
757
|
-
'<div class="tool-call-body"><pre>' + escHtml(typeof event.content === 'string' ? event.content : JSON.stringify(event.content, null, 2)) + '</pre></div>';
|
|
758
|
-
block.querySelector('.tool-call-header').onclick = () => block.classList.toggle('open');
|
|
759
|
-
textArea.insertBefore(block, textArea.querySelector('.cursor-blink'));
|
|
1050
|
+
var content = typeof event.content === 'string' ? event.content : JSON.stringify(event.content, null, 2);
|
|
1051
|
+
var block2 = makeToolBlock('Result' + (event.isError ? ' (error)' : ''), content, event.isError ? 'result-err' : 'result-ok');
|
|
1052
|
+
textArea.insertBefore(block2, textArea.querySelector('.cursor-blink'));
|
|
760
1053
|
} else if (event.type === 'result') {
|
|
761
|
-
|
|
762
|
-
const cur = textArea.querySelector('.cursor-blink');
|
|
1054
|
+
var cur = textArea.querySelector('.cursor-blink');
|
|
763
1055
|
if (cur) cur.remove();
|
|
764
|
-
// Show summary
|
|
765
1056
|
summary.classList.remove('hidden');
|
|
766
1057
|
summary.innerHTML =
|
|
767
|
-
'<span>Cost
|
|
768
|
-
'<span>Duration
|
|
769
|
-
(event.usage ? '<span>Tokens
|
|
770
|
-
|
|
771
|
-
// Update session ID
|
|
772
|
-
const sidInput = $('#session-id-stream');
|
|
1058
|
+
'<span><span class="label">Cost</span>$' + (event.cost || 0).toFixed(4) + '</span>' +
|
|
1059
|
+
'<span><span class="label">Duration</span>' + (event.duration || 0) + 'ms</span>' +
|
|
1060
|
+
(event.usage ? '<span><span class="label">Tokens</span>' + (event.usage.inputTokens || 0) + ' in / ' + (event.usage.outputTokens || 0) + ' out</span>' : '');
|
|
1061
|
+
var sidInput = $('#session-id-stream');
|
|
773
1062
|
if (event.sessionId && sidInput) sidInput.value = event.sessionId;
|
|
774
1063
|
} else if (event.type === 'session_init' && event.sessionId) {
|
|
775
|
-
|
|
776
|
-
if (
|
|
1064
|
+
var sidInput2 = $('#session-id-stream');
|
|
1065
|
+
if (sidInput2) sidInput2.value = event.sessionId;
|
|
777
1066
|
}
|
|
778
1067
|
|
|
779
1068
|
if (state.autoScroll) output.scrollTop = output.scrollHeight;
|
|
@@ -781,81 +1070,89 @@ function playgroundJS(port) {
|
|
|
781
1070
|
}
|
|
782
1071
|
} catch (err) {
|
|
783
1072
|
if (err.name !== 'AbortError') {
|
|
784
|
-
textArea.innerHTML = fullText + '\\n<span style="color:var(--
|
|
1073
|
+
textArea.innerHTML = fullText + '\\n<span style="color:var(--red)">Error: ' + escHtml(err.message) + '</span>';
|
|
785
1074
|
} else {
|
|
786
|
-
|
|
787
|
-
if (
|
|
788
|
-
textArea.innerHTML += '\\n<span style="color:var(--text-
|
|
1075
|
+
var cur2 = textArea.querySelector('.cursor-blink');
|
|
1076
|
+
if (cur2) cur2.remove();
|
|
1077
|
+
textArea.innerHTML += '\\n<span style="color:var(--text-2)">[Cancelled]</span>';
|
|
789
1078
|
}
|
|
790
1079
|
} finally {
|
|
791
1080
|
sendBtn.disabled = false;
|
|
792
1081
|
state.activeAbort = null;
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
if (cur) cur.remove();
|
|
1082
|
+
var cur3 = textArea.querySelector('.cursor-blink');
|
|
1083
|
+
if (cur3) cur3.remove();
|
|
796
1084
|
}
|
|
797
1085
|
}
|
|
798
1086
|
|
|
1087
|
+
function makeToolBlock(name, content, extraClass) {
|
|
1088
|
+
var block = document.createElement('div');
|
|
1089
|
+
block.className = 'tool-block' + (extraClass ? ' ' + extraClass : '');
|
|
1090
|
+
block.innerHTML =
|
|
1091
|
+
'<div class="tool-block-head">' + chevronSvg + ' ' + escHtml(name) + '</div>' +
|
|
1092
|
+
'<div class="tool-block-body"><pre>' + escHtml(content) + '</pre></div>';
|
|
1093
|
+
block.querySelector('.tool-block-head').onclick = function() { block.classList.toggle('open'); };
|
|
1094
|
+
return block;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
799
1097
|
// ── Chat (OpenAI) ──
|
|
800
1098
|
async function sendChat() {
|
|
801
|
-
|
|
802
|
-
|
|
1099
|
+
var bodyEl = $('#body-chat');
|
|
1100
|
+
var sendBtn = $('#send-chat');
|
|
803
1101
|
|
|
804
|
-
|
|
805
|
-
try {
|
|
806
|
-
|
|
807
|
-
} catch (e) {
|
|
808
|
-
$('#res-bar-chat').innerHTML = '<span class="status-badge err">JSON Error</span> <span>' + escHtml(e.message) + '</span>';
|
|
1102
|
+
var body;
|
|
1103
|
+
try { body = JSON.parse(bodyEl.value); } catch (e) {
|
|
1104
|
+
$('#res-status-chat').innerHTML = '<span class="status-pill err">JSON Error</span> <span style="color:var(--text-1);font-size:12px">' + escHtml(e.message) + '</span>';
|
|
809
1105
|
return;
|
|
810
1106
|
}
|
|
811
1107
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
if (isStream) {
|
|
1108
|
+
if (body.stream === true) {
|
|
815
1109
|
await sendChatStream(body, sendBtn);
|
|
816
1110
|
} else {
|
|
817
|
-
// Non-streaming: reuse sendRequest-style logic
|
|
818
1111
|
sendBtn.disabled = true;
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1112
|
+
var resStatus = $('#res-status-chat');
|
|
1113
|
+
var resFormatted = $('#res-formatted-chat');
|
|
1114
|
+
var resRaw = $('#res-raw-chat');
|
|
1115
|
+
var emptyEl = $('#res-empty-chat');
|
|
1116
|
+
resStatus.innerHTML = '<span class="status-pill pending">Pending</span>';
|
|
1117
|
+
resFormatted.innerHTML = '';
|
|
824
1118
|
resRaw.textContent = '';
|
|
1119
|
+
if (emptyEl) emptyEl.classList.add('hidden');
|
|
825
1120
|
|
|
826
|
-
// Show standard response, hide streaming
|
|
827
1121
|
$('#chat-response-standard').classList.remove('hidden');
|
|
828
1122
|
$('#chat-stream-output').classList.add('hidden');
|
|
829
1123
|
|
|
830
|
-
|
|
831
|
-
|
|
1124
|
+
var startTime = Date.now();
|
|
1125
|
+
var controller = new AbortController();
|
|
832
1126
|
state.activeAbort = controller;
|
|
833
1127
|
|
|
834
1128
|
try {
|
|
835
|
-
|
|
1129
|
+
var res = await fetch(BASE + '/v1/chat/completions', {
|
|
836
1130
|
method: 'POST',
|
|
837
1131
|
headers: getHeaders(),
|
|
838
1132
|
body: JSON.stringify(body),
|
|
839
1133
|
signal: controller.signal,
|
|
840
1134
|
});
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
let parsed;
|
|
1135
|
+
var duration = Date.now() - startTime;
|
|
1136
|
+
var raw = await res.text();
|
|
1137
|
+
var parsed;
|
|
845
1138
|
try { parsed = JSON.parse(raw); } catch { parsed = raw; }
|
|
846
1139
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
'<span class="status-
|
|
850
|
-
'<span class="duration">' + duration + 'ms</span>';
|
|
1140
|
+
var ok = res.status >= 200 && res.status < 300;
|
|
1141
|
+
resStatus.innerHTML =
|
|
1142
|
+
'<span class="status-pill ' + (ok ? 'ok' : 'err') + '">' + res.status + '</span>' +
|
|
1143
|
+
'<span class="res-duration">' + duration + 'ms</span>';
|
|
851
1144
|
|
|
852
|
-
|
|
1145
|
+
if (typeof parsed === 'object') {
|
|
1146
|
+
resFormatted.innerHTML = syntaxHighlight(parsed);
|
|
1147
|
+
} else {
|
|
1148
|
+
resFormatted.textContent = raw;
|
|
1149
|
+
}
|
|
853
1150
|
resRaw.textContent = raw;
|
|
854
1151
|
} catch (err) {
|
|
855
1152
|
if (err.name === 'AbortError') {
|
|
856
|
-
|
|
1153
|
+
resStatus.innerHTML = '<span class="status-pill cancelled">Cancelled</span>';
|
|
857
1154
|
} else {
|
|
858
|
-
|
|
1155
|
+
resStatus.innerHTML = '<span class="status-pill err">Error</span> <span style="color:var(--text-1);font-size:12px">' + escHtml(err.message) + '</span>';
|
|
859
1156
|
}
|
|
860
1157
|
} finally {
|
|
861
1158
|
sendBtn.disabled = false;
|
|
@@ -865,73 +1162,68 @@ function playgroundJS(port) {
|
|
|
865
1162
|
}
|
|
866
1163
|
|
|
867
1164
|
async function sendChatStream(body, sendBtn) {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1165
|
+
var output = $('#chat-stream-output');
|
|
1166
|
+
var textArea = $('#chat-stream-text');
|
|
1167
|
+
var summary = $('#chat-stream-summary');
|
|
871
1168
|
|
|
872
1169
|
sendBtn.disabled = true;
|
|
873
|
-
// Hide standard response, show streaming
|
|
874
1170
|
$('#chat-response-standard').classList.add('hidden');
|
|
875
1171
|
output.classList.remove('hidden');
|
|
876
1172
|
textArea.innerHTML = '<span class="cursor-blink"></span>';
|
|
877
1173
|
summary.classList.add('hidden');
|
|
878
1174
|
state.autoScroll = true;
|
|
879
1175
|
|
|
880
|
-
|
|
1176
|
+
var controller = new AbortController();
|
|
881
1177
|
state.activeAbort = controller;
|
|
882
|
-
|
|
883
|
-
|
|
1178
|
+
var fullText = '';
|
|
1179
|
+
var startTime = Date.now();
|
|
884
1180
|
|
|
885
1181
|
try {
|
|
886
|
-
|
|
1182
|
+
var res = await fetch(BASE + '/v1/chat/completions', {
|
|
887
1183
|
method: 'POST',
|
|
888
1184
|
headers: getHeaders(),
|
|
889
1185
|
body: JSON.stringify(body),
|
|
890
1186
|
signal: controller.signal,
|
|
891
1187
|
});
|
|
892
1188
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1189
|
+
var reader = res.body.getReader();
|
|
1190
|
+
var decoder = new TextDecoder();
|
|
1191
|
+
var buffer = '';
|
|
896
1192
|
|
|
897
1193
|
while (true) {
|
|
898
|
-
|
|
899
|
-
if (done) break;
|
|
1194
|
+
var chunk = await reader.read();
|
|
1195
|
+
if (chunk.done) break;
|
|
900
1196
|
|
|
901
|
-
buffer += decoder.decode(value, { stream: true });
|
|
902
|
-
|
|
1197
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
1198
|
+
var lines = buffer.split('\\n');
|
|
903
1199
|
buffer = lines.pop();
|
|
904
1200
|
|
|
905
|
-
for (
|
|
1201
|
+
for (var j = 0; j < lines.length; j++) {
|
|
1202
|
+
var line = lines[j];
|
|
906
1203
|
if (!line.startsWith('data: ')) continue;
|
|
907
|
-
|
|
1204
|
+
var data = line.slice(6);
|
|
908
1205
|
if (data === '[DONE]') {
|
|
909
|
-
|
|
1206
|
+
var cur = textArea.querySelector('.cursor-blink');
|
|
910
1207
|
if (cur) cur.remove();
|
|
911
|
-
|
|
1208
|
+
var duration = Date.now() - startTime;
|
|
912
1209
|
summary.classList.remove('hidden');
|
|
913
|
-
summary.innerHTML = '<span>Duration
|
|
1210
|
+
summary.innerHTML = '<span><span class="label">Duration</span>' + duration + 'ms</span><span>Stream complete</span>';
|
|
914
1211
|
continue;
|
|
915
1212
|
}
|
|
916
1213
|
|
|
917
|
-
|
|
1214
|
+
var event;
|
|
918
1215
|
try { event = JSON.parse(data); } catch { continue; }
|
|
919
1216
|
|
|
920
|
-
|
|
921
|
-
if (delta
|
|
1217
|
+
var delta = event.choices && event.choices[0] && event.choices[0].delta;
|
|
1218
|
+
if (delta && delta.content) {
|
|
922
1219
|
fullText += delta.content;
|
|
923
1220
|
textArea.innerHTML = escHtml(fullText) + '<span class="cursor-blink"></span>';
|
|
924
1221
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
if (tc.function
|
|
929
|
-
|
|
930
|
-
block.className = 'tool-call-block';
|
|
931
|
-
block.innerHTML =
|
|
932
|
-
'<div class="tool-call-header">' + escHtml(tc.function.name) + '</div>' +
|
|
933
|
-
'<div class="tool-call-body"><pre>' + escHtml(tc.function.arguments || '') + '</pre></div>';
|
|
934
|
-
block.querySelector('.tool-call-header').onclick = () => block.classList.toggle('open');
|
|
1222
|
+
if (delta && delta.tool_calls) {
|
|
1223
|
+
for (var k = 0; k < delta.tool_calls.length; k++) {
|
|
1224
|
+
var tc = delta.tool_calls[k];
|
|
1225
|
+
if (tc.function && tc.function.name) {
|
|
1226
|
+
var block = makeToolBlock(tc.function.name, tc.function.arguments || '');
|
|
935
1227
|
textArea.insertBefore(block, textArea.querySelector('.cursor-blink'));
|
|
936
1228
|
}
|
|
937
1229
|
}
|
|
@@ -942,89 +1234,81 @@ function playgroundJS(port) {
|
|
|
942
1234
|
}
|
|
943
1235
|
} catch (err) {
|
|
944
1236
|
if (err.name !== 'AbortError') {
|
|
945
|
-
textArea.innerHTML = fullText + '\\n<span style="color:var(--
|
|
1237
|
+
textArea.innerHTML = fullText + '\\n<span style="color:var(--red)">Error: ' + escHtml(err.message) + '</span>';
|
|
946
1238
|
} else {
|
|
947
|
-
|
|
948
|
-
if (
|
|
949
|
-
textArea.innerHTML += '\\n<span style="color:var(--text-
|
|
1239
|
+
var cur2 = textArea.querySelector('.cursor-blink');
|
|
1240
|
+
if (cur2) cur2.remove();
|
|
1241
|
+
textArea.innerHTML += '\\n<span style="color:var(--text-2)">[Cancelled]</span>';
|
|
950
1242
|
}
|
|
951
1243
|
} finally {
|
|
952
1244
|
sendBtn.disabled = false;
|
|
953
1245
|
state.activeAbort = null;
|
|
954
|
-
|
|
955
|
-
if (
|
|
1246
|
+
var cur3 = textArea.querySelector('.cursor-blink');
|
|
1247
|
+
if (cur3) cur3.remove();
|
|
956
1248
|
}
|
|
957
1249
|
}
|
|
958
1250
|
|
|
959
1251
|
// ── WebSocket ──
|
|
960
1252
|
function wsConnect() {
|
|
961
1253
|
if (state.ws) return;
|
|
962
|
-
|
|
963
|
-
const dot = $('#ws-dot');
|
|
1254
|
+
var dot = $('#ws-dot');
|
|
964
1255
|
|
|
965
|
-
dot.className = 'ws-
|
|
1256
|
+
dot.className = 'ws-dot connecting';
|
|
966
1257
|
wsLog('Connecting to ' + WS_BASE + '/ws ...', 'system');
|
|
967
1258
|
|
|
968
|
-
|
|
1259
|
+
var ws = new WebSocket(WS_BASE + '/ws');
|
|
969
1260
|
state.ws = ws;
|
|
970
1261
|
|
|
971
|
-
ws.onopen = ()
|
|
1262
|
+
ws.onopen = function() {
|
|
972
1263
|
state.wsConnected = true;
|
|
973
|
-
dot.className = 'ws-
|
|
1264
|
+
dot.className = 'ws-dot connected';
|
|
974
1265
|
wsLog('Connected', 'system');
|
|
975
1266
|
$('#ws-connect').disabled = true;
|
|
976
1267
|
$('#ws-disconnect').disabled = false;
|
|
977
1268
|
$('#ws-send').disabled = false;
|
|
1269
|
+
$('#ws-label').textContent = 'Connected';
|
|
978
1270
|
};
|
|
979
1271
|
|
|
980
|
-
ws.onmessage = (e)
|
|
981
|
-
|
|
982
|
-
try {
|
|
983
|
-
|
|
984
|
-
display = JSON.stringify(parsed, null, 2);
|
|
985
|
-
} catch {
|
|
986
|
-
display = e.data;
|
|
987
|
-
}
|
|
1272
|
+
ws.onmessage = function(e) {
|
|
1273
|
+
var display;
|
|
1274
|
+
try { display = JSON.stringify(JSON.parse(e.data), null, 2); }
|
|
1275
|
+
catch { display = e.data; }
|
|
988
1276
|
wsLog(display, 'received');
|
|
989
1277
|
};
|
|
990
1278
|
|
|
991
|
-
ws.onerror = ()
|
|
992
|
-
wsLog('Connection error', 'system');
|
|
993
|
-
};
|
|
1279
|
+
ws.onerror = function() { wsLog('Connection error', 'system'); };
|
|
994
1280
|
|
|
995
|
-
ws.onclose = (e)
|
|
1281
|
+
ws.onclose = function(e) {
|
|
996
1282
|
state.ws = null;
|
|
997
1283
|
state.wsConnected = false;
|
|
998
|
-
dot.className = 'ws-
|
|
1284
|
+
dot.className = 'ws-dot';
|
|
999
1285
|
wsLog('Disconnected (code: ' + e.code + ')', 'system');
|
|
1000
1286
|
$('#ws-connect').disabled = false;
|
|
1001
1287
|
$('#ws-disconnect').disabled = true;
|
|
1002
1288
|
$('#ws-send').disabled = true;
|
|
1289
|
+
$('#ws-label').textContent = 'Disconnected';
|
|
1003
1290
|
};
|
|
1004
1291
|
}
|
|
1005
1292
|
|
|
1006
1293
|
function wsDisconnect() {
|
|
1007
|
-
if (state.ws)
|
|
1008
|
-
state.ws.close();
|
|
1009
|
-
}
|
|
1294
|
+
if (state.ws) state.ws.close();
|
|
1010
1295
|
}
|
|
1011
1296
|
|
|
1012
1297
|
function wsSend() {
|
|
1013
1298
|
if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return;
|
|
1014
|
-
|
|
1015
|
-
|
|
1299
|
+
var input = $('#ws-input');
|
|
1300
|
+
var msg = input.value.trim();
|
|
1016
1301
|
if (!msg) return;
|
|
1017
|
-
|
|
1018
1302
|
state.ws.send(msg);
|
|
1019
1303
|
wsLog(msg, 'sent');
|
|
1020
1304
|
}
|
|
1021
1305
|
|
|
1022
1306
|
function wsLog(text, type) {
|
|
1023
|
-
|
|
1024
|
-
|
|
1307
|
+
var log = $('#ws-log');
|
|
1308
|
+
var div = document.createElement('div');
|
|
1025
1309
|
div.className = 'ws-msg ' + type;
|
|
1026
|
-
|
|
1027
|
-
div.
|
|
1310
|
+
var prefix = type === 'sent' ? '\\u25B6' : type === 'received' ? '\\u25C0' : '\\u2022';
|
|
1311
|
+
div.innerHTML = '<span class="ws-prefix">' + prefix + '</span>' + escHtml(text);
|
|
1028
1312
|
log.appendChild(div);
|
|
1029
1313
|
log.scrollTop = log.scrollHeight;
|
|
1030
1314
|
}
|
|
@@ -1046,168 +1330,283 @@ function playgroundJS(port) {
|
|
|
1046
1330
|
}
|
|
1047
1331
|
function playgroundHTML() {
|
|
1048
1332
|
return `
|
|
1049
|
-
<div class="
|
|
1050
|
-
<
|
|
1051
|
-
<
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
<input type="password" id="api-key" class="api-key-input" placeholder="API Key (optional)">
|
|
1056
|
-
</div>
|
|
1057
|
-
</div>
|
|
1058
|
-
|
|
1059
|
-
<div class="tabs">
|
|
1060
|
-
<button class="tab active" data-tab="status">Status</button>
|
|
1061
|
-
<button class="tab" data-tab="run">Run</button>
|
|
1062
|
-
<button class="tab" data-tab="stream">Stream</button>
|
|
1063
|
-
<button class="tab" data-tab="chat">Chat (OpenAI)</button>
|
|
1064
|
-
<button class="tab" data-tab="ws">WebSocket</button>
|
|
1065
|
-
</div>
|
|
1066
|
-
|
|
1067
|
-
<div class="main">
|
|
1068
|
-
<!-- Status Panel -->
|
|
1069
|
-
<div class="panel active" id="panel-status">
|
|
1070
|
-
<div class="endpoint"><span class="method">GET</span>/api/status</div>
|
|
1071
|
-
<div id="status-content"><div style="color:var(--text-muted)">Loading...</div></div>
|
|
1072
|
-
</div>
|
|
1073
|
-
|
|
1074
|
-
<!-- Run Panel -->
|
|
1075
|
-
<div class="panel" id="panel-run">
|
|
1076
|
-
<div class="endpoint"><span class="method">POST</span>/api/run</div>
|
|
1077
|
-
|
|
1078
|
-
<div class="section-header">Headers</div>
|
|
1079
|
-
<div class="collapsible">
|
|
1080
|
-
<div class="header-row">
|
|
1081
|
-
<span class="label">X-Session-Id</span>
|
|
1082
|
-
<input type="text" id="session-id-run" placeholder="auto-populated from response">
|
|
1083
|
-
</div>
|
|
1333
|
+
<div class="app">
|
|
1334
|
+
<header class="header">
|
|
1335
|
+
<div class="header-left">
|
|
1336
|
+
<span class="logo">\u{1F9A6}</span>
|
|
1337
|
+
<span class="logo-text">otterly</span>
|
|
1338
|
+
<span class="version-badge">v0.3.1</span>
|
|
1084
1339
|
</div>
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
<div class="response-bar" id="res-bar-run"></div>
|
|
1095
|
-
<div class="response-tabs">
|
|
1096
|
-
<button class="response-tab active" data-mode="formatted">Formatted</button>
|
|
1097
|
-
<button class="response-tab" data-mode="raw">Raw</button>
|
|
1340
|
+
<div class="header-center">
|
|
1341
|
+
<nav class="nav">
|
|
1342
|
+
<button class="nav-item active" data-tab="status">Status</button>
|
|
1343
|
+
<button class="nav-item" data-tab="run">Run</button>
|
|
1344
|
+
<button class="nav-item" data-tab="stream">Stream</button>
|
|
1345
|
+
<button class="nav-item" data-tab="chat">Chat</button>
|
|
1346
|
+
<button class="nav-item" data-tab="ws">WebSocket</button>
|
|
1347
|
+
</nav>
|
|
1098
1348
|
</div>
|
|
1099
|
-
<div class="
|
|
1100
|
-
<
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
</div>
|
|
1104
|
-
|
|
1105
|
-
<!-- Stream Panel -->
|
|
1106
|
-
<div class="panel" id="panel-stream">
|
|
1107
|
-
<div class="endpoint"><span class="method">POST</span>/api/stream</div>
|
|
1108
|
-
|
|
1109
|
-
<div class="section-header">Headers</div>
|
|
1110
|
-
<div class="collapsible">
|
|
1111
|
-
<div class="header-row">
|
|
1112
|
-
<span class="label">X-Session-Id</span>
|
|
1113
|
-
<input type="text" id="session-id-stream" placeholder="auto-populated from response">
|
|
1349
|
+
<div class="header-right">
|
|
1350
|
+
<div class="key-wrapper">
|
|
1351
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
|
1352
|
+
<input type="password" id="api-key" class="key-input" placeholder="API Key">
|
|
1114
1353
|
</div>
|
|
1115
1354
|
</div>
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
<div class="
|
|
1121
|
-
<
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
<button class="jump-bottom">Jump to bottom</button>
|
|
1130
|
-
</div>
|
|
1131
|
-
<div class="summary-bar hidden" id="stream-summary"></div>
|
|
1132
|
-
</div>
|
|
1133
|
-
|
|
1134
|
-
<!-- Chat (OpenAI) Panel -->
|
|
1135
|
-
<div class="panel" id="panel-chat">
|
|
1136
|
-
<div class="endpoint"><span class="method">POST</span>/v1/chat/completions</div>
|
|
1137
|
-
|
|
1138
|
-
<div class="section-header">Headers</div>
|
|
1139
|
-
<div class="collapsible">
|
|
1140
|
-
<div class="header-row">
|
|
1141
|
-
<span class="label">X-Session-Id</span>
|
|
1142
|
-
<input type="text" id="session-id-chat" placeholder="auto-populated from response">
|
|
1355
|
+
</header>
|
|
1356
|
+
|
|
1357
|
+
<div class="content">
|
|
1358
|
+
<!-- ── Status ── -->
|
|
1359
|
+
<div class="panel active" id="panel-status">
|
|
1360
|
+
<div class="status-panel" id="status-content">
|
|
1361
|
+
<div class="status-inner">
|
|
1362
|
+
<div class="status-hero">
|
|
1363
|
+
<div class="skeleton" style="width:10px;height:10px;border-radius:50%"></div>
|
|
1364
|
+
<div><div class="skeleton" style="width:180px;height:18px;margin-bottom:4px"></div><div class="skeleton" style="width:60px;height:14px"></div></div>
|
|
1365
|
+
</div>
|
|
1366
|
+
<div class="skeleton" style="width:100%;height:100px;border-radius:8px"></div>
|
|
1367
|
+
</div>
|
|
1143
1368
|
</div>
|
|
1144
1369
|
</div>
|
|
1145
1370
|
|
|
1146
|
-
|
|
1147
|
-
<
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1371
|
+
<!-- ── Run ── -->
|
|
1372
|
+
<div class="panel" id="panel-run">
|
|
1373
|
+
<div class="split-pane">
|
|
1374
|
+
<div class="pane">
|
|
1375
|
+
<div class="pane-head">
|
|
1376
|
+
<span class="method-badge method-post">POST</span>
|
|
1377
|
+
<span class="endpoint-path">/api/run</span>
|
|
1378
|
+
</div>
|
|
1379
|
+
<div class="pane-body">
|
|
1380
|
+
<button class="section-toggle" type="button">
|
|
1381
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M4.5 3L7.5 6L4.5 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
1382
|
+
Headers
|
|
1383
|
+
</button>
|
|
1384
|
+
<div class="collapsible">
|
|
1385
|
+
<div class="field-row">
|
|
1386
|
+
<span class="label-inline">X-Session-Id</span>
|
|
1387
|
+
<input type="text" id="session-id-run" placeholder="auto from response">
|
|
1388
|
+
</div>
|
|
1389
|
+
</div>
|
|
1390
|
+
<div class="field-label">Request body</div>
|
|
1391
|
+
<textarea id="body-run"></textarea>
|
|
1392
|
+
<div class="actions">
|
|
1393
|
+
<button class="btn btn-primary" id="send-run">
|
|
1394
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
|
1395
|
+
Send
|
|
1396
|
+
</button>
|
|
1397
|
+
<button class="btn btn-ghost btn-cancel">Cancel</button>
|
|
1398
|
+
</div>
|
|
1399
|
+
</div>
|
|
1400
|
+
</div>
|
|
1401
|
+
<div class="pane">
|
|
1402
|
+
<div class="pane-head">
|
|
1403
|
+
<span style="font-size:13px;font-weight:500;color:var(--text-1)">Response</span>
|
|
1404
|
+
<div class="pane-head-right">
|
|
1405
|
+
<div id="res-status-run" class="res-status"></div>
|
|
1406
|
+
<div class="mode-toggle">
|
|
1407
|
+
<button class="active" data-mode="formatted">Formatted</button>
|
|
1408
|
+
<button data-mode="raw">Raw</button>
|
|
1409
|
+
</div>
|
|
1410
|
+
</div>
|
|
1411
|
+
</div>
|
|
1412
|
+
<div class="pane-body">
|
|
1413
|
+
<div id="res-empty-run" class="code-output empty-state">Send a request to see the response</div>
|
|
1414
|
+
<pre id="res-formatted-run" class="code-output hidden response-formatted"></pre>
|
|
1415
|
+
<pre id="res-raw-run" class="code-output hidden response-raw"></pre>
|
|
1416
|
+
</div>
|
|
1417
|
+
</div>
|
|
1418
|
+
</div>
|
|
1152
1419
|
</div>
|
|
1153
1420
|
|
|
1154
|
-
<!--
|
|
1155
|
-
<div id="
|
|
1156
|
-
<div class="
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1421
|
+
<!-- ── Stream ── -->
|
|
1422
|
+
<div class="panel" id="panel-stream">
|
|
1423
|
+
<div class="split-pane">
|
|
1424
|
+
<div class="pane">
|
|
1425
|
+
<div class="pane-head">
|
|
1426
|
+
<span class="method-badge method-post">POST</span>
|
|
1427
|
+
<span class="endpoint-path">/api/stream</span>
|
|
1428
|
+
</div>
|
|
1429
|
+
<div class="pane-body">
|
|
1430
|
+
<button class="section-toggle" type="button">
|
|
1431
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M4.5 3L7.5 6L4.5 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
1432
|
+
Headers
|
|
1433
|
+
</button>
|
|
1434
|
+
<div class="collapsible">
|
|
1435
|
+
<div class="field-row">
|
|
1436
|
+
<span class="label-inline">X-Session-Id</span>
|
|
1437
|
+
<input type="text" id="session-id-stream" placeholder="auto from response">
|
|
1438
|
+
</div>
|
|
1439
|
+
</div>
|
|
1440
|
+
<div class="field-label">Request body</div>
|
|
1441
|
+
<textarea id="body-stream"></textarea>
|
|
1442
|
+
<div class="actions">
|
|
1443
|
+
<button class="btn btn-primary" id="send-stream">
|
|
1444
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
|
1445
|
+
Send
|
|
1446
|
+
</button>
|
|
1447
|
+
<button class="btn btn-ghost btn-cancel">Cancel</button>
|
|
1448
|
+
<button class="btn btn-ghost btn-sm" id="stream-raw-toggle">Raw</button>
|
|
1449
|
+
</div>
|
|
1450
|
+
</div>
|
|
1451
|
+
</div>
|
|
1452
|
+
<div class="pane">
|
|
1453
|
+
<div class="pane-head">
|
|
1454
|
+
<span style="font-size:13px;font-weight:500;color:var(--text-1)">Stream output</span>
|
|
1455
|
+
</div>
|
|
1456
|
+
<div class="pane-body">
|
|
1457
|
+
<div class="stream-area hidden" id="stream-output">
|
|
1458
|
+
<div id="stream-text"></div>
|
|
1459
|
+
<pre id="stream-raw" class="hidden"></pre>
|
|
1460
|
+
<button class="jump-btn">Jump to bottom</button>
|
|
1461
|
+
</div>
|
|
1462
|
+
<div id="stream-empty" class="code-output empty-state">Send a request to start streaming</div>
|
|
1463
|
+
<div class="summary-bar hidden" id="stream-summary"></div>
|
|
1464
|
+
</div>
|
|
1465
|
+
</div>
|
|
1164
1466
|
</div>
|
|
1165
1467
|
</div>
|
|
1166
1468
|
|
|
1167
|
-
<!--
|
|
1168
|
-
<div class="
|
|
1169
|
-
<div
|
|
1170
|
-
|
|
1469
|
+
<!-- ── Chat ── -->
|
|
1470
|
+
<div class="panel" id="panel-chat">
|
|
1471
|
+
<div class="split-pane">
|
|
1472
|
+
<div class="pane">
|
|
1473
|
+
<div class="pane-head">
|
|
1474
|
+
<span class="method-badge method-post">POST</span>
|
|
1475
|
+
<span class="endpoint-path">/v1/chat/completions</span>
|
|
1476
|
+
</div>
|
|
1477
|
+
<div class="pane-body">
|
|
1478
|
+
<button class="section-toggle" type="button">
|
|
1479
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M4.5 3L7.5 6L4.5 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
1480
|
+
Headers
|
|
1481
|
+
</button>
|
|
1482
|
+
<div class="collapsible">
|
|
1483
|
+
<div class="field-row">
|
|
1484
|
+
<span class="label-inline">X-Session-Id</span>
|
|
1485
|
+
<input type="text" id="session-id-chat" placeholder="auto from response">
|
|
1486
|
+
</div>
|
|
1487
|
+
</div>
|
|
1488
|
+
<div class="field-label">Request body</div>
|
|
1489
|
+
<textarea id="body-chat"></textarea>
|
|
1490
|
+
<div class="actions">
|
|
1491
|
+
<button class="btn btn-primary" id="send-chat">
|
|
1492
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
|
1493
|
+
Send
|
|
1494
|
+
</button>
|
|
1495
|
+
<button class="btn btn-ghost btn-cancel">Cancel</button>
|
|
1496
|
+
</div>
|
|
1497
|
+
</div>
|
|
1498
|
+
</div>
|
|
1499
|
+
<div class="pane">
|
|
1500
|
+
<div class="pane-head">
|
|
1501
|
+
<span style="font-size:13px;font-weight:500;color:var(--text-1)">Response</span>
|
|
1502
|
+
<div class="pane-head-right">
|
|
1503
|
+
<div id="res-status-chat" class="res-status"></div>
|
|
1504
|
+
<div class="mode-toggle">
|
|
1505
|
+
<button class="active" data-mode="formatted">Formatted</button>
|
|
1506
|
+
<button data-mode="raw">Raw</button>
|
|
1507
|
+
</div>
|
|
1508
|
+
</div>
|
|
1509
|
+
</div>
|
|
1510
|
+
<div class="pane-body">
|
|
1511
|
+
<div id="chat-response-standard">
|
|
1512
|
+
<div id="res-empty-chat" class="code-output empty-state">Send a request to see the response</div>
|
|
1513
|
+
<pre id="res-formatted-chat" class="code-output hidden response-formatted"></pre>
|
|
1514
|
+
<pre id="res-raw-chat" class="code-output hidden response-raw"></pre>
|
|
1515
|
+
</div>
|
|
1516
|
+
<div class="stream-area hidden" id="chat-stream-output">
|
|
1517
|
+
<div id="chat-stream-text"></div>
|
|
1518
|
+
<button class="jump-btn">Jump to bottom</button>
|
|
1519
|
+
</div>
|
|
1520
|
+
<div class="summary-bar hidden" id="chat-stream-summary"></div>
|
|
1521
|
+
</div>
|
|
1522
|
+
</div>
|
|
1523
|
+
</div>
|
|
1171
1524
|
</div>
|
|
1172
|
-
<div class="summary-bar hidden" id="chat-stream-summary"></div>
|
|
1173
|
-
</div>
|
|
1174
1525
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1526
|
+
<!-- ── WebSocket ── -->
|
|
1527
|
+
<div class="panel" id="panel-ws">
|
|
1528
|
+
<div class="split-pane">
|
|
1529
|
+
<div class="pane">
|
|
1530
|
+
<div class="pane-head">
|
|
1531
|
+
<span class="method-badge method-ws">WS</span>
|
|
1532
|
+
<span class="endpoint-path">/ws</span>
|
|
1533
|
+
</div>
|
|
1534
|
+
<div class="pane-body">
|
|
1535
|
+
<div class="ws-controls">
|
|
1536
|
+
<span class="ws-dot" id="ws-dot"></span>
|
|
1537
|
+
<span class="ws-label" id="ws-label">Disconnected</span>
|
|
1538
|
+
<div style="margin-left:auto;display:flex;gap:6px">
|
|
1539
|
+
<button class="btn btn-primary btn-sm" id="ws-connect">Connect</button>
|
|
1540
|
+
<button class="btn btn-ghost btn-sm" id="ws-disconnect" disabled>Disconnect</button>
|
|
1541
|
+
</div>
|
|
1542
|
+
</div>
|
|
1543
|
+
<div class="field-label mt-12">Message</div>
|
|
1544
|
+
<textarea id="ws-input" style="min-height:80px"></textarea>
|
|
1545
|
+
<div class="actions">
|
|
1546
|
+
<button class="btn btn-primary" id="ws-send" disabled>
|
|
1547
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
|
1548
|
+
Send
|
|
1549
|
+
</button>
|
|
1550
|
+
</div>
|
|
1551
|
+
</div>
|
|
1552
|
+
</div>
|
|
1553
|
+
<div class="pane">
|
|
1554
|
+
<div class="pane-head">
|
|
1555
|
+
<span style="font-size:13px;font-weight:500;color:var(--text-1)">Messages</span>
|
|
1556
|
+
</div>
|
|
1557
|
+
<div class="pane-body">
|
|
1558
|
+
<div class="ws-log" id="ws-log" style="flex:1"></div>
|
|
1559
|
+
</div>
|
|
1560
|
+
</div>
|
|
1561
|
+
</div>
|
|
1183
1562
|
</div>
|
|
1184
1563
|
|
|
1185
|
-
<div class="ws-log" id="ws-log"></div>
|
|
1186
|
-
|
|
1187
|
-
<div class="ws-input-row">
|
|
1188
|
-
<textarea id="ws-input"></textarea>
|
|
1189
|
-
<button class="btn btn-primary" id="ws-send" disabled style="align-self:flex-end;">Send</button>
|
|
1190
|
-
</div>
|
|
1191
1564
|
</div>
|
|
1192
1565
|
</div>
|
|
1193
1566
|
`;
|
|
1194
1567
|
}
|
|
1195
1568
|
export function getPlaygroundHtml(port) {
|
|
1196
|
-
// Fill in templates via inline script
|
|
1197
1569
|
const templateScript = `
|
|
1198
1570
|
<script>
|
|
1199
1571
|
document.addEventListener('DOMContentLoaded', function() {
|
|
1200
|
-
var
|
|
1572
|
+
var T = {
|
|
1201
1573
|
run: ${JSON.stringify(JSON.stringify({ prompt: "What is 2 + 2?", options: { maxTurns: 1 } }, null, 2))},
|
|
1202
1574
|
stream: ${JSON.stringify(JSON.stringify({ prompt: "Write a haiku about coding", options: { maxTurns: 1 } }, null, 2))},
|
|
1203
1575
|
chat: ${JSON.stringify(JSON.stringify({ model: "claude-sonnet-4-20250514", messages: [{ role: "user", content: "Hello! What can you do?" }], stream: false }, null, 2))},
|
|
1204
1576
|
ws: ${JSON.stringify(JSON.stringify({ type: "start", prompt: "Hello from WebSocket" }, null, 2))}
|
|
1205
1577
|
};
|
|
1206
1578
|
var el;
|
|
1207
|
-
el = document.getElementById('body-run'); if (el) el.value =
|
|
1208
|
-
el = document.getElementById('body-stream'); if (el) el.value =
|
|
1209
|
-
el = document.getElementById('body-chat'); if (el) el.value =
|
|
1210
|
-
el = document.getElementById('ws-input'); if (el) el.value =
|
|
1579
|
+
el = document.getElementById('body-run'); if (el) el.value = T.run;
|
|
1580
|
+
el = document.getElementById('body-stream'); if (el) el.value = T.stream;
|
|
1581
|
+
el = document.getElementById('body-chat'); if (el) el.value = T.chat;
|
|
1582
|
+
el = document.getElementById('ws-input'); if (el) el.value = T.ws;
|
|
1583
|
+
|
|
1584
|
+
// Hide empty states when content appears
|
|
1585
|
+
document.querySelectorAll('.code-output.empty-state').forEach(function(empty) {
|
|
1586
|
+
var observer = new MutationObserver(function() {
|
|
1587
|
+
var panel = empty.closest('.pane-body');
|
|
1588
|
+
if (!panel) return;
|
|
1589
|
+
var formatted = panel.querySelector('.response-formatted');
|
|
1590
|
+
var raw = panel.querySelector('.response-raw');
|
|
1591
|
+
if ((formatted && formatted.innerHTML) || (raw && raw.textContent)) {
|
|
1592
|
+
empty.classList.add('hidden');
|
|
1593
|
+
if (formatted) formatted.classList.remove('hidden');
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
var panel = empty.closest('.pane-body');
|
|
1597
|
+
if (panel) observer.observe(panel, { childList: true, subtree: true, characterData: true });
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
// Hide stream empty state when streaming starts
|
|
1601
|
+
var streamEmpty = document.getElementById('stream-empty');
|
|
1602
|
+
var streamOutput = document.getElementById('stream-output');
|
|
1603
|
+
if (streamEmpty && streamOutput) {
|
|
1604
|
+
new MutationObserver(function() {
|
|
1605
|
+
if (!streamOutput.classList.contains('hidden')) {
|
|
1606
|
+
streamEmpty.classList.add('hidden');
|
|
1607
|
+
}
|
|
1608
|
+
}).observe(streamOutput, { attributes: true, attributeFilter: ['class'] });
|
|
1609
|
+
}
|
|
1211
1610
|
});
|
|
1212
1611
|
</script>`;
|
|
1213
1612
|
return `<!DOCTYPE html>
|