markform 0.1.20 → 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 +44 -12
- package/dist/ai-sdk.d.mts +1 -1
- package/dist/ai-sdk.mjs +2 -2
- package/dist/{apply-DIvm1b1s.mjs → apply-C7mO7VkZ.mjs} +158 -95
- package/dist/apply-C7mO7VkZ.mjs.map +1 -0
- package/dist/bin.mjs +1 -1
- package/dist/{cli-FFMoEhFS.mjs → cli-C8F9yDsv.mjs} +102 -1225
- package/dist/cli-C8F9yDsv.mjs.map +1 -0
- package/dist/cli.mjs +1 -1
- package/dist/{coreTypes-CkxML8g2.d.mts → coreTypes-BlsJkU1w.d.mts} +28 -2
- package/dist/{coreTypes-CPKXf2dc.mjs → coreTypes-CTLr-NGd.mjs} +24 -3
- package/dist/coreTypes-CTLr-NGd.mjs.map +1 -0
- 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 +22 -342
- package/dist/index.mjs +5 -5
- package/dist/render.d.mts +74 -0
- package/dist/render.mjs +4 -0
- package/dist/{session-CK0x28RO.mjs → session-BCcltrLA.mjs} +2 -2
- package/dist/{session-CK0x28RO.mjs.map → session-BCcltrLA.mjs.map} +1 -1
- package/dist/{session-ZHBi3LVQ.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-wR7GoftB.mjs → src-CbRnGzMK.mjs} +205 -138
- package/dist/src-CbRnGzMK.mjs.map +1 -0
- package/dist/urlFormat-lls7CsEP.mjs +71 -0
- package/dist/urlFormat-lls7CsEP.mjs.map +1 -0
- package/docs/markform-apis.md +53 -0
- package/examples/movie-research/movie-deep-research-mock-filled.form.md +320 -343
- package/examples/movie-research/movie-deep-research.form.md +273 -308
- package/examples/movie-research/movie-research-demo.form.md +27 -41
- package/examples/parallel/parallel-research.form.md +33 -29
- package/examples/parallel/parallel-research.mock.filled.form.md +88 -0
- package/examples/rejection-test/rejection-test-mock-filled.form.md +21 -16
- package/examples/rejection-test/rejection-test-mock-filled.schema.json +1 -1
- package/examples/rejection-test/rejection-test.form.md +17 -15
- package/examples/rejection-test/rejection-test.session.yaml +88 -60
- package/examples/simple/simple-mock-filled.form.md +113 -126
- package/examples/simple/simple-mock-filled.schema.json +2 -3
- package/examples/simple/simple-skipped-filled.form.md +112 -129
- package/examples/simple/simple-skipped-filled.report.md +8 -8
- package/examples/simple/simple-skipped-filled.schema.json +2 -3
- package/examples/simple/simple-tags-syntax.form.md +32 -0
- package/examples/simple/simple-with-skips.session.yaml +663 -627
- package/examples/simple/simple.form.md +97 -113
- package/examples/simple/simple.schema.json +2 -3
- package/examples/simple/simple.session.yaml +663 -627
- package/examples/startup-deep-research/startup-deep-research.form.md +191 -235
- package/examples/startup-research/startup-research-mock-filled.form.md +128 -147
- package/examples/startup-research/startup-research.form.md +90 -129
- package/examples/twitter-thread/twitter-thread.form.md +373 -0
- package/package.json +5 -1
- package/dist/apply-DIvm1b1s.mjs.map +0 -1
- package/dist/cli-FFMoEhFS.mjs.map +0 -1
- package/dist/coreTypes-CPKXf2dc.mjs.map +0 -1
- package/dist/src-wR7GoftB.mjs.map +0 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
import {
|
|
3
|
-
import { $ as
|
|
4
|
-
import { C as resolveModel, D as computeExecutionPlan, E as FillRecordCollector, H as formToJsonSchema, S as getProviderNames, T as createLiveAgent, U as parseForm, _ as fillForm, g as resolveHarnessConfig, h as formatFillRecordSummary, i as runResearch, j as createHarness, k as createMockAgent, m as stripUnstableFillRecordFields, n as isResearchForm, t as VERSION, w as buildMockWireFormat, x as getProviderInfo } from "./src-
|
|
5
|
-
import { n as serializeSession } from "./session-
|
|
6
|
-
import { _ as writeFile, a as formatPath, c as logError, d as logTiming, f as logVerbose, g as stripHtmlComments, h as shouldUseColors, i as formatOutput, l as logInfo, m as readFile$1, n as createSpinner, o as getCommandContext, p as logWarn, r as ensureFormsDir, s as logDryRun, t as OUTPUT_FORMATS, u as logSuccess } from "./shared-
|
|
2
|
+
import { R as PatchSchema } from "./coreTypes-CTLr-NGd.mjs";
|
|
3
|
+
import { $ as WEB_SEARCH_CONFIG, A as DEFAULT_FORMS_DIR, F as DEFAULT_MAX_TURNS, G as deriveExportPath, H as MAX_FORMS_IN_MENU, I as DEFAULT_PORT, J as deriveSchemaPath, K as deriveFillRecordPath, N as DEFAULT_MAX_PATCHES_PER_TURN, Q as SUGGESTED_LLMS, R as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, U as REPORT_EXTENSION, W as USER_ROLE, X as parseRolesFlag, Y as detectFileType, c as computeProgressSummary, d as serializeForm, et as formatSuggestedLlms, f as serializeRawMarkdown, g as validateSyntaxConsistency, i as inspect, j as DEFAULT_MAX_ISSUES_PER_TURN, k as AGENT_ROLE, l as computeStructureSummary, n as getAllFields, nt as hasWebSearchSupport, p as serializeReport, q as deriveReportPath, rt as parseModelIdForDisplay, t as applyPatches, z as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN } from "./apply-C7mO7VkZ.mjs";
|
|
4
|
+
import { C as resolveModel, D as computeExecutionPlan, E as FillRecordCollector, H as formToJsonSchema, S as getProviderNames, T as createLiveAgent, U as parseForm, _ as fillForm, g as resolveHarnessConfig, h as formatFillRecordSummary, i as runResearch, j as createHarness, k as createMockAgent, m as stripUnstableFillRecordFields, n as isResearchForm, t as VERSION, w as buildMockWireFormat, x as getProviderInfo } from "./src-CbRnGzMK.mjs";
|
|
5
|
+
import { n as serializeSession } from "./session-BCcltrLA.mjs";
|
|
6
|
+
import { _ as writeFile, a as formatPath, c as logError, d as logTiming, f as logVerbose, g as stripHtmlComments, h as shouldUseColors, i as formatOutput, l as logInfo, m as readFile$1, n as createSpinner, o as getCommandContext, p as logWarn, r as ensureFormsDir, s as logDryRun, t as OUTPUT_FORMATS, u as logSuccess } from "./shared-CsdT2T7k.mjs";
|
|
7
|
+
import { a as renderJsonContent, c as renderViewContent, i as highlightYamlValue, l as renderYamlContent, o as renderMarkdownContent, r as renderFillRecordContent, s as renderSourceContent, u as escapeHtml } from "./fillRecordRenderer-CruJrLkj.mjs";
|
|
7
8
|
import Markdoc from "@markdoc/markdoc";
|
|
8
9
|
import YAML from "yaml";
|
|
9
10
|
import { Command } from "commander";
|
|
@@ -1947,7 +1948,7 @@ function truncate(value, maxLength = PATCH_VALUE_MAX_LENGTH) {
|
|
|
1947
1948
|
/**
|
|
1948
1949
|
* Format a patch value for display with truncation.
|
|
1949
1950
|
*/
|
|
1950
|
-
function formatPatchValue
|
|
1951
|
+
function formatPatchValue(patch) {
|
|
1951
1952
|
switch (patch.op) {
|
|
1952
1953
|
case "set_string": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
1953
1954
|
case "set_number": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
@@ -2052,7 +2053,7 @@ function createFillLoggingCallbacks(ctx, options = {}) {
|
|
|
2052
2053
|
logInfo(ctx, ` -> ${pc.yellow(String(patches.length))} patch(es):`);
|
|
2053
2054
|
for (const patch of patches) {
|
|
2054
2055
|
const typeName = formatPatchType(patch);
|
|
2055
|
-
const value = formatPatchValue
|
|
2056
|
+
const value = formatPatchValue(patch);
|
|
2056
2057
|
const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
|
|
2057
2058
|
if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
|
|
2058
2059
|
else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
|
|
@@ -2638,7 +2639,10 @@ function registerExamplesCommand(program) {
|
|
|
2638
2639
|
* Register the export command.
|
|
2639
2640
|
*/
|
|
2640
2641
|
function registerExportCommand(program) {
|
|
2641
|
-
program.command("export <file>").description("Export form as markform (default), markdown (readable), or json/yaml for structured data").option("--compact", "Output compact JSON (no formatting, only for JSON format)").option("--normalize", "Regenerate form without preserving external content").
|
|
2642
|
+
program.command("export <file>").description("Export form as markform (default), markdown (readable), or json/yaml for structured data").option("--compact", "Output compact JSON (no formatting, only for JSON format)").option("--normalize", "Regenerate form without preserving external content").option("--syntax <style>", "Output syntax style: comments (HTML comments) or tags (Markdoc). Default: preserve original.", (value) => {
|
|
2643
|
+
if (value !== "comments" && value !== "tags") throw new Error(`Invalid syntax value: ${value}. Must be 'comments' or 'tags'.`);
|
|
2644
|
+
return value;
|
|
2645
|
+
}).action(async (file, options, cmd) => {
|
|
2642
2646
|
const ctx = getCommandContext(cmd);
|
|
2643
2647
|
let format = "markform";
|
|
2644
2648
|
if (ctx.format === "json") format = "json";
|
|
@@ -2651,7 +2655,10 @@ function registerExportCommand(program) {
|
|
|
2651
2655
|
logVerbose(ctx, "Parsing form...");
|
|
2652
2656
|
const form = parseForm(content);
|
|
2653
2657
|
if (format === "markform") {
|
|
2654
|
-
console.log(serializeForm(form, {
|
|
2658
|
+
console.log(serializeForm(form, {
|
|
2659
|
+
preserveContent: !options.normalize,
|
|
2660
|
+
syntaxStyle: options.syntax
|
|
2661
|
+
}));
|
|
2655
2662
|
return;
|
|
2656
2663
|
}
|
|
2657
2664
|
if (format === "markdown") {
|
|
@@ -2684,7 +2691,10 @@ function registerExportCommand(program) {
|
|
|
2684
2691
|
schema,
|
|
2685
2692
|
values,
|
|
2686
2693
|
notes: form.notes,
|
|
2687
|
-
markdown: serializeForm(form, {
|
|
2694
|
+
markdown: serializeForm(form, {
|
|
2695
|
+
preserveContent: !options.normalize,
|
|
2696
|
+
syntaxStyle: options.syntax
|
|
2697
|
+
})
|
|
2688
2698
|
};
|
|
2689
2699
|
if (format === "json") if (options.compact) console.log(JSON.stringify(output));
|
|
2690
2700
|
else console.log(JSON.stringify(output, null, 2));
|
|
@@ -2769,7 +2779,7 @@ function formatConsoleSession(transcript, useColors) {
|
|
|
2769
2779
|
* Register the fill command.
|
|
2770
2780
|
*/
|
|
2771
2781
|
function registerFillCommand(program) {
|
|
2772
|
-
program.command("fill <file>").description("Run an agent to autonomously fill a form").option("--mock", "Use mock agent (requires --mock-source)").option("--model <id>", "Model ID for live agent (format: provider/model-id, e.g. openai/gpt-5-mini)").option("--mock-source <file>", "Path to completed form for mock agent").option("--record <file>", "Record session transcript to file").option("--max-turns <n>", `Maximum turns (default: ${DEFAULT_MAX_TURNS})`, String(DEFAULT_MAX_TURNS)).option("--max-patches <n>", `Maximum patches per turn (default: ${DEFAULT_MAX_PATCHES_PER_TURN})`, String(DEFAULT_MAX_PATCHES_PER_TURN)).option("--max-issues <n>", `Maximum issues shown per turn (default: ${DEFAULT_MAX_ISSUES_PER_TURN})`, String(DEFAULT_MAX_ISSUES_PER_TURN)).option("--max-fields <n>", "Maximum unique fields per turn (applied before --max-issues)").option("--max-groups <n>", "Maximum unique groups per turn (applied before --max-issues)").option("--roles <roles>", "Target roles to fill (comma-separated, or '*' for all; default: 'agent', or 'user' in --interactive mode)").option("--mode <mode>", "Fill mode: continue (skip filled fields) or overwrite (re-fill; default: continue)").option("-o, --output <file>", "Write final form to file").option("--prompt <file>", "Path to custom system prompt file (appends to default)").option("--instructions <text>", "Inline system prompt (appends to default; takes precedence over --prompt)").option("-i, --interactive", "Interactive mode: prompt user for field values (defaults to user role)").option("--normalize", "Regenerate form without preserving external content").option("--record-fill", "Write fill record to sidecar .fill.json file").option("--record-fill-stable", "Write fill record without timestamps/durations (for golden tests)").action(async (file, options, cmd) => {
|
|
2782
|
+
program.command("fill <file>").description("Run an agent to autonomously fill a form").option("--mock", "Use mock agent (requires --mock-source)").option("--model <id>", "Model ID for live agent (format: provider/model-id, e.g. openai/gpt-5-mini)").option("--mock-source <file>", "Path to completed form for mock agent").option("--record <file>", "Record session transcript to file").option("--max-turns <n>", `Maximum turns (default: ${DEFAULT_MAX_TURNS})`, String(DEFAULT_MAX_TURNS)).option("--max-patches <n>", `Maximum patches per turn (default: ${DEFAULT_MAX_PATCHES_PER_TURN})`, String(DEFAULT_MAX_PATCHES_PER_TURN)).option("--max-issues <n>", `Maximum issues shown per turn (default: ${DEFAULT_MAX_ISSUES_PER_TURN})`, String(DEFAULT_MAX_ISSUES_PER_TURN)).option("--max-fields <n>", "Maximum unique fields per turn (applied before --max-issues)").option("--max-groups <n>", "Maximum unique groups per turn (applied before --max-issues)").option("--roles <roles>", "Target roles to fill (comma-separated, or '*' for all; default: 'agent', or 'user' in --interactive mode)").option("--mode <mode>", "Fill mode: continue (skip filled fields) or overwrite (re-fill; default: continue)").option("-o, --output <file>", "Write final form to file").option("--prompt <file>", "Path to custom system prompt file (appends to default)").option("--instructions <text>", "Inline system prompt (appends to default; takes precedence over --prompt)").option("-i, --interactive", "Interactive mode: prompt user for field values (defaults to user role)").option("--normalize", "Regenerate form without preserving external content").option("--record-fill", "Write fill record to sidecar .fill.json file").option("--record-fill-stable", "Write fill record without timestamps/durations (for golden tests)").option("--parallel", "Enable parallel execution for forms with parallel batches").action(async (file, options, cmd) => {
|
|
2773
2783
|
const ctx = getCommandContext(cmd);
|
|
2774
2784
|
const filePath = resolve(file);
|
|
2775
2785
|
let harness;
|
|
@@ -2861,6 +2871,74 @@ function registerFillCommand(program) {
|
|
|
2861
2871
|
process.exit(1);
|
|
2862
2872
|
}
|
|
2863
2873
|
if (targetRoles.includes("*")) logWarn(ctx, "Warning: Filling all roles including user-designated fields");
|
|
2874
|
+
if (options.parallel && !options.mock) {
|
|
2875
|
+
logInfo(ctx, pc.cyan(`Filling form (parallel): ${filePath}`));
|
|
2876
|
+
logInfo(ctx, `Agent: live (${options.model})`);
|
|
2877
|
+
let systemPrompt;
|
|
2878
|
+
if (options.instructions) systemPrompt = options.instructions;
|
|
2879
|
+
else if (options.prompt) systemPrompt = await readFile$1(resolve(options.prompt));
|
|
2880
|
+
let outputPath;
|
|
2881
|
+
if (options.output) outputPath = resolve(options.output);
|
|
2882
|
+
else {
|
|
2883
|
+
const formsDir = getFormsDir(ctx.formsDir);
|
|
2884
|
+
await ensureFormsDir(formsDir);
|
|
2885
|
+
outputPath = generateVersionedPathInFormsDir(filePath, formsDir);
|
|
2886
|
+
}
|
|
2887
|
+
const result = await fillForm({
|
|
2888
|
+
form,
|
|
2889
|
+
model: options.model,
|
|
2890
|
+
enableParallel: true,
|
|
2891
|
+
enableWebSearch: true,
|
|
2892
|
+
captureWireFormat: false,
|
|
2893
|
+
recordFill: true,
|
|
2894
|
+
targetRoles,
|
|
2895
|
+
fillMode,
|
|
2896
|
+
maxTurnsTotal: options.maxTurns ? parseInt(options.maxTurns, 10) : void 0,
|
|
2897
|
+
maxPatchesPerTurn: options.maxPatches ? parseInt(options.maxPatches, 10) : void 0,
|
|
2898
|
+
maxIssuesPerTurn: options.maxIssues ? parseInt(options.maxIssues, 10) : void 0,
|
|
2899
|
+
systemPromptAddition: systemPrompt,
|
|
2900
|
+
callbacks: {
|
|
2901
|
+
onTurnStart: ({ turnNumber, executionId }) => {
|
|
2902
|
+
logInfo(ctx, `${pc.bold(`Turn ${turnNumber}:`)} started (${pc.dim(executionId)})`);
|
|
2903
|
+
},
|
|
2904
|
+
onTurnComplete: (progress) => {
|
|
2905
|
+
const patchText = progress.patchesApplied === 1 ? "patch" : "patches";
|
|
2906
|
+
logInfo(ctx, ` → ${pc.yellow(String(progress.patchesApplied))} ${patchText} applied`);
|
|
2907
|
+
if (progress.isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
|
|
2908
|
+
},
|
|
2909
|
+
onBatchStart: ({ batchId, itemCount }) => {
|
|
2910
|
+
logInfo(ctx, pc.cyan(`Starting batch "${batchId}" with ${itemCount} items`));
|
|
2911
|
+
},
|
|
2912
|
+
onBatchComplete: ({ batchId, patchesApplied }) => {
|
|
2913
|
+
logInfo(ctx, pc.cyan(`Batch "${batchId}" completed: ${patchesApplied} patches`));
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
});
|
|
2917
|
+
const durationMs = Date.now() - startTime;
|
|
2918
|
+
if (result.status.ok) logSuccess(ctx, `Form completed in ${result.turns} turn(s)`);
|
|
2919
|
+
else logWarn(ctx, `Fill incomplete: ${result.status.reason}`);
|
|
2920
|
+
logTiming(ctx, "Fill time", durationMs);
|
|
2921
|
+
if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath}`);
|
|
2922
|
+
else {
|
|
2923
|
+
await writeFile(outputPath, result.markdown);
|
|
2924
|
+
logSuccess(ctx, `Form written to: ${outputPath}`);
|
|
2925
|
+
}
|
|
2926
|
+
if (result.record && !ctx.quiet) {
|
|
2927
|
+
console.log("");
|
|
2928
|
+
const summary = formatFillRecordSummary(result.record, { verbose: ctx.verbose });
|
|
2929
|
+
console.error(summary);
|
|
2930
|
+
}
|
|
2931
|
+
if ((options.recordFill || options.recordFillStable) && result.record) {
|
|
2932
|
+
const sidecarPath = deriveFillRecordPath(outputPath);
|
|
2933
|
+
const recordToWrite = options.recordFillStable ? stripUnstableFillRecordFields(result.record) : result.record;
|
|
2934
|
+
if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write fill record to: ${sidecarPath}`);
|
|
2935
|
+
else {
|
|
2936
|
+
writeFileSync(sidecarPath, JSON.stringify(recordToWrite, null, 2));
|
|
2937
|
+
logSuccess(ctx, `Fill record written to: ${sidecarPath}`);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
process.exit(result.status.ok ? 0 : 1);
|
|
2941
|
+
}
|
|
2864
2942
|
const cliOptions = {
|
|
2865
2943
|
maxTurns: options.maxTurns ? parseInt(options.maxTurns, 10) : void 0,
|
|
2866
2944
|
maxPatchesPerTurn: options.maxPatches ? parseInt(options.maxPatches, 10) : void 0,
|
|
@@ -2935,7 +3013,7 @@ function registerFillCommand(program) {
|
|
|
2935
3013
|
turnNumber: turn.turnNumber,
|
|
2936
3014
|
issuesCount: turn.issuesCount,
|
|
2937
3015
|
order: turn.order ?? 0,
|
|
2938
|
-
executionId: turn.executionId ?? "
|
|
3016
|
+
executionId: turn.executionId ?? "eid:serial:o0"
|
|
2939
3017
|
});
|
|
2940
3018
|
},
|
|
2941
3019
|
onTurnComplete: (progress) => {
|
|
@@ -2986,7 +3064,7 @@ function registerFillCommand(program) {
|
|
|
2986
3064
|
turnNumber: stepResult.turnNumber,
|
|
2987
3065
|
issuesCount: stepResult.issues.length,
|
|
2988
3066
|
order: 0,
|
|
2989
|
-
executionId: "
|
|
3067
|
+
executionId: "eid:serial:o0"
|
|
2990
3068
|
});
|
|
2991
3069
|
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
2992
3070
|
let spinner = null;
|
|
@@ -3014,7 +3092,7 @@ function registerFillCommand(program) {
|
|
|
3014
3092
|
logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches${tokenSuffix}:`);
|
|
3015
3093
|
for (const patch of patches) {
|
|
3016
3094
|
const typeName = formatPatchType(patch);
|
|
3017
|
-
const value = formatPatchValue
|
|
3095
|
+
const value = formatPatchValue(patch);
|
|
3018
3096
|
const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
|
|
3019
3097
|
if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
|
|
3020
3098
|
else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
|
|
@@ -3076,7 +3154,7 @@ function registerFillCommand(program) {
|
|
|
3076
3154
|
turnNumber: stepResult.turnNumber,
|
|
3077
3155
|
issuesCount: stepResult.issues.length,
|
|
3078
3156
|
order: 0,
|
|
3079
|
-
executionId: "
|
|
3157
|
+
executionId: "eid:serial:o0"
|
|
3080
3158
|
});
|
|
3081
3159
|
}
|
|
3082
3160
|
}
|
|
@@ -4956,7 +5034,9 @@ function renderGroup(group, responses) {
|
|
|
4956
5034
|
const groupTitle = group.title ?? group.id;
|
|
4957
5035
|
const fieldsHtml = group.children.map((field) => {
|
|
4958
5036
|
const response = responses[field.id];
|
|
4959
|
-
|
|
5037
|
+
const value = response?.state === "answered" ? response.value : void 0;
|
|
5038
|
+
const isSkipped = response?.state === "skipped";
|
|
5039
|
+
return renderFieldHtml(field, value, isSkipped, isSkipped ? response?.reason : void 0);
|
|
4960
5040
|
}).join("\n");
|
|
4961
5041
|
return `
|
|
4962
5042
|
<div class="group">
|
|
@@ -4968,11 +5048,12 @@ function renderGroup(group, responses) {
|
|
|
4968
5048
|
* Render a field as HTML.
|
|
4969
5049
|
* @public Exported for testing.
|
|
4970
5050
|
*/
|
|
4971
|
-
function renderFieldHtml(field, value, isSkipped) {
|
|
5051
|
+
function renderFieldHtml(field, value, isSkipped, skipReason) {
|
|
4972
5052
|
const skipped = isSkipped === true;
|
|
4973
5053
|
const requiredMark = field.required ? "<span class=\"required\">*</span>" : "";
|
|
4974
5054
|
const typeLabel = `<span class="type-badge">${field.kind}</span>`;
|
|
4975
|
-
const
|
|
5055
|
+
const skippedText = skipped && skipReason ? `Skipped: ${escapeHtml(skipReason)}` : "Skipped";
|
|
5056
|
+
const skippedBadge = skipped ? `<span class="skipped-badge">${skippedText}</span>` : "";
|
|
4976
5057
|
const fieldClass = skipped ? "field field-skipped" : "field";
|
|
4977
5058
|
const disabledAttr = skipped ? " disabled" : "";
|
|
4978
5059
|
let inputHtml;
|
|
@@ -5205,13 +5286,6 @@ function renderTableInput(field, value, _disabledAttr) {
|
|
|
5205
5286
|
<div class="field-help">(table fields are currently read-only in the web UI)</div>
|
|
5206
5287
|
</div>`;
|
|
5207
5288
|
}
|
|
5208
|
-
/**
|
|
5209
|
-
* Escape HTML special characters.
|
|
5210
|
-
* @public Exported for testing.
|
|
5211
|
-
*/
|
|
5212
|
-
function escapeHtml(str) {
|
|
5213
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
5214
|
-
}
|
|
5215
5289
|
/** Common styles for read-only viewers */
|
|
5216
5290
|
const READ_ONLY_STYLES = `
|
|
5217
5291
|
* { box-sizing: border-box; }
|
|
@@ -5363,17 +5437,6 @@ function renderYamlHtml(content, filename) {
|
|
|
5363
5437
|
</html>`;
|
|
5364
5438
|
}
|
|
5365
5439
|
/**
|
|
5366
|
-
* Highlight a YAML value based on its type.
|
|
5367
|
-
*/
|
|
5368
|
-
function highlightYamlValue(value) {
|
|
5369
|
-
const trimmed = value.trim();
|
|
5370
|
-
if (trimmed === "true" || trimmed === "false") return `<span class="syn-bool">${escapeHtml(value)}</span>`;
|
|
5371
|
-
if (trimmed === "null" || trimmed === "~") return `<span class="syn-null">${escapeHtml(value)}</span>`;
|
|
5372
|
-
if (/^-?\d+\.?\d*$/.test(trimmed)) return `<span class="syn-number">${escapeHtml(value)}</span>`;
|
|
5373
|
-
if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return `<span class="syn-string">${escapeHtml(value)}</span>`;
|
|
5374
|
-
return `<span class="syn-string">${escapeHtml(value)}</span>`;
|
|
5375
|
-
}
|
|
5376
|
-
/**
|
|
5377
5440
|
* Render JSON content with syntax highlighting and formatting.
|
|
5378
5441
|
*/
|
|
5379
5442
|
function renderJsonHtml(content, filename) {
|
|
@@ -5421,1192 +5484,6 @@ function renderPlainTextHtml(content, filename) {
|
|
|
5421
5484
|
</body>
|
|
5422
5485
|
</html>`;
|
|
5423
5486
|
}
|
|
5424
|
-
/**
|
|
5425
|
-
* Render form view content (read-only display of form fields).
|
|
5426
|
-
* Used for View tab content.
|
|
5427
|
-
* @public Exported for testing.
|
|
5428
|
-
*/
|
|
5429
|
-
function renderViewContent(form) {
|
|
5430
|
-
const { schema, responsesByFieldId } = form;
|
|
5431
|
-
let html = "<div class=\"view-content\">";
|
|
5432
|
-
for (const group of schema.groups) {
|
|
5433
|
-
const groupTitle = group.title ?? group.id;
|
|
5434
|
-
html += `<div class="view-group"><h2>${escapeHtml(groupTitle)}</h2>`;
|
|
5435
|
-
for (const field of group.children) {
|
|
5436
|
-
const response = responsesByFieldId[field.id];
|
|
5437
|
-
const value = response?.state === "answered" ? response.value : void 0;
|
|
5438
|
-
const isSkipped = response?.state === "skipped";
|
|
5439
|
-
html += "<div class=\"view-field\">";
|
|
5440
|
-
html += `<div class="view-field-label">${escapeHtml(field.label)}`;
|
|
5441
|
-
html += ` <span class="type-badge">${field.kind}</span>`;
|
|
5442
|
-
if (field.required) html += " <span class=\"required\">*</span>";
|
|
5443
|
-
if (isSkipped) html += " <span class=\"skipped-badge\">Skipped</span>";
|
|
5444
|
-
html += "</div>";
|
|
5445
|
-
html += renderViewFieldValue(field, value, isSkipped);
|
|
5446
|
-
html += "</div>";
|
|
5447
|
-
}
|
|
5448
|
-
html += "</div>";
|
|
5449
|
-
}
|
|
5450
|
-
html += "</div>";
|
|
5451
|
-
return html;
|
|
5452
|
-
}
|
|
5453
|
-
/**
|
|
5454
|
-
* Format a checkbox state for display.
|
|
5455
|
-
*/
|
|
5456
|
-
function formatCheckboxState(state) {
|
|
5457
|
-
switch (state) {
|
|
5458
|
-
case "done": return "<span class=\"checkbox checked\">☑</span>";
|
|
5459
|
-
case "todo": return "<span class=\"checkbox unchecked\">☐</span>";
|
|
5460
|
-
case "active": return "<span class=\"state-badge state-active\">●</span>";
|
|
5461
|
-
case "incomplete": return "<span class=\"state-badge state-incomplete\">○</span>";
|
|
5462
|
-
case "na": return "<span class=\"state-badge state-na\">—</span>";
|
|
5463
|
-
case "yes": return "<span class=\"checkbox checked\">☑</span>";
|
|
5464
|
-
case "no": return "<span class=\"checkbox unchecked\">☐</span>";
|
|
5465
|
-
case "unfilled": return "<span class=\"state-badge state-unfilled\">?</span>";
|
|
5466
|
-
default: return `<span class="state-badge">${escapeHtml(state)}</span>`;
|
|
5467
|
-
}
|
|
5468
|
-
}
|
|
5469
|
-
/**
|
|
5470
|
-
* Render a field value for the View tab.
|
|
5471
|
-
*/
|
|
5472
|
-
function renderViewFieldValue(field, value, isSkipped) {
|
|
5473
|
-
if (isSkipped) return "<div class=\"view-field-empty\">(skipped)</div>";
|
|
5474
|
-
if (value === void 0) return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
5475
|
-
switch (field.kind) {
|
|
5476
|
-
case "string": {
|
|
5477
|
-
const v = value.kind === "string" ? value.value : null;
|
|
5478
|
-
if (v === null || v === "") return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
5479
|
-
return `<div class="view-field-value">${formatBareUrlsAsHtmlLinks(v, escapeHtml)}</div>`;
|
|
5480
|
-
}
|
|
5481
|
-
case "number": {
|
|
5482
|
-
const v = value.kind === "number" ? value.value : null;
|
|
5483
|
-
if (v === null) return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
5484
|
-
return `<div class="view-field-value">${v}</div>`;
|
|
5485
|
-
}
|
|
5486
|
-
case "string_list": {
|
|
5487
|
-
const items = value.kind === "string_list" ? value.items : [];
|
|
5488
|
-
if (items.length === 0) return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
5489
|
-
return `<div class="view-field-value"><ul>${items.map((i) => `<li>${formatBareUrlsAsHtmlLinks(i, escapeHtml)}</li>`).join("")}</ul></div>`;
|
|
5490
|
-
}
|
|
5491
|
-
case "single_select": {
|
|
5492
|
-
const selected = value.kind === "single_select" ? value.selected : null;
|
|
5493
|
-
if (selected === null) return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
5494
|
-
return `<div class="view-field-value">${escapeHtml(field.options.find((o) => o.id === selected)?.label ?? selected)}</div>`;
|
|
5495
|
-
}
|
|
5496
|
-
case "multi_select": {
|
|
5497
|
-
const selected = value.kind === "multi_select" ? value.selected : [];
|
|
5498
|
-
return `<div class="view-field-value"><ul class="checkbox-list">${field.options.map((opt) => {
|
|
5499
|
-
return `<li class="checkbox-item">${selected.includes(opt.id) ? "<span class=\"checkbox checked\">☑</span>" : "<span class=\"checkbox unchecked\">☐</span>"} ${escapeHtml(opt.label)}</li>`;
|
|
5500
|
-
}).join("")}</ul></div>`;
|
|
5501
|
-
}
|
|
5502
|
-
case "checkboxes": {
|
|
5503
|
-
const values = value.kind === "checkboxes" ? value.values : {};
|
|
5504
|
-
const mode = field.checkboxMode ?? "multi";
|
|
5505
|
-
return `<div class="view-field-value"><ul class="checkbox-list">${field.options.map((opt) => {
|
|
5506
|
-
const state = values[opt.id] ?? (mode === "explicit" ? "unfilled" : "todo");
|
|
5507
|
-
if (mode === "simple") return `<li class="checkbox-item">${state === "done" ? "<span class=\"checkbox checked\">☑</span>" : "<span class=\"checkbox unchecked\">☐</span>"} ${escapeHtml(opt.label)}</li>`;
|
|
5508
|
-
return `<li class="checkbox-item">${formatCheckboxState(state)} ${escapeHtml(opt.label)}</li>`;
|
|
5509
|
-
}).join("")}</ul></div>`;
|
|
5510
|
-
}
|
|
5511
|
-
case "url": {
|
|
5512
|
-
const v = value.kind === "url" ? value.value : null;
|
|
5513
|
-
if (v === null || v === "") return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
5514
|
-
const domain = friendlyUrlAbbrev(v);
|
|
5515
|
-
return `<div class="view-field-value"><a href="${escapeHtml(v)}" target="_blank" class="url-link" data-url="${escapeHtml(v)}">${escapeHtml(domain)}</a></div>`;
|
|
5516
|
-
}
|
|
5517
|
-
case "url_list": {
|
|
5518
|
-
const items = value.kind === "url_list" ? value.items : [];
|
|
5519
|
-
if (items.length === 0) return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
5520
|
-
return `<div class="view-field-value"><ul>${items.map((u) => {
|
|
5521
|
-
const domain = friendlyUrlAbbrev(u);
|
|
5522
|
-
return `<li><a href="${escapeHtml(u)}" target="_blank" class="url-link" data-url="${escapeHtml(u)}">${escapeHtml(domain)}</a></li>`;
|
|
5523
|
-
}).join("")}</ul></div>`;
|
|
5524
|
-
}
|
|
5525
|
-
case "date": {
|
|
5526
|
-
const v = value.kind === "date" ? value.value : null;
|
|
5527
|
-
if (v === null || v === "") return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
5528
|
-
return `<div class="view-field-value">${escapeHtml(v)}</div>`;
|
|
5529
|
-
}
|
|
5530
|
-
case "year": {
|
|
5531
|
-
const v = value.kind === "year" ? value.value : null;
|
|
5532
|
-
if (v === null) return "<div class=\"view-field-empty\">(not filled)</div>";
|
|
5533
|
-
return `<div class="view-field-value">${v}</div>`;
|
|
5534
|
-
}
|
|
5535
|
-
case "table": {
|
|
5536
|
-
const rows = value.kind === "table" ? value.rows : [];
|
|
5537
|
-
if (rows.length === 0) return "<div class=\"view-field-empty\">(no data)</div>";
|
|
5538
|
-
let tableHtml = "<div class=\"table-container\"><table class=\"data-table\">";
|
|
5539
|
-
tableHtml += "<thead><tr>";
|
|
5540
|
-
for (const col of field.columns) tableHtml += `<th>${escapeHtml(col.label)}</th>`;
|
|
5541
|
-
tableHtml += "</tr></thead><tbody>";
|
|
5542
|
-
for (const row of rows) {
|
|
5543
|
-
tableHtml += "<tr>";
|
|
5544
|
-
for (const col of field.columns) {
|
|
5545
|
-
const cell = row[col.id];
|
|
5546
|
-
let cellValue = "";
|
|
5547
|
-
let cellHtml = "";
|
|
5548
|
-
if (cell?.state === "answered" && cell.value !== void 0 && cell.value !== null) {
|
|
5549
|
-
cellValue = String(cell.value);
|
|
5550
|
-
if (col.type === "url" && cellValue) {
|
|
5551
|
-
const domain = friendlyUrlAbbrev(cellValue);
|
|
5552
|
-
cellHtml = `<a href="${escapeHtml(cellValue)}" target="_blank" class="url-link" data-url="${escapeHtml(cellValue)}">${escapeHtml(domain)}</a>`;
|
|
5553
|
-
} else cellHtml = formatBareUrlsAsHtmlLinks(cellValue, escapeHtml);
|
|
5554
|
-
}
|
|
5555
|
-
tableHtml += `<td>${cellHtml}</td>`;
|
|
5556
|
-
}
|
|
5557
|
-
tableHtml += "</tr>";
|
|
5558
|
-
}
|
|
5559
|
-
tableHtml += "</tbody></table></div>";
|
|
5560
|
-
return tableHtml;
|
|
5561
|
-
}
|
|
5562
|
-
default: {
|
|
5563
|
-
const _exhaustive = field;
|
|
5564
|
-
throw new Error(`Unhandled field kind: ${_exhaustive.kind}`);
|
|
5565
|
-
}
|
|
5566
|
-
}
|
|
5567
|
-
}
|
|
5568
|
-
/**
|
|
5569
|
-
* Render source content with Markdown and Jinja syntax highlighting.
|
|
5570
|
-
* Used for Source tab content.
|
|
5571
|
-
* @public Exported for testing.
|
|
5572
|
-
*/
|
|
5573
|
-
function renderSourceContent(content) {
|
|
5574
|
-
return `<pre>${content.split("\n").map((line) => highlightSourceLine(line)).join("\n")}</pre>`;
|
|
5575
|
-
}
|
|
5576
|
-
/**
|
|
5577
|
-
* Highlight a single line of source code (Markdown + Jinja).
|
|
5578
|
-
*/
|
|
5579
|
-
function highlightSourceLine(line) {
|
|
5580
|
-
let result = escapeHtml(line);
|
|
5581
|
-
result = result.replace(/(\{%\s*)([a-zA-Z_/]+)(\s+[^%]*)?(%\})/g, (_, open, keyword, attrs, close) => {
|
|
5582
|
-
let attrHtml = "";
|
|
5583
|
-
if (attrs) attrHtml = attrs.replace(/([a-zA-Z_]+)(=)("[^"]*"|'[^&#]*'|[^\s%]+)?/g, (_m, attrName, eq, attrValue) => {
|
|
5584
|
-
return `<span class="syn-jinja-attr">${attrName}</span>${eq}${attrValue ? `<span class="syn-jinja-value">${attrValue}</span>` : ""}`;
|
|
5585
|
-
});
|
|
5586
|
-
return `<span class="syn-jinja-tag">${open}</span><span class="syn-jinja-keyword">${keyword}</span>${attrHtml}<span class="syn-jinja-tag">${close}</span>`;
|
|
5587
|
-
});
|
|
5588
|
-
result = result.replace(/(\{#)(.*?)(#\})/g, `<span class="syn-comment">$1$2$3</span>`);
|
|
5589
|
-
result = result.replace(/^(#{1,6}\s.*)$/gm, "<span class=\"syn-md-header\">$1</span>");
|
|
5590
|
-
if (result === "---") result = "<span class=\"syn-comment\">---</span>";
|
|
5591
|
-
return result;
|
|
5592
|
-
}
|
|
5593
|
-
/**
|
|
5594
|
-
* Render markdown content (content only, no page wrapper).
|
|
5595
|
-
* Used for tab content.
|
|
5596
|
-
* @public Exported for testing.
|
|
5597
|
-
*/
|
|
5598
|
-
function renderMarkdownContent(content) {
|
|
5599
|
-
const lines = content.split("\n");
|
|
5600
|
-
let html = "<div class=\"markdown-content\">";
|
|
5601
|
-
let inParagraph = false;
|
|
5602
|
-
let inCodeBlock = false;
|
|
5603
|
-
let codeBlockContent = "";
|
|
5604
|
-
let inUnorderedList = false;
|
|
5605
|
-
let inOrderedList = false;
|
|
5606
|
-
let inTable = false;
|
|
5607
|
-
let tableHeaderDone = false;
|
|
5608
|
-
const closeList = () => {
|
|
5609
|
-
if (inUnorderedList) {
|
|
5610
|
-
html += "</ul>";
|
|
5611
|
-
inUnorderedList = false;
|
|
5612
|
-
}
|
|
5613
|
-
if (inOrderedList) {
|
|
5614
|
-
html += "</ol>";
|
|
5615
|
-
inOrderedList = false;
|
|
5616
|
-
}
|
|
5617
|
-
};
|
|
5618
|
-
const closeTable = () => {
|
|
5619
|
-
if (inTable) {
|
|
5620
|
-
html += "</tbody></table></div>";
|
|
5621
|
-
inTable = false;
|
|
5622
|
-
tableHeaderDone = false;
|
|
5623
|
-
}
|
|
5624
|
-
};
|
|
5625
|
-
const isTableRow = (line) => {
|
|
5626
|
-
const trimmed = line.trim();
|
|
5627
|
-
return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|");
|
|
5628
|
-
};
|
|
5629
|
-
const isTableSeparator = (line) => {
|
|
5630
|
-
const trimmed = line.trim();
|
|
5631
|
-
return /^\|[\s-:|]+\|$/.test(trimmed);
|
|
5632
|
-
};
|
|
5633
|
-
const parseTableCells = (line) => {
|
|
5634
|
-
return line.trim().slice(1, -1).split("|").map((cell) => cell.trim());
|
|
5635
|
-
};
|
|
5636
|
-
for (const line of lines) {
|
|
5637
|
-
const trimmed = line.trim();
|
|
5638
|
-
if (trimmed.startsWith("```")) {
|
|
5639
|
-
if (inCodeBlock) {
|
|
5640
|
-
html += `<pre><code>${escapeHtml(codeBlockContent.trim())}</code></pre>`;
|
|
5641
|
-
codeBlockContent = "";
|
|
5642
|
-
inCodeBlock = false;
|
|
5643
|
-
} else {
|
|
5644
|
-
if (inParagraph) {
|
|
5645
|
-
html += "</p>";
|
|
5646
|
-
inParagraph = false;
|
|
5647
|
-
}
|
|
5648
|
-
closeList();
|
|
5649
|
-
closeTable();
|
|
5650
|
-
inCodeBlock = true;
|
|
5651
|
-
}
|
|
5652
|
-
continue;
|
|
5653
|
-
}
|
|
5654
|
-
if (inCodeBlock) {
|
|
5655
|
-
codeBlockContent += line + "\n";
|
|
5656
|
-
continue;
|
|
5657
|
-
}
|
|
5658
|
-
if (isTableRow(trimmed)) {
|
|
5659
|
-
if (inParagraph) {
|
|
5660
|
-
html += "</p>";
|
|
5661
|
-
inParagraph = false;
|
|
5662
|
-
}
|
|
5663
|
-
closeList();
|
|
5664
|
-
if (isTableSeparator(trimmed)) {
|
|
5665
|
-
tableHeaderDone = true;
|
|
5666
|
-
continue;
|
|
5667
|
-
}
|
|
5668
|
-
const cells = parseTableCells(trimmed);
|
|
5669
|
-
if (!inTable) {
|
|
5670
|
-
html += "<div class=\"table-container\"><table class=\"data-table\"><thead><tr>";
|
|
5671
|
-
for (const cell of cells) html += `<th>${formatInlineMarkdown(cell)}</th>`;
|
|
5672
|
-
html += "</tr></thead><tbody>";
|
|
5673
|
-
inTable = true;
|
|
5674
|
-
} else if (tableHeaderDone) {
|
|
5675
|
-
html += "<tr>";
|
|
5676
|
-
for (const cell of cells) html += `<td>${formatInlineMarkdown(cell)}</td>`;
|
|
5677
|
-
html += "</tr>";
|
|
5678
|
-
}
|
|
5679
|
-
continue;
|
|
5680
|
-
}
|
|
5681
|
-
if (inTable && !isTableRow(trimmed)) closeTable();
|
|
5682
|
-
if (trimmed.startsWith("# ")) {
|
|
5683
|
-
if (inParagraph) {
|
|
5684
|
-
html += "</p>";
|
|
5685
|
-
inParagraph = false;
|
|
5686
|
-
}
|
|
5687
|
-
closeList();
|
|
5688
|
-
html += `<h2>${formatInlineMarkdown(trimmed.slice(2))}</h2>`;
|
|
5689
|
-
} else if (trimmed.startsWith("## ")) {
|
|
5690
|
-
if (inParagraph) {
|
|
5691
|
-
html += "</p>";
|
|
5692
|
-
inParagraph = false;
|
|
5693
|
-
}
|
|
5694
|
-
closeList();
|
|
5695
|
-
html += `<h3>${formatInlineMarkdown(trimmed.slice(3))}</h3>`;
|
|
5696
|
-
} else if (trimmed.startsWith("### ")) {
|
|
5697
|
-
if (inParagraph) {
|
|
5698
|
-
html += "</p>";
|
|
5699
|
-
inParagraph = false;
|
|
5700
|
-
}
|
|
5701
|
-
closeList();
|
|
5702
|
-
html += `<h4>${formatInlineMarkdown(trimmed.slice(4))}</h4>`;
|
|
5703
|
-
} else if (trimmed.startsWith("#### ")) {
|
|
5704
|
-
if (inParagraph) {
|
|
5705
|
-
html += "</p>";
|
|
5706
|
-
inParagraph = false;
|
|
5707
|
-
}
|
|
5708
|
-
closeList();
|
|
5709
|
-
html += `<h5>${formatInlineMarkdown(trimmed.slice(5))}</h5>`;
|
|
5710
|
-
} else if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
|
|
5711
|
-
if (inParagraph) {
|
|
5712
|
-
html += "</p>";
|
|
5713
|
-
inParagraph = false;
|
|
5714
|
-
}
|
|
5715
|
-
if (inOrderedList) {
|
|
5716
|
-
html += "</ol>";
|
|
5717
|
-
inOrderedList = false;
|
|
5718
|
-
}
|
|
5719
|
-
if (!inUnorderedList) {
|
|
5720
|
-
html += "<ul>";
|
|
5721
|
-
inUnorderedList = true;
|
|
5722
|
-
}
|
|
5723
|
-
const itemContent = trimmed.slice(2);
|
|
5724
|
-
const liClass = /^\[[ xX]\]/.test(itemContent) ? " class=\"checkbox-item\"" : "";
|
|
5725
|
-
html += `<li${liClass}>${formatInlineMarkdown(itemContent)}</li>`;
|
|
5726
|
-
} else if (/^\d+\.\s/.test(trimmed)) {
|
|
5727
|
-
if (inParagraph) {
|
|
5728
|
-
html += "</p>";
|
|
5729
|
-
inParagraph = false;
|
|
5730
|
-
}
|
|
5731
|
-
if (inUnorderedList) {
|
|
5732
|
-
html += "</ul>";
|
|
5733
|
-
inUnorderedList = false;
|
|
5734
|
-
}
|
|
5735
|
-
if (!inOrderedList) {
|
|
5736
|
-
html += "<ol>";
|
|
5737
|
-
inOrderedList = true;
|
|
5738
|
-
}
|
|
5739
|
-
const text = trimmed.replace(/^\d+\.\s/, "");
|
|
5740
|
-
html += `<li>${formatInlineMarkdown(text)}</li>`;
|
|
5741
|
-
} else if (trimmed === "") {
|
|
5742
|
-
if (inParagraph) {
|
|
5743
|
-
html += "</p>";
|
|
5744
|
-
inParagraph = false;
|
|
5745
|
-
}
|
|
5746
|
-
closeList();
|
|
5747
|
-
} else {
|
|
5748
|
-
closeList();
|
|
5749
|
-
if (!inParagraph) {
|
|
5750
|
-
html += "<p>";
|
|
5751
|
-
inParagraph = true;
|
|
5752
|
-
} else html += "<br>";
|
|
5753
|
-
html += formatInlineMarkdown(trimmed);
|
|
5754
|
-
}
|
|
5755
|
-
}
|
|
5756
|
-
if (inParagraph) html += "</p>";
|
|
5757
|
-
closeList();
|
|
5758
|
-
closeTable();
|
|
5759
|
-
html += "</div>";
|
|
5760
|
-
return html;
|
|
5761
|
-
}
|
|
5762
|
-
/**
|
|
5763
|
-
* Format inline markdown (bold, italic, code, links, checkboxes).
|
|
5764
|
-
* Also auto-links bare URLs for consistency.
|
|
5765
|
-
*/
|
|
5766
|
-
function formatInlineMarkdown(text) {
|
|
5767
|
-
let result = escapeHtml(text);
|
|
5768
|
-
result = result.replace(/\[x\]/gi, "<span class=\"checkbox checked\">☑</span>");
|
|
5769
|
-
result = result.replace(/\[ \]/g, "<span class=\"checkbox unchecked\">☐</span>");
|
|
5770
|
-
result = result.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
5771
|
-
result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
5772
|
-
result = result.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
5773
|
-
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, linkText, url) => {
|
|
5774
|
-
const cleanUrl = url.replace(/&/g, "&");
|
|
5775
|
-
return `<a href="${cleanUrl}" target="_blank" class="url-link" data-url="${cleanUrl}">${linkText}</a>`;
|
|
5776
|
-
});
|
|
5777
|
-
result = result.replace(/(?<!href="|data-url="|">|\]\()(?:https?:\/\/|www\.)[^\s<>"]+(?<![.,;:!?'")])/g, (url) => {
|
|
5778
|
-
const cleanUrl = url.replace(/&/g, "&");
|
|
5779
|
-
const fullUrl = cleanUrl.startsWith("www.") ? `https://${cleanUrl}` : cleanUrl;
|
|
5780
|
-
const display = friendlyUrlAbbrev(fullUrl);
|
|
5781
|
-
return `<a href="${escapeHtml(fullUrl)}" target="_blank" class="url-link" data-url="${escapeHtml(fullUrl)}">${escapeHtml(display)}</a>`;
|
|
5782
|
-
});
|
|
5783
|
-
return result;
|
|
5784
|
-
}
|
|
5785
|
-
/**
|
|
5786
|
-
* Render YAML content (content only, no page wrapper).
|
|
5787
|
-
* Used for tab content.
|
|
5788
|
-
* @public Exported for testing.
|
|
5789
|
-
*/
|
|
5790
|
-
function renderYamlContent(content) {
|
|
5791
|
-
return `<pre>${content.split("\n").map((line) => {
|
|
5792
|
-
if (line.trim().startsWith("#")) return `<span class="syn-comment">${escapeHtml(line)}</span>`;
|
|
5793
|
-
const colonIndex = line.indexOf(":");
|
|
5794
|
-
if (colonIndex > 0 && !line.trim().startsWith("-")) {
|
|
5795
|
-
const key = escapeHtml(line.slice(0, colonIndex));
|
|
5796
|
-
const afterColon = line.slice(colonIndex + 1).trim();
|
|
5797
|
-
const colonAndSpace = escapeHtml(line.slice(colonIndex, colonIndex + 1));
|
|
5798
|
-
if (afterColon === "") return `<span class="syn-key">${key}</span>${colonAndSpace}`;
|
|
5799
|
-
const valueStart = line.indexOf(afterColon, colonIndex);
|
|
5800
|
-
return `<span class="syn-key">${key}</span>${escapeHtml(line.slice(colonIndex, valueStart))}${highlightYamlValue(afterColon)}`;
|
|
5801
|
-
}
|
|
5802
|
-
if (line.trim().startsWith("-")) {
|
|
5803
|
-
const dashIndex = line.indexOf("-");
|
|
5804
|
-
const beforeDash = escapeHtml(line.slice(0, dashIndex));
|
|
5805
|
-
const afterDash = line.slice(dashIndex + 1).trim();
|
|
5806
|
-
if (afterDash === "") return `${beforeDash}-`;
|
|
5807
|
-
return `${beforeDash}- ${highlightYamlValue(afterDash)}`;
|
|
5808
|
-
}
|
|
5809
|
-
return escapeHtml(line);
|
|
5810
|
-
}).join("\n")}</pre>`;
|
|
5811
|
-
}
|
|
5812
|
-
/**
|
|
5813
|
-
* Render JSON content (content only, no page wrapper).
|
|
5814
|
-
* Used for tab content.
|
|
5815
|
-
* @public Exported for testing.
|
|
5816
|
-
*/
|
|
5817
|
-
function renderJsonContent(content) {
|
|
5818
|
-
let formatted;
|
|
5819
|
-
try {
|
|
5820
|
-
const parsed = JSON.parse(content);
|
|
5821
|
-
formatted = JSON.stringify(parsed, null, 2);
|
|
5822
|
-
} catch {
|
|
5823
|
-
formatted = content;
|
|
5824
|
-
}
|
|
5825
|
-
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>`;
|
|
5826
|
-
}
|
|
5827
|
-
/**
|
|
5828
|
-
* Format milliseconds as human-readable duration.
|
|
5829
|
-
* @public Exported for reuse in other visualizations.
|
|
5830
|
-
*/
|
|
5831
|
-
function formatDuration(ms) {
|
|
5832
|
-
if (ms < 1e3) return `${ms.toFixed(0)}ms`;
|
|
5833
|
-
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
5834
|
-
return `${Math.floor(ms / 6e4)}m ${(ms % 6e4 / 1e3).toFixed(0)}s`;
|
|
5835
|
-
}
|
|
5836
|
-
/**
|
|
5837
|
-
* Format token count with K suffix for large numbers.
|
|
5838
|
-
* @public Exported for reuse in other visualizations.
|
|
5839
|
-
*/
|
|
5840
|
-
function formatTokens(count) {
|
|
5841
|
-
if (count >= 1e4) return `${(count / 1e3).toFixed(1)}k`;
|
|
5842
|
-
if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
|
|
5843
|
-
return count.toLocaleString();
|
|
5844
|
-
}
|
|
5845
|
-
/**
|
|
5846
|
-
* Format a patch value for display.
|
|
5847
|
-
* Shows full content - the container has max-height with scroll for long values.
|
|
5848
|
-
*/
|
|
5849
|
-
function formatPatchValue(value) {
|
|
5850
|
-
if (value === null || value === void 0) return "<em class=\"fr-turn__patch-value--clear\">(cleared)</em>";
|
|
5851
|
-
if (typeof value === "string") return escapeHtml(value);
|
|
5852
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
5853
|
-
return escapeHtml(JSON.stringify(value, null, 2));
|
|
5854
|
-
}
|
|
5855
|
-
/**
|
|
5856
|
-
* Render patches from a fill_form tool call input.
|
|
5857
|
-
* Returns HTML for the patch details section.
|
|
5858
|
-
*/
|
|
5859
|
-
function renderPatchDetails(input) {
|
|
5860
|
-
const patches = input.patches;
|
|
5861
|
-
if (!Array.isArray(patches) || patches.length === 0) return "";
|
|
5862
|
-
return `<div class="fr-turn__patches">${patches.map((patch) => {
|
|
5863
|
-
if (!patch || typeof patch !== "object") return "";
|
|
5864
|
-
const p = patch;
|
|
5865
|
-
const op = typeof p.op === "string" ? p.op : "unknown";
|
|
5866
|
-
const fieldId = typeof p.fieldId === "string" ? p.fieldId : typeof p.noteId === "string" ? p.noteId : "";
|
|
5867
|
-
const opLabel = op.replace(/_/g, " ");
|
|
5868
|
-
let valueHtml = "";
|
|
5869
|
-
if (op === "skip_field") valueHtml = "<em class=\"fr-turn__patch-value--skip\">(skipped)</em>";
|
|
5870
|
-
else if (op === "abort_field") valueHtml = "<em class=\"fr-turn__patch-value--skip\">(aborted)</em>";
|
|
5871
|
-
else if (op === "clear_field") valueHtml = "<em class=\"fr-turn__patch-value--clear\">(cleared)</em>";
|
|
5872
|
-
else if ("value" in p) valueHtml = formatPatchValue(p.value);
|
|
5873
|
-
else if ("values" in p) valueHtml = formatPatchValue(p.values);
|
|
5874
|
-
else if ("rows" in p) valueHtml = formatPatchValue(p.rows);
|
|
5875
|
-
return `
|
|
5876
|
-
<div class="fr-turn__patch">
|
|
5877
|
-
<span class="fr-turn__patch-field">${escapeHtml(fieldId)}</span>
|
|
5878
|
-
<span class="fr-turn__patch-op">${escapeHtml(opLabel)}</span>
|
|
5879
|
-
<span class="fr-turn__patch-value">${valueHtml}</span>
|
|
5880
|
-
</div>
|
|
5881
|
-
`;
|
|
5882
|
-
}).filter(Boolean).join("")}</div>`;
|
|
5883
|
-
}
|
|
5884
|
-
/**
|
|
5885
|
-
* Render a single tool call with enhanced details.
|
|
5886
|
-
* Shows query for web_search, patch details for fill_form.
|
|
5887
|
-
*/
|
|
5888
|
-
function renderToolCall(tc) {
|
|
5889
|
-
const hasError = !!tc.result?.error;
|
|
5890
|
-
const icon = tc.success ? "✓" : "✕";
|
|
5891
|
-
const errorClass = hasError ? " fr-turn__tool--error" : "";
|
|
5892
|
-
let resultSummary = "";
|
|
5893
|
-
if (hasError) resultSummary = `Error: ${escapeHtml(tc.result?.error ?? "")}`;
|
|
5894
|
-
else if (tc.result?.resultCount !== void 0) resultSummary = `${tc.result.resultCount} results`;
|
|
5895
|
-
else resultSummary = "OK";
|
|
5896
|
-
let detailHtml = "";
|
|
5897
|
-
if (tc.tool === "web_search" && typeof tc.input.query === "string") detailHtml = ` <span class="fr-turn__query">"${escapeHtml(tc.input.query)}"</span>`;
|
|
5898
|
-
const toolLine = `<li class="fr-turn__tool${errorClass}">${icon} <strong>${escapeHtml(tc.tool)}</strong>${detailHtml}: ${resultSummary} (${formatDuration(tc.durationMs)})</li>`;
|
|
5899
|
-
if (tc.tool === "fill_form" && tc.input.patches) {
|
|
5900
|
-
const patchDetails = renderPatchDetails(tc.input);
|
|
5901
|
-
if (patchDetails) return toolLine + patchDetails;
|
|
5902
|
-
}
|
|
5903
|
-
return toolLine;
|
|
5904
|
-
}
|
|
5905
|
-
/**
|
|
5906
|
-
* CSS styles for fill record visualization.
|
|
5907
|
-
* Uses CSS custom properties for theming (supports dark mode via prefers-color-scheme).
|
|
5908
|
-
* Designed to be lightweight, reusable, and embeddable.
|
|
5909
|
-
*/
|
|
5910
|
-
const FILL_RECORD_STYLES = `
|
|
5911
|
-
<style>
|
|
5912
|
-
.fr-dashboard {
|
|
5913
|
-
--fr-bg: #ffffff;
|
|
5914
|
-
--fr-bg-muted: #f9fafb;
|
|
5915
|
-
--fr-bg-subtle: #f3f4f6;
|
|
5916
|
-
--fr-border: #e5e7eb;
|
|
5917
|
-
--fr-text: #111827;
|
|
5918
|
-
--fr-text-muted: #6b7280;
|
|
5919
|
-
--fr-primary: #3b82f6;
|
|
5920
|
-
--fr-success: #22c55e;
|
|
5921
|
-
--fr-warning: #f59e0b;
|
|
5922
|
-
--fr-error: #ef4444;
|
|
5923
|
-
--fr-info: #6b7280;
|
|
5924
|
-
|
|
5925
|
-
/* Typography - consolidated to fewer sizes */
|
|
5926
|
-
--fr-font-sm: 13px;
|
|
5927
|
-
--fr-font-base: 14px;
|
|
5928
|
-
--fr-font-lg: 20px;
|
|
5929
|
-
|
|
5930
|
-
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
5931
|
-
padding: 20px;
|
|
5932
|
-
max-width: 900px;
|
|
5933
|
-
margin: 0 auto;
|
|
5934
|
-
color: var(--fr-text);
|
|
5935
|
-
line-height: 1.5;
|
|
5936
|
-
}
|
|
5937
|
-
|
|
5938
|
-
@media (prefers-color-scheme: dark) {
|
|
5939
|
-
.fr-dashboard {
|
|
5940
|
-
--fr-bg: #1f2937;
|
|
5941
|
-
--fr-bg-muted: #374151;
|
|
5942
|
-
--fr-bg-subtle: #4b5563;
|
|
5943
|
-
--fr-border: #4b5563;
|
|
5944
|
-
--fr-text: #f9fafb;
|
|
5945
|
-
--fr-text-muted: #9ca3af;
|
|
5946
|
-
}
|
|
5947
|
-
}
|
|
5948
|
-
|
|
5949
|
-
.fr-header {
|
|
5950
|
-
display: flex;
|
|
5951
|
-
justify-content: space-between;
|
|
5952
|
-
align-items: center;
|
|
5953
|
-
margin-bottom: 16px;
|
|
5954
|
-
padding-bottom: 12px;
|
|
5955
|
-
border-bottom: 1px solid var(--fr-border);
|
|
5956
|
-
}
|
|
5957
|
-
.fr-header__model {
|
|
5958
|
-
font-weight: 600;
|
|
5959
|
-
font-size: var(--fr-font-base);
|
|
5960
|
-
color: var(--fr-text);
|
|
5961
|
-
}
|
|
5962
|
-
.fr-header__time {
|
|
5963
|
-
font-weight: 600;
|
|
5964
|
-
font-size: var(--fr-font-base);
|
|
5965
|
-
color: var(--fr-text);
|
|
5966
|
-
}
|
|
5967
|
-
|
|
5968
|
-
.fr-banner {
|
|
5969
|
-
border-radius: 8px;
|
|
5970
|
-
padding: 12px 16px;
|
|
5971
|
-
margin-bottom: 20px;
|
|
5972
|
-
font-size: var(--fr-font-base);
|
|
5973
|
-
}
|
|
5974
|
-
.fr-banner--error {
|
|
5975
|
-
background: color-mix(in srgb, var(--fr-error) 10%, var(--fr-bg));
|
|
5976
|
-
border: 1px solid var(--fr-error);
|
|
5977
|
-
}
|
|
5978
|
-
.fr-banner--warning {
|
|
5979
|
-
background: color-mix(in srgb, var(--fr-warning) 10%, var(--fr-bg));
|
|
5980
|
-
border: 1px solid var(--fr-warning);
|
|
5981
|
-
}
|
|
5982
|
-
|
|
5983
|
-
.fr-cards {
|
|
5984
|
-
display: grid;
|
|
5985
|
-
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
5986
|
-
gap: 16px;
|
|
5987
|
-
margin-bottom: 24px;
|
|
5988
|
-
}
|
|
5989
|
-
|
|
5990
|
-
.fr-card {
|
|
5991
|
-
padding: 16px;
|
|
5992
|
-
background: var(--fr-bg-muted);
|
|
5993
|
-
border-radius: 8px;
|
|
5994
|
-
text-align: center;
|
|
5995
|
-
}
|
|
5996
|
-
.fr-card__label {
|
|
5997
|
-
font-size: var(--fr-font-sm);
|
|
5998
|
-
color: var(--fr-text-muted);
|
|
5999
|
-
margin-bottom: 4px;
|
|
6000
|
-
}
|
|
6001
|
-
.fr-card__value {
|
|
6002
|
-
font-size: var(--fr-font-lg);
|
|
6003
|
-
font-weight: 600;
|
|
6004
|
-
}
|
|
6005
|
-
.fr-card__sub {
|
|
6006
|
-
font-size: var(--fr-font-sm);
|
|
6007
|
-
color: var(--fr-text-muted);
|
|
6008
|
-
margin-top: 2px;
|
|
6009
|
-
}
|
|
6010
|
-
|
|
6011
|
-
.fr-badge {
|
|
6012
|
-
display: inline-flex;
|
|
6013
|
-
align-items: center;
|
|
6014
|
-
gap: 4px;
|
|
6015
|
-
padding: 4px 10px;
|
|
6016
|
-
border-radius: 4px;
|
|
6017
|
-
font-weight: 600;
|
|
6018
|
-
font-size: var(--fr-font-sm);
|
|
6019
|
-
}
|
|
6020
|
-
.fr-badge--completed { background: color-mix(in srgb, var(--fr-success) 15%, transparent); color: var(--fr-success); }
|
|
6021
|
-
.fr-badge--partial { background: color-mix(in srgb, var(--fr-warning) 15%, transparent); color: var(--fr-warning); }
|
|
6022
|
-
.fr-badge--cancelled { background: color-mix(in srgb, var(--fr-info) 15%, transparent); color: var(--fr-info); }
|
|
6023
|
-
.fr-badge--failed { background: color-mix(in srgb, var(--fr-error) 15%, transparent); color: var(--fr-error); }
|
|
6024
|
-
|
|
6025
|
-
.fr-section {
|
|
6026
|
-
margin-bottom: 24px;
|
|
6027
|
-
}
|
|
6028
|
-
.fr-section__title {
|
|
6029
|
-
font-size: var(--fr-font-base);
|
|
6030
|
-
font-weight: 500;
|
|
6031
|
-
color: var(--fr-text);
|
|
6032
|
-
margin-bottom: 8px;
|
|
6033
|
-
}
|
|
6034
|
-
|
|
6035
|
-
.fr-progress {
|
|
6036
|
-
background: var(--fr-border);
|
|
6037
|
-
border-radius: 4px;
|
|
6038
|
-
height: 20px;
|
|
6039
|
-
overflow: hidden;
|
|
6040
|
-
}
|
|
6041
|
-
.fr-progress__bar {
|
|
6042
|
-
background: var(--fr-primary);
|
|
6043
|
-
height: 100%;
|
|
6044
|
-
transition: width 0.3s ease;
|
|
6045
|
-
}
|
|
6046
|
-
.fr-progress__text {
|
|
6047
|
-
font-size: var(--fr-font-sm);
|
|
6048
|
-
color: var(--fr-text-muted);
|
|
6049
|
-
margin-top: 4px;
|
|
6050
|
-
}
|
|
6051
|
-
|
|
6052
|
-
.fr-progress__segments {
|
|
6053
|
-
display: flex;
|
|
6054
|
-
height: 100%;
|
|
6055
|
-
width: 100%;
|
|
6056
|
-
}
|
|
6057
|
-
.fr-progress-segment {
|
|
6058
|
-
height: 100%;
|
|
6059
|
-
min-width: 2px;
|
|
6060
|
-
border-right: 2px solid var(--fr-bg);
|
|
6061
|
-
cursor: pointer;
|
|
6062
|
-
}
|
|
6063
|
-
.fr-progress-segment:last-child {
|
|
6064
|
-
border-right: none;
|
|
6065
|
-
}
|
|
6066
|
-
.fr-progress-segment--filled {
|
|
6067
|
-
background: var(--fr-primary);
|
|
6068
|
-
}
|
|
6069
|
-
.fr-progress-segment--filled:hover {
|
|
6070
|
-
background: color-mix(in srgb, var(--fr-primary) 70%, white);
|
|
6071
|
-
}
|
|
6072
|
-
.fr-progress-segment--prefilled {
|
|
6073
|
-
background: #8b5cf6;
|
|
6074
|
-
}
|
|
6075
|
-
.fr-progress-segment--prefilled:hover {
|
|
6076
|
-
background: color-mix(in srgb, #8b5cf6 70%, white);
|
|
6077
|
-
}
|
|
6078
|
-
.fr-progress-segment--skipped {
|
|
6079
|
-
background: var(--fr-warning);
|
|
6080
|
-
}
|
|
6081
|
-
.fr-progress-segment--skipped:hover {
|
|
6082
|
-
background: color-mix(in srgb, var(--fr-warning) 70%, white);
|
|
6083
|
-
}
|
|
6084
|
-
.fr-progress-segment--empty {
|
|
6085
|
-
background: var(--fr-border);
|
|
6086
|
-
}
|
|
6087
|
-
|
|
6088
|
-
/* Gantt chart - each call on its own row */
|
|
6089
|
-
.fr-gantt {
|
|
6090
|
-
margin-bottom: 8px;
|
|
6091
|
-
}
|
|
6092
|
-
.fr-gantt__row {
|
|
6093
|
-
display: flex;
|
|
6094
|
-
align-items: center;
|
|
6095
|
-
height: 20px;
|
|
6096
|
-
margin-bottom: 3px;
|
|
6097
|
-
}
|
|
6098
|
-
.fr-gantt__label {
|
|
6099
|
-
width: 90px;
|
|
6100
|
-
flex-shrink: 0;
|
|
6101
|
-
font-size: 11px;
|
|
6102
|
-
color: var(--fr-text-muted);
|
|
6103
|
-
white-space: nowrap;
|
|
6104
|
-
overflow: hidden;
|
|
6105
|
-
text-overflow: ellipsis;
|
|
6106
|
-
padding-right: 8px;
|
|
6107
|
-
text-align: right;
|
|
6108
|
-
}
|
|
6109
|
-
.fr-gantt__track {
|
|
6110
|
-
flex: 1;
|
|
6111
|
-
background: var(--fr-bg-subtle);
|
|
6112
|
-
border-radius: 3px;
|
|
6113
|
-
height: 14px;
|
|
6114
|
-
position: relative;
|
|
6115
|
-
}
|
|
6116
|
-
.fr-gantt__bar {
|
|
6117
|
-
position: absolute;
|
|
6118
|
-
top: 2px;
|
|
6119
|
-
height: calc(100% - 4px);
|
|
6120
|
-
min-width: 6px;
|
|
6121
|
-
border-radius: 2px;
|
|
6122
|
-
cursor: pointer;
|
|
6123
|
-
}
|
|
6124
|
-
.fr-gantt__bar:hover {
|
|
6125
|
-
filter: brightness(1.15);
|
|
6126
|
-
}
|
|
6127
|
-
.fr-gantt__bar--llm {
|
|
6128
|
-
background: var(--fr-primary);
|
|
6129
|
-
}
|
|
6130
|
-
.fr-gantt__bar--tool {
|
|
6131
|
-
background: var(--fr-success);
|
|
6132
|
-
}
|
|
6133
|
-
.fr-gantt__legend {
|
|
6134
|
-
display: flex;
|
|
6135
|
-
gap: 16px;
|
|
6136
|
-
font-size: var(--fr-font-sm);
|
|
6137
|
-
color: var(--fr-text-muted);
|
|
6138
|
-
margin-top: 12px;
|
|
6139
|
-
padding-top: 8px;
|
|
6140
|
-
border-top: 1px solid var(--fr-border);
|
|
6141
|
-
}
|
|
6142
|
-
.fr-gantt__legend-item {
|
|
6143
|
-
display: flex;
|
|
6144
|
-
align-items: center;
|
|
6145
|
-
gap: 6px;
|
|
6146
|
-
}
|
|
6147
|
-
.fr-gantt__legend-dot {
|
|
6148
|
-
width: 10px;
|
|
6149
|
-
height: 10px;
|
|
6150
|
-
border-radius: 2px;
|
|
6151
|
-
}
|
|
6152
|
-
.fr-gantt__legend-dot--llm { background: var(--fr-primary); }
|
|
6153
|
-
.fr-gantt__legend-dot--tool { background: var(--fr-success); }
|
|
6154
|
-
|
|
6155
|
-
/* Tooltip container */
|
|
6156
|
-
.fr-tooltip {
|
|
6157
|
-
position: fixed;
|
|
6158
|
-
background: #1f2937;
|
|
6159
|
-
color: #f9fafb;
|
|
6160
|
-
padding: 8px 12px;
|
|
6161
|
-
border-radius: 4px;
|
|
6162
|
-
font-size: var(--fr-font-sm);
|
|
6163
|
-
white-space: pre-line;
|
|
6164
|
-
pointer-events: none;
|
|
6165
|
-
z-index: 1000;
|
|
6166
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
6167
|
-
opacity: 0;
|
|
6168
|
-
visibility: hidden;
|
|
6169
|
-
transition: opacity 0.05s ease-out, visibility 0.05s ease-out;
|
|
6170
|
-
}
|
|
6171
|
-
.fr-tooltip.visible {
|
|
6172
|
-
opacity: 1;
|
|
6173
|
-
visibility: visible;
|
|
6174
|
-
transition: opacity 0.2s ease-in, visibility 0.2s ease-in;
|
|
6175
|
-
}
|
|
6176
|
-
|
|
6177
|
-
.fr-table {
|
|
6178
|
-
width: 100%;
|
|
6179
|
-
border-collapse: collapse;
|
|
6180
|
-
font-size: var(--fr-font-sm);
|
|
6181
|
-
}
|
|
6182
|
-
.fr-table th {
|
|
6183
|
-
padding: 8px 12px;
|
|
6184
|
-
text-align: left;
|
|
6185
|
-
font-weight: 600;
|
|
6186
|
-
background: var(--fr-bg-subtle);
|
|
6187
|
-
}
|
|
6188
|
-
.fr-table th:not(:first-child) { text-align: center; }
|
|
6189
|
-
.fr-table td {
|
|
6190
|
-
padding: 8px 12px;
|
|
6191
|
-
border-bottom: 1px solid var(--fr-border);
|
|
6192
|
-
}
|
|
6193
|
-
.fr-table td:not(:first-child) { text-align: center; }
|
|
6194
|
-
|
|
6195
|
-
.fr-details {
|
|
6196
|
-
border: none;
|
|
6197
|
-
background: none;
|
|
6198
|
-
}
|
|
6199
|
-
.fr-details > summary {
|
|
6200
|
-
cursor: pointer;
|
|
6201
|
-
font-size: var(--fr-font-base);
|
|
6202
|
-
font-weight: 500;
|
|
6203
|
-
color: var(--fr-text);
|
|
6204
|
-
padding: 8px 0;
|
|
6205
|
-
list-style: none;
|
|
6206
|
-
}
|
|
6207
|
-
.fr-details > summary::-webkit-details-marker { display: none; }
|
|
6208
|
-
.fr-details > summary::before {
|
|
6209
|
-
content: '▶';
|
|
6210
|
-
display: inline-block;
|
|
6211
|
-
margin-right: 8px;
|
|
6212
|
-
transition: transform 0.2s;
|
|
6213
|
-
font-size: 11px;
|
|
6214
|
-
}
|
|
6215
|
-
.fr-details[open] > summary::before {
|
|
6216
|
-
transform: rotate(90deg);
|
|
6217
|
-
}
|
|
6218
|
-
.fr-details__content {
|
|
6219
|
-
background: var(--fr-bg-muted);
|
|
6220
|
-
border-radius: 8px;
|
|
6221
|
-
padding: 16px;
|
|
6222
|
-
margin-top: 8px;
|
|
6223
|
-
}
|
|
6224
|
-
|
|
6225
|
-
.fr-turn {
|
|
6226
|
-
margin-bottom: 8px;
|
|
6227
|
-
background: var(--fr-bg-muted);
|
|
6228
|
-
border-radius: 4px;
|
|
6229
|
-
}
|
|
6230
|
-
.fr-turn summary {
|
|
6231
|
-
cursor: pointer;
|
|
6232
|
-
padding: 12px;
|
|
6233
|
-
font-size: var(--fr-font-sm);
|
|
6234
|
-
list-style: none;
|
|
6235
|
-
}
|
|
6236
|
-
.fr-turn summary::-webkit-details-marker { display: none; }
|
|
6237
|
-
.fr-turn summary::before {
|
|
6238
|
-
content: '▶';
|
|
6239
|
-
display: inline-block;
|
|
6240
|
-
margin-right: 8px;
|
|
6241
|
-
transition: transform 0.2s;
|
|
6242
|
-
font-size: 11px;
|
|
6243
|
-
}
|
|
6244
|
-
.fr-turn[open] summary::before {
|
|
6245
|
-
transform: rotate(90deg);
|
|
6246
|
-
}
|
|
6247
|
-
.fr-turn__content {
|
|
6248
|
-
padding: 0 12px 12px;
|
|
6249
|
-
}
|
|
6250
|
-
.fr-turn__tools {
|
|
6251
|
-
margin: 0;
|
|
6252
|
-
padding-left: 20px;
|
|
6253
|
-
list-style: none;
|
|
6254
|
-
}
|
|
6255
|
-
.fr-turn__tool {
|
|
6256
|
-
margin: 4px 0;
|
|
6257
|
-
font-size: var(--fr-font-sm);
|
|
6258
|
-
color: var(--fr-text-muted);
|
|
6259
|
-
}
|
|
6260
|
-
.fr-turn__tool--error { color: var(--fr-error); }
|
|
6261
|
-
|
|
6262
|
-
.fr-turn__query {
|
|
6263
|
-
color: var(--fr-primary);
|
|
6264
|
-
font-style: italic;
|
|
6265
|
-
}
|
|
6266
|
-
|
|
6267
|
-
.fr-turn__patches {
|
|
6268
|
-
margin: 4px 0 8px 20px;
|
|
6269
|
-
padding: 8px 12px;
|
|
6270
|
-
background: var(--fr-bg-subtle);
|
|
6271
|
-
border-radius: 4px;
|
|
6272
|
-
font-size: var(--fr-font-sm);
|
|
6273
|
-
}
|
|
6274
|
-
.fr-turn__patch {
|
|
6275
|
-
margin: 4px 0;
|
|
6276
|
-
padding: 4px 0;
|
|
6277
|
-
border-bottom: 1px solid var(--fr-border);
|
|
6278
|
-
}
|
|
6279
|
-
.fr-turn__patch:last-child {
|
|
6280
|
-
border-bottom: none;
|
|
6281
|
-
margin-bottom: 0;
|
|
6282
|
-
padding-bottom: 0;
|
|
6283
|
-
}
|
|
6284
|
-
.fr-turn__patch-field {
|
|
6285
|
-
font-weight: 600;
|
|
6286
|
-
color: var(--fr-text);
|
|
6287
|
-
}
|
|
6288
|
-
.fr-turn__patch-op {
|
|
6289
|
-
font-size: 11px;
|
|
6290
|
-
padding: 1px 4px;
|
|
6291
|
-
border-radius: 2px;
|
|
6292
|
-
background: var(--fr-bg-muted);
|
|
6293
|
-
color: var(--fr-text-muted);
|
|
6294
|
-
margin-left: 6px;
|
|
6295
|
-
}
|
|
6296
|
-
.fr-turn__patch-value {
|
|
6297
|
-
display: block;
|
|
6298
|
-
margin-top: 2px;
|
|
6299
|
-
color: var(--fr-text-muted);
|
|
6300
|
-
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
|
6301
|
-
word-break: break-word;
|
|
6302
|
-
white-space: pre-wrap;
|
|
6303
|
-
max-height: 200px;
|
|
6304
|
-
overflow: auto;
|
|
6305
|
-
}
|
|
6306
|
-
.fr-turn__patch-value--skip {
|
|
6307
|
-
color: var(--fr-warning);
|
|
6308
|
-
font-style: italic;
|
|
6309
|
-
}
|
|
6310
|
-
.fr-turn__patch-value--clear {
|
|
6311
|
-
color: var(--fr-info);
|
|
6312
|
-
font-style: italic;
|
|
6313
|
-
}
|
|
6314
|
-
|
|
6315
|
-
.fr-raw {
|
|
6316
|
-
position: relative;
|
|
6317
|
-
}
|
|
6318
|
-
.fr-copy-btn {
|
|
6319
|
-
position: absolute;
|
|
6320
|
-
top: 8px;
|
|
6321
|
-
right: 8px;
|
|
6322
|
-
padding: 4px 8px;
|
|
6323
|
-
font-size: var(--fr-font-sm);
|
|
6324
|
-
background: var(--fr-bg-subtle);
|
|
6325
|
-
border: 1px solid var(--fr-border);
|
|
6326
|
-
border-radius: 4px;
|
|
6327
|
-
cursor: pointer;
|
|
6328
|
-
color: var(--fr-text-muted);
|
|
6329
|
-
transition: all 0.15s;
|
|
6330
|
-
}
|
|
6331
|
-
.fr-copy-btn:hover {
|
|
6332
|
-
background: var(--fr-border);
|
|
6333
|
-
color: var(--fr-text);
|
|
6334
|
-
}
|
|
6335
|
-
.fr-copy-btn:active {
|
|
6336
|
-
transform: scale(0.95);
|
|
6337
|
-
}
|
|
6338
|
-
|
|
6339
|
-
/* Scoped pre styles to override parent .tab-content pre */
|
|
6340
|
-
.fr-dashboard pre {
|
|
6341
|
-
background: var(--fr-bg-muted);
|
|
6342
|
-
color: var(--fr-text);
|
|
6343
|
-
padding: 1rem;
|
|
6344
|
-
border-radius: 6px;
|
|
6345
|
-
border: 1px solid var(--fr-border);
|
|
6346
|
-
overflow-x: auto;
|
|
6347
|
-
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
|
6348
|
-
font-size: 0.85rem;
|
|
6349
|
-
line-height: 1.5;
|
|
6350
|
-
margin: 0;
|
|
6351
|
-
}
|
|
6352
|
-
|
|
6353
|
-
/* Override syntax highlighting colors for dark mode compatibility */
|
|
6354
|
-
.fr-dashboard .syn-key { color: var(--fr-primary); }
|
|
6355
|
-
.fr-dashboard .syn-string { color: var(--fr-success); }
|
|
6356
|
-
.fr-dashboard .syn-number { color: var(--fr-primary); }
|
|
6357
|
-
.fr-dashboard .syn-bool { color: var(--fr-warning); }
|
|
6358
|
-
.fr-dashboard .syn-null { color: var(--fr-error); }
|
|
6359
|
-
|
|
6360
|
-
@media (max-width: 600px) {
|
|
6361
|
-
.fr-dashboard { padding: 12px; }
|
|
6362
|
-
.fr-cards { grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
|
6363
|
-
.fr-card { padding: 12px; }
|
|
6364
|
-
.fr-card__value { font-size: 18px; }
|
|
6365
|
-
.fr-table { font-size: var(--fr-font-sm); }
|
|
6366
|
-
.fr-table th, .fr-table td { padding: 6px 8px; }
|
|
6367
|
-
}
|
|
6368
|
-
</style>
|
|
6369
|
-
`;
|
|
6370
|
-
/**
|
|
6371
|
-
* Render fill record content (dashboard-style visualization).
|
|
6372
|
-
* Uses CSS custom properties for theming with automatic dark mode support.
|
|
6373
|
-
* Mobile responsive with grid-based layout.
|
|
6374
|
-
*
|
|
6375
|
-
* @public Exported for testing and reuse.
|
|
6376
|
-
*/
|
|
6377
|
-
function renderFillRecordContent(record) {
|
|
6378
|
-
const { status, statusDetail, startedAt, durationMs, llm, formProgress, toolSummary, timeline } = record;
|
|
6379
|
-
const startDate = new Date(startedAt);
|
|
6380
|
-
const formattedDate = startDate.toLocaleDateString("en-US", {
|
|
6381
|
-
month: "short",
|
|
6382
|
-
day: "numeric",
|
|
6383
|
-
year: "numeric"
|
|
6384
|
-
});
|
|
6385
|
-
const formattedTime = startDate.toLocaleTimeString("en-US", {
|
|
6386
|
-
hour: "numeric",
|
|
6387
|
-
minute: "2-digit",
|
|
6388
|
-
hour12: true
|
|
6389
|
-
});
|
|
6390
|
-
const headerInfo = `
|
|
6391
|
-
<div class="fr-header">
|
|
6392
|
-
<div class="fr-header__model">${escapeHtml(llm.model)}</div>
|
|
6393
|
-
<div class="fr-header__time">${formattedDate} at ${formattedTime}</div>
|
|
6394
|
-
</div>
|
|
6395
|
-
`;
|
|
6396
|
-
let statusBanner = "";
|
|
6397
|
-
if (status !== "completed") {
|
|
6398
|
-
const bannerClass = status === "failed" ? "fr-banner--error" : "fr-banner--warning";
|
|
6399
|
-
const icon = status === "failed" ? "✕" : "⚠";
|
|
6400
|
-
const title = status === "failed" ? "FAILED" : status === "cancelled" ? "CANCELLED" : "PARTIAL";
|
|
6401
|
-
const msg = statusDetail ?? (status === "partial" ? "Did not complete all fields" : "");
|
|
6402
|
-
statusBanner = `<div class="fr-banner ${bannerClass}"><strong>${icon} ${title}${msg ? ":" : ""}</strong>${msg ? ` ${escapeHtml(msg)}` : ""}</div>`;
|
|
6403
|
-
}
|
|
6404
|
-
const totalTokens = llm.inputTokens + llm.outputTokens;
|
|
6405
|
-
const summaryCards = `
|
|
6406
|
-
<div class="fr-cards">
|
|
6407
|
-
<div class="fr-card">
|
|
6408
|
-
<div class="fr-card__label">Status</div>
|
|
6409
|
-
<div><span class="${`fr-badge fr-badge--${status}`}">${{
|
|
6410
|
-
completed: "✓",
|
|
6411
|
-
partial: "⚠",
|
|
6412
|
-
cancelled: "⊘",
|
|
6413
|
-
failed: "✕"
|
|
6414
|
-
}[status] ?? "?"} ${status.charAt(0).toUpperCase() + status.slice(1)}</span></div>
|
|
6415
|
-
</div>
|
|
6416
|
-
<div class="fr-card">
|
|
6417
|
-
<div class="fr-card__label">Duration</div>
|
|
6418
|
-
<div class="fr-card__value">${formatDuration(durationMs)}</div>
|
|
6419
|
-
</div>
|
|
6420
|
-
<div class="fr-card">
|
|
6421
|
-
<div class="fr-card__label">Turns</div>
|
|
6422
|
-
<div class="fr-card__value">${timeline.length}</div>
|
|
6423
|
-
</div>
|
|
6424
|
-
<div class="fr-card">
|
|
6425
|
-
<div class="fr-card__label">Tokens</div>
|
|
6426
|
-
<div class="fr-card__value">${formatTokens(totalTokens)}</div>
|
|
6427
|
-
<div class="fr-card__sub">${formatTokens(llm.inputTokens)} in / ${formatTokens(llm.outputTokens)} out</div>
|
|
6428
|
-
</div>
|
|
6429
|
-
</div>
|
|
6430
|
-
`;
|
|
6431
|
-
const fieldsMap = /* @__PURE__ */ new Map();
|
|
6432
|
-
for (const turn of timeline) for (const tc of turn.toolCalls) if (tc.tool === "fill_form" && tc.input.patches) {
|
|
6433
|
-
const patches = tc.input.patches;
|
|
6434
|
-
for (const patch of patches) if (patch.fieldId && patch.op) fieldsMap.set(patch.fieldId, {
|
|
6435
|
-
fieldId: patch.fieldId,
|
|
6436
|
-
op: patch.op,
|
|
6437
|
-
turnNumber: turn.turnNumber
|
|
6438
|
-
});
|
|
6439
|
-
}
|
|
6440
|
-
const fieldsFilled = Array.from(fieldsMap.values());
|
|
6441
|
-
const totalFields = formProgress.totalFields;
|
|
6442
|
-
const filledFields = formProgress.filledFields;
|
|
6443
|
-
const skippedFields = formProgress.skippedFields;
|
|
6444
|
-
const abortedFields = formProgress.abortedFields ?? 0;
|
|
6445
|
-
const progressPercent = totalFields > 0 ? Math.round(filledFields / totalFields * 100) : 0;
|
|
6446
|
-
const segmentWidth = totalFields > 0 ? 100 / totalFields : 0;
|
|
6447
|
-
const aiFilledFields = fieldsFilled.filter((f) => f.op !== "skip_field" && f.op !== "abort_field");
|
|
6448
|
-
const aiFilledSegmentsHtml = aiFilledFields.map((f) => {
|
|
6449
|
-
const opLabel = f.op.replace(/_/g, " ");
|
|
6450
|
-
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>`;
|
|
6451
|
-
}).join("");
|
|
6452
|
-
const prefilledCount = Math.max(0, filledFields - aiFilledFields.length);
|
|
6453
|
-
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>` : "";
|
|
6454
|
-
const skippedSegmentsHtml = fieldsFilled.filter((f) => f.op === "skip_field" || f.op === "abort_field").map((f) => {
|
|
6455
|
-
const opLabel = f.op === "skip_field" ? "skipped" : "aborted";
|
|
6456
|
-
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>`;
|
|
6457
|
-
}).join("");
|
|
6458
|
-
const unfilledCount = totalFields - filledFields - skippedFields - abortedFields;
|
|
6459
|
-
const unfilledSegmentsHtml = unfilledCount > 0 ? `<div class="fr-progress-segment fr-progress-segment--empty" style="width: ${segmentWidth * unfilledCount}%"></div>` : "";
|
|
6460
|
-
const progressDetails = [];
|
|
6461
|
-
if (prefilledCount > 0) progressDetails.push(`${prefilledCount} pre-filled`);
|
|
6462
|
-
if (skippedFields > 0) progressDetails.push(`${skippedFields} skipped`);
|
|
6463
|
-
const progressBar = `
|
|
6464
|
-
<div class="fr-section">
|
|
6465
|
-
<div class="fr-section__title">Progress</div>
|
|
6466
|
-
<div class="fr-progress">
|
|
6467
|
-
<div class="fr-progress__segments">
|
|
6468
|
-
${prefilledSegmentsHtml}${aiFilledSegmentsHtml}${skippedSegmentsHtml}${unfilledSegmentsHtml}
|
|
6469
|
-
</div>
|
|
6470
|
-
</div>
|
|
6471
|
-
<div class="fr-progress__text">
|
|
6472
|
-
${filledFields}/${totalFields} fields filled (${progressPercent}%)${progressDetails.length > 0 ? ` • ${progressDetails.join(" • ")}` : ""}
|
|
6473
|
-
</div>
|
|
6474
|
-
</div>
|
|
6475
|
-
`;
|
|
6476
|
-
const totalMs = durationMs;
|
|
6477
|
-
const llmCallCount = llm.totalCalls;
|
|
6478
|
-
const toolCallCount = toolSummary.totalCalls;
|
|
6479
|
-
const timelineEvents = [];
|
|
6480
|
-
let currentTime = 0;
|
|
6481
|
-
for (const turn of timeline) {
|
|
6482
|
-
const toolTimeInTurn = turn.toolCalls.reduce((sum, tc) => sum + tc.durationMs, 0);
|
|
6483
|
-
const llmTimeInTurn = Math.max(0, turn.durationMs - toolTimeInTurn);
|
|
6484
|
-
if (llmTimeInTurn > 0) timelineEvents.push({
|
|
6485
|
-
type: "llm",
|
|
6486
|
-
startMs: currentTime,
|
|
6487
|
-
durationMs: llmTimeInTurn,
|
|
6488
|
-
turnNumber: turn.turnNumber,
|
|
6489
|
-
label: `Turn ${turn.turnNumber}`,
|
|
6490
|
-
tokens: {
|
|
6491
|
-
input: turn.tokens.input,
|
|
6492
|
-
output: turn.tokens.output,
|
|
6493
|
-
total: turn.tokens.input + turn.tokens.output
|
|
6494
|
-
}
|
|
6495
|
-
});
|
|
6496
|
-
currentTime += llmTimeInTurn;
|
|
6497
|
-
for (const tc of turn.toolCalls) {
|
|
6498
|
-
timelineEvents.push({
|
|
6499
|
-
type: "tool",
|
|
6500
|
-
startMs: currentTime,
|
|
6501
|
-
durationMs: tc.durationMs,
|
|
6502
|
-
turnNumber: turn.turnNumber,
|
|
6503
|
-
label: tc.tool
|
|
6504
|
-
});
|
|
6505
|
-
currentTime += tc.durationMs;
|
|
6506
|
-
}
|
|
6507
|
-
}
|
|
6508
|
-
const ganttRowsHtml = timelineEvents.map((e) => {
|
|
6509
|
-
const leftPct = totalMs > 0 ? e.startMs / totalMs * 100 : 0;
|
|
6510
|
-
const widthPct = totalMs > 0 ? e.durationMs / totalMs * 100 : 0;
|
|
6511
|
-
const barClass = e.type === "llm" ? "fr-gantt__bar--llm" : "fr-gantt__bar--tool";
|
|
6512
|
-
const tooltip = e.type === "llm" ? `${e.label} ${formatDuration(e.durationMs)} ${formatTokens(e.tokens?.total ?? 0)} tokens (${formatTokens(e.tokens?.input ?? 0)} in / ${formatTokens(e.tokens?.output ?? 0)} out)` : `${e.label} ${formatDuration(e.durationMs)} Turn ${e.turnNumber}`;
|
|
6513
|
-
return `
|
|
6514
|
-
<div class="fr-gantt__row">
|
|
6515
|
-
<div class="fr-gantt__label">${escapeHtml(e.label)}</div>
|
|
6516
|
-
<div class="fr-gantt__track">
|
|
6517
|
-
<div class="fr-gantt__bar ${barClass}" style="left: ${leftPct}%; width: ${widthPct}%" data-tooltip="${tooltip}" onmouseenter="frShowTip(this)" onmouseleave="frHideTip()"></div>
|
|
6518
|
-
</div>
|
|
6519
|
-
</div>`;
|
|
6520
|
-
}).join("");
|
|
6521
|
-
const llmTotalMs = timelineEvents.filter((e) => e.type === "llm").reduce((sum, e) => sum + e.durationMs, 0);
|
|
6522
|
-
const toolTotalMs = timelineEvents.filter((e) => e.type === "tool").reduce((sum, e) => sum + e.durationMs, 0);
|
|
6523
|
-
const timingSection = `
|
|
6524
|
-
<details class="fr-details fr-section" open>
|
|
6525
|
-
<summary>Timeline (${formatDuration(totalMs)} total)</summary>
|
|
6526
|
-
<div class="fr-details__content">
|
|
6527
|
-
<div class="fr-gantt">
|
|
6528
|
-
${ganttRowsHtml}
|
|
6529
|
-
<div class="fr-gantt__legend">
|
|
6530
|
-
<div class="fr-gantt__legend-item">
|
|
6531
|
-
<div class="fr-gantt__legend-dot fr-gantt__legend-dot--llm"></div>
|
|
6532
|
-
<span>LLM (${llmCallCount} call${llmCallCount !== 1 ? "s" : ""}, ${formatDuration(llmTotalMs)})</span>
|
|
6533
|
-
</div>
|
|
6534
|
-
<div class="fr-gantt__legend-item">
|
|
6535
|
-
<div class="fr-gantt__legend-dot fr-gantt__legend-dot--tool"></div>
|
|
6536
|
-
<span>Tools (${toolCallCount} call${toolCallCount !== 1 ? "s" : ""}, ${formatDuration(toolTotalMs)})</span>
|
|
6537
|
-
</div>
|
|
6538
|
-
</div>
|
|
6539
|
-
</div>
|
|
6540
|
-
</div>
|
|
6541
|
-
</details>
|
|
6542
|
-
`;
|
|
6543
|
-
let toolSection = "";
|
|
6544
|
-
if (toolSummary.byTool.length > 0) toolSection = `
|
|
6545
|
-
<details class="fr-details fr-section" open>
|
|
6546
|
-
<summary>Tool Summary</summary>
|
|
6547
|
-
<div style="overflow-x: auto; margin-top: 8px;">
|
|
6548
|
-
<table class="fr-table">
|
|
6549
|
-
<thead><tr><th>Tool</th><th>Calls</th><th>Success</th><th>Avg</th><th>p95</th></tr></thead>
|
|
6550
|
-
<tbody>${toolSummary.byTool.map((t) => `
|
|
6551
|
-
<tr>
|
|
6552
|
-
<td>${escapeHtml(t.toolName)}</td>
|
|
6553
|
-
<td>${t.callCount}</td>
|
|
6554
|
-
<td>${t.successCount === t.callCount ? "100%" : `${Math.round(t.successCount / t.callCount * 100)}%`}</td>
|
|
6555
|
-
<td>${formatDuration(t.timing.avgMs)}</td>
|
|
6556
|
-
<td>${formatDuration(t.timing.p95Ms)}</td>
|
|
6557
|
-
</tr>
|
|
6558
|
-
`).join("")}</tbody>
|
|
6559
|
-
</table>
|
|
6560
|
-
</div>
|
|
6561
|
-
</details>
|
|
6562
|
-
`;
|
|
6563
|
-
let timelineSection = "";
|
|
6564
|
-
if (timeline.length > 0) {
|
|
6565
|
-
const timelineItems = timeline.map((turn) => {
|
|
6566
|
-
const turnTokens = turn.tokens.input + turn.tokens.output;
|
|
6567
|
-
const toolCallsList = turn.toolCalls.map((tc) => renderToolCall(tc)).join("");
|
|
6568
|
-
const patchInfo = turn.patchesApplied > 0 ? ` • ${turn.patchesApplied} patches` : "";
|
|
6569
|
-
const rejectedInfo = turn.patchesRejected > 0 ? ` <span style="color: var(--fr-error)">(${turn.patchesRejected} rejected)</span>` : "";
|
|
6570
|
-
return `
|
|
6571
|
-
<details class="fr-turn">
|
|
6572
|
-
<summary><strong>Turn ${turn.turnNumber}</strong> • Order ${turn.order} • ${formatDuration(turn.durationMs)} • ${formatTokens(turnTokens)} tokens${patchInfo}${rejectedInfo}</summary>
|
|
6573
|
-
<div class="fr-turn__content">
|
|
6574
|
-
${turn.toolCalls.length > 0 ? `<ul class="fr-turn__tools">${toolCallsList}</ul>` : "<span class=\"fr-turn__tool\">No tool calls</span>"}
|
|
6575
|
-
</div>
|
|
6576
|
-
</details>
|
|
6577
|
-
`;
|
|
6578
|
-
}).join("");
|
|
6579
|
-
timelineSection = `
|
|
6580
|
-
<details class="fr-details fr-section">
|
|
6581
|
-
<summary>Turn Details (${timeline.length} turns)</summary>
|
|
6582
|
-
<div style="margin-top: 8px;">${timelineItems}</div>
|
|
6583
|
-
</details>
|
|
6584
|
-
`;
|
|
6585
|
-
}
|
|
6586
|
-
const rawSection = `
|
|
6587
|
-
<details class="fr-details fr-section">
|
|
6588
|
-
<summary>Raw YAML</summary>
|
|
6589
|
-
<div class="fr-raw" style="margin-top: 8px;">
|
|
6590
|
-
<button class="fr-copy-btn" onclick="frCopyYaml(this)">Copy</button>
|
|
6591
|
-
${renderYamlContent(YAML.stringify(record, { lineWidth: 0 }))}
|
|
6592
|
-
</div>
|
|
6593
|
-
</details>
|
|
6594
|
-
`;
|
|
6595
|
-
return `
|
|
6596
|
-
${FILL_RECORD_STYLES}
|
|
6597
|
-
<div id="fr-tooltip" class="fr-tooltip"></div>
|
|
6598
|
-
<div class="fr-dashboard">
|
|
6599
|
-
${headerInfo}
|
|
6600
|
-
${statusBanner}
|
|
6601
|
-
${summaryCards}
|
|
6602
|
-
${progressBar}
|
|
6603
|
-
${timingSection}
|
|
6604
|
-
${toolSection}
|
|
6605
|
-
${timelineSection}
|
|
6606
|
-
${rawSection}
|
|
6607
|
-
</div>
|
|
6608
|
-
`;
|
|
6609
|
-
}
|
|
6610
5487
|
|
|
6611
5488
|
//#endregion
|
|
6612
5489
|
//#region src/cli/commands/render.ts
|
|
@@ -6801,9 +5678,9 @@ function registerResearchCommand(program) {
|
|
|
6801
5678
|
console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
|
|
6802
5679
|
console.log(` ${schemaPath} ${pc.dim("(JSON Schema)")}`);
|
|
6803
5680
|
if (options.transcript && result.transcript) {
|
|
6804
|
-
const { serializeSession } = await import("./session-
|
|
5681
|
+
const { serializeSession } = await import("./session-VeSkVrck.mjs");
|
|
6805
5682
|
const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
|
|
6806
|
-
const { writeFile } = await import("./shared-
|
|
5683
|
+
const { writeFile } = await import("./shared-fb0nkzQi.mjs");
|
|
6807
5684
|
await writeFile(transcriptPath, serializeSession(result.transcript));
|
|
6808
5685
|
logInfo(ctx, `Transcript: ${transcriptPath}`);
|
|
6809
5686
|
}
|
|
@@ -7208,4 +6085,4 @@ async function runCli() {
|
|
|
7208
6085
|
|
|
7209
6086
|
//#endregion
|
|
7210
6087
|
export { runCli as t };
|
|
7211
|
-
//# sourceMappingURL=cli-
|
|
6088
|
+
//# sourceMappingURL=cli-C8F9yDsv.mjs.map
|