markform 0.1.21 → 0.1.22
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 +32 -4
- package/dist/ai-sdk.d.mts +1 -1
- package/dist/ai-sdk.mjs +1 -1
- package/dist/{apply-CD-t7ovb.mjs → apply-C7mO7VkZ.mjs} +100 -74
- package/dist/apply-C7mO7VkZ.mjs.map +1 -0
- package/dist/bin.mjs +1 -1
- package/dist/{cli-ChdIy1a7.mjs → cli-C8F9yDsv.mjs} +17 -1213
- package/dist/cli-C8F9yDsv.mjs.map +1 -0
- package/dist/cli.mjs +1 -1
- package/dist/{coreTypes-BQrWf_Wt.d.mts → coreTypes-BlsJkU1w.d.mts} +1 -1
- package/dist/fillRecord-DTl5lnK0.d.mts +345 -0
- package/dist/fillRecordRenderer-CruJrLkj.mjs +1256 -0
- package/dist/fillRecordRenderer-CruJrLkj.mjs.map +1 -0
- package/dist/index.d.mts +5 -342
- package/dist/index.mjs +3 -3
- package/dist/render.d.mts +74 -0
- package/dist/render.mjs +4 -0
- package/dist/{session-ZgegwtkT.mjs → session-BCcltrLA.mjs} +1 -1
- package/dist/{session-ZgegwtkT.mjs.map → session-BCcltrLA.mjs.map} +1 -1
- package/dist/{session-BPuQ-ok0.mjs → session-VeSkVrck.mjs} +1 -1
- package/dist/{shared-DwdyWmvE.mjs → shared-CsdT2T7k.mjs} +1 -1
- package/dist/{shared-DwdyWmvE.mjs.map → shared-CsdT2T7k.mjs.map} +1 -1
- package/dist/{shared-BTR35aMz.mjs → shared-fb0nkzQi.mjs} +1 -1
- package/dist/{src-DOPe4tmu.mjs → src-CbRnGzMK.mjs} +16 -11
- package/dist/{src-DOPe4tmu.mjs.map → src-CbRnGzMK.mjs.map} +1 -1
- package/dist/urlFormat-lls7CsEP.mjs +71 -0
- package/dist/urlFormat-lls7CsEP.mjs.map +1 -0
- package/docs/markform-apis.md +53 -0
- package/examples/simple/simple-skipped-filled.report.md +8 -8
- package/examples/twitter-thread/twitter-thread.form.md +373 -0
- package/package.json +5 -1
- package/dist/apply-CD-t7ovb.mjs.map +0 -1
- package/dist/cli-ChdIy1a7.mjs.map +0 -1
|
@@ -0,0 +1,1256 @@
|
|
|
1
|
+
|
|
2
|
+
import { r as friendlyUrlAbbrev, t as formatBareUrlsAsHtmlLinks } from "./urlFormat-lls7CsEP.mjs";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
|
|
5
|
+
//#region src/render/renderUtils.ts
|
|
6
|
+
/**
|
|
7
|
+
* Rendering utility functions for HTML generation.
|
|
8
|
+
*
|
|
9
|
+
* Pure utility functions used across all renderers. No CLI or server dependencies.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Escape HTML special characters.
|
|
13
|
+
*/
|
|
14
|
+
function escapeHtml(str) {
|
|
15
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Format milliseconds as human-readable duration.
|
|
19
|
+
*/
|
|
20
|
+
function formatDuration(ms) {
|
|
21
|
+
if (ms < 1e3) return `${ms.toFixed(0)}ms`;
|
|
22
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
23
|
+
return `${Math.floor(ms / 6e4)}m ${(ms % 6e4 / 1e3).toFixed(0)}s`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Format token count with K suffix for large numbers.
|
|
27
|
+
*/
|
|
28
|
+
function formatTokens(count) {
|
|
29
|
+
if (count >= 1e4) return `${(count / 1e3).toFixed(1)}k`;
|
|
30
|
+
if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
|
|
31
|
+
return count.toLocaleString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/render/contentRenderers.ts
|
|
36
|
+
/**
|
|
37
|
+
* Format a checkbox state for display.
|
|
38
|
+
*/
|
|
39
|
+
function formatCheckboxState(state) {
|
|
40
|
+
switch (state) {
|
|
41
|
+
case "done": return "<span class=\"checkbox checked\">☑</span>";
|
|
42
|
+
case "todo": return "<span class=\"checkbox unchecked\">☐</span>";
|
|
43
|
+
case "active": return "<span class=\"state-badge state-active\">●</span>";
|
|
44
|
+
case "incomplete": return "<span class=\"state-badge state-incomplete\">○</span>";
|
|
45
|
+
case "na": return "<span class=\"state-badge state-na\">—</span>";
|
|
46
|
+
case "yes": return "<span class=\"checkbox checked\">☑</span>";
|
|
47
|
+
case "no": return "<span class=\"checkbox unchecked\">☐</span>";
|
|
48
|
+
case "unfilled": return "<span class=\"state-badge state-unfilled\">?</span>";
|
|
49
|
+
default: return `<span class="state-badge">${escapeHtml(state)}</span>`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Render a field value for the View tab.
|
|
54
|
+
*/
|
|
55
|
+
function renderViewFieldValue(field, value, isSkipped, skipReason) {
|
|
56
|
+
if (isSkipped) return `<div class="view-field-empty">${skipReason ? `(skipped: ${escapeHtml(skipReason)})` : "(skipped)"}</div>`;
|
|
57
|
+
if (value === void 0) return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
58
|
+
switch (field.kind) {
|
|
59
|
+
case "string": {
|
|
60
|
+
const v = value.kind === "string" ? value.value : null;
|
|
61
|
+
if (v === null || v === "") return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
62
|
+
return `<div class="view-field-value">${formatBareUrlsAsHtmlLinks(v, escapeHtml)}</div>`;
|
|
63
|
+
}
|
|
64
|
+
case "number": {
|
|
65
|
+
const v = value.kind === "number" ? value.value : null;
|
|
66
|
+
if (v === null) return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
67
|
+
return `<div class="view-field-value">${v}</div>`;
|
|
68
|
+
}
|
|
69
|
+
case "string_list": {
|
|
70
|
+
const items = value.kind === "string_list" ? value.items : [];
|
|
71
|
+
if (items.length === 0) return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
72
|
+
return `<div class="view-field-value"><ul>${items.map((i) => `<li>${formatBareUrlsAsHtmlLinks(i, escapeHtml)}</li>`).join("")}</ul></div>`;
|
|
73
|
+
}
|
|
74
|
+
case "single_select": {
|
|
75
|
+
const selected = value.kind === "single_select" ? value.selected : null;
|
|
76
|
+
if (selected === null) return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
77
|
+
return `<div class="view-field-value">${escapeHtml(field.options.find((o) => o.id === selected)?.label ?? selected)}</div>`;
|
|
78
|
+
}
|
|
79
|
+
case "multi_select": {
|
|
80
|
+
const selected = value.kind === "multi_select" ? value.selected : [];
|
|
81
|
+
return `<div class="view-field-value"><ul class="checkbox-list">${field.options.map((opt) => {
|
|
82
|
+
return `<li class="checkbox-item">${selected.includes(opt.id) ? "<span class=\"checkbox checked\">☑</span>" : "<span class=\"checkbox unchecked\">☐</span>"} ${escapeHtml(opt.label)}</li>`;
|
|
83
|
+
}).join("")}</ul></div>`;
|
|
84
|
+
}
|
|
85
|
+
case "checkboxes": {
|
|
86
|
+
const values = value.kind === "checkboxes" ? value.values : {};
|
|
87
|
+
const mode = field.checkboxMode ?? "multi";
|
|
88
|
+
return `<div class="view-field-value"><ul class="checkbox-list">${field.options.map((opt) => {
|
|
89
|
+
const state = values[opt.id] ?? (mode === "explicit" ? "unfilled" : "todo");
|
|
90
|
+
if (mode === "simple") return `<li class="checkbox-item">${state === "done" ? "<span class=\"checkbox checked\">☑</span>" : "<span class=\"checkbox unchecked\">☐</span>"} ${escapeHtml(opt.label)}</li>`;
|
|
91
|
+
return `<li class="checkbox-item">${formatCheckboxState(state)} ${escapeHtml(opt.label)}</li>`;
|
|
92
|
+
}).join("")}</ul></div>`;
|
|
93
|
+
}
|
|
94
|
+
case "url": {
|
|
95
|
+
const v = value.kind === "url" ? value.value : null;
|
|
96
|
+
if (v === null || v === "") return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
97
|
+
const domain = friendlyUrlAbbrev(v);
|
|
98
|
+
return `<div class="view-field-value"><a href="${escapeHtml(v)}" target="_blank" class="url-link" data-url="${escapeHtml(v)}">${escapeHtml(domain)}</a></div>`;
|
|
99
|
+
}
|
|
100
|
+
case "url_list": {
|
|
101
|
+
const items = value.kind === "url_list" ? value.items : [];
|
|
102
|
+
if (items.length === 0) return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
103
|
+
return `<div class="view-field-value"><ul>${items.map((u) => {
|
|
104
|
+
const domain = friendlyUrlAbbrev(u);
|
|
105
|
+
return `<li><a href="${escapeHtml(u)}" target="_blank" class="url-link" data-url="${escapeHtml(u)}">${escapeHtml(domain)}</a></li>`;
|
|
106
|
+
}).join("")}</ul></div>`;
|
|
107
|
+
}
|
|
108
|
+
case "date": {
|
|
109
|
+
const v = value.kind === "date" ? value.value : null;
|
|
110
|
+
if (v === null || v === "") return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
111
|
+
return `<div class="view-field-value">${escapeHtml(v)}</div>`;
|
|
112
|
+
}
|
|
113
|
+
case "year": {
|
|
114
|
+
const v = value.kind === "year" ? value.value : null;
|
|
115
|
+
if (v === null) return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
116
|
+
return `<div class="view-field-value">${v}</div>`;
|
|
117
|
+
}
|
|
118
|
+
case "table": {
|
|
119
|
+
const rows = value.kind === "table" ? value.rows : [];
|
|
120
|
+
if (rows.length === 0) return "<div class=\"view-field-empty\">(no data)</div>";
|
|
121
|
+
let tableHtml = "<div class=\"table-container\"><table class=\"data-table\">";
|
|
122
|
+
tableHtml += "<thead><tr>";
|
|
123
|
+
for (const col of field.columns) tableHtml += `<th>${escapeHtml(col.label)}</th>`;
|
|
124
|
+
tableHtml += "</tr></thead><tbody>";
|
|
125
|
+
for (const row of rows) {
|
|
126
|
+
tableHtml += "<tr>";
|
|
127
|
+
for (const col of field.columns) {
|
|
128
|
+
const cell = row[col.id];
|
|
129
|
+
let cellValue = "";
|
|
130
|
+
let cellHtml = "";
|
|
131
|
+
if (cell?.state === "answered" && cell.value !== void 0 && cell.value !== null) {
|
|
132
|
+
cellValue = String(cell.value);
|
|
133
|
+
if (col.type === "url" && cellValue) {
|
|
134
|
+
const domain = friendlyUrlAbbrev(cellValue);
|
|
135
|
+
cellHtml = `<a href="${escapeHtml(cellValue)}" target="_blank" class="url-link" data-url="${escapeHtml(cellValue)}">${escapeHtml(domain)}</a>`;
|
|
136
|
+
} else cellHtml = formatBareUrlsAsHtmlLinks(cellValue, escapeHtml);
|
|
137
|
+
}
|
|
138
|
+
tableHtml += `<td>${cellHtml}</td>`;
|
|
139
|
+
}
|
|
140
|
+
tableHtml += "</tr>";
|
|
141
|
+
}
|
|
142
|
+
tableHtml += "</tbody></table></div>";
|
|
143
|
+
return tableHtml;
|
|
144
|
+
}
|
|
145
|
+
default: {
|
|
146
|
+
const _exhaustive = field;
|
|
147
|
+
throw new Error(`Unhandled field kind: ${_exhaustive.kind}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Render form view content (read-only display of form fields).
|
|
153
|
+
* Used for View tab content.
|
|
154
|
+
*/
|
|
155
|
+
function renderViewContent(form) {
|
|
156
|
+
const { schema, responsesByFieldId } = form;
|
|
157
|
+
let html = "<div class=\"view-content\">";
|
|
158
|
+
for (const group of schema.groups) {
|
|
159
|
+
const groupTitle = group.title ?? group.id;
|
|
160
|
+
html += `<div class="view-group"><h2>${escapeHtml(groupTitle)}</h2>`;
|
|
161
|
+
for (const field of group.children) {
|
|
162
|
+
const response = responsesByFieldId[field.id];
|
|
163
|
+
const value = response?.state === "answered" ? response.value : void 0;
|
|
164
|
+
const isSkipped = response?.state === "skipped";
|
|
165
|
+
const skipReason = isSkipped ? response?.reason : void 0;
|
|
166
|
+
html += "<div class=\"view-field\">";
|
|
167
|
+
html += `<div class="view-field-label">${escapeHtml(field.label)}`;
|
|
168
|
+
html += ` <span class="type-badge">${field.kind}</span>`;
|
|
169
|
+
if (field.required) html += " <span class=\"required\">*</span>";
|
|
170
|
+
if (isSkipped) {
|
|
171
|
+
const reasonText = skipReason ? `Skipped: ${escapeHtml(skipReason)}` : "Skipped";
|
|
172
|
+
html += ` <span class="skipped-badge">${reasonText}</span>`;
|
|
173
|
+
}
|
|
174
|
+
html += "</div>";
|
|
175
|
+
html += renderViewFieldValue(field, value, isSkipped, skipReason);
|
|
176
|
+
html += "</div>";
|
|
177
|
+
}
|
|
178
|
+
html += "</div>";
|
|
179
|
+
}
|
|
180
|
+
html += "</div>";
|
|
181
|
+
return html;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Highlight a single line of source code (Markdown + Jinja).
|
|
185
|
+
*/
|
|
186
|
+
function highlightSourceLine(line) {
|
|
187
|
+
let result = escapeHtml(line);
|
|
188
|
+
result = result.replace(/(\{%\s*)([a-zA-Z_/]+)(\s+[^%]*)?(%\})/g, (_, open, keyword, attrs, close) => {
|
|
189
|
+
let attrHtml = "";
|
|
190
|
+
if (attrs) attrHtml = attrs.replace(/([a-zA-Z_]+)(=)("[^"]*"|'[^&#]*'|[^\s%]+)?/g, (_m, attrName, eq, attrValue) => {
|
|
191
|
+
return `<span class="syn-jinja-attr">${attrName}</span>${eq}${attrValue ? `<span class="syn-jinja-value">${attrValue}</span>` : ""}`;
|
|
192
|
+
});
|
|
193
|
+
return `<span class="syn-jinja-tag">${open}</span><span class="syn-jinja-keyword">${keyword}</span>${attrHtml}<span class="syn-jinja-tag">${close}</span>`;
|
|
194
|
+
});
|
|
195
|
+
result = result.replace(/(\{#)(.*?)(#\})/g, `<span class="syn-comment">$1$2$3</span>`);
|
|
196
|
+
result = result.replace(/^(#{1,6}\s.*)$/gm, "<span class=\"syn-md-header\">$1</span>");
|
|
197
|
+
if (result === "---") result = "<span class=\"syn-comment\">---</span>";
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Render source content with Markdown and Jinja syntax highlighting.
|
|
202
|
+
* Used for Source tab content.
|
|
203
|
+
*/
|
|
204
|
+
function renderSourceContent(content) {
|
|
205
|
+
return `<pre>${content.split("\n").map((line) => highlightSourceLine(line)).join("\n")}</pre>`;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Format inline markdown (bold, italic, code, links, checkboxes).
|
|
209
|
+
* Also auto-links bare URLs for consistency.
|
|
210
|
+
*/
|
|
211
|
+
function formatInlineMarkdown(text) {
|
|
212
|
+
let result = escapeHtml(text);
|
|
213
|
+
result = result.replace(/\[x\]/gi, "<span class=\"checkbox checked\">☑</span>");
|
|
214
|
+
result = result.replace(/\[ \]/g, "<span class=\"checkbox unchecked\">☐</span>");
|
|
215
|
+
result = result.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
216
|
+
result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
217
|
+
result = result.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
218
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, linkText, url) => {
|
|
219
|
+
const cleanUrl = url.replace(/&/g, "&");
|
|
220
|
+
return `<a href="${cleanUrl}" target="_blank" class="url-link" data-url="${cleanUrl}">${linkText}</a>`;
|
|
221
|
+
});
|
|
222
|
+
result = result.replace(/(?<!href="|data-url="|">|\]\()(?:https?:\/\/|www\.)[^\s<>"]+(?<![.,;:!?'")])/g, (url) => {
|
|
223
|
+
const cleanUrl = url.replace(/&/g, "&");
|
|
224
|
+
const fullUrl = cleanUrl.startsWith("www.") ? `https://${cleanUrl}` : cleanUrl;
|
|
225
|
+
const display = friendlyUrlAbbrev(fullUrl);
|
|
226
|
+
return `<a href="${escapeHtml(fullUrl)}" target="_blank" class="url-link" data-url="${escapeHtml(fullUrl)}">${escapeHtml(display)}</a>`;
|
|
227
|
+
});
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Render markdown content (content only, no page wrapper).
|
|
232
|
+
* Used for tab content.
|
|
233
|
+
*/
|
|
234
|
+
function renderMarkdownContent(content) {
|
|
235
|
+
const lines = content.split("\n");
|
|
236
|
+
let html = "<div class=\"markdown-content\">";
|
|
237
|
+
let inParagraph = false;
|
|
238
|
+
let inCodeBlock = false;
|
|
239
|
+
let codeBlockContent = "";
|
|
240
|
+
let inUnorderedList = false;
|
|
241
|
+
let inOrderedList = false;
|
|
242
|
+
let inTable = false;
|
|
243
|
+
let tableHeaderDone = false;
|
|
244
|
+
const closeList = () => {
|
|
245
|
+
if (inUnorderedList) {
|
|
246
|
+
html += "</ul>";
|
|
247
|
+
inUnorderedList = false;
|
|
248
|
+
}
|
|
249
|
+
if (inOrderedList) {
|
|
250
|
+
html += "</ol>";
|
|
251
|
+
inOrderedList = false;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
const closeTable = () => {
|
|
255
|
+
if (inTable) {
|
|
256
|
+
html += "</tbody></table></div>";
|
|
257
|
+
inTable = false;
|
|
258
|
+
tableHeaderDone = false;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const isTableRow = (line) => {
|
|
262
|
+
const trimmed = line.trim();
|
|
263
|
+
return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|");
|
|
264
|
+
};
|
|
265
|
+
const isTableSeparator = (line) => {
|
|
266
|
+
const trimmed = line.trim();
|
|
267
|
+
return /^\|[\s-:|]+\|$/.test(trimmed);
|
|
268
|
+
};
|
|
269
|
+
const parseTableCells = (line) => {
|
|
270
|
+
return line.trim().slice(1, -1).split("|").map((cell) => cell.trim());
|
|
271
|
+
};
|
|
272
|
+
for (const line of lines) {
|
|
273
|
+
const trimmed = line.trim();
|
|
274
|
+
if (trimmed.startsWith("```")) {
|
|
275
|
+
if (inCodeBlock) {
|
|
276
|
+
html += `<pre><code>${escapeHtml(codeBlockContent.trim())}</code></pre>`;
|
|
277
|
+
codeBlockContent = "";
|
|
278
|
+
inCodeBlock = false;
|
|
279
|
+
} else {
|
|
280
|
+
if (inParagraph) {
|
|
281
|
+
html += "</p>";
|
|
282
|
+
inParagraph = false;
|
|
283
|
+
}
|
|
284
|
+
closeList();
|
|
285
|
+
closeTable();
|
|
286
|
+
inCodeBlock = true;
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (inCodeBlock) {
|
|
291
|
+
codeBlockContent += line + "\n";
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (isTableRow(trimmed)) {
|
|
295
|
+
if (inParagraph) {
|
|
296
|
+
html += "</p>";
|
|
297
|
+
inParagraph = false;
|
|
298
|
+
}
|
|
299
|
+
closeList();
|
|
300
|
+
if (isTableSeparator(trimmed)) {
|
|
301
|
+
tableHeaderDone = true;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const cells = parseTableCells(trimmed);
|
|
305
|
+
if (!inTable) {
|
|
306
|
+
html += "<div class=\"table-container\"><table class=\"data-table\"><thead><tr>";
|
|
307
|
+
for (const cell of cells) html += `<th>${formatInlineMarkdown(cell)}</th>`;
|
|
308
|
+
html += "</tr></thead><tbody>";
|
|
309
|
+
inTable = true;
|
|
310
|
+
} else if (tableHeaderDone) {
|
|
311
|
+
html += "<tr>";
|
|
312
|
+
for (const cell of cells) html += `<td>${formatInlineMarkdown(cell)}</td>`;
|
|
313
|
+
html += "</tr>";
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (inTable && !isTableRow(trimmed)) closeTable();
|
|
318
|
+
if (trimmed.startsWith("# ")) {
|
|
319
|
+
if (inParagraph) {
|
|
320
|
+
html += "</p>";
|
|
321
|
+
inParagraph = false;
|
|
322
|
+
}
|
|
323
|
+
closeList();
|
|
324
|
+
html += `<h2>${formatInlineMarkdown(trimmed.slice(2))}</h2>`;
|
|
325
|
+
} else if (trimmed.startsWith("## ")) {
|
|
326
|
+
if (inParagraph) {
|
|
327
|
+
html += "</p>";
|
|
328
|
+
inParagraph = false;
|
|
329
|
+
}
|
|
330
|
+
closeList();
|
|
331
|
+
html += `<h3>${formatInlineMarkdown(trimmed.slice(3))}</h3>`;
|
|
332
|
+
} else if (trimmed.startsWith("### ")) {
|
|
333
|
+
if (inParagraph) {
|
|
334
|
+
html += "</p>";
|
|
335
|
+
inParagraph = false;
|
|
336
|
+
}
|
|
337
|
+
closeList();
|
|
338
|
+
html += `<h4>${formatInlineMarkdown(trimmed.slice(4))}</h4>`;
|
|
339
|
+
} else if (trimmed.startsWith("#### ")) {
|
|
340
|
+
if (inParagraph) {
|
|
341
|
+
html += "</p>";
|
|
342
|
+
inParagraph = false;
|
|
343
|
+
}
|
|
344
|
+
closeList();
|
|
345
|
+
html += `<h5>${formatInlineMarkdown(trimmed.slice(5))}</h5>`;
|
|
346
|
+
} else if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
|
|
347
|
+
if (inParagraph) {
|
|
348
|
+
html += "</p>";
|
|
349
|
+
inParagraph = false;
|
|
350
|
+
}
|
|
351
|
+
if (inOrderedList) {
|
|
352
|
+
html += "</ol>";
|
|
353
|
+
inOrderedList = false;
|
|
354
|
+
}
|
|
355
|
+
if (!inUnorderedList) {
|
|
356
|
+
html += "<ul>";
|
|
357
|
+
inUnorderedList = true;
|
|
358
|
+
}
|
|
359
|
+
const itemContent = trimmed.slice(2);
|
|
360
|
+
const liClass = /^\[[ xX]\]/.test(itemContent) ? " class=\"checkbox-item\"" : "";
|
|
361
|
+
html += `<li${liClass}>${formatInlineMarkdown(itemContent)}</li>`;
|
|
362
|
+
} else if (/^\d+\.\s/.test(trimmed)) {
|
|
363
|
+
if (inParagraph) {
|
|
364
|
+
html += "</p>";
|
|
365
|
+
inParagraph = false;
|
|
366
|
+
}
|
|
367
|
+
if (inUnorderedList) {
|
|
368
|
+
html += "</ul>";
|
|
369
|
+
inUnorderedList = false;
|
|
370
|
+
}
|
|
371
|
+
if (!inOrderedList) {
|
|
372
|
+
html += "<ol>";
|
|
373
|
+
inOrderedList = true;
|
|
374
|
+
}
|
|
375
|
+
const text = trimmed.replace(/^\d+\.\s/, "");
|
|
376
|
+
html += `<li>${formatInlineMarkdown(text)}</li>`;
|
|
377
|
+
} else if (trimmed === "") {
|
|
378
|
+
if (inParagraph) {
|
|
379
|
+
html += "</p>";
|
|
380
|
+
inParagraph = false;
|
|
381
|
+
}
|
|
382
|
+
closeList();
|
|
383
|
+
} else {
|
|
384
|
+
closeList();
|
|
385
|
+
if (!inParagraph) {
|
|
386
|
+
html += "<p>";
|
|
387
|
+
inParagraph = true;
|
|
388
|
+
} else html += "<br>";
|
|
389
|
+
html += formatInlineMarkdown(trimmed);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (inParagraph) html += "</p>";
|
|
393
|
+
closeList();
|
|
394
|
+
closeTable();
|
|
395
|
+
html += "</div>";
|
|
396
|
+
return html;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Highlight a YAML value with appropriate syntax class.
|
|
400
|
+
*/
|
|
401
|
+
function highlightYamlValue(value) {
|
|
402
|
+
const trimmed = value.trim();
|
|
403
|
+
if (trimmed === "true" || trimmed === "false") return `<span class="syn-bool">${escapeHtml(value)}</span>`;
|
|
404
|
+
if (trimmed === "null" || trimmed === "~") return `<span class="syn-null">${escapeHtml(value)}</span>`;
|
|
405
|
+
if (/^-?\d+\.?\d*$/.test(trimmed)) return `<span class="syn-number">${escapeHtml(value)}</span>`;
|
|
406
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return `<span class="syn-string">${escapeHtml(value)}</span>`;
|
|
407
|
+
return `<span class="syn-string">${escapeHtml(value)}</span>`;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Render YAML content with syntax highlighting (content only, no page wrapper).
|
|
411
|
+
* Used for tab content.
|
|
412
|
+
*/
|
|
413
|
+
function renderYamlContent(content) {
|
|
414
|
+
return `<pre>${content.split("\n").map((line) => {
|
|
415
|
+
if (line.trim().startsWith("#")) return `<span class="syn-comment">${escapeHtml(line)}</span>`;
|
|
416
|
+
const colonIndex = line.indexOf(":");
|
|
417
|
+
if (colonIndex > 0 && !line.trim().startsWith("-")) {
|
|
418
|
+
const key = escapeHtml(line.slice(0, colonIndex));
|
|
419
|
+
const afterColon = line.slice(colonIndex + 1).trim();
|
|
420
|
+
const colonAndSpace = escapeHtml(line.slice(colonIndex, colonIndex + 1));
|
|
421
|
+
if (afterColon === "") return `<span class="syn-key">${key}</span>${colonAndSpace}`;
|
|
422
|
+
const valueStart = line.indexOf(afterColon, colonIndex);
|
|
423
|
+
return `<span class="syn-key">${key}</span>${escapeHtml(line.slice(colonIndex, valueStart))}${highlightYamlValue(afterColon)}`;
|
|
424
|
+
}
|
|
425
|
+
if (line.trim().startsWith("-")) {
|
|
426
|
+
const dashIndex = line.indexOf("-");
|
|
427
|
+
const beforeDash = escapeHtml(line.slice(0, dashIndex));
|
|
428
|
+
const afterDash = line.slice(dashIndex + 1).trim();
|
|
429
|
+
if (afterDash === "") return `${beforeDash}-`;
|
|
430
|
+
return `${beforeDash}- ${highlightYamlValue(afterDash)}`;
|
|
431
|
+
}
|
|
432
|
+
return escapeHtml(line);
|
|
433
|
+
}).join("\n")}</pre>`;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Render JSON content with syntax highlighting (content only, no page wrapper).
|
|
437
|
+
* Used for tab content.
|
|
438
|
+
*/
|
|
439
|
+
function renderJsonContent(content) {
|
|
440
|
+
let formatted;
|
|
441
|
+
try {
|
|
442
|
+
const parsed = JSON.parse(content);
|
|
443
|
+
formatted = JSON.stringify(parsed, null, 2);
|
|
444
|
+
} catch {
|
|
445
|
+
formatted = content;
|
|
446
|
+
}
|
|
447
|
+
return `<pre>${formatted.replace(/"([^"]+)":/g, "<span class=\"syn-key\">\"$1\"</span>:").replace(/: "([^"]*)"/g, ": <span class=\"syn-string\">\"$1\"</span>").replace(/: (-?\d+\.?\d*)/g, ": <span class=\"syn-number\">$1</span>").replace(/: (true|false)/g, ": <span class=\"syn-bool\">$1</span>").replace(/: (null)/g, ": <span class=\"syn-null\">$1</span>")}</pre>`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/render/fillRecordRenderer.ts
|
|
452
|
+
/**
|
|
453
|
+
* Fill record HTML renderer and associated styles/scripts.
|
|
454
|
+
*
|
|
455
|
+
* Renders FillRecord data as an interactive dashboard with Gantt timeline,
|
|
456
|
+
* progress bars, tool summaries, and turn details. No CLI or server dependencies.
|
|
457
|
+
*/
|
|
458
|
+
/**
|
|
459
|
+
* JavaScript for fill record interactive features.
|
|
460
|
+
* Consumers should include this in a <script> tag on their page.
|
|
461
|
+
* Provides: frShowTip(el), frHideTip(), frCopyYaml(btn)
|
|
462
|
+
*/
|
|
463
|
+
const FILL_RECORD_SCRIPTS = `
|
|
464
|
+
// Copy YAML content handler for Fill Record tab (must be global for dynamically loaded content)
|
|
465
|
+
function frCopyYaml(btn) {
|
|
466
|
+
const pre = btn.parentElement.querySelector('pre');
|
|
467
|
+
navigator.clipboard.writeText(pre.textContent).then(() => {
|
|
468
|
+
const orig = btn.textContent;
|
|
469
|
+
btn.textContent = 'Copied!';
|
|
470
|
+
setTimeout(() => btn.textContent = orig, 1500);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Tooltip handlers for Fill Record visualizations (must be global for dynamically loaded content)
|
|
475
|
+
function frShowTip(el) {
|
|
476
|
+
var tip = document.getElementById('fr-tooltip');
|
|
477
|
+
if (tip && el.dataset.tooltip) {
|
|
478
|
+
tip.textContent = el.dataset.tooltip;
|
|
479
|
+
// Position tooltip centered above the element
|
|
480
|
+
var rect = el.getBoundingClientRect();
|
|
481
|
+
tip.style.left = (rect.left + rect.width / 2) + 'px';
|
|
482
|
+
tip.style.top = (rect.top - 8) + 'px';
|
|
483
|
+
tip.style.transform = 'translate(-50%, -100%)';
|
|
484
|
+
tip.classList.add('visible');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function frHideTip() {
|
|
488
|
+
var tip = document.getElementById('fr-tooltip');
|
|
489
|
+
if (tip) tip.classList.remove('visible');
|
|
490
|
+
}
|
|
491
|
+
`;
|
|
492
|
+
/**
|
|
493
|
+
* Format a patch value for display.
|
|
494
|
+
* Shows full content - the container has max-height with scroll for long values.
|
|
495
|
+
*/
|
|
496
|
+
function formatPatchValue(value) {
|
|
497
|
+
if (value === null || value === void 0) return "<em class=\"fr-turn__patch-value--clear\">(cleared)</em>";
|
|
498
|
+
if (typeof value === "string") return escapeHtml(value);
|
|
499
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
500
|
+
return escapeHtml(JSON.stringify(value, null, 2));
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Render patches from a fill_form tool call input.
|
|
504
|
+
* Returns HTML for the patch details section.
|
|
505
|
+
*/
|
|
506
|
+
function renderPatchDetails(input) {
|
|
507
|
+
const patches = input.patches;
|
|
508
|
+
if (!Array.isArray(patches) || patches.length === 0) return "";
|
|
509
|
+
return `<div class="fr-turn__patches">${patches.map((patch) => {
|
|
510
|
+
if (!patch || typeof patch !== "object") return "";
|
|
511
|
+
const p = patch;
|
|
512
|
+
const op = typeof p.op === "string" ? p.op : "unknown";
|
|
513
|
+
const fieldId = typeof p.fieldId === "string" ? p.fieldId : typeof p.noteId === "string" ? p.noteId : "";
|
|
514
|
+
const opLabel = op.replace(/_/g, " ");
|
|
515
|
+
let valueHtml = "";
|
|
516
|
+
if (op === "skip_field") valueHtml = "<em class=\"fr-turn__patch-value--skip\">(skipped)</em>";
|
|
517
|
+
else if (op === "abort_field") valueHtml = "<em class=\"fr-turn__patch-value--skip\">(aborted)</em>";
|
|
518
|
+
else if (op === "clear_field") valueHtml = "<em class=\"fr-turn__patch-value--clear\">(cleared)</em>";
|
|
519
|
+
else if ("value" in p) valueHtml = formatPatchValue(p.value);
|
|
520
|
+
else if ("values" in p) valueHtml = formatPatchValue(p.values);
|
|
521
|
+
else if ("rows" in p) valueHtml = formatPatchValue(p.rows);
|
|
522
|
+
return `
|
|
523
|
+
<div class="fr-turn__patch">
|
|
524
|
+
<span class="fr-turn__patch-field">${escapeHtml(fieldId)}</span>
|
|
525
|
+
<span class="fr-turn__patch-op">${escapeHtml(opLabel)}</span>
|
|
526
|
+
<span class="fr-turn__patch-value">${valueHtml}</span>
|
|
527
|
+
</div>
|
|
528
|
+
`;
|
|
529
|
+
}).filter(Boolean).join("")}</div>`;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Render a single tool call with enhanced details.
|
|
533
|
+
* Shows query for web_search, patch details for fill_form.
|
|
534
|
+
*/
|
|
535
|
+
function renderToolCall(tc) {
|
|
536
|
+
const hasError = !!tc.result?.error;
|
|
537
|
+
const icon = tc.success ? "✓" : "✕";
|
|
538
|
+
const errorClass = hasError ? " fr-turn__tool--error" : "";
|
|
539
|
+
let resultSummary = "";
|
|
540
|
+
if (hasError) resultSummary = `Error: ${escapeHtml(tc.result?.error ?? "")}`;
|
|
541
|
+
else if (tc.result?.resultCount !== void 0) resultSummary = `${tc.result.resultCount} results`;
|
|
542
|
+
else resultSummary = "OK";
|
|
543
|
+
let detailHtml = "";
|
|
544
|
+
if (tc.tool === "web_search" && typeof tc.input.query === "string") detailHtml = ` <span class="fr-turn__query">"${escapeHtml(tc.input.query)}"</span>`;
|
|
545
|
+
const toolLine = `<li class="fr-turn__tool${errorClass}">${icon} <strong>${escapeHtml(tc.tool)}</strong>${detailHtml}: ${resultSummary} (${formatDuration(tc.durationMs)})</li>`;
|
|
546
|
+
if (tc.tool === "fill_form" && tc.input.patches) {
|
|
547
|
+
const patchDetails = renderPatchDetails(tc.input);
|
|
548
|
+
if (patchDetails) return toolLine + patchDetails;
|
|
549
|
+
}
|
|
550
|
+
return toolLine;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* CSS styles for fill record visualization.
|
|
554
|
+
* Uses CSS custom properties for theming (supports dark mode via prefers-color-scheme).
|
|
555
|
+
* Designed to be lightweight, reusable, and embeddable.
|
|
556
|
+
*/
|
|
557
|
+
const FILL_RECORD_STYLES = `
|
|
558
|
+
<style>
|
|
559
|
+
.fr-dashboard {
|
|
560
|
+
--fr-bg: #ffffff;
|
|
561
|
+
--fr-bg-muted: #f9fafb;
|
|
562
|
+
--fr-bg-subtle: #f3f4f6;
|
|
563
|
+
--fr-border: #e5e7eb;
|
|
564
|
+
--fr-text: #111827;
|
|
565
|
+
--fr-text-muted: #6b7280;
|
|
566
|
+
--fr-primary: #3b82f6;
|
|
567
|
+
--fr-success: #22c55e;
|
|
568
|
+
--fr-warning: #f59e0b;
|
|
569
|
+
--fr-error: #ef4444;
|
|
570
|
+
--fr-info: #6b7280;
|
|
571
|
+
|
|
572
|
+
/* Typography - consolidated to fewer sizes */
|
|
573
|
+
--fr-font-sm: 13px;
|
|
574
|
+
--fr-font-base: 14px;
|
|
575
|
+
--fr-font-lg: 20px;
|
|
576
|
+
|
|
577
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
578
|
+
padding: 20px;
|
|
579
|
+
max-width: 900px;
|
|
580
|
+
margin: 0 auto;
|
|
581
|
+
color: var(--fr-text);
|
|
582
|
+
line-height: 1.5;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
@media (prefers-color-scheme: dark) {
|
|
586
|
+
.fr-dashboard {
|
|
587
|
+
--fr-bg: #1f2937;
|
|
588
|
+
--fr-bg-muted: #374151;
|
|
589
|
+
--fr-bg-subtle: #4b5563;
|
|
590
|
+
--fr-border: #4b5563;
|
|
591
|
+
--fr-text: #f9fafb;
|
|
592
|
+
--fr-text-muted: #9ca3af;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.fr-header {
|
|
597
|
+
display: flex;
|
|
598
|
+
justify-content: space-between;
|
|
599
|
+
align-items: center;
|
|
600
|
+
margin-bottom: 16px;
|
|
601
|
+
padding-bottom: 12px;
|
|
602
|
+
border-bottom: 1px solid var(--fr-border);
|
|
603
|
+
}
|
|
604
|
+
.fr-header__model {
|
|
605
|
+
font-weight: 600;
|
|
606
|
+
font-size: var(--fr-font-base);
|
|
607
|
+
color: var(--fr-text);
|
|
608
|
+
}
|
|
609
|
+
.fr-header__time {
|
|
610
|
+
font-weight: 600;
|
|
611
|
+
font-size: var(--fr-font-base);
|
|
612
|
+
color: var(--fr-text);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.fr-banner {
|
|
616
|
+
border-radius: 8px;
|
|
617
|
+
padding: 12px 16px;
|
|
618
|
+
margin-bottom: 20px;
|
|
619
|
+
font-size: var(--fr-font-base);
|
|
620
|
+
}
|
|
621
|
+
.fr-banner--error {
|
|
622
|
+
background: color-mix(in srgb, var(--fr-error) 10%, var(--fr-bg));
|
|
623
|
+
border: 1px solid var(--fr-error);
|
|
624
|
+
}
|
|
625
|
+
.fr-banner--warning {
|
|
626
|
+
background: color-mix(in srgb, var(--fr-warning) 10%, var(--fr-bg));
|
|
627
|
+
border: 1px solid var(--fr-warning);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.fr-cards {
|
|
631
|
+
display: grid;
|
|
632
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
633
|
+
gap: 16px;
|
|
634
|
+
margin-bottom: 24px;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.fr-card {
|
|
638
|
+
padding: 16px;
|
|
639
|
+
background: var(--fr-bg-muted);
|
|
640
|
+
border-radius: 8px;
|
|
641
|
+
text-align: center;
|
|
642
|
+
}
|
|
643
|
+
.fr-card__label {
|
|
644
|
+
font-size: var(--fr-font-sm);
|
|
645
|
+
color: var(--fr-text-muted);
|
|
646
|
+
margin-bottom: 4px;
|
|
647
|
+
}
|
|
648
|
+
.fr-card__value {
|
|
649
|
+
font-size: var(--fr-font-lg);
|
|
650
|
+
font-weight: 600;
|
|
651
|
+
}
|
|
652
|
+
.fr-card__sub {
|
|
653
|
+
font-size: var(--fr-font-sm);
|
|
654
|
+
color: var(--fr-text-muted);
|
|
655
|
+
margin-top: 2px;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.fr-badge {
|
|
659
|
+
display: inline-flex;
|
|
660
|
+
align-items: center;
|
|
661
|
+
gap: 4px;
|
|
662
|
+
padding: 4px 10px;
|
|
663
|
+
border-radius: 4px;
|
|
664
|
+
font-weight: 600;
|
|
665
|
+
font-size: var(--fr-font-sm);
|
|
666
|
+
}
|
|
667
|
+
.fr-badge--completed { background: color-mix(in srgb, var(--fr-success) 15%, transparent); color: var(--fr-success); }
|
|
668
|
+
.fr-badge--partial { background: color-mix(in srgb, var(--fr-warning) 15%, transparent); color: var(--fr-warning); }
|
|
669
|
+
.fr-badge--cancelled { background: color-mix(in srgb, var(--fr-info) 15%, transparent); color: var(--fr-info); }
|
|
670
|
+
.fr-badge--failed { background: color-mix(in srgb, var(--fr-error) 15%, transparent); color: var(--fr-error); }
|
|
671
|
+
|
|
672
|
+
.fr-section {
|
|
673
|
+
margin-bottom: 24px;
|
|
674
|
+
}
|
|
675
|
+
.fr-section__title {
|
|
676
|
+
font-size: var(--fr-font-base);
|
|
677
|
+
font-weight: 500;
|
|
678
|
+
color: var(--fr-text);
|
|
679
|
+
margin-bottom: 8px;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.fr-progress {
|
|
683
|
+
background: var(--fr-border);
|
|
684
|
+
border-radius: 4px;
|
|
685
|
+
height: 20px;
|
|
686
|
+
overflow: hidden;
|
|
687
|
+
}
|
|
688
|
+
.fr-progress__bar {
|
|
689
|
+
background: var(--fr-primary);
|
|
690
|
+
height: 100%;
|
|
691
|
+
transition: width 0.3s ease;
|
|
692
|
+
}
|
|
693
|
+
.fr-progress__text {
|
|
694
|
+
font-size: var(--fr-font-sm);
|
|
695
|
+
color: var(--fr-text-muted);
|
|
696
|
+
margin-top: 4px;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.fr-progress__segments {
|
|
700
|
+
display: flex;
|
|
701
|
+
height: 100%;
|
|
702
|
+
width: 100%;
|
|
703
|
+
}
|
|
704
|
+
.fr-progress-segment {
|
|
705
|
+
height: 100%;
|
|
706
|
+
min-width: 2px;
|
|
707
|
+
border-right: 2px solid var(--fr-bg);
|
|
708
|
+
cursor: pointer;
|
|
709
|
+
}
|
|
710
|
+
.fr-progress-segment:last-child {
|
|
711
|
+
border-right: none;
|
|
712
|
+
}
|
|
713
|
+
.fr-progress-segment--filled {
|
|
714
|
+
background: var(--fr-primary);
|
|
715
|
+
}
|
|
716
|
+
.fr-progress-segment--filled:hover {
|
|
717
|
+
background: color-mix(in srgb, var(--fr-primary) 70%, white);
|
|
718
|
+
}
|
|
719
|
+
.fr-progress-segment--prefilled {
|
|
720
|
+
background: #8b5cf6;
|
|
721
|
+
}
|
|
722
|
+
.fr-progress-segment--prefilled:hover {
|
|
723
|
+
background: color-mix(in srgb, #8b5cf6 70%, white);
|
|
724
|
+
}
|
|
725
|
+
.fr-progress-segment--skipped {
|
|
726
|
+
background: var(--fr-warning);
|
|
727
|
+
}
|
|
728
|
+
.fr-progress-segment--skipped:hover {
|
|
729
|
+
background: color-mix(in srgb, var(--fr-warning) 70%, white);
|
|
730
|
+
}
|
|
731
|
+
.fr-progress-segment--empty {
|
|
732
|
+
background: var(--fr-border);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/* Gantt chart - each call on its own row */
|
|
736
|
+
.fr-gantt {
|
|
737
|
+
margin-bottom: 8px;
|
|
738
|
+
}
|
|
739
|
+
.fr-gantt__row {
|
|
740
|
+
display: flex;
|
|
741
|
+
align-items: center;
|
|
742
|
+
height: 20px;
|
|
743
|
+
margin-bottom: 3px;
|
|
744
|
+
}
|
|
745
|
+
.fr-gantt__label {
|
|
746
|
+
width: 90px;
|
|
747
|
+
flex-shrink: 0;
|
|
748
|
+
font-size: 11px;
|
|
749
|
+
color: var(--fr-text-muted);
|
|
750
|
+
white-space: nowrap;
|
|
751
|
+
overflow: hidden;
|
|
752
|
+
text-overflow: ellipsis;
|
|
753
|
+
padding-right: 8px;
|
|
754
|
+
text-align: right;
|
|
755
|
+
}
|
|
756
|
+
.fr-gantt__track {
|
|
757
|
+
flex: 1;
|
|
758
|
+
background: var(--fr-bg-subtle);
|
|
759
|
+
border-radius: 3px;
|
|
760
|
+
height: 14px;
|
|
761
|
+
position: relative;
|
|
762
|
+
}
|
|
763
|
+
.fr-gantt__bar {
|
|
764
|
+
position: absolute;
|
|
765
|
+
top: 2px;
|
|
766
|
+
height: calc(100% - 4px);
|
|
767
|
+
min-width: 6px;
|
|
768
|
+
border-radius: 2px;
|
|
769
|
+
cursor: pointer;
|
|
770
|
+
}
|
|
771
|
+
.fr-gantt__bar:hover {
|
|
772
|
+
filter: brightness(1.15);
|
|
773
|
+
}
|
|
774
|
+
.fr-gantt__bar--llm {
|
|
775
|
+
background: var(--fr-primary);
|
|
776
|
+
}
|
|
777
|
+
.fr-gantt__bar--tool {
|
|
778
|
+
background: var(--fr-success);
|
|
779
|
+
}
|
|
780
|
+
.fr-gantt__legend {
|
|
781
|
+
display: flex;
|
|
782
|
+
gap: 16px;
|
|
783
|
+
font-size: var(--fr-font-sm);
|
|
784
|
+
color: var(--fr-text-muted);
|
|
785
|
+
margin-top: 12px;
|
|
786
|
+
padding-top: 8px;
|
|
787
|
+
border-top: 1px solid var(--fr-border);
|
|
788
|
+
}
|
|
789
|
+
.fr-gantt__legend-item {
|
|
790
|
+
display: flex;
|
|
791
|
+
align-items: center;
|
|
792
|
+
gap: 6px;
|
|
793
|
+
}
|
|
794
|
+
.fr-gantt__legend-dot {
|
|
795
|
+
width: 10px;
|
|
796
|
+
height: 10px;
|
|
797
|
+
border-radius: 2px;
|
|
798
|
+
}
|
|
799
|
+
.fr-gantt__legend-dot--llm { background: var(--fr-primary); }
|
|
800
|
+
.fr-gantt__legend-dot--tool { background: var(--fr-success); }
|
|
801
|
+
|
|
802
|
+
/* Tooltip container */
|
|
803
|
+
.fr-tooltip {
|
|
804
|
+
position: fixed;
|
|
805
|
+
background: #1f2937;
|
|
806
|
+
color: #f9fafb;
|
|
807
|
+
padding: 8px 12px;
|
|
808
|
+
border-radius: 4px;
|
|
809
|
+
font-size: var(--fr-font-sm);
|
|
810
|
+
white-space: pre-line;
|
|
811
|
+
pointer-events: none;
|
|
812
|
+
z-index: 1000;
|
|
813
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
814
|
+
opacity: 0;
|
|
815
|
+
visibility: hidden;
|
|
816
|
+
transition: opacity 0.05s ease-out, visibility 0.05s ease-out;
|
|
817
|
+
}
|
|
818
|
+
.fr-tooltip.visible {
|
|
819
|
+
opacity: 1;
|
|
820
|
+
visibility: visible;
|
|
821
|
+
transition: opacity 0.2s ease-in, visibility 0.2s ease-in;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.fr-table {
|
|
825
|
+
width: 100%;
|
|
826
|
+
border-collapse: collapse;
|
|
827
|
+
font-size: var(--fr-font-sm);
|
|
828
|
+
}
|
|
829
|
+
.fr-table th {
|
|
830
|
+
padding: 8px 12px;
|
|
831
|
+
text-align: left;
|
|
832
|
+
font-weight: 600;
|
|
833
|
+
background: var(--fr-bg-subtle);
|
|
834
|
+
}
|
|
835
|
+
.fr-table th:not(:first-child) { text-align: center; }
|
|
836
|
+
.fr-table td {
|
|
837
|
+
padding: 8px 12px;
|
|
838
|
+
border-bottom: 1px solid var(--fr-border);
|
|
839
|
+
}
|
|
840
|
+
.fr-table td:not(:first-child) { text-align: center; }
|
|
841
|
+
|
|
842
|
+
.fr-details {
|
|
843
|
+
border: none;
|
|
844
|
+
background: none;
|
|
845
|
+
}
|
|
846
|
+
.fr-details > summary {
|
|
847
|
+
cursor: pointer;
|
|
848
|
+
font-size: var(--fr-font-base);
|
|
849
|
+
font-weight: 500;
|
|
850
|
+
color: var(--fr-text);
|
|
851
|
+
padding: 8px 0;
|
|
852
|
+
list-style: none;
|
|
853
|
+
}
|
|
854
|
+
.fr-details > summary::-webkit-details-marker { display: none; }
|
|
855
|
+
.fr-details > summary::before {
|
|
856
|
+
content: '▶';
|
|
857
|
+
display: inline-block;
|
|
858
|
+
margin-right: 8px;
|
|
859
|
+
transition: transform 0.2s;
|
|
860
|
+
font-size: 11px;
|
|
861
|
+
}
|
|
862
|
+
.fr-details[open] > summary::before {
|
|
863
|
+
transform: rotate(90deg);
|
|
864
|
+
}
|
|
865
|
+
.fr-details__content {
|
|
866
|
+
background: var(--fr-bg-muted);
|
|
867
|
+
border-radius: 8px;
|
|
868
|
+
padding: 16px;
|
|
869
|
+
margin-top: 8px;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.fr-turn {
|
|
873
|
+
margin-bottom: 8px;
|
|
874
|
+
background: var(--fr-bg-muted);
|
|
875
|
+
border-radius: 4px;
|
|
876
|
+
}
|
|
877
|
+
.fr-turn summary {
|
|
878
|
+
cursor: pointer;
|
|
879
|
+
padding: 12px;
|
|
880
|
+
font-size: var(--fr-font-sm);
|
|
881
|
+
list-style: none;
|
|
882
|
+
}
|
|
883
|
+
.fr-turn summary::-webkit-details-marker { display: none; }
|
|
884
|
+
.fr-turn summary::before {
|
|
885
|
+
content: '▶';
|
|
886
|
+
display: inline-block;
|
|
887
|
+
margin-right: 8px;
|
|
888
|
+
transition: transform 0.2s;
|
|
889
|
+
font-size: 11px;
|
|
890
|
+
}
|
|
891
|
+
.fr-turn[open] summary::before {
|
|
892
|
+
transform: rotate(90deg);
|
|
893
|
+
}
|
|
894
|
+
.fr-turn__content {
|
|
895
|
+
padding: 0 12px 12px;
|
|
896
|
+
}
|
|
897
|
+
.fr-turn__tools {
|
|
898
|
+
margin: 0;
|
|
899
|
+
padding-left: 20px;
|
|
900
|
+
list-style: none;
|
|
901
|
+
}
|
|
902
|
+
.fr-turn__tool {
|
|
903
|
+
margin: 4px 0;
|
|
904
|
+
font-size: var(--fr-font-sm);
|
|
905
|
+
color: var(--fr-text-muted);
|
|
906
|
+
}
|
|
907
|
+
.fr-turn__tool--error { color: var(--fr-error); }
|
|
908
|
+
|
|
909
|
+
.fr-turn__query {
|
|
910
|
+
color: var(--fr-primary);
|
|
911
|
+
font-style: italic;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.fr-turn__patches {
|
|
915
|
+
margin: 4px 0 8px 20px;
|
|
916
|
+
padding: 8px 12px;
|
|
917
|
+
background: var(--fr-bg-subtle);
|
|
918
|
+
border-radius: 4px;
|
|
919
|
+
font-size: var(--fr-font-sm);
|
|
920
|
+
}
|
|
921
|
+
.fr-turn__patch {
|
|
922
|
+
margin: 4px 0;
|
|
923
|
+
padding: 4px 0;
|
|
924
|
+
border-bottom: 1px solid var(--fr-border);
|
|
925
|
+
}
|
|
926
|
+
.fr-turn__patch:last-child {
|
|
927
|
+
border-bottom: none;
|
|
928
|
+
margin-bottom: 0;
|
|
929
|
+
padding-bottom: 0;
|
|
930
|
+
}
|
|
931
|
+
.fr-turn__patch-field {
|
|
932
|
+
font-weight: 600;
|
|
933
|
+
color: var(--fr-text);
|
|
934
|
+
}
|
|
935
|
+
.fr-turn__patch-op {
|
|
936
|
+
font-size: 11px;
|
|
937
|
+
padding: 1px 4px;
|
|
938
|
+
border-radius: 2px;
|
|
939
|
+
background: var(--fr-bg-muted);
|
|
940
|
+
color: var(--fr-text-muted);
|
|
941
|
+
margin-left: 6px;
|
|
942
|
+
}
|
|
943
|
+
.fr-turn__patch-value {
|
|
944
|
+
display: block;
|
|
945
|
+
margin-top: 2px;
|
|
946
|
+
color: var(--fr-text-muted);
|
|
947
|
+
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
|
948
|
+
word-break: break-word;
|
|
949
|
+
white-space: pre-wrap;
|
|
950
|
+
max-height: 200px;
|
|
951
|
+
overflow: auto;
|
|
952
|
+
}
|
|
953
|
+
.fr-turn__patch-value--skip {
|
|
954
|
+
color: var(--fr-warning);
|
|
955
|
+
font-style: italic;
|
|
956
|
+
}
|
|
957
|
+
.fr-turn__patch-value--clear {
|
|
958
|
+
color: var(--fr-info);
|
|
959
|
+
font-style: italic;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
.fr-raw {
|
|
963
|
+
position: relative;
|
|
964
|
+
}
|
|
965
|
+
.fr-copy-btn {
|
|
966
|
+
position: absolute;
|
|
967
|
+
top: 8px;
|
|
968
|
+
right: 8px;
|
|
969
|
+
padding: 4px 8px;
|
|
970
|
+
font-size: var(--fr-font-sm);
|
|
971
|
+
background: var(--fr-bg-subtle);
|
|
972
|
+
border: 1px solid var(--fr-border);
|
|
973
|
+
border-radius: 4px;
|
|
974
|
+
cursor: pointer;
|
|
975
|
+
color: var(--fr-text-muted);
|
|
976
|
+
transition: all 0.15s;
|
|
977
|
+
}
|
|
978
|
+
.fr-copy-btn:hover {
|
|
979
|
+
background: var(--fr-border);
|
|
980
|
+
color: var(--fr-text);
|
|
981
|
+
}
|
|
982
|
+
.fr-copy-btn:active {
|
|
983
|
+
transform: scale(0.95);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/* Scoped pre styles to override parent .tab-content pre */
|
|
987
|
+
.fr-dashboard pre {
|
|
988
|
+
background: var(--fr-bg-muted);
|
|
989
|
+
color: var(--fr-text);
|
|
990
|
+
padding: 1rem;
|
|
991
|
+
border-radius: 6px;
|
|
992
|
+
border: 1px solid var(--fr-border);
|
|
993
|
+
overflow-x: auto;
|
|
994
|
+
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
|
995
|
+
font-size: 0.85rem;
|
|
996
|
+
line-height: 1.5;
|
|
997
|
+
margin: 0;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/* Override syntax highlighting colors for dark mode compatibility */
|
|
1001
|
+
.fr-dashboard .syn-key { color: var(--fr-primary); }
|
|
1002
|
+
.fr-dashboard .syn-string { color: var(--fr-success); }
|
|
1003
|
+
.fr-dashboard .syn-number { color: var(--fr-primary); }
|
|
1004
|
+
.fr-dashboard .syn-bool { color: var(--fr-warning); }
|
|
1005
|
+
.fr-dashboard .syn-null { color: var(--fr-error); }
|
|
1006
|
+
|
|
1007
|
+
@media (max-width: 600px) {
|
|
1008
|
+
.fr-dashboard { padding: 12px; }
|
|
1009
|
+
.fr-cards { grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
|
1010
|
+
.fr-card { padding: 12px; }
|
|
1011
|
+
.fr-card__value { font-size: 18px; }
|
|
1012
|
+
.fr-table { font-size: var(--fr-font-sm); }
|
|
1013
|
+
.fr-table th, .fr-table td { padding: 6px 8px; }
|
|
1014
|
+
}
|
|
1015
|
+
</style>
|
|
1016
|
+
`;
|
|
1017
|
+
/**
|
|
1018
|
+
* Render fill record content (dashboard-style visualization).
|
|
1019
|
+
* Uses CSS custom properties for theming with automatic dark mode support.
|
|
1020
|
+
* Mobile responsive with grid-based layout.
|
|
1021
|
+
*
|
|
1022
|
+
* @public Exported for testing and reuse.
|
|
1023
|
+
*/
|
|
1024
|
+
function renderFillRecordContent(record) {
|
|
1025
|
+
const { status, statusDetail, startedAt, durationMs, llm, formProgress, toolSummary, timeline } = record;
|
|
1026
|
+
const startDate = new Date(startedAt);
|
|
1027
|
+
const formattedDate = startDate.toLocaleDateString("en-US", {
|
|
1028
|
+
month: "short",
|
|
1029
|
+
day: "numeric",
|
|
1030
|
+
year: "numeric"
|
|
1031
|
+
});
|
|
1032
|
+
const formattedTime = startDate.toLocaleTimeString("en-US", {
|
|
1033
|
+
hour: "numeric",
|
|
1034
|
+
minute: "2-digit",
|
|
1035
|
+
hour12: true
|
|
1036
|
+
});
|
|
1037
|
+
const headerInfo = `
|
|
1038
|
+
<div class="fr-header">
|
|
1039
|
+
<div class="fr-header__model">${escapeHtml(llm.model)}</div>
|
|
1040
|
+
<div class="fr-header__time">${formattedDate} at ${formattedTime}</div>
|
|
1041
|
+
</div>
|
|
1042
|
+
`;
|
|
1043
|
+
let statusBanner = "";
|
|
1044
|
+
if (status !== "completed") {
|
|
1045
|
+
const bannerClass = status === "failed" ? "fr-banner--error" : "fr-banner--warning";
|
|
1046
|
+
const icon = status === "failed" ? "✕" : "⚠";
|
|
1047
|
+
const title = status === "failed" ? "FAILED" : status === "cancelled" ? "CANCELLED" : "PARTIAL";
|
|
1048
|
+
const msg = statusDetail ?? (status === "partial" ? "Did not complete all fields" : "");
|
|
1049
|
+
statusBanner = `<div class="fr-banner ${bannerClass}"><strong>${icon} ${title}${msg ? ":" : ""}</strong>${msg ? ` ${escapeHtml(msg)}` : ""}</div>`;
|
|
1050
|
+
}
|
|
1051
|
+
const totalTokens = llm.inputTokens + llm.outputTokens;
|
|
1052
|
+
const summaryCards = `
|
|
1053
|
+
<div class="fr-cards">
|
|
1054
|
+
<div class="fr-card">
|
|
1055
|
+
<div class="fr-card__label">Status</div>
|
|
1056
|
+
<div><span class="${`fr-badge fr-badge--${status}`}">${{
|
|
1057
|
+
completed: "✓",
|
|
1058
|
+
partial: "⚠",
|
|
1059
|
+
cancelled: "⊘",
|
|
1060
|
+
failed: "✕"
|
|
1061
|
+
}[status] ?? "?"} ${status.charAt(0).toUpperCase() + status.slice(1)}</span></div>
|
|
1062
|
+
</div>
|
|
1063
|
+
<div class="fr-card">
|
|
1064
|
+
<div class="fr-card__label">Duration</div>
|
|
1065
|
+
<div class="fr-card__value">${formatDuration(durationMs)}</div>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div class="fr-card">
|
|
1068
|
+
<div class="fr-card__label">Turns</div>
|
|
1069
|
+
<div class="fr-card__value">${timeline.length}</div>
|
|
1070
|
+
</div>
|
|
1071
|
+
<div class="fr-card">
|
|
1072
|
+
<div class="fr-card__label">Tokens</div>
|
|
1073
|
+
<div class="fr-card__value">${formatTokens(totalTokens)}</div>
|
|
1074
|
+
<div class="fr-card__sub">${formatTokens(llm.inputTokens)} in / ${formatTokens(llm.outputTokens)} out</div>
|
|
1075
|
+
</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
`;
|
|
1078
|
+
const fieldsMap = /* @__PURE__ */ new Map();
|
|
1079
|
+
for (const turn of timeline) for (const tc of turn.toolCalls) if (tc.tool === "fill_form" && tc.input.patches) {
|
|
1080
|
+
const patches = tc.input.patches;
|
|
1081
|
+
for (const patch of patches) if (patch.fieldId && patch.op) fieldsMap.set(patch.fieldId, {
|
|
1082
|
+
fieldId: patch.fieldId,
|
|
1083
|
+
op: patch.op,
|
|
1084
|
+
turnNumber: turn.turnNumber
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
const fieldsFilled = Array.from(fieldsMap.values());
|
|
1088
|
+
const totalFields = formProgress.totalFields;
|
|
1089
|
+
const filledFields = formProgress.filledFields;
|
|
1090
|
+
const skippedFields = formProgress.skippedFields;
|
|
1091
|
+
const abortedFields = formProgress.abortedFields ?? 0;
|
|
1092
|
+
const progressPercent = totalFields > 0 ? Math.round(filledFields / totalFields * 100) : 0;
|
|
1093
|
+
const segmentWidth = totalFields > 0 ? 100 / totalFields : 0;
|
|
1094
|
+
const aiFilledFields = fieldsFilled.filter((f) => f.op !== "skip_field" && f.op !== "abort_field");
|
|
1095
|
+
const aiFilledSegmentsHtml = aiFilledFields.map((f) => {
|
|
1096
|
+
const opLabel = f.op.replace(/_/g, " ");
|
|
1097
|
+
return `<div class="fr-progress-segment fr-progress-segment--filled" style="width: ${segmentWidth}%" data-tooltip="${escapeHtml(`${f.fieldId}\n${opLabel}\nTurn ${f.turnNumber}`)}" onmouseenter="frShowTip(this)" onmouseleave="frHideTip()"></div>`;
|
|
1098
|
+
}).join("");
|
|
1099
|
+
const prefilledCount = Math.max(0, filledFields - aiFilledFields.length);
|
|
1100
|
+
const prefilledSegmentsHtml = prefilledCount > 0 ? `<div class="fr-progress-segment fr-progress-segment--prefilled" style="width: ${segmentWidth * prefilledCount}%" data-tooltip="Pre-filled (${prefilledCount} field${prefilledCount !== 1 ? "s" : ""})" onmouseenter="frShowTip(this)" onmouseleave="frHideTip()"></div>` : "";
|
|
1101
|
+
const skippedSegmentsHtml = fieldsFilled.filter((f) => f.op === "skip_field" || f.op === "abort_field").map((f) => {
|
|
1102
|
+
const opLabel = f.op === "skip_field" ? "skipped" : "aborted";
|
|
1103
|
+
return `<div class="fr-progress-segment fr-progress-segment--skipped" style="width: ${segmentWidth}%" data-tooltip="${escapeHtml(`${f.fieldId}\n${opLabel}\nTurn ${f.turnNumber}`)}" onmouseenter="frShowTip(this)" onmouseleave="frHideTip()"></div>`;
|
|
1104
|
+
}).join("");
|
|
1105
|
+
const unfilledCount = totalFields - filledFields - skippedFields - abortedFields;
|
|
1106
|
+
const unfilledSegmentsHtml = unfilledCount > 0 ? `<div class="fr-progress-segment fr-progress-segment--empty" style="width: ${segmentWidth * unfilledCount}%"></div>` : "";
|
|
1107
|
+
const progressDetails = [];
|
|
1108
|
+
if (prefilledCount > 0) progressDetails.push(`${prefilledCount} pre-filled`);
|
|
1109
|
+
if (skippedFields > 0) progressDetails.push(`${skippedFields} skipped`);
|
|
1110
|
+
const progressBar = `
|
|
1111
|
+
<div class="fr-section">
|
|
1112
|
+
<div class="fr-section__title">Progress</div>
|
|
1113
|
+
<div class="fr-progress">
|
|
1114
|
+
<div class="fr-progress__segments">
|
|
1115
|
+
${prefilledSegmentsHtml}${aiFilledSegmentsHtml}${skippedSegmentsHtml}${unfilledSegmentsHtml}
|
|
1116
|
+
</div>
|
|
1117
|
+
</div>
|
|
1118
|
+
<div class="fr-progress__text">
|
|
1119
|
+
${filledFields}/${totalFields} fields filled (${progressPercent}%)${progressDetails.length > 0 ? ` • ${progressDetails.join(" • ")}` : ""}
|
|
1120
|
+
</div>
|
|
1121
|
+
</div>
|
|
1122
|
+
`;
|
|
1123
|
+
const totalMs = durationMs;
|
|
1124
|
+
const llmCallCount = llm.totalCalls;
|
|
1125
|
+
const toolCallCount = toolSummary.totalCalls;
|
|
1126
|
+
const timelineEvents = [];
|
|
1127
|
+
for (const turn of timeline) {
|
|
1128
|
+
const toolTimeInTurn = turn.toolCalls.reduce((sum, tc) => sum + tc.durationMs, 0);
|
|
1129
|
+
const llmTimeInTurn = Math.max(0, turn.durationMs - toolTimeInTurn);
|
|
1130
|
+
if (llmTimeInTurn > 0) timelineEvents.push({
|
|
1131
|
+
type: "llm",
|
|
1132
|
+
startMs: turn.startMs,
|
|
1133
|
+
durationMs: llmTimeInTurn,
|
|
1134
|
+
turnNumber: turn.turnNumber,
|
|
1135
|
+
label: `Turn ${turn.turnNumber}`,
|
|
1136
|
+
tokens: {
|
|
1137
|
+
input: turn.tokens.input,
|
|
1138
|
+
output: turn.tokens.output,
|
|
1139
|
+
total: turn.tokens.input + turn.tokens.output
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
for (const tc of turn.toolCalls) timelineEvents.push({
|
|
1143
|
+
type: "tool",
|
|
1144
|
+
startMs: tc.startMs,
|
|
1145
|
+
durationMs: tc.durationMs,
|
|
1146
|
+
turnNumber: turn.turnNumber,
|
|
1147
|
+
label: tc.tool
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
const ganttRowsHtml = timelineEvents.map((e) => {
|
|
1151
|
+
const leftPct = totalMs > 0 ? e.startMs / totalMs * 100 : 0;
|
|
1152
|
+
const widthPct = totalMs > 0 ? e.durationMs / totalMs * 100 : 0;
|
|
1153
|
+
const barClass = e.type === "llm" ? "fr-gantt__bar--llm" : "fr-gantt__bar--tool";
|
|
1154
|
+
const startTime = `Start: ${formatDuration(e.startMs)}`;
|
|
1155
|
+
const tooltip = e.type === "llm" ? `${e.label} ${startTime} Duration: ${formatDuration(e.durationMs)} ${formatTokens(e.tokens?.total ?? 0)} tokens (${formatTokens(e.tokens?.input ?? 0)} in / ${formatTokens(e.tokens?.output ?? 0)} out)` : `${e.label} ${startTime} Duration: ${formatDuration(e.durationMs)} Turn ${e.turnNumber}`;
|
|
1156
|
+
return `
|
|
1157
|
+
<div class="fr-gantt__row">
|
|
1158
|
+
<div class="fr-gantt__label">${escapeHtml(e.label)}</div>
|
|
1159
|
+
<div class="fr-gantt__track">
|
|
1160
|
+
<div class="fr-gantt__bar ${barClass}" style="left: ${leftPct}%; width: ${widthPct}%" data-tooltip="${tooltip}" onmouseenter="frShowTip(this)" onmouseleave="frHideTip()"></div>
|
|
1161
|
+
</div>
|
|
1162
|
+
</div>`;
|
|
1163
|
+
}).join("");
|
|
1164
|
+
const llmTotalMs = timelineEvents.filter((e) => e.type === "llm").reduce((sum, e) => sum + e.durationMs, 0);
|
|
1165
|
+
const toolTotalMs = timelineEvents.filter((e) => e.type === "tool").reduce((sum, e) => sum + e.durationMs, 0);
|
|
1166
|
+
const timingSection = `
|
|
1167
|
+
<details class="fr-details fr-section" open>
|
|
1168
|
+
<summary>Timeline (${formatDuration(totalMs)} total)</summary>
|
|
1169
|
+
<div class="fr-details__content">
|
|
1170
|
+
<div class="fr-gantt">
|
|
1171
|
+
${ganttRowsHtml}
|
|
1172
|
+
<div class="fr-gantt__legend">
|
|
1173
|
+
<div class="fr-gantt__legend-item">
|
|
1174
|
+
<div class="fr-gantt__legend-dot fr-gantt__legend-dot--llm"></div>
|
|
1175
|
+
<span>LLM (${llmCallCount} call${llmCallCount !== 1 ? "s" : ""}, ${formatDuration(llmTotalMs)})</span>
|
|
1176
|
+
</div>
|
|
1177
|
+
<div class="fr-gantt__legend-item">
|
|
1178
|
+
<div class="fr-gantt__legend-dot fr-gantt__legend-dot--tool"></div>
|
|
1179
|
+
<span>Tools (${toolCallCount} call${toolCallCount !== 1 ? "s" : ""}, ${formatDuration(toolTotalMs)})</span>
|
|
1180
|
+
</div>
|
|
1181
|
+
</div>
|
|
1182
|
+
</div>
|
|
1183
|
+
</div>
|
|
1184
|
+
</details>
|
|
1185
|
+
`;
|
|
1186
|
+
let toolSection = "";
|
|
1187
|
+
if (toolSummary.byTool.length > 0) toolSection = `
|
|
1188
|
+
<details class="fr-details fr-section" open>
|
|
1189
|
+
<summary>Tool Summary</summary>
|
|
1190
|
+
<div style="overflow-x: auto; margin-top: 8px;">
|
|
1191
|
+
<table class="fr-table">
|
|
1192
|
+
<thead><tr><th>Tool</th><th>Calls</th><th>Success</th><th>Avg</th><th>p95</th></tr></thead>
|
|
1193
|
+
<tbody>${toolSummary.byTool.map((t) => `
|
|
1194
|
+
<tr>
|
|
1195
|
+
<td>${escapeHtml(t.toolName)}</td>
|
|
1196
|
+
<td>${t.callCount}</td>
|
|
1197
|
+
<td>${t.successCount === t.callCount ? "100%" : `${Math.round(t.successCount / t.callCount * 100)}%`}</td>
|
|
1198
|
+
<td>${formatDuration(t.timing.avgMs)}</td>
|
|
1199
|
+
<td>${formatDuration(t.timing.p95Ms)}</td>
|
|
1200
|
+
</tr>
|
|
1201
|
+
`).join("")}</tbody>
|
|
1202
|
+
</table>
|
|
1203
|
+
</div>
|
|
1204
|
+
</details>
|
|
1205
|
+
`;
|
|
1206
|
+
let timelineSection = "";
|
|
1207
|
+
if (timeline.length > 0) {
|
|
1208
|
+
const timelineItems = timeline.map((turn) => {
|
|
1209
|
+
const turnTokens = turn.tokens.input + turn.tokens.output;
|
|
1210
|
+
const toolCallsList = turn.toolCalls.map((tc) => renderToolCall(tc)).join("");
|
|
1211
|
+
const patchInfo = turn.patchesApplied > 0 ? ` • ${turn.patchesApplied} patches` : "";
|
|
1212
|
+
const rejectedInfo = turn.patchesRejected > 0 ? ` <span style="color: var(--fr-error)">(${turn.patchesRejected} rejected)</span>` : "";
|
|
1213
|
+
return `
|
|
1214
|
+
<details class="fr-turn">
|
|
1215
|
+
<summary><strong>Turn ${turn.turnNumber}</strong> • Order ${turn.order} • ${formatDuration(turn.durationMs)} • ${formatTokens(turnTokens)} tokens${patchInfo}${rejectedInfo}</summary>
|
|
1216
|
+
<div class="fr-turn__content">
|
|
1217
|
+
${turn.toolCalls.length > 0 ? `<ul class="fr-turn__tools">${toolCallsList}</ul>` : "<span class=\"fr-turn__tool\">No tool calls</span>"}
|
|
1218
|
+
</div>
|
|
1219
|
+
</details>
|
|
1220
|
+
`;
|
|
1221
|
+
}).join("");
|
|
1222
|
+
timelineSection = `
|
|
1223
|
+
<details class="fr-details fr-section">
|
|
1224
|
+
<summary>Turn Details (${timeline.length} turns)</summary>
|
|
1225
|
+
<div style="margin-top: 8px;">${timelineItems}</div>
|
|
1226
|
+
</details>
|
|
1227
|
+
`;
|
|
1228
|
+
}
|
|
1229
|
+
const rawSection = `
|
|
1230
|
+
<details class="fr-details fr-section">
|
|
1231
|
+
<summary>Raw YAML</summary>
|
|
1232
|
+
<div class="fr-raw" style="margin-top: 8px;">
|
|
1233
|
+
<button class="fr-copy-btn" onclick="frCopyYaml(this)">Copy</button>
|
|
1234
|
+
${renderYamlContent(YAML.stringify(record, { lineWidth: 0 }))}
|
|
1235
|
+
</div>
|
|
1236
|
+
</details>
|
|
1237
|
+
`;
|
|
1238
|
+
return `
|
|
1239
|
+
${FILL_RECORD_STYLES}
|
|
1240
|
+
<div id="fr-tooltip" class="fr-tooltip"></div>
|
|
1241
|
+
<div class="fr-dashboard">
|
|
1242
|
+
${headerInfo}
|
|
1243
|
+
${statusBanner}
|
|
1244
|
+
${summaryCards}
|
|
1245
|
+
${progressBar}
|
|
1246
|
+
${timingSection}
|
|
1247
|
+
${toolSection}
|
|
1248
|
+
${timelineSection}
|
|
1249
|
+
${rawSection}
|
|
1250
|
+
</div>
|
|
1251
|
+
`;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
//#endregion
|
|
1255
|
+
export { renderJsonContent as a, renderViewContent as c, formatDuration as d, formatTokens as f, highlightYamlValue as i, renderYamlContent as l, FILL_RECORD_STYLES as n, renderMarkdownContent as o, renderFillRecordContent as r, renderSourceContent as s, FILL_RECORD_SCRIPTS as t, escapeHtml as u };
|
|
1256
|
+
//# sourceMappingURL=fillRecordRenderer-CruJrLkj.mjs.map
|