@prismshadow/mmsp 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -0
- package/dist/ant_messages/client.d.ts +59 -0
- package/dist/ant_messages/client.d.ts.map +1 -0
- package/dist/ant_messages/client.js +419 -0
- package/dist/ant_messages/index.d.ts +2 -0
- package/dist/ant_messages/index.d.ts.map +1 -0
- package/dist/ant_messages/index.js +18 -0
- package/dist/anthropic_official/client.d.ts +63 -0
- package/dist/anthropic_official/client.d.ts.map +1 -0
- package/dist/anthropic_official/client.js +540 -0
- package/dist/anthropic_official/index.d.ts +2 -0
- package/dist/anthropic_official/index.d.ts.map +1 -0
- package/dist/anthropic_official/index.js +18 -0
- package/dist/autoClient.d.ts +96 -0
- package/dist/autoClient.d.ts.map +1 -0
- package/dist/autoClient.js +258 -0
- package/dist/baseClient.d.ts +111 -0
- package/dist/baseClient.d.ts.map +1 -0
- package/dist/baseClient.js +284 -0
- package/dist/deepseek_official/client.d.ts +60 -0
- package/dist/deepseek_official/client.d.ts.map +1 -0
- package/dist/deepseek_official/client.js +407 -0
- package/dist/deepseek_official/index.d.ts +2 -0
- package/dist/deepseek_official/index.d.ts.map +1 -0
- package/dist/deepseek_official/index.js +18 -0
- package/dist/errors.d.ts +84 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +140 -0
- package/dist/gemini_generate_content/client.d.ts +86 -0
- package/dist/gemini_generate_content/client.d.ts.map +1 -0
- package/dist/gemini_generate_content/client.js +809 -0
- package/dist/gemini_generate_content/index.d.ts +2 -0
- package/dist/gemini_generate_content/index.d.ts.map +1 -0
- package/dist/gemini_generate_content/index.js +18 -0
- package/dist/gemini_official/client.d.ts +87 -0
- package/dist/gemini_official/client.d.ts.map +1 -0
- package/dist/gemini_official/client.js +780 -0
- package/dist/gemini_official/index.d.ts +2 -0
- package/dist/gemini_official/index.d.ts.map +1 -0
- package/dist/gemini_official/index.js +18 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/integration/index.d.ts +1 -0
- package/dist/integration/index.d.ts.map +1 -0
- package/dist/integration/index.js +14 -0
- package/dist/integration/playground.d.ts +17 -0
- package/dist/integration/playground.d.ts.map +1 -0
- package/dist/integration/playground.js +3246 -0
- package/dist/integration/tracer.d.ts +188 -0
- package/dist/integration/tracer.d.ts.map +1 -0
- package/dist/integration/tracer.js +1928 -0
- package/dist/legacy.d.ts +13 -0
- package/dist/legacy.d.ts.map +1 -0
- package/dist/legacy.js +59 -0
- package/dist/minimax_official/client.d.ts +43 -0
- package/dist/minimax_official/client.d.ts.map +1 -0
- package/dist/minimax_official/client.js +346 -0
- package/dist/minimax_official/index.d.ts +2 -0
- package/dist/minimax_official/index.d.ts.map +1 -0
- package/dist/minimax_official/index.js +18 -0
- package/dist/moonshot_official/client.d.ts +73 -0
- package/dist/moonshot_official/client.d.ts.map +1 -0
- package/dist/moonshot_official/client.js +477 -0
- package/dist/moonshot_official/index.d.ts +2 -0
- package/dist/moonshot_official/index.d.ts.map +1 -0
- package/dist/moonshot_official/index.js +18 -0
- package/dist/openai_chat/client.d.ts +67 -0
- package/dist/openai_chat/client.d.ts.map +1 -0
- package/dist/openai_chat/client.js +419 -0
- package/dist/openai_chat/index.d.ts +2 -0
- package/dist/openai_chat/index.d.ts.map +1 -0
- package/dist/openai_chat/index.js +18 -0
- package/dist/openai_chat_vllm_adapter/client.d.ts +15 -0
- package/dist/openai_chat_vllm_adapter/client.d.ts.map +1 -0
- package/dist/openai_chat_vllm_adapter/client.js +118 -0
- package/dist/openai_chat_vllm_adapter/index.d.ts +2 -0
- package/dist/openai_chat_vllm_adapter/index.d.ts.map +1 -0
- package/dist/openai_chat_vllm_adapter/index.js +5 -0
- package/dist/openai_embedding/client.d.ts +46 -0
- package/dist/openai_embedding/client.d.ts.map +1 -0
- package/dist/openai_embedding/client.js +124 -0
- package/dist/openai_embedding/index.d.ts +2 -0
- package/dist/openai_embedding/index.d.ts.map +1 -0
- package/dist/openai_embedding/index.js +18 -0
- package/dist/openai_official/client.d.ts +61 -0
- package/dist/openai_official/client.d.ts.map +1 -0
- package/dist/openai_official/client.js +464 -0
- package/dist/openai_official/index.d.ts +2 -0
- package/dist/openai_official/index.d.ts.map +1 -0
- package/dist/openai_official/index.js +18 -0
- package/dist/openai_responses/client.d.ts +61 -0
- package/dist/openai_responses/client.d.ts.map +1 -0
- package/dist/openai_responses/client.js +449 -0
- package/dist/openai_responses/index.d.ts +2 -0
- package/dist/openai_responses/index.d.ts.map +1 -0
- package/dist/openai_responses/index.js +18 -0
- package/dist/registry.d.ts +49 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +798 -0
- package/dist/streamItems.d.ts +36 -0
- package/dist/streamItems.d.ts.map +1 -0
- package/dist/streamItems.js +183 -0
- package/dist/types.d.ts +190 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +37 -0
- package/dist/utils.d.ts +99 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +312 -0
- package/dist/zai_official/client.d.ts +78 -0
- package/dist/zai_official/client.d.ts.map +1 -0
- package/dist/zai_official/client.js +420 -0
- package/dist/zai_official/index.d.ts +2 -0
- package/dist/zai_official/index.d.ts.map +1 -0
- package/dist/zai_official/index.js +18 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1928 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright 2025 Prism Shadow. and/or its affiliates
|
|
3
|
+
//
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// you may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
//
|
|
8
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
//
|
|
10
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
// See the License for the specific language governing permissions and
|
|
14
|
+
// limitations under the License.
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
49
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
50
|
+
};
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.Tracer = void 0;
|
|
53
|
+
/**
|
|
54
|
+
* Conversation tracer module for saving and viewing conversation history.
|
|
55
|
+
*
|
|
56
|
+
* This module provides functionality to save conversation history to local files
|
|
57
|
+
* and serve them via a web interface for real-time monitoring.
|
|
58
|
+
*/
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const path = __importStar(require("path"));
|
|
61
|
+
const express_1 = __importDefault(require("express"));
|
|
62
|
+
const legacy_1 = require("../legacy");
|
|
63
|
+
// The head every tracer page shares: the playground's tokens, fonts and light and dark themes.
|
|
64
|
+
const TRACER_HEAD = `
|
|
65
|
+
<meta charset="utf-8">
|
|
66
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
67
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 32 32%22%3E%3Cstyle%3E.d{fill:%23111116}@media (prefers-color-scheme: dark){.d{fill:%232a2a33}}%3C/style%3E%3Crect width=%2232%22 height=%2232%22 rx=%227%22 class=%22d%22/%3E%3Cpath d=%22M0 16V7a7 7 0 0 1 7-7h9v16Z%22 fill=%22%237c4dff%22/%3E%3Cpath d=%22M16 16h16v9a7 7 0 0 1-7 7h-9Z%22 fill=%22%237c4dff%22/%3E%3Cg fill=%22%23fff%22 font-family=%22ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif%22 font-size=%2210.5%22 font-weight=%22700%22 text-anchor=%22middle%22 dominant-baseline=%22central%22%3E%3Ctext x=%228.5%22 y=%228.5%22%3EM%3C/text%3E%3Ctext x=%2223.5%22 y=%228.5%22%3EM%3C/text%3E%3Ctext x=%228.5%22 y=%2223.5%22%3ES%3C/text%3E%3Ctext x=%2223.5%22 y=%2223.5%22%3EP%3C/text%3E%3C/g%3E%3C/svg%3E">
|
|
68
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
69
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
70
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap">
|
|
71
|
+
<script>
|
|
72
|
+
// the playground and the tracer share one origin, so they share the theme a reader picked
|
|
73
|
+
try {
|
|
74
|
+
const theme = localStorage.getItem('mmsp.playground.theme');
|
|
75
|
+
if (theme === 'light' || theme === 'dark') {
|
|
76
|
+
document.documentElement.dataset.theme = theme;
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// storage refused: the system theme it is
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
<style>
|
|
83
|
+
:root {
|
|
84
|
+
--bg: #f5f5f6;
|
|
85
|
+
--surface: #ffffff;
|
|
86
|
+
--raised: #f0f0f2;
|
|
87
|
+
--hover: rgba(20, 22, 28, 0.05);
|
|
88
|
+
--ring: rgba(20, 22, 28, 0.09);
|
|
89
|
+
--ring-strong: rgba(20, 22, 28, 0.17);
|
|
90
|
+
--text: #16181d;
|
|
91
|
+
--muted: #5c616c;
|
|
92
|
+
--subtle: #8a8f99;
|
|
93
|
+
--accent: #2f6fed;
|
|
94
|
+
--accent-soft: rgba(47, 111, 237, 0.14);
|
|
95
|
+
--green: #16945b;
|
|
96
|
+
--green-soft: rgba(22, 148, 91, 0.12);
|
|
97
|
+
--amber: #b16a0a;
|
|
98
|
+
--amber-soft: rgba(177, 106, 10, 0.12);
|
|
99
|
+
--red: #d23b3b;
|
|
100
|
+
--red-soft: rgba(210, 59, 59, 0.1);
|
|
101
|
+
--violet: #7a4fd6;
|
|
102
|
+
--violet-soft: rgba(122, 79, 214, 0.12);
|
|
103
|
+
--indigo: #4a57c9;
|
|
104
|
+
--shadow-card: 0 0 0 1px var(--ring), 0 1px 2px rgba(20, 22, 28, 0.04);
|
|
105
|
+
--shadow-menu: 0 0 0 1px var(--ring), 0 12px 32px -10px rgba(20, 22, 28, 0.22);
|
|
106
|
+
--ease: cubic-bezier(0.23, 1, 0.32, 1);
|
|
107
|
+
--font: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
108
|
+
--mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
|
|
109
|
+
color-scheme: light;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@media (prefers-color-scheme: dark) {
|
|
113
|
+
:root:not([data-theme="light"]) {
|
|
114
|
+
--bg: #1b1c1f;
|
|
115
|
+
--surface: #222327;
|
|
116
|
+
--raised: #28292e;
|
|
117
|
+
--hover: rgba(255, 255, 255, 0.05);
|
|
118
|
+
--ring: rgba(255, 255, 255, 0.08);
|
|
119
|
+
--ring-strong: rgba(255, 255, 255, 0.15);
|
|
120
|
+
--text: #eceef1;
|
|
121
|
+
--muted: #a3a8b1;
|
|
122
|
+
--subtle: #6f747e;
|
|
123
|
+
--accent: #4d8ef7;
|
|
124
|
+
--accent-soft: rgba(77, 142, 247, 0.2);
|
|
125
|
+
--green: #43c283;
|
|
126
|
+
--green-soft: rgba(67, 194, 131, 0.14);
|
|
127
|
+
--amber: #e3a646;
|
|
128
|
+
--amber-soft: rgba(227, 166, 70, 0.14);
|
|
129
|
+
--red: #f06a6a;
|
|
130
|
+
--red-soft: rgba(240, 106, 106, 0.14);
|
|
131
|
+
--violet: #a98bf0;
|
|
132
|
+
--violet-soft: rgba(169, 139, 240, 0.16);
|
|
133
|
+
--indigo: #8d97f2;
|
|
134
|
+
--shadow-card: 0 0 0 1px var(--ring), 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
135
|
+
--shadow-menu: 0 0 0 1px rgba(255, 255, 255, 0.1), 0 16px 36px -10px rgba(0, 0, 0, 0.7);
|
|
136
|
+
color-scheme: dark;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
:root[data-theme="dark"] {
|
|
141
|
+
--bg: #1b1c1f;
|
|
142
|
+
--surface: #222327;
|
|
143
|
+
--raised: #28292e;
|
|
144
|
+
--hover: rgba(255, 255, 255, 0.05);
|
|
145
|
+
--ring: rgba(255, 255, 255, 0.08);
|
|
146
|
+
--ring-strong: rgba(255, 255, 255, 0.15);
|
|
147
|
+
--text: #eceef1;
|
|
148
|
+
--muted: #a3a8b1;
|
|
149
|
+
--subtle: #6f747e;
|
|
150
|
+
--accent: #4d8ef7;
|
|
151
|
+
--accent-soft: rgba(77, 142, 247, 0.2);
|
|
152
|
+
--green: #43c283;
|
|
153
|
+
--green-soft: rgba(67, 194, 131, 0.14);
|
|
154
|
+
--amber: #e3a646;
|
|
155
|
+
--amber-soft: rgba(227, 166, 70, 0.14);
|
|
156
|
+
--red: #f06a6a;
|
|
157
|
+
--red-soft: rgba(240, 106, 106, 0.14);
|
|
158
|
+
--violet: #a98bf0;
|
|
159
|
+
--violet-soft: rgba(169, 139, 240, 0.16);
|
|
160
|
+
--indigo: #8d97f2;
|
|
161
|
+
--shadow-card: 0 0 0 1px var(--ring), 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
162
|
+
--shadow-menu: 0 0 0 1px rgba(255, 255, 255, 0.1), 0 16px 36px -10px rgba(0, 0, 0, 0.7);
|
|
163
|
+
color-scheme: dark;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
*, *::before, *::after {
|
|
167
|
+
box-sizing: border-box;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
html {
|
|
171
|
+
scroll-padding-top: 72px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
body {
|
|
175
|
+
margin: 0;
|
|
176
|
+
min-height: 100vh;
|
|
177
|
+
background: var(--bg);
|
|
178
|
+
color: var(--text);
|
|
179
|
+
font: 14px/1.55 var(--font);
|
|
180
|
+
-webkit-font-smoothing: antialiased;
|
|
181
|
+
text-rendering: optimizeLegibility;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
a {
|
|
185
|
+
color: inherit;
|
|
186
|
+
text-decoration: none;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
button {
|
|
190
|
+
font: inherit;
|
|
191
|
+
color: inherit;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
background: none;
|
|
194
|
+
border: 0;
|
|
195
|
+
padding: 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
svg {
|
|
199
|
+
flex: none;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.mono {
|
|
203
|
+
font-family: var(--mono);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
:focus-visible {
|
|
207
|
+
outline: 2px solid var(--accent);
|
|
208
|
+
outline-offset: 2px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
::selection {
|
|
212
|
+
background: var(--accent-soft);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* top bar */
|
|
216
|
+
|
|
217
|
+
.topbar {
|
|
218
|
+
position: sticky;
|
|
219
|
+
top: 0;
|
|
220
|
+
z-index: 10;
|
|
221
|
+
display: flex;
|
|
222
|
+
align-items: center;
|
|
223
|
+
gap: 16px;
|
|
224
|
+
height: 56px;
|
|
225
|
+
padding: 0 20px;
|
|
226
|
+
background: var(--bg);
|
|
227
|
+
box-shadow: 0 1px 0 var(--ring);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.brand {
|
|
231
|
+
display: flex;
|
|
232
|
+
align-items: center;
|
|
233
|
+
gap: 8px;
|
|
234
|
+
flex: none;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.brand-mark {
|
|
238
|
+
width: 22px;
|
|
239
|
+
height: 22px;
|
|
240
|
+
flex: none;
|
|
241
|
+
align-self: center;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.mark-bg {
|
|
245
|
+
fill: #111116;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@media (prefers-color-scheme: dark) {
|
|
249
|
+
:root:not([data-theme="light"]) .mark-bg {
|
|
250
|
+
fill: #2a2a33;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
:root[data-theme="dark"] .mark-bg {
|
|
255
|
+
fill: #2a2a33;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.brand-name {
|
|
259
|
+
font-size: 15px;
|
|
260
|
+
font-weight: 600;
|
|
261
|
+
letter-spacing: -0.01em;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.brand-sub {
|
|
265
|
+
color: var(--subtle);
|
|
266
|
+
font-size: 13px;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.page-nav {
|
|
270
|
+
display: flex;
|
|
271
|
+
align-items: center;
|
|
272
|
+
gap: 8px;
|
|
273
|
+
margin-bottom: 20px;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.nav-buttons {
|
|
277
|
+
display: flex;
|
|
278
|
+
flex: none;
|
|
279
|
+
gap: 2px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.nav-btn {
|
|
283
|
+
display: grid;
|
|
284
|
+
place-items: center;
|
|
285
|
+
width: 32px;
|
|
286
|
+
height: 32px;
|
|
287
|
+
border-radius: 8px;
|
|
288
|
+
color: var(--muted);
|
|
289
|
+
transition: color 0.15s, background-color 0.15s;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.nav-btn:hover {
|
|
293
|
+
color: var(--text);
|
|
294
|
+
background: var(--hover);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.nav-btn:disabled, .nav-btn[aria-disabled="true"] {
|
|
298
|
+
color: var(--subtle);
|
|
299
|
+
opacity: 0.45;
|
|
300
|
+
cursor: default;
|
|
301
|
+
pointer-events: none;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.address {
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: center;
|
|
307
|
+
gap: 2px;
|
|
308
|
+
flex: 1;
|
|
309
|
+
min-width: 0;
|
|
310
|
+
height: 34px;
|
|
311
|
+
padding: 0 6px;
|
|
312
|
+
overflow: hidden;
|
|
313
|
+
border-radius: 8px;
|
|
314
|
+
background: var(--surface);
|
|
315
|
+
box-shadow: var(--shadow-card);
|
|
316
|
+
font-size: 13px;
|
|
317
|
+
white-space: nowrap;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.address-icon {
|
|
321
|
+
display: grid;
|
|
322
|
+
place-items: center;
|
|
323
|
+
flex: none;
|
|
324
|
+
width: 24px;
|
|
325
|
+
color: var(--subtle);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.crumb {
|
|
329
|
+
flex: none;
|
|
330
|
+
padding: 3px 6px;
|
|
331
|
+
border-radius: 5px;
|
|
332
|
+
color: var(--muted);
|
|
333
|
+
transition: color 0.15s, background-color 0.15s;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
a.crumb:hover {
|
|
337
|
+
color: var(--text);
|
|
338
|
+
background: var(--hover);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.crumb-current {
|
|
342
|
+
flex: 0 1 auto;
|
|
343
|
+
min-width: 0;
|
|
344
|
+
overflow: hidden;
|
|
345
|
+
text-overflow: ellipsis;
|
|
346
|
+
color: var(--text);
|
|
347
|
+
font-weight: 500;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.crumb-sep {
|
|
351
|
+
display: grid;
|
|
352
|
+
place-items: center;
|
|
353
|
+
flex: none;
|
|
354
|
+
color: var(--subtle);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.topbar-actions {
|
|
358
|
+
display: flex;
|
|
359
|
+
align-items: center;
|
|
360
|
+
gap: 8px;
|
|
361
|
+
margin-left: auto;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.ghost-btn {
|
|
365
|
+
display: inline-flex;
|
|
366
|
+
align-items: center;
|
|
367
|
+
gap: 6px;
|
|
368
|
+
height: 32px;
|
|
369
|
+
padding: 0 10px;
|
|
370
|
+
border-radius: 8px;
|
|
371
|
+
color: var(--muted);
|
|
372
|
+
font-size: 13px;
|
|
373
|
+
font-weight: 500;
|
|
374
|
+
white-space: nowrap;
|
|
375
|
+
transition: color 0.15s, background-color 0.15s;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.ghost-btn:hover {
|
|
379
|
+
color: var(--text);
|
|
380
|
+
background: var(--hover);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.segmented {
|
|
384
|
+
position: relative;
|
|
385
|
+
display: flex;
|
|
386
|
+
padding: 3px;
|
|
387
|
+
border-radius: 9px;
|
|
388
|
+
background: var(--raised);
|
|
389
|
+
box-shadow: inset 0 0 0 1px var(--ring);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.segmented button, .segmented a {
|
|
393
|
+
position: relative;
|
|
394
|
+
z-index: 1;
|
|
395
|
+
display: grid;
|
|
396
|
+
place-items: center;
|
|
397
|
+
min-width: 0;
|
|
398
|
+
height: 26px;
|
|
399
|
+
padding: 0 10px;
|
|
400
|
+
border-radius: 6px;
|
|
401
|
+
color: var(--muted);
|
|
402
|
+
font-size: 12.5px;
|
|
403
|
+
font-weight: 500;
|
|
404
|
+
white-space: nowrap;
|
|
405
|
+
transition: color 0.15s, background-color 0.15s;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.segmented button:hover, .segmented a:hover {
|
|
409
|
+
color: var(--text);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.segmented [aria-checked="true"], .segmented [aria-current="true"] {
|
|
413
|
+
color: var(--text);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.segmented a[aria-current="true"] {
|
|
417
|
+
background: var(--surface);
|
|
418
|
+
box-shadow: 0 0 0 1px var(--ring), 0 1px 2px rgba(0, 0, 0, 0.12);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.seg-thumb {
|
|
422
|
+
position: absolute;
|
|
423
|
+
top: 3px;
|
|
424
|
+
bottom: 3px;
|
|
425
|
+
left: 0;
|
|
426
|
+
width: 0;
|
|
427
|
+
border-radius: 6px;
|
|
428
|
+
background: var(--surface);
|
|
429
|
+
box-shadow: 0 0 0 1px var(--ring), 0 1px 2px rgba(0, 0, 0, 0.12);
|
|
430
|
+
transition: transform 0.25s var(--ease), width 0.25s var(--ease);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.theme-toggle button {
|
|
434
|
+
width: 29px;
|
|
435
|
+
padding: 0;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* pages */
|
|
439
|
+
|
|
440
|
+
.page {
|
|
441
|
+
max-width: 920px;
|
|
442
|
+
margin: 0 auto;
|
|
443
|
+
padding: 32px 20px 64px;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.page-head {
|
|
447
|
+
display: flex;
|
|
448
|
+
align-items: flex-end;
|
|
449
|
+
justify-content: space-between;
|
|
450
|
+
gap: 16px;
|
|
451
|
+
margin-bottom: 20px;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.page-head h1 {
|
|
455
|
+
margin: 0;
|
|
456
|
+
font-size: 22px;
|
|
457
|
+
font-weight: 600;
|
|
458
|
+
letter-spacing: -0.015em;
|
|
459
|
+
overflow-wrap: anywhere;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.page-meta {
|
|
463
|
+
display: flex;
|
|
464
|
+
flex-wrap: wrap;
|
|
465
|
+
gap: 4px 14px;
|
|
466
|
+
margin: 6px 0 0;
|
|
467
|
+
color: var(--subtle);
|
|
468
|
+
font-size: 13px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.page-meta b {
|
|
472
|
+
color: var(--muted);
|
|
473
|
+
font-weight: 500;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.card {
|
|
477
|
+
background: var(--surface);
|
|
478
|
+
border-radius: 12px;
|
|
479
|
+
box-shadow: var(--shadow-card);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/* directory */
|
|
483
|
+
|
|
484
|
+
.list {
|
|
485
|
+
overflow: hidden;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.row {
|
|
489
|
+
display: flex;
|
|
490
|
+
align-items: center;
|
|
491
|
+
gap: 12px;
|
|
492
|
+
padding: 10px 16px;
|
|
493
|
+
transition: background-color 0.12s;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.row + .row {
|
|
497
|
+
box-shadow: inset 0 1px 0 var(--ring);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.row:hover {
|
|
501
|
+
background: var(--hover);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.row-icon {
|
|
505
|
+
display: grid;
|
|
506
|
+
place-items: center;
|
|
507
|
+
width: 28px;
|
|
508
|
+
height: 28px;
|
|
509
|
+
border-radius: 7px;
|
|
510
|
+
background: var(--raised);
|
|
511
|
+
color: var(--muted);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.row-icon.dir {
|
|
515
|
+
background: var(--accent-soft);
|
|
516
|
+
color: var(--accent);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.row-name {
|
|
520
|
+
flex: 1;
|
|
521
|
+
min-width: 0;
|
|
522
|
+
overflow: hidden;
|
|
523
|
+
text-overflow: ellipsis;
|
|
524
|
+
white-space: nowrap;
|
|
525
|
+
font-weight: 500;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.row-size, .row-time {
|
|
529
|
+
flex: none;
|
|
530
|
+
color: var(--subtle);
|
|
531
|
+
font-family: var(--mono);
|
|
532
|
+
font-size: 12px;
|
|
533
|
+
font-variant-numeric: tabular-nums;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.row-size {
|
|
537
|
+
width: 72px;
|
|
538
|
+
text-align: right;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.row-time {
|
|
542
|
+
width: 150px;
|
|
543
|
+
text-align: right;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.empty {
|
|
547
|
+
padding: 48px 20px;
|
|
548
|
+
color: var(--muted);
|
|
549
|
+
text-align: center;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.empty code {
|
|
553
|
+
padding: 1px 5px;
|
|
554
|
+
border-radius: 5px;
|
|
555
|
+
background: var(--raised);
|
|
556
|
+
font-family: var(--mono);
|
|
557
|
+
font-size: 12.5px;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/* trace */
|
|
561
|
+
|
|
562
|
+
.rail {
|
|
563
|
+
position: fixed;
|
|
564
|
+
top: 76px;
|
|
565
|
+
right: 20px;
|
|
566
|
+
z-index: 20;
|
|
567
|
+
display: flex;
|
|
568
|
+
flex-direction: column;
|
|
569
|
+
align-items: flex-end;
|
|
570
|
+
gap: 8px;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.rail-toggle {
|
|
574
|
+
display: none;
|
|
575
|
+
align-items: center;
|
|
576
|
+
gap: 8px;
|
|
577
|
+
height: 32px;
|
|
578
|
+
padding: 0 10px 0 12px;
|
|
579
|
+
border-radius: 999px;
|
|
580
|
+
background: var(--surface);
|
|
581
|
+
box-shadow: var(--shadow-menu);
|
|
582
|
+
color: var(--muted);
|
|
583
|
+
font-size: 12.5px;
|
|
584
|
+
font-weight: 500;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.rail-toggle .mono {
|
|
588
|
+
color: var(--text);
|
|
589
|
+
font-size: 12px;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.rail-panel {
|
|
593
|
+
width: 200px;
|
|
594
|
+
max-height: calc(100vh - 120px);
|
|
595
|
+
overflow-y: auto;
|
|
596
|
+
padding: 12px 12px 10px;
|
|
597
|
+
border-radius: 12px;
|
|
598
|
+
background: var(--surface);
|
|
599
|
+
box-shadow: var(--shadow-menu);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
@keyframes panel-in {
|
|
603
|
+
from {
|
|
604
|
+
opacity: 0;
|
|
605
|
+
transform: translateY(-4px) scale(0.98);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.rail-title {
|
|
610
|
+
margin-bottom: 8px;
|
|
611
|
+
color: var(--subtle);
|
|
612
|
+
font-size: 12px;
|
|
613
|
+
font-weight: 500;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.rail-list {
|
|
617
|
+
margin: 0;
|
|
618
|
+
padding: 0;
|
|
619
|
+
list-style: none;
|
|
620
|
+
box-shadow: inset 1px 0 0 var(--ring);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.rail-list a {
|
|
624
|
+
display: flex;
|
|
625
|
+
align-items: baseline;
|
|
626
|
+
justify-content: space-between;
|
|
627
|
+
gap: 8px;
|
|
628
|
+
padding: 4px 0 4px 12px;
|
|
629
|
+
border-left: 1.5px solid transparent;
|
|
630
|
+
color: var(--muted);
|
|
631
|
+
font-size: 13px;
|
|
632
|
+
transition: color 0.15s, border-color 0.15s;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.rail-list a:hover {
|
|
636
|
+
color: var(--text);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.rail-list a.active {
|
|
640
|
+
border-left-color: var(--accent);
|
|
641
|
+
color: var(--text);
|
|
642
|
+
font-weight: 500;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.rail-list a span {
|
|
646
|
+
color: var(--subtle);
|
|
647
|
+
font-family: var(--mono);
|
|
648
|
+
font-size: 11px;
|
|
649
|
+
font-weight: 400;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
@media (max-width: 1400px) {
|
|
653
|
+
.rail-toggle {
|
|
654
|
+
display: inline-flex;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.rail-panel {
|
|
658
|
+
display: none;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.rail.open .rail-panel {
|
|
662
|
+
display: block;
|
|
663
|
+
transform-origin: top right;
|
|
664
|
+
animation: panel-in 0.18s var(--ease);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.rail.open .rail-toggle .chevron {
|
|
668
|
+
transform: rotate(180deg);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.fold summary, .msg-card summary {
|
|
673
|
+
list-style: none;
|
|
674
|
+
cursor: pointer;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.fold summary::-webkit-details-marker, .msg-card summary::-webkit-details-marker {
|
|
678
|
+
display: none;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.chevron {
|
|
682
|
+
color: var(--subtle);
|
|
683
|
+
transition: transform 0.25s var(--ease);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
details[open] > summary .chevron {
|
|
687
|
+
transform: rotate(180deg);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.config {
|
|
691
|
+
margin-bottom: 20px;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.card-head {
|
|
695
|
+
padding: 12px 16px;
|
|
696
|
+
box-shadow: inset 0 -1px 0 var(--ring);
|
|
697
|
+
color: var(--muted);
|
|
698
|
+
font-size: 12.5px;
|
|
699
|
+
font-weight: 500;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.kv {
|
|
703
|
+
margin: 0;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.kv-row {
|
|
707
|
+
display: grid;
|
|
708
|
+
grid-template-columns: 160px 1fr;
|
|
709
|
+
gap: 16px;
|
|
710
|
+
padding: 9px 16px;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.kv-row + .kv-row {
|
|
714
|
+
box-shadow: inset 0 1px 0 var(--ring);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.kv-row dt {
|
|
718
|
+
color: var(--muted);
|
|
719
|
+
font-family: var(--mono);
|
|
720
|
+
font-size: 12.5px;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.kv-row dd {
|
|
724
|
+
margin: 0;
|
|
725
|
+
min-width: 0;
|
|
726
|
+
font-family: var(--mono);
|
|
727
|
+
font-size: 12.5px;
|
|
728
|
+
overflow-wrap: anywhere;
|
|
729
|
+
white-space: pre-wrap;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.fold summary {
|
|
733
|
+
display: inline-flex;
|
|
734
|
+
align-items: center;
|
|
735
|
+
gap: 4px;
|
|
736
|
+
color: var(--accent);
|
|
737
|
+
font-family: var(--font);
|
|
738
|
+
font-size: 12.5px;
|
|
739
|
+
font-weight: 500;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.fold pre {
|
|
743
|
+
margin: 8px 0 0;
|
|
744
|
+
padding: 10px 12px;
|
|
745
|
+
max-height: 360px;
|
|
746
|
+
overflow: auto;
|
|
747
|
+
border-radius: 8px;
|
|
748
|
+
background: var(--raised);
|
|
749
|
+
font-family: var(--mono);
|
|
750
|
+
font-size: 12px;
|
|
751
|
+
line-height: 1.6;
|
|
752
|
+
white-space: pre-wrap;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.msg-card {
|
|
756
|
+
margin-bottom: 14px;
|
|
757
|
+
overflow: hidden;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.msg-summary {
|
|
761
|
+
display: flex;
|
|
762
|
+
flex-wrap: wrap;
|
|
763
|
+
align-items: center;
|
|
764
|
+
gap: 6px 14px;
|
|
765
|
+
padding: 11px 16px;
|
|
766
|
+
color: var(--subtle);
|
|
767
|
+
font-size: 12.5px;
|
|
768
|
+
transition: background-color 0.12s;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.msg-summary:hover {
|
|
772
|
+
background: var(--hover);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
details[open] > .msg-summary {
|
|
776
|
+
box-shadow: inset 0 -1px 0 var(--ring);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.role {
|
|
780
|
+
font-size: 12px;
|
|
781
|
+
font-weight: 600;
|
|
782
|
+
letter-spacing: 0.06em;
|
|
783
|
+
text-transform: uppercase;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.role-user { color: var(--accent); }
|
|
787
|
+
.role-assistant { color: var(--green); }
|
|
788
|
+
|
|
789
|
+
.msg-summary .spacer {
|
|
790
|
+
flex: 1;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.msg-summary .num {
|
|
794
|
+
font-family: var(--mono);
|
|
795
|
+
font-size: 11.5px;
|
|
796
|
+
font-variant-numeric: tabular-nums;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.msg-body {
|
|
800
|
+
padding: 4px 16px 14px;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/* a list, carried by alignment: the type in its kind's color on the left, the content beside it */
|
|
804
|
+
.item {
|
|
805
|
+
--k: var(--muted);
|
|
806
|
+
display: grid;
|
|
807
|
+
grid-template-columns: 136px minmax(0, 1fr);
|
|
808
|
+
column-gap: 16px;
|
|
809
|
+
padding: 12px 0;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.item + .item {
|
|
813
|
+
box-shadow: inset 0 1px 0 var(--ring);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.item > :not(.item-head) {
|
|
817
|
+
grid-column: 2;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
.kind-text { --k: var(--muted); }
|
|
821
|
+
.kind-thinking, .kind-inline_thinking { --k: var(--accent); }
|
|
822
|
+
.kind-tool_call { --k: var(--amber); }
|
|
823
|
+
.kind-tool_result { --k: var(--green); }
|
|
824
|
+
.kind-image_url, .kind-inline_data { --k: var(--violet); }
|
|
825
|
+
.kind-embedding { --k: var(--indigo); }
|
|
826
|
+
|
|
827
|
+
.item-head {
|
|
828
|
+
grid-column: 1;
|
|
829
|
+
grid-row: 1 / span 4;
|
|
830
|
+
display: flex;
|
|
831
|
+
flex-direction: column;
|
|
832
|
+
gap: 2px;
|
|
833
|
+
padding-top: 2px;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.item-type {
|
|
837
|
+
color: var(--k);
|
|
838
|
+
font-family: var(--mono);
|
|
839
|
+
font-size: 11.5px;
|
|
840
|
+
font-weight: 500;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.item-id {
|
|
844
|
+
color: var(--subtle);
|
|
845
|
+
font-family: var(--mono);
|
|
846
|
+
font-size: 11px;
|
|
847
|
+
overflow-wrap: anywhere;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.item-text {
|
|
851
|
+
white-space: pre-wrap;
|
|
852
|
+
overflow-wrap: anywhere;
|
|
853
|
+
line-height: 1.65;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.item-thinking {
|
|
857
|
+
color: var(--muted);
|
|
858
|
+
font-size: 13px;
|
|
859
|
+
line-height: 1.6;
|
|
860
|
+
white-space: pre-wrap;
|
|
861
|
+
overflow-wrap: anywhere;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.item-note {
|
|
865
|
+
color: var(--subtle);
|
|
866
|
+
font-family: var(--mono);
|
|
867
|
+
font-size: 12px;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.item-note + * {
|
|
871
|
+
margin-top: 8px;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
.item-img {
|
|
875
|
+
display: block;
|
|
876
|
+
max-width: min(100%, 360px);
|
|
877
|
+
max-height: 280px;
|
|
878
|
+
border-radius: 8px;
|
|
879
|
+
box-shadow: 0 0 0 1px var(--ring);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
.item-images {
|
|
883
|
+
display: flex;
|
|
884
|
+
flex-wrap: wrap;
|
|
885
|
+
gap: 8px;
|
|
886
|
+
margin-top: 10px;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.item-audio {
|
|
890
|
+
display: block;
|
|
891
|
+
width: min(100%, 360px);
|
|
892
|
+
height: 36px;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.tool-sig, .result-text, .embedding {
|
|
896
|
+
font-family: var(--mono);
|
|
897
|
+
font-size: 12.5px;
|
|
898
|
+
line-height: 1.6;
|
|
899
|
+
white-space: pre-wrap;
|
|
900
|
+
overflow-wrap: anywhere;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.msg-foot {
|
|
904
|
+
display: flex;
|
|
905
|
+
flex-wrap: wrap;
|
|
906
|
+
align-items: center;
|
|
907
|
+
gap: 6px 14px;
|
|
908
|
+
margin-top: 16px;
|
|
909
|
+
padding-top: 12px;
|
|
910
|
+
box-shadow: inset 0 1px 0 var(--ring);
|
|
911
|
+
color: var(--subtle);
|
|
912
|
+
font-size: 12px;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
.reason {
|
|
916
|
+
display: inline-flex;
|
|
917
|
+
align-items: center;
|
|
918
|
+
gap: 6px;
|
|
919
|
+
height: 22px;
|
|
920
|
+
padding: 0 8px;
|
|
921
|
+
border-radius: 999px;
|
|
922
|
+
background: var(--raised);
|
|
923
|
+
color: var(--muted);
|
|
924
|
+
font-family: var(--mono);
|
|
925
|
+
font-size: 11.5px;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
.reason::before {
|
|
929
|
+
content: "";
|
|
930
|
+
width: 6px;
|
|
931
|
+
height: 6px;
|
|
932
|
+
border-radius: 50%;
|
|
933
|
+
background: currentColor;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
.reason-stop { background: var(--green-soft); color: var(--green); }
|
|
937
|
+
.reason-tool_call { background: var(--amber-soft); color: var(--amber); }
|
|
938
|
+
.reason-length, .reason-unknown { background: var(--red-soft); color: var(--red); }
|
|
939
|
+
|
|
940
|
+
.usage {
|
|
941
|
+
display: inline-flex;
|
|
942
|
+
flex-wrap: wrap;
|
|
943
|
+
gap: 4px 12px;
|
|
944
|
+
font-family: var(--mono);
|
|
945
|
+
font-size: 11.5px;
|
|
946
|
+
font-variant-numeric: tabular-nums;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
.usage b {
|
|
950
|
+
color: var(--muted);
|
|
951
|
+
font-weight: 500;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
.text-file {
|
|
955
|
+
margin: 0;
|
|
956
|
+
padding: 16px 18px;
|
|
957
|
+
overflow-x: auto;
|
|
958
|
+
font-family: var(--mono);
|
|
959
|
+
font-size: 12.5px;
|
|
960
|
+
line-height: 1.65;
|
|
961
|
+
white-space: pre-wrap;
|
|
962
|
+
overflow-wrap: anywhere;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
@media (max-width: 640px) {
|
|
966
|
+
.topbar {
|
|
967
|
+
gap: 10px;
|
|
968
|
+
padding: 0 12px;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
.label-wide {
|
|
972
|
+
display: none;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
.page {
|
|
976
|
+
padding: 24px 16px 48px;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.row-time {
|
|
980
|
+
display: none;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.kv-row {
|
|
984
|
+
grid-template-columns: 1fr;
|
|
985
|
+
gap: 2px;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.item {
|
|
989
|
+
grid-template-columns: minmax(0, 1fr);
|
|
990
|
+
row-gap: 4px;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
.item > :not(.item-head), .item-head {
|
|
994
|
+
grid-column: 1;
|
|
995
|
+
grid-row: auto;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.item-head {
|
|
999
|
+
flex-direction: row;
|
|
1000
|
+
gap: 8px;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
.page-head {
|
|
1004
|
+
flex-direction: column;
|
|
1005
|
+
align-items: flex-start;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.nav-btn.forward {
|
|
1009
|
+
display: none;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.rail {
|
|
1013
|
+
top: auto;
|
|
1014
|
+
bottom: 16px;
|
|
1015
|
+
right: 16px;
|
|
1016
|
+
flex-direction: column-reverse;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1021
|
+
*, *::before, *::after {
|
|
1022
|
+
animation-duration: 0.01ms !important;
|
|
1023
|
+
transition-duration: 0.01ms !important;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
</style>
|
|
1027
|
+
`;
|
|
1028
|
+
const TRACER_SCRIPT = `
|
|
1029
|
+
<script>
|
|
1030
|
+
function updateSegmentThumb(root) {
|
|
1031
|
+
const thumb = root && root.querySelector('.seg-thumb');
|
|
1032
|
+
const checked = root && root.querySelector('[aria-checked="true"]');
|
|
1033
|
+
if (!thumb || !checked || !checked.offsetWidth) {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
thumb.style.width = checked.offsetWidth + 'px';
|
|
1037
|
+
thumb.style.transform = 'translateX(' + checked.offsetLeft + 'px)';
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function updateThemeToggle() {
|
|
1041
|
+
const stored = document.documentElement.dataset.theme;
|
|
1042
|
+
const theme = stored || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
1043
|
+
document.querySelectorAll('#themeToggle [data-theme-choice]').forEach(function (button) {
|
|
1044
|
+
button.setAttribute('aria-checked', button.dataset.themeChoice === theme ? 'true' : 'false');
|
|
1045
|
+
});
|
|
1046
|
+
updateSegmentThumb(document.getElementById('themeToggle'));
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function setTheme(theme) {
|
|
1050
|
+
document.documentElement.dataset.theme = theme;
|
|
1051
|
+
try {
|
|
1052
|
+
localStorage.setItem('mmsp.playground.theme', theme);
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
// a browser that refuses storage keeps the choice for this page only
|
|
1055
|
+
}
|
|
1056
|
+
updateThemeToggle();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// back and forward grey out, as in a file explorer, when the history has nowhere to go
|
|
1060
|
+
(function () {
|
|
1061
|
+
const back = document.getElementById('navBack');
|
|
1062
|
+
const forward = document.getElementById('navForward');
|
|
1063
|
+
const navigation = window.navigation;
|
|
1064
|
+
if (back) {
|
|
1065
|
+
back.disabled = navigation ? !navigation.canGoBack : history.length <= 1;
|
|
1066
|
+
}
|
|
1067
|
+
if (forward && navigation) {
|
|
1068
|
+
forward.disabled = !navigation.canGoForward;
|
|
1069
|
+
}
|
|
1070
|
+
})();
|
|
1071
|
+
|
|
1072
|
+
function toggleRail(open) {
|
|
1073
|
+
const rail = document.getElementById('rail');
|
|
1074
|
+
const isOpen = typeof open === 'boolean' ? open : !rail.classList.contains('open');
|
|
1075
|
+
rail.classList.toggle('open', isOpen);
|
|
1076
|
+
rail.querySelector('.rail-toggle').setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
document.addEventListener('click', function (event) {
|
|
1080
|
+
const rail = document.getElementById('rail');
|
|
1081
|
+
if (rail && rail.classList.contains('open') && !rail.contains(event.target)) {
|
|
1082
|
+
toggleRail(false);
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
updateThemeToggle();
|
|
1087
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateThemeToggle);
|
|
1088
|
+
document.fonts.ready.then(updateThemeToggle);
|
|
1089
|
+
|
|
1090
|
+
// the rail marks the round whose first message is nearest the top of the view
|
|
1091
|
+
const railLinks = Array.from(document.querySelectorAll('.rail-list a'));
|
|
1092
|
+
if (railLinks.length) {
|
|
1093
|
+
const targets = railLinks.map(function (link) {
|
|
1094
|
+
return document.querySelector(link.getAttribute('href'));
|
|
1095
|
+
});
|
|
1096
|
+
const markActive = function () {
|
|
1097
|
+
let active = 0;
|
|
1098
|
+
targets.forEach(function (target, index) {
|
|
1099
|
+
if (target && target.getBoundingClientRect().top < 140) {
|
|
1100
|
+
active = index;
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
// at the bottom of the page the last round is the one being read, however high it sits
|
|
1104
|
+
if (window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 4) {
|
|
1105
|
+
active = targets.length - 1;
|
|
1106
|
+
}
|
|
1107
|
+
railLinks.forEach(function (link, index) {
|
|
1108
|
+
link.classList.toggle('active', index === active);
|
|
1109
|
+
});
|
|
1110
|
+
const current = document.getElementById('railCurrent');
|
|
1111
|
+
if (current) {
|
|
1112
|
+
current.textContent = (active + 1) + ' / ' + railLinks.length;
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
railLinks.forEach(function (link) {
|
|
1116
|
+
link.addEventListener('click', function () {
|
|
1117
|
+
toggleRail(false);
|
|
1118
|
+
});
|
|
1119
|
+
});
|
|
1120
|
+
window.addEventListener('scroll', markActive, { passive: true });
|
|
1121
|
+
markActive();
|
|
1122
|
+
}
|
|
1123
|
+
</script>
|
|
1124
|
+
`;
|
|
1125
|
+
const ICONS = {
|
|
1126
|
+
mark: '<svg class="brand-mark" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="7" class="mark-bg"></rect><path d="M0 16V7a7 7 0 0 1 7-7h9v16Z" fill="#7c4dff"></path><path d="M16 16h16v9a7 7 0 0 1-7 7h-9Z" fill="#7c4dff"></path><g fill="#fff" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="10.5" font-weight="700" text-anchor="middle" dominant-baseline="central"><text x="8.5" y="8.5">M</text><text x="23.5" y="8.5">M</text><text x="8.5" y="23.5">S</text><text x="23.5" y="23.5">P</text></g></svg>',
|
|
1127
|
+
github: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48v-1.7c-2.78.6-3.37-1.34-3.37-1.34-.45-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.61.07-.61 1 .07 1.53 1.03 1.53 1.03.9 1.52 2.34 1.08 2.91.83.09-.65.35-1.08.63-1.33-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.64 0 0 .84-.27 2.75 1.02a9.6 9.6 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.37.2 2.39.1 2.64.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85v2.74c0 .27.18.58.69.48A10 10 0 0 0 12 2Z"></path></svg>',
|
|
1128
|
+
sun: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"></path></svg>',
|
|
1129
|
+
moon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"></path></svg>',
|
|
1130
|
+
folder: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"></path></svg>',
|
|
1131
|
+
file: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8Z"></path><path d="M14 3v5h5M9 13h6M9 17h4"></path></svg>',
|
|
1132
|
+
chevron: '<svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg>',
|
|
1133
|
+
arrow_left: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 12H5M12 19l-7-7 7-7"></path></svg>',
|
|
1134
|
+
arrow_right: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>',
|
|
1135
|
+
arrow_up: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"></path></svg>',
|
|
1136
|
+
crumb: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"></path></svg>',
|
|
1137
|
+
};
|
|
1138
|
+
/**
|
|
1139
|
+
* Tracer for saving conversation history to local files.
|
|
1140
|
+
*
|
|
1141
|
+
* This class handles saving conversation history to files in a cache directory.
|
|
1142
|
+
*/
|
|
1143
|
+
class Tracer {
|
|
1144
|
+
/**
|
|
1145
|
+
* Initialize the tracer.
|
|
1146
|
+
*
|
|
1147
|
+
* @param cacheDir - Directory to store conversation history files
|
|
1148
|
+
*/
|
|
1149
|
+
constructor(cacheDir) {
|
|
1150
|
+
this.cacheDir = path.resolve(cacheDir || process.env.MMSP_CACHE_DIR || "cache");
|
|
1151
|
+
this._ensureDirectoryExists(this.cacheDir);
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Ensure directory exists, create if it doesn't.
|
|
1155
|
+
*
|
|
1156
|
+
* @param dirPath - Directory path to create
|
|
1157
|
+
*/
|
|
1158
|
+
_ensureDirectoryExists(dirPath) {
|
|
1159
|
+
if (!fs.existsSync(dirPath)) {
|
|
1160
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Recursively serialize objects for JSON, converting Buffer to base64.
|
|
1165
|
+
*
|
|
1166
|
+
* @param obj - Object to serialize
|
|
1167
|
+
* @returns JSON-serializable object
|
|
1168
|
+
*/
|
|
1169
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1170
|
+
_serializeForJson(obj) {
|
|
1171
|
+
if (Buffer.isBuffer(obj)) {
|
|
1172
|
+
return obj.toString("base64");
|
|
1173
|
+
}
|
|
1174
|
+
else if (obj && typeof obj === "object") {
|
|
1175
|
+
if (Array.isArray(obj)) {
|
|
1176
|
+
return obj.map((item) => this._serializeForJson(item));
|
|
1177
|
+
}
|
|
1178
|
+
else {
|
|
1179
|
+
const result = {};
|
|
1180
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1181
|
+
result[key] = this._serializeForJson(value);
|
|
1182
|
+
}
|
|
1183
|
+
return result;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return obj;
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Return whether browsers can usually play this MIME type directly.
|
|
1190
|
+
*
|
|
1191
|
+
* @param mimeType - MIME type to inspect
|
|
1192
|
+
* @returns Whether the browser can typically play the audio type
|
|
1193
|
+
*/
|
|
1194
|
+
_isBrowserPlayableAudioMimeType(mimeType) {
|
|
1195
|
+
return new Set([
|
|
1196
|
+
"audio/wav",
|
|
1197
|
+
"audio/x-wav",
|
|
1198
|
+
"audio/mpeg",
|
|
1199
|
+
"audio/mp3",
|
|
1200
|
+
"audio/ogg",
|
|
1201
|
+
"audio/webm",
|
|
1202
|
+
"audio/flac",
|
|
1203
|
+
"audio/aac",
|
|
1204
|
+
"audio/mp4",
|
|
1205
|
+
]).has((mimeType || "").toLowerCase());
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Decode an inline_data payload to raw bytes.
|
|
1209
|
+
*
|
|
1210
|
+
* @param item - inline_data content item
|
|
1211
|
+
* @returns Raw payload bytes
|
|
1212
|
+
*/
|
|
1213
|
+
_decodeInlineData(item) {
|
|
1214
|
+
if (typeof item.data === "string") {
|
|
1215
|
+
return Buffer.from(item.data, "base64");
|
|
1216
|
+
}
|
|
1217
|
+
return item.data || Buffer.alloc(0);
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Wrap raw PCM bytes in a WAV header using Gemini TTS defaults.
|
|
1221
|
+
*
|
|
1222
|
+
* @param item - inline_data content item
|
|
1223
|
+
* @returns WAV bytes
|
|
1224
|
+
*/
|
|
1225
|
+
_buildWaveBytesFromPcm(item) {
|
|
1226
|
+
const pcmBytes = this._decodeInlineData(item);
|
|
1227
|
+
const channels = 1;
|
|
1228
|
+
const sampleRate = 24000;
|
|
1229
|
+
const bitsPerSample = 16;
|
|
1230
|
+
const byteRate = (sampleRate * channels * bitsPerSample) / 8;
|
|
1231
|
+
const blockAlign = (channels * bitsPerSample) / 8;
|
|
1232
|
+
const header = Buffer.alloc(44);
|
|
1233
|
+
header.write("RIFF", 0, "ascii");
|
|
1234
|
+
header.writeUInt32LE(36 + pcmBytes.length, 4);
|
|
1235
|
+
header.write("WAVE", 8, "ascii");
|
|
1236
|
+
header.write("fmt ", 12, "ascii");
|
|
1237
|
+
header.writeUInt32LE(16, 16);
|
|
1238
|
+
header.writeUInt16LE(1, 20);
|
|
1239
|
+
header.writeUInt16LE(channels, 22);
|
|
1240
|
+
header.writeUInt32LE(sampleRate, 24);
|
|
1241
|
+
header.writeUInt32LE(byteRate, 28);
|
|
1242
|
+
header.writeUInt16LE(blockAlign, 32);
|
|
1243
|
+
header.writeUInt16LE(bitsPerSample, 34);
|
|
1244
|
+
header.write("data", 36, "ascii");
|
|
1245
|
+
header.writeUInt32LE(pcmBytes.length, 40);
|
|
1246
|
+
return Buffer.concat([header, pcmBytes]);
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Format inline_data metadata without emitting raw payloads.
|
|
1250
|
+
*
|
|
1251
|
+
* @param item - inline_data content item
|
|
1252
|
+
* @returns Human-readable summary of the inline payload
|
|
1253
|
+
*/
|
|
1254
|
+
_formatInlineDataSummary(item, isThinking) {
|
|
1255
|
+
const mimeType = item.mime_type || "application/octet-stream";
|
|
1256
|
+
const data = item.data;
|
|
1257
|
+
let byteCount = 0;
|
|
1258
|
+
if (typeof data === "string") {
|
|
1259
|
+
byteCount = Buffer.from(data, "base64").length;
|
|
1260
|
+
}
|
|
1261
|
+
else if (Buffer.isBuffer(data)) {
|
|
1262
|
+
byteCount = data.length;
|
|
1263
|
+
}
|
|
1264
|
+
const kbCount = byteCount / 1024;
|
|
1265
|
+
const mbCount = byteCount / (1024 * 1024);
|
|
1266
|
+
let label = isThinking ? "Thinking " : "";
|
|
1267
|
+
if (mimeType.startsWith("image/")) {
|
|
1268
|
+
label += "Inline Image";
|
|
1269
|
+
}
|
|
1270
|
+
else if (mimeType.startsWith("audio/")) {
|
|
1271
|
+
label += "Inline Audio";
|
|
1272
|
+
}
|
|
1273
|
+
else {
|
|
1274
|
+
label += "Inline Data";
|
|
1275
|
+
}
|
|
1276
|
+
if (kbCount < 1000) {
|
|
1277
|
+
return `${label}: ${mimeType} (${kbCount.toFixed(2)} KB)`;
|
|
1278
|
+
}
|
|
1279
|
+
return `${label}: ${mimeType} (${mbCount.toFixed(2)} MB)`;
|
|
1280
|
+
}
|
|
1281
|
+
_formatEmbeddingPreview(item) {
|
|
1282
|
+
const preview = (item.embedding || [])
|
|
1283
|
+
.slice(0, 5)
|
|
1284
|
+
.map((value) => String(value))
|
|
1285
|
+
.join(", ");
|
|
1286
|
+
return `Embedding: [${preview}]`;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Build a data URL for inline_data content.
|
|
1290
|
+
*
|
|
1291
|
+
* @param item - inline_data content item
|
|
1292
|
+
* @returns Data URL string
|
|
1293
|
+
*/
|
|
1294
|
+
_inlineDataUrl(item) {
|
|
1295
|
+
const mimeType = item.mime_type || "application/octet-stream";
|
|
1296
|
+
let rawBytes = this._decodeInlineData(item);
|
|
1297
|
+
let normalizedMimeType = mimeType;
|
|
1298
|
+
if (mimeType.startsWith("audio/")) {
|
|
1299
|
+
if (!this._isBrowserPlayableAudioMimeType(mimeType)) {
|
|
1300
|
+
normalizedMimeType = "audio/wav";
|
|
1301
|
+
rawBytes = this._buildWaveBytesFromPcm(item);
|
|
1302
|
+
}
|
|
1303
|
+
return `data:${normalizedMimeType};base64,${rawBytes.toString("base64")}`;
|
|
1304
|
+
}
|
|
1305
|
+
return `data:${normalizedMimeType};base64,${rawBytes.toString("base64")}`;
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Return whether inline_data should be rendered as audio.
|
|
1309
|
+
*
|
|
1310
|
+
* @param item - inline_data content item
|
|
1311
|
+
* @returns Whether the payload is audio
|
|
1312
|
+
*/
|
|
1313
|
+
_inlineDataIsAudio(item) {
|
|
1314
|
+
return (item.mime_type || "").toLowerCase().startsWith("audio/");
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Return the browser-facing audio MIME type after any WAV fallback.
|
|
1318
|
+
*
|
|
1319
|
+
* @param item - inline_data content item
|
|
1320
|
+
* @returns Playable audio MIME type
|
|
1321
|
+
*/
|
|
1322
|
+
_inlineDataAudioType(item) {
|
|
1323
|
+
const mimeType = item.mime_type || "application/octet-stream";
|
|
1324
|
+
if (this._isBrowserPlayableAudioMimeType(mimeType)) {
|
|
1325
|
+
return mimeType;
|
|
1326
|
+
}
|
|
1327
|
+
return "audio/wav";
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Save conversation history to files.
|
|
1331
|
+
*
|
|
1332
|
+
* @param history - List of UniMessage objects representing the conversation
|
|
1333
|
+
* @param fileId - File identifier without extension (e.g., "agent1/00001")
|
|
1334
|
+
* @param config - The UniConfig used for this conversation
|
|
1335
|
+
*/
|
|
1336
|
+
saveHistory(model, history, fileId, config) {
|
|
1337
|
+
const filePathBase = path.join(this.cacheDir, fileId);
|
|
1338
|
+
const dirPath = path.dirname(filePathBase);
|
|
1339
|
+
this._ensureDirectoryExists(dirPath);
|
|
1340
|
+
const configWithModel = {
|
|
1341
|
+
...config,
|
|
1342
|
+
model,
|
|
1343
|
+
};
|
|
1344
|
+
const jsonPath = filePathBase + ".json";
|
|
1345
|
+
const jsonData = {
|
|
1346
|
+
history: this._serializeForJson(history),
|
|
1347
|
+
config: this._serializeForJson(configWithModel),
|
|
1348
|
+
timestamp: new Date().toISOString(),
|
|
1349
|
+
};
|
|
1350
|
+
fs.writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2), "utf-8");
|
|
1351
|
+
const txtPath = filePathBase + ".txt";
|
|
1352
|
+
const formattedContent = this._formatHistory(history, configWithModel);
|
|
1353
|
+
fs.writeFileSync(txtPath, formattedContent, "utf-8");
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Format conversation history in a readable text format.
|
|
1357
|
+
*
|
|
1358
|
+
* @param history - List of UniMessage objects
|
|
1359
|
+
* @param config - The UniConfig used for this conversation
|
|
1360
|
+
* @returns Formatted string representation of the conversation
|
|
1361
|
+
*/
|
|
1362
|
+
_formatHistory(history, config) {
|
|
1363
|
+
const lines = [];
|
|
1364
|
+
lines.push("=".repeat(80));
|
|
1365
|
+
lines.push(`Conversation History - ${new Date().toLocaleString()}`);
|
|
1366
|
+
lines.push("=".repeat(80));
|
|
1367
|
+
lines.push("");
|
|
1368
|
+
lines.push("Configuration:");
|
|
1369
|
+
for (const [key, value] of Object.entries(config)) {
|
|
1370
|
+
if (key !== "trace_id") {
|
|
1371
|
+
if (key === "tools" && Array.isArray(value)) {
|
|
1372
|
+
lines.push(` ${key}:`);
|
|
1373
|
+
lines.push(` ${JSON.stringify(value, null, 2)}`);
|
|
1374
|
+
}
|
|
1375
|
+
else {
|
|
1376
|
+
lines.push(` ${key}: ${value}`);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
lines.push("");
|
|
1381
|
+
for (let i = 0; i < history.length; i++) {
|
|
1382
|
+
const message = history[i];
|
|
1383
|
+
const role = message.role.toUpperCase();
|
|
1384
|
+
lines.push(`[${i + 1}] ${role}:`);
|
|
1385
|
+
lines.push("-".repeat(80));
|
|
1386
|
+
for (const item of message.content_items) {
|
|
1387
|
+
if (item.type === "text.done") {
|
|
1388
|
+
lines.push(`Text: ${item.text}`);
|
|
1389
|
+
}
|
|
1390
|
+
else if (item.type === "thinking.done") {
|
|
1391
|
+
lines.push(`Thinking: ${item.thinking}`);
|
|
1392
|
+
}
|
|
1393
|
+
else if (item.type === "inline_thinking.done") {
|
|
1394
|
+
lines.push(this._formatInlineDataSummary(item, true));
|
|
1395
|
+
}
|
|
1396
|
+
else if (item.type === "image_url.done") {
|
|
1397
|
+
lines.push(`Image URL: ${item.image_url}`);
|
|
1398
|
+
}
|
|
1399
|
+
else if (item.type === "inline_data.done") {
|
|
1400
|
+
lines.push(this._formatInlineDataSummary(item));
|
|
1401
|
+
}
|
|
1402
|
+
else if (item.type === "embedding.done") {
|
|
1403
|
+
lines.push(this._formatEmbeddingPreview(item));
|
|
1404
|
+
}
|
|
1405
|
+
else if (item.type === "tool_call.done") {
|
|
1406
|
+
lines.push(`Tool Call: ${item.name}`);
|
|
1407
|
+
lines.push(` Arguments: ${JSON.stringify(item.arguments, null, 2)}`);
|
|
1408
|
+
lines.push(` Tool Call ID: ${item.tool_call_id}`);
|
|
1409
|
+
}
|
|
1410
|
+
else if (item.type === "tool_result.done") {
|
|
1411
|
+
lines.push(`Tool Result (ID: ${item.tool_call_id}): ${item.text}`);
|
|
1412
|
+
if (item.images && item.images.length > 0) {
|
|
1413
|
+
item.images.forEach((imageUrl, i) => {
|
|
1414
|
+
lines.push(` Image ${i + 1}: ${imageUrl}`);
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (message.usage_metadata) {
|
|
1420
|
+
const metadata = message.usage_metadata;
|
|
1421
|
+
lines.push("\nUsage Metadata:");
|
|
1422
|
+
if (metadata.cached_tokens !== null) {
|
|
1423
|
+
lines.push(` Cached Tokens: ${metadata.cached_tokens}`);
|
|
1424
|
+
}
|
|
1425
|
+
if (metadata.prompt_tokens !== null) {
|
|
1426
|
+
lines.push(` Prompt Tokens: ${metadata.prompt_tokens}`);
|
|
1427
|
+
}
|
|
1428
|
+
if (metadata.thoughts_tokens !== null) {
|
|
1429
|
+
lines.push(` Thoughts Tokens: ${metadata.thoughts_tokens}`);
|
|
1430
|
+
}
|
|
1431
|
+
if (metadata.response_tokens !== null) {
|
|
1432
|
+
lines.push(` Response Tokens: ${metadata.response_tokens}`);
|
|
1433
|
+
}
|
|
1434
|
+
const inputTokens = (metadata.cached_tokens || 0) + (metadata.prompt_tokens || 0);
|
|
1435
|
+
const outputTokens = (metadata.thoughts_tokens || 0) + (metadata.response_tokens || 0);
|
|
1436
|
+
const totalTokens = inputTokens + outputTokens;
|
|
1437
|
+
lines.push(` Total Tokens: ${totalTokens}`);
|
|
1438
|
+
}
|
|
1439
|
+
if (message.finish_reason) {
|
|
1440
|
+
lines.push(`\nFinish Reason: ${message.finish_reason}`);
|
|
1441
|
+
}
|
|
1442
|
+
lines.push("");
|
|
1443
|
+
}
|
|
1444
|
+
return lines.join("\n");
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Create an Express web application for browsing conversation files.
|
|
1448
|
+
*
|
|
1449
|
+
* @returns Express application instance
|
|
1450
|
+
*/
|
|
1451
|
+
createWebApp(options = {}) {
|
|
1452
|
+
const app = (0, express_1.default)();
|
|
1453
|
+
const basePath = this._normalizeBasePath(options.basePath);
|
|
1454
|
+
const rootUrl = this._prefixUrl(basePath, "/");
|
|
1455
|
+
const esc = (value) => this._escapeHtml(String(value));
|
|
1456
|
+
const plural = (count, one, many) => `${count} ${count === 1 ? one : many}`;
|
|
1457
|
+
app.get("*", (req, res) => {
|
|
1458
|
+
const subpath = req.path.slice(1);
|
|
1459
|
+
const fullPath = path.resolve(path.join(this.cacheDir, subpath));
|
|
1460
|
+
if (!fullPath.startsWith(path.resolve(this.cacheDir))) {
|
|
1461
|
+
return res.status(403).send("Access denied");
|
|
1462
|
+
}
|
|
1463
|
+
if (!fs.existsSync(fullPath)) {
|
|
1464
|
+
return res.status(404).send("Path not found");
|
|
1465
|
+
}
|
|
1466
|
+
if (fs.statSync(fullPath).isFile()) {
|
|
1467
|
+
try {
|
|
1468
|
+
const parts = subpath ? subpath.split("/") : [];
|
|
1469
|
+
const breadcrumb = this._breadcrumb(basePath, parts);
|
|
1470
|
+
const backUrl = this._prefixUrl(basePath, parts.length > 1 ? "/" + parts.slice(0, -1).join("/") : "/");
|
|
1471
|
+
const filename = path.basename(fullPath);
|
|
1472
|
+
const pageHead = (meta) => `
|
|
1473
|
+
${this._nav(breadcrumb, backUrl, true)}
|
|
1474
|
+
<div class="page-head">
|
|
1475
|
+
<div>
|
|
1476
|
+
<h1>${esc(filename)}</h1>
|
|
1477
|
+
<p class="page-meta">${meta}</p>
|
|
1478
|
+
</div>
|
|
1479
|
+
</div>`;
|
|
1480
|
+
if (!fullPath.endsWith(".json")) {
|
|
1481
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
1482
|
+
const body = `
|
|
1483
|
+
<main class="page">${pageHead("<span>Plain-text transcript</span>")}
|
|
1484
|
+
<pre class="card text-file">${esc(content)}</pre>
|
|
1485
|
+
</main>`;
|
|
1486
|
+
return res.send(this._page(`${filename} - MMSP Tracer`, rootUrl, body));
|
|
1487
|
+
}
|
|
1488
|
+
const data = JSON.parse(fs.readFileSync(fullPath, "utf-8"));
|
|
1489
|
+
const config = data.config || {};
|
|
1490
|
+
// trace files written before 0.5.0 carry the legacy content item types
|
|
1491
|
+
const history = (0, legacy_1.normalizeLegacyMessages)(data.history || []);
|
|
1492
|
+
const totalRounds = Math.ceil(history.length / 2);
|
|
1493
|
+
// Python saves the time local and TypeScript in UTC; both show local
|
|
1494
|
+
const savedMs = Date.parse(data.timestamp || "");
|
|
1495
|
+
const savedAt = Number.isNaN(savedMs)
|
|
1496
|
+
? String(data.timestamp || "")
|
|
1497
|
+
: this._formatTimestamp(savedMs);
|
|
1498
|
+
const metaParts = [];
|
|
1499
|
+
if (config.model) {
|
|
1500
|
+
metaParts.push(`<span class="mono"><b>${esc(config.model)}</b></span>`);
|
|
1501
|
+
}
|
|
1502
|
+
metaParts.push(`<span>${plural(history.length, "message", "messages")}</span>`);
|
|
1503
|
+
metaParts.push(`<span>${plural(totalRounds, "round", "rounds")}</span>`);
|
|
1504
|
+
if (savedAt) {
|
|
1505
|
+
metaParts.push(`<span>Saved ${esc(savedAt)}</span>`);
|
|
1506
|
+
}
|
|
1507
|
+
const configRows = Object.entries(config)
|
|
1508
|
+
.filter(([key]) => key !== "trace_id")
|
|
1509
|
+
.map(([key, value]) => {
|
|
1510
|
+
let valueHtml;
|
|
1511
|
+
if (key === "system_prompt" && value != null) {
|
|
1512
|
+
valueHtml = `<details class="fold"><summary>Show ${ICONS.chevron}</summary><pre>${esc(value)}</pre></details>`;
|
|
1513
|
+
}
|
|
1514
|
+
else if (key === "tools" && Array.isArray(value)) {
|
|
1515
|
+
valueHtml = `<details class="fold"><summary>${plural(value.length, "tool", "tools")} ${ICONS.chevron}</summary><pre>${esc(JSON.stringify(value, null, 2))}</pre></details>`;
|
|
1516
|
+
}
|
|
1517
|
+
else if (typeof value === "string") {
|
|
1518
|
+
valueHtml = esc(value);
|
|
1519
|
+
}
|
|
1520
|
+
else {
|
|
1521
|
+
valueHtml = esc(JSON.stringify(value));
|
|
1522
|
+
}
|
|
1523
|
+
return `
|
|
1524
|
+
<div class="kv-row"><dt>${esc(key)}</dt><dd>${valueHtml}</dd></div>`;
|
|
1525
|
+
})
|
|
1526
|
+
.join("");
|
|
1527
|
+
const configHtml = configRows
|
|
1528
|
+
? `
|
|
1529
|
+
<section class="card config">
|
|
1530
|
+
<div class="card-head">Configuration</div>
|
|
1531
|
+
<dl class="kv">${configRows}
|
|
1532
|
+
</dl>
|
|
1533
|
+
</section>`
|
|
1534
|
+
: "";
|
|
1535
|
+
const messagesHtml = history
|
|
1536
|
+
.map((msg, idx) => {
|
|
1537
|
+
const itemsHtml = msg.content_items
|
|
1538
|
+
.map((item) => {
|
|
1539
|
+
let inner = "";
|
|
1540
|
+
if (item.type === "text.done") {
|
|
1541
|
+
inner = `<div class="item-text">${esc(item.text)}</div>`;
|
|
1542
|
+
}
|
|
1543
|
+
else if (item.type === "thinking.done") {
|
|
1544
|
+
inner = item.thinking.trim()
|
|
1545
|
+
? `<div class="item-thinking">${esc(item.thinking.trim())}</div>`
|
|
1546
|
+
: `<div class="item-note">No text${item.fidelity ? ", only fidelity" : ""}</div>`;
|
|
1547
|
+
}
|
|
1548
|
+
else if (item.type === "inline_thinking.done") {
|
|
1549
|
+
inner = `<div class="item-note">${esc(this._formatInlineDataSummary(item, true))}</div>`;
|
|
1550
|
+
if (item.mime_type?.startsWith("image/")) {
|
|
1551
|
+
inner += `<img class="item-img" src="${esc(this._inlineDataUrl(item))}" alt="Thinking image">`;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
else if (item.type === "tool_call.done") {
|
|
1555
|
+
const args = Object.entries(item.arguments)
|
|
1556
|
+
.map(([key, value]) => `${esc(key)}="${esc(value)}"`)
|
|
1557
|
+
.join(", ");
|
|
1558
|
+
inner = `<div class="tool-sig">${esc(item.name)}(${args})</div>`;
|
|
1559
|
+
}
|
|
1560
|
+
else if (item.type === "tool_result.done") {
|
|
1561
|
+
const images = item.images && item.images.length > 0
|
|
1562
|
+
? `<div class="item-images">${item.images
|
|
1563
|
+
.map((imageUrl) => `<img class="item-img" src="${esc(imageUrl)}" alt="Tool result image">`)
|
|
1564
|
+
.join("")}</div>`
|
|
1565
|
+
: "";
|
|
1566
|
+
inner = `<div class="result-text">${esc(item.text)}</div>${images}`;
|
|
1567
|
+
}
|
|
1568
|
+
else if (item.type === "image_url.done") {
|
|
1569
|
+
inner = `<img class="item-img" src="${esc(item.image_url)}" alt="Image">`;
|
|
1570
|
+
}
|
|
1571
|
+
else if (item.type === "inline_data.done") {
|
|
1572
|
+
inner = `<div class="item-note">${esc(this._formatInlineDataSummary(item))}</div>`;
|
|
1573
|
+
if (item.mime_type?.startsWith("image/")) {
|
|
1574
|
+
inner += `<img class="item-img" src="${esc(this._inlineDataUrl(item))}" alt="Inline image">`;
|
|
1575
|
+
}
|
|
1576
|
+
else if (this._inlineDataIsAudio(item)) {
|
|
1577
|
+
inner += `<audio class="item-audio" controls preload="metadata"><source src="${esc(this._inlineDataUrl(item))}" type="${esc(this._inlineDataAudioType(item))}"></audio>`;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
else if (item.type === "embedding.done") {
|
|
1581
|
+
inner = `<div class="embedding">${esc(this._formatEmbeddingPreview(item))}</div>`;
|
|
1582
|
+
}
|
|
1583
|
+
const callId = item.type === "tool_call.done" ||
|
|
1584
|
+
item.type === "tool_result.done"
|
|
1585
|
+
? `<span class="item-id">${esc(item.tool_call_id)}</span>`
|
|
1586
|
+
: "";
|
|
1587
|
+
return `
|
|
1588
|
+
<div class="item kind-${esc(item.type.split(".")[0])}">
|
|
1589
|
+
<div class="item-head"><div class="item-type">${esc(item.type)}</div>${callId}</div>
|
|
1590
|
+
${inner}
|
|
1591
|
+
</div>`;
|
|
1592
|
+
})
|
|
1593
|
+
.join("");
|
|
1594
|
+
let footHtml = "";
|
|
1595
|
+
if (msg.usage_metadata || msg.finish_reason) {
|
|
1596
|
+
footHtml = `
|
|
1597
|
+
<div class="msg-foot">`;
|
|
1598
|
+
if (msg.finish_reason) {
|
|
1599
|
+
footHtml += `<span class="reason reason-${esc(msg.finish_reason)}" title="Finish reason">${esc(msg.finish_reason)}</span>`;
|
|
1600
|
+
}
|
|
1601
|
+
const usage = msg.usage_metadata;
|
|
1602
|
+
if (usage) {
|
|
1603
|
+
const parts = [];
|
|
1604
|
+
const add = (label, value) => {
|
|
1605
|
+
if (value) {
|
|
1606
|
+
parts.push(`<span>${label} <b>${this._formatCount(value)}</b></span>`);
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
add("Cached", usage.cached_tokens);
|
|
1610
|
+
add("Prompt", usage.prompt_tokens);
|
|
1611
|
+
add("Thoughts", usage.thoughts_tokens);
|
|
1612
|
+
add("Response", usage.response_tokens);
|
|
1613
|
+
const total = (usage.cached_tokens || 0) +
|
|
1614
|
+
(usage.prompt_tokens || 0) +
|
|
1615
|
+
(usage.thoughts_tokens || 0) +
|
|
1616
|
+
(usage.response_tokens || 0);
|
|
1617
|
+
parts.push(`<span>Total <b>${this._formatCount(total)}</b></span>`);
|
|
1618
|
+
footHtml += `<span class="usage" title="Token usage">${parts.join("")}</span>`;
|
|
1619
|
+
}
|
|
1620
|
+
footHtml += "</div>";
|
|
1621
|
+
}
|
|
1622
|
+
const prevMsg = idx > 0 ? history[idx - 1] : null;
|
|
1623
|
+
const tookHtml = prevMsg && msg.created_at && prevMsg.created_at
|
|
1624
|
+
? `<span class="num" title="Time since the message before">${this._formatDuration(Math.abs(msg.created_at - prevMsg.created_at))}</span>`
|
|
1625
|
+
: "";
|
|
1626
|
+
const timestampHtml = msg.created_at
|
|
1627
|
+
? `<span class="num">${this._formatTimestamp(msg.created_at)}</span>`
|
|
1628
|
+
: "";
|
|
1629
|
+
return `
|
|
1630
|
+
<details class="card msg-card" id="msg-${idx}" open>
|
|
1631
|
+
<summary class="msg-summary">
|
|
1632
|
+
<span class="role role-${esc(msg.role)}">${esc(msg.role)}</span>
|
|
1633
|
+
<span>${plural(msg.content_items.length, "item", "items")}</span>
|
|
1634
|
+
<span>Round ${Math.floor(idx / 2) + 1} / ${totalRounds}</span>
|
|
1635
|
+
<span class="spacer"></span>
|
|
1636
|
+
${tookHtml}
|
|
1637
|
+
${timestampHtml}
|
|
1638
|
+
${ICONS.chevron}
|
|
1639
|
+
</summary>
|
|
1640
|
+
<div class="msg-body">${itemsHtml}${footHtml}
|
|
1641
|
+
</div>
|
|
1642
|
+
</details>`;
|
|
1643
|
+
})
|
|
1644
|
+
.join("");
|
|
1645
|
+
const body = `
|
|
1646
|
+
<main class="page">${pageHead(metaParts.join("\n"))}${configHtml}${messagesHtml}
|
|
1647
|
+
</main>
|
|
1648
|
+
${this._buildSidebarHtml(totalRounds, history)}`;
|
|
1649
|
+
return res.send(this._page(`${filename} - MMSP Tracer`, rootUrl, body));
|
|
1650
|
+
}
|
|
1651
|
+
catch (error) {
|
|
1652
|
+
return res.status(500).send(`Error reading file: ${error}`);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
try {
|
|
1656
|
+
const sortBy = req.query["sort"] === "mtime" ? "mtime" : "name";
|
|
1657
|
+
const entries = fs
|
|
1658
|
+
.readdirSync(fullPath, { withFileTypes: true })
|
|
1659
|
+
.filter((entry) => entry.name !== ".DS_Store");
|
|
1660
|
+
const entryStats = new Map();
|
|
1661
|
+
for (const entry of entries) {
|
|
1662
|
+
entryStats.set(entry.name, fs.statSync(path.join(fullPath, entry.name)));
|
|
1663
|
+
}
|
|
1664
|
+
entries.sort((a, b) => {
|
|
1665
|
+
const aIsDir = a.isDirectory();
|
|
1666
|
+
const bIsDir = b.isDirectory();
|
|
1667
|
+
if (aIsDir !== bIsDir)
|
|
1668
|
+
return aIsDir ? -1 : 1;
|
|
1669
|
+
if (sortBy === "mtime") {
|
|
1670
|
+
const aMtime = entryStats.get(a.name).mtimeMs;
|
|
1671
|
+
const bMtime = entryStats.get(b.name).mtimeMs;
|
|
1672
|
+
return bMtime - aMtime;
|
|
1673
|
+
}
|
|
1674
|
+
return a.name.localeCompare(b.name);
|
|
1675
|
+
});
|
|
1676
|
+
const rowsHtml = entries
|
|
1677
|
+
.map((entry) => {
|
|
1678
|
+
const entryPath = path.join(fullPath, entry.name);
|
|
1679
|
+
const relativePath = path.relative(this.cacheDir, entryPath);
|
|
1680
|
+
const stat = entryStats.get(entry.name);
|
|
1681
|
+
const isDir = stat.isDirectory();
|
|
1682
|
+
let size = "";
|
|
1683
|
+
if (!isDir) {
|
|
1684
|
+
if (stat.size < 1024) {
|
|
1685
|
+
size = `${stat.size} B`;
|
|
1686
|
+
}
|
|
1687
|
+
else if (stat.size < 1024 * 1024) {
|
|
1688
|
+
size = `${(stat.size / 1024).toFixed(1)} KB`;
|
|
1689
|
+
}
|
|
1690
|
+
else {
|
|
1691
|
+
size = `${(stat.size / (1024 * 1024)).toFixed(1)} MB`;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
const url = this._prefixUrl(basePath, "/" + relativePath.replace(/\\/g, "/"));
|
|
1695
|
+
return `
|
|
1696
|
+
<a class="row" href="${esc(url)}">
|
|
1697
|
+
<span class="row-icon${isDir ? " dir" : ""}">${isDir ? ICONS.folder : ICONS.file}</span>
|
|
1698
|
+
<span class="row-name">${esc(entry.name)}</span>
|
|
1699
|
+
<span class="row-size">${size}</span>
|
|
1700
|
+
<span class="row-time">${this._formatTimestamp(stat.mtimeMs)}</span>
|
|
1701
|
+
</a>`;
|
|
1702
|
+
})
|
|
1703
|
+
.join("");
|
|
1704
|
+
const parts = subpath ? subpath.split("/").filter(Boolean) : [];
|
|
1705
|
+
const title = parts.length ? parts[parts.length - 1] : "Traces";
|
|
1706
|
+
const baseUrl = subpath ? "/" + subpath : "/";
|
|
1707
|
+
const sortNameUrl = this._prefixUrl(basePath, baseUrl + "?sort=name");
|
|
1708
|
+
const sortMtimeUrl = this._prefixUrl(basePath, baseUrl + "?sort=mtime");
|
|
1709
|
+
const current = (sort) => sortBy === sort ? ' aria-current="true"' : "";
|
|
1710
|
+
const body = `
|
|
1711
|
+
<main class="page">
|
|
1712
|
+
${this._nav(this._breadcrumb(basePath, parts), parts.length
|
|
1713
|
+
? this._prefixUrl(basePath, "/" + parts.slice(0, -1).join("/"))
|
|
1714
|
+
: undefined)}
|
|
1715
|
+
<div class="page-head">
|
|
1716
|
+
<div>
|
|
1717
|
+
<h1>${esc(title)}</h1>
|
|
1718
|
+
<p class="page-meta"><span>${plural(entries.length, "entry", "entries")}</span></p>
|
|
1719
|
+
</div>
|
|
1720
|
+
<div class="segmented" role="group" aria-label="Sort by">
|
|
1721
|
+
<a href="${esc(sortNameUrl)}"${current("name")}>Name</a>
|
|
1722
|
+
<a href="${esc(sortMtimeUrl)}"${current("mtime")}>Modified</a>
|
|
1723
|
+
</div>
|
|
1724
|
+
</div>
|
|
1725
|
+
<div class="card list">${rowsHtml ||
|
|
1726
|
+
`
|
|
1727
|
+
<div class="empty">No traces here yet. Requests with a <code>trace_id</code> in their config are saved here.</div>`}
|
|
1728
|
+
</div>
|
|
1729
|
+
</main>`;
|
|
1730
|
+
return res.send(this._page(`${title} - MMSP Tracer`, rootUrl, body));
|
|
1731
|
+
}
|
|
1732
|
+
catch (error) {
|
|
1733
|
+
return res.status(500).send(`Error listing directory: ${error}`);
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
return app;
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Normalize the base path used when tracer is mounted inside another app.
|
|
1740
|
+
*
|
|
1741
|
+
* @param basePath - Optional URL prefix for tracer routes
|
|
1742
|
+
* @returns Normalized URL prefix without a trailing slash
|
|
1743
|
+
*/
|
|
1744
|
+
_normalizeBasePath(basePath = "") {
|
|
1745
|
+
if (!basePath || basePath === "/") {
|
|
1746
|
+
return "";
|
|
1747
|
+
}
|
|
1748
|
+
const prefixed = basePath.startsWith("/") ? basePath : "/" + basePath;
|
|
1749
|
+
return prefixed.endsWith("/") ? prefixed.slice(0, -1) : prefixed;
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Prefix an internal tracer URL with the mount path.
|
|
1753
|
+
*
|
|
1754
|
+
* @param basePath - Normalized tracer mount path
|
|
1755
|
+
* @param url - Internal URL beginning with /
|
|
1756
|
+
* @returns URL safe to render into tracer links
|
|
1757
|
+
*/
|
|
1758
|
+
_prefixUrl(basePath, url) {
|
|
1759
|
+
if (!basePath) {
|
|
1760
|
+
return url;
|
|
1761
|
+
}
|
|
1762
|
+
if (url === "/") {
|
|
1763
|
+
return basePath + "/";
|
|
1764
|
+
}
|
|
1765
|
+
return basePath + url;
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Format a Unix timestamp in milliseconds as YYYY-MM-DD HH:MM:SS.
|
|
1769
|
+
*
|
|
1770
|
+
* @param ms - Unix timestamp in milliseconds
|
|
1771
|
+
* @returns Formatted date string
|
|
1772
|
+
*/
|
|
1773
|
+
_formatTimestamp(ms) {
|
|
1774
|
+
const d = new Date(ms);
|
|
1775
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
1776
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Build the rail that lists the rounds of a trace.
|
|
1780
|
+
*
|
|
1781
|
+
* @param totalRounds - Total number of rounds
|
|
1782
|
+
* @param history - Full message history array
|
|
1783
|
+
* @returns HTML string for the rail
|
|
1784
|
+
*/
|
|
1785
|
+
_buildSidebarHtml(totalRounds, history) {
|
|
1786
|
+
let links = "";
|
|
1787
|
+
for (let roundIdx = 0; roundIdx < totalRounds; roundIdx++) {
|
|
1788
|
+
let tookHtml = "";
|
|
1789
|
+
if (roundIdx > 0) {
|
|
1790
|
+
const currAssistant = history[roundIdx * 2 + 1];
|
|
1791
|
+
const prevAssistant = history[(roundIdx - 1) * 2 + 1];
|
|
1792
|
+
if (currAssistant?.created_at && prevAssistant?.created_at) {
|
|
1793
|
+
tookHtml = `<span>${this._formatDuration(Math.abs(currAssistant.created_at - prevAssistant.created_at))}</span>`;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
links += `
|
|
1797
|
+
<li><a href="#msg-${roundIdx * 2}">Round ${roundIdx + 1}${tookHtml}</a></li>`;
|
|
1798
|
+
}
|
|
1799
|
+
return `<aside class="rail" id="rail" aria-label="Rounds">
|
|
1800
|
+
<button type="button" class="rail-toggle" onclick="toggleRail()" aria-expanded="false">Round <span class="mono" id="railCurrent">1 / ${totalRounds}</span>${ICONS.chevron}</button>
|
|
1801
|
+
<div class="rail-panel">
|
|
1802
|
+
<div class="rail-title">Rounds (${totalRounds})</div>
|
|
1803
|
+
<ol class="rail-list">${links}
|
|
1804
|
+
</ol>
|
|
1805
|
+
</div>
|
|
1806
|
+
</aside>`;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Format milliseconds the way the playground does: 840 ms, 2.41 s, 1 min 5 s.
|
|
1810
|
+
*
|
|
1811
|
+
* @param ms - Duration in milliseconds
|
|
1812
|
+
* @returns Formatted duration
|
|
1813
|
+
*/
|
|
1814
|
+
_formatDuration(ms) {
|
|
1815
|
+
if (ms < 1000) {
|
|
1816
|
+
return `${Math.round(ms)} ms`;
|
|
1817
|
+
}
|
|
1818
|
+
if (ms < 60000) {
|
|
1819
|
+
return `${(ms / 1000).toFixed(ms < 10000 ? 2 : 1)} s`;
|
|
1820
|
+
}
|
|
1821
|
+
return `${Math.floor(ms / 60000)} min ${Math.round((ms % 60000) / 1000)} s`;
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Group a count's digits by thousands.
|
|
1825
|
+
*
|
|
1826
|
+
* @param value - Count to format
|
|
1827
|
+
* @returns Formatted count
|
|
1828
|
+
*/
|
|
1829
|
+
_formatCount(value) {
|
|
1830
|
+
return value.toLocaleString("en-US");
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Build the path from the cache root to the page, every step but the last a link.
|
|
1834
|
+
*
|
|
1835
|
+
* @param basePath - Normalized tracer mount path
|
|
1836
|
+
* @param parts - Path segments below the cache root
|
|
1837
|
+
* @returns Breadcrumb HTML
|
|
1838
|
+
*/
|
|
1839
|
+
_breadcrumb(basePath, parts) {
|
|
1840
|
+
const steps = ["cache", ...parts];
|
|
1841
|
+
return steps
|
|
1842
|
+
.map((step, i) => {
|
|
1843
|
+
if (i === steps.length - 1) {
|
|
1844
|
+
return `<span class="crumb crumb-current">${this._escapeHtml(step)}</span>`;
|
|
1845
|
+
}
|
|
1846
|
+
const url = this._prefixUrl(basePath, "/" + parts.slice(0, i).join("/"));
|
|
1847
|
+
return `<a class="crumb" href="${this._escapeHtml(url)}">${this._escapeHtml(step)}</a>`;
|
|
1848
|
+
})
|
|
1849
|
+
.join(`<span class="crumb-sep">${ICONS.crumb}</span>`);
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Build the bar that opens a page, as in a file explorer: back, forward and up, then the address.
|
|
1853
|
+
*
|
|
1854
|
+
* @param breadcrumb - Breadcrumb HTML
|
|
1855
|
+
* @param upUrl - The parent the up button leads to, if there is one
|
|
1856
|
+
* @param isFile - Whether the page shows a file rather than a folder
|
|
1857
|
+
* @returns Navigation bar HTML
|
|
1858
|
+
*/
|
|
1859
|
+
_nav(breadcrumb, upUrl, isFile = false) {
|
|
1860
|
+
const up = upUrl
|
|
1861
|
+
? `<a class="nav-btn" href="${this._escapeHtml(upUrl)}" aria-label="Up" title="Up">${ICONS.arrow_up}</a>`
|
|
1862
|
+
: `<span class="nav-btn" aria-disabled="true" title="Up">${ICONS.arrow_up}</span>`;
|
|
1863
|
+
return ('<div class="page-nav"><div class="nav-buttons">' +
|
|
1864
|
+
'<button type="button" class="nav-btn" id="navBack" onclick="history.back()" aria-label="Back" ' +
|
|
1865
|
+
`title="Back">${ICONS.arrow_left}</button>` +
|
|
1866
|
+
'<button type="button" class="nav-btn forward" id="navForward" onclick="history.forward()" ' +
|
|
1867
|
+
`aria-label="Forward" title="Forward">${ICONS.arrow_right}</button>` +
|
|
1868
|
+
`${up}</div>` +
|
|
1869
|
+
`<nav class="address" aria-label="Path"><span class="address-icon">${isFile ? ICONS.file : ICONS.folder}` +
|
|
1870
|
+
`</span>${breadcrumb}</nav></div>`);
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Wrap a page body in the head, top bar and script every tracer page shares.
|
|
1874
|
+
*
|
|
1875
|
+
* @param title - Page title
|
|
1876
|
+
* @param rootUrl - URL of the cache root
|
|
1877
|
+
* @param body - Page body HTML
|
|
1878
|
+
* @returns Full HTML page
|
|
1879
|
+
*/
|
|
1880
|
+
_page(title, rootUrl, body) {
|
|
1881
|
+
return ('<!DOCTYPE html>\n<html lang="en">\n<head>\n' +
|
|
1882
|
+
`<title>${this._escapeHtml(title)}</title>\n` +
|
|
1883
|
+
`${TRACER_HEAD}\n</head>\n<body>\n` +
|
|
1884
|
+
'<header class="topbar">' +
|
|
1885
|
+
`<a class="brand" href="${this._escapeHtml(rootUrl)}">${ICONS.mark}<span class="brand-name">MMSP</span>` +
|
|
1886
|
+
'<span class="brand-sub">Tracer</span></a>' +
|
|
1887
|
+
'<div class="topbar-actions">' +
|
|
1888
|
+
'<a href="https://github.com/Prism-Shadow/model-message-stream-protocol" target="_blank" ' +
|
|
1889
|
+
`rel="noopener noreferrer" class="ghost-btn" title="GitHub">${ICONS.github}` +
|
|
1890
|
+
'<span class="label-wide">GitHub</span></a>' +
|
|
1891
|
+
'<div class="segmented theme-toggle" id="themeToggle" role="radiogroup" aria-label="Theme">' +
|
|
1892
|
+
'<span class="seg-thumb" aria-hidden="true"></span>' +
|
|
1893
|
+
'<button type="button" role="radio" aria-checked="false" aria-label="Light theme" title="Light" ' +
|
|
1894
|
+
`data-theme-choice="light" onclick="setTheme('light')">${ICONS.sun}</button>` +
|
|
1895
|
+
'<button type="button" role="radio" aria-checked="false" aria-label="Dark theme" title="Dark" ' +
|
|
1896
|
+
`data-theme-choice="dark" onclick="setTheme('dark')">${ICONS.moon}</button>` +
|
|
1897
|
+
"</div></div></header>\n" +
|
|
1898
|
+
`${body}\n${TRACER_SCRIPT}\n</body>\n</html>\n`);
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Escape HTML special characters.
|
|
1902
|
+
*
|
|
1903
|
+
* @param text - Text to escape
|
|
1904
|
+
* @returns Escaped text
|
|
1905
|
+
*/
|
|
1906
|
+
_escapeHtml(text) {
|
|
1907
|
+
return text
|
|
1908
|
+
.replace(/&/g, "&")
|
|
1909
|
+
.replace(/</g, "<")
|
|
1910
|
+
.replace(/>/g, ">")
|
|
1911
|
+
.replace(/"/g, """)
|
|
1912
|
+
.replace(/'/g, "'");
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Start the web server for browsing conversation files.
|
|
1916
|
+
*
|
|
1917
|
+
* @param host - Host address to bind to
|
|
1918
|
+
* @param port - Port number to listen on
|
|
1919
|
+
*/
|
|
1920
|
+
startWebServer(host = "127.0.0.1", port = 25750) {
|
|
1921
|
+
const app = this.createWebApp();
|
|
1922
|
+
app.listen(port, host, () => {
|
|
1923
|
+
console.log(`Starting tracer web server at http://${host}:${port}`);
|
|
1924
|
+
console.log(`Cache directory: ${path.resolve(this.cacheDir)}`);
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
exports.Tracer = Tracer;
|