footprint-explainable-ui 0.13.3 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -35,10 +35,13 @@ __export(src_exports, {
35
35
  StoryNarrative: () => StoryNarrative,
36
36
  SubflowTree: () => SubflowTree,
37
37
  TimeTravelControls: () => TimeTravelControls,
38
+ buildEntryRangeIndex: () => buildEntryRangeIndex,
39
+ computeRevealedEntryCount: () => computeRevealedEntryCount,
38
40
  coolDark: () => coolDark,
39
41
  coolLight: () => coolLight,
40
42
  createSnapshots: () => createSnapshots,
41
43
  defaultTokens: () => defaultTokens,
44
+ extractSubflowNarrative: () => extractSubflowNarrative,
42
45
  rawDefaults: () => rawDefaults,
43
46
  subflowResultToSnapshots: () => subflowResultToSnapshots,
44
47
  themePresets: () => themePresets,
@@ -2256,6 +2259,81 @@ function TimeTravelControls({
2256
2259
  // src/components/ExplainableShell/ExplainableShell.tsx
2257
2260
  var import_react19 = require("react");
2258
2261
 
2262
+ // src/utils/narrativeSync.ts
2263
+ function buildEntryRangeIndex(entries) {
2264
+ const ranges = /* @__PURE__ */ new Map();
2265
+ let lastId;
2266
+ for (let i = 0; i < entries.length; i++) {
2267
+ const id = entries[i].runtimeStageId;
2268
+ if (id) {
2269
+ const existing = ranges.get(id);
2270
+ if (!existing) {
2271
+ ranges.set(id, { firstIdx: i, endIdx: i + 1 });
2272
+ } else {
2273
+ existing.endIdx = i + 1;
2274
+ }
2275
+ lastId = id;
2276
+ } else if (lastId) {
2277
+ ranges.get(lastId).endIdx = i + 1;
2278
+ }
2279
+ }
2280
+ return ranges;
2281
+ }
2282
+ function computeRevealedEntryCount(narrativeEntries, snapshots, selectedIndex, rangeIndex) {
2283
+ if (!narrativeEntries.length || snapshots.length === 0) return 0;
2284
+ if (rangeIndex) {
2285
+ let maxEndIdx = 0;
2286
+ for (let si = 0; si <= selectedIndex && si < snapshots.length; si++) {
2287
+ const targetId = snapshots[si].runtimeStageId;
2288
+ if (!targetId) continue;
2289
+ const range = rangeIndex.get(targetId);
2290
+ if (range && range.endIdx > maxEndIdx) {
2291
+ maxEndIdx = range.endIdx;
2292
+ }
2293
+ }
2294
+ return maxEndIdx;
2295
+ }
2296
+ let entryIdx = 0;
2297
+ for (let si = 0; si <= selectedIndex && si < snapshots.length; si++) {
2298
+ const targetId = snapshots[si].runtimeStageId;
2299
+ if (!targetId) continue;
2300
+ let found = false;
2301
+ for (let j = entryIdx; j < narrativeEntries.length; j++) {
2302
+ if (narrativeEntries[j].runtimeStageId === targetId) {
2303
+ found = true;
2304
+ entryIdx = j;
2305
+ break;
2306
+ }
2307
+ }
2308
+ if (!found) continue;
2309
+ while (entryIdx < narrativeEntries.length) {
2310
+ const eId = narrativeEntries[entryIdx].runtimeStageId;
2311
+ if (eId && eId !== targetId) break;
2312
+ entryIdx++;
2313
+ }
2314
+ }
2315
+ return entryIdx;
2316
+ }
2317
+ function extractSubflowNarrative(entries, subflowId, subflowName) {
2318
+ const prefix = subflowId + "/";
2319
+ const byPrefix = entries.filter((e) => e.stageName?.startsWith(prefix));
2320
+ if (byPrefix.length > 0) return byPrefix;
2321
+ const byId = entries.filter((e) => e.subflowId === subflowId);
2322
+ if (byId.length > 0) return byId;
2323
+ const result = [];
2324
+ const searchName = subflowName ?? subflowId;
2325
+ let inside = false;
2326
+ for (const entry of entries) {
2327
+ if (entry.type === "subflow" && entry.direction === "entry" && entry.stageName === searchName) {
2328
+ inside = true;
2329
+ continue;
2330
+ }
2331
+ if (inside && entry.type === "subflow" && entry.direction === "exit" && entry.stageName === searchName) break;
2332
+ if (inside) result.push(entry);
2333
+ }
2334
+ return result;
2335
+ }
2336
+
2259
2337
  // src/adapters/fromRuntimeSnapshot.ts
2260
2338
  function toVisualizationSnapshots(runtime, narrativeEntries) {
2261
2339
  const stageNarrativeMap = narrativeEntries?.length ? buildStageNarrativeMap(narrativeEntries) : /* @__PURE__ */ new Map();
@@ -2684,37 +2762,14 @@ function NarrativePanel({
2684
2762
  const endIdx = groupsToShow < stageBoundaries.length ? stageBoundaries[groupsToShow] : narrative.length;
2685
2763
  return Math.max(1, endIdx);
2686
2764
  }, [snapshots.length, selectedIndex, narrative]);
2687
- const revealedEntryCount = (0, import_react12.useMemo)(() => {
2688
- if (!narrativeEntries?.length || snapshots.length === 0) return 0;
2689
- let entryIdx = 0;
2690
- for (let si = 0; si <= selectedIndex && si < snapshots.length; si++) {
2691
- const snap = snapshots[si];
2692
- const keys = /* @__PURE__ */ new Set();
2693
- if (snap.stageLabel) keys.add(snap.stageLabel);
2694
- if (snap.stageName) keys.add(snap.stageName);
2695
- if (snap.subflowId) keys.add(snap.subflowId);
2696
- let found = false;
2697
- for (let j = entryIdx; j < narrativeEntries.length; j++) {
2698
- const e = narrativeEntries[j];
2699
- const eKey = e.stageId ?? e.subflowId ?? e.stageName;
2700
- if (eKey && keys.has(eKey)) {
2701
- found = true;
2702
- entryIdx = j;
2703
- break;
2704
- }
2705
- if (!eKey && !found) {
2706
- }
2707
- }
2708
- if (!found) continue;
2709
- while (entryIdx < narrativeEntries.length) {
2710
- const e = narrativeEntries[entryIdx];
2711
- const eKey = e.stageId ?? e.subflowId ?? e.stageName;
2712
- if (eKey && !keys.has(eKey)) break;
2713
- entryIdx++;
2714
- }
2715
- }
2716
- return entryIdx;
2717
- }, [narrativeEntries, snapshots, selectedIndex]);
2765
+ const rangeIndex = (0, import_react12.useMemo)(
2766
+ () => narrativeEntries?.length ? buildEntryRangeIndex(narrativeEntries) : void 0,
2767
+ [narrativeEntries]
2768
+ );
2769
+ const revealedEntryCount = (0, import_react12.useMemo)(
2770
+ () => narrativeEntries?.length ? computeRevealedEntryCount(narrativeEntries, snapshots, selectedIndex, rangeIndex) : 0,
2771
+ [narrativeEntries, snapshots, selectedIndex, rangeIndex]
2772
+ );
2718
2773
  const hasStructured = narrativeEntries && narrativeEntries.length > 0;
2719
2774
  const [copied, setCopied] = (0, import_react12.useState)(false);
2720
2775
  const buildLLMNarrative = (0, import_react12.useCallback)(() => {
@@ -2730,7 +2785,7 @@ function NarrativePanel({
2730
2785
  root.push(entry);
2731
2786
  } else {
2732
2787
  if (entry.type === "subflow") {
2733
- const isExit = entry.text.startsWith("Done:") || entry.text.startsWith("Exiting");
2788
+ const isExit = entry.direction === "exit";
2734
2789
  if (!isExit) {
2735
2790
  root.push(entry);
2736
2791
  }
@@ -2750,7 +2805,11 @@ function NarrativePanel({
2750
2805
  if (opts?.inSubflow && e.type === "subflow") continue;
2751
2806
  let text = e.text;
2752
2807
  if (opts?.inSubflow) {
2753
- text = text.replace(new RegExp(`\\[${opts.inSubflow}/`), "[");
2808
+ const prefix = `[${opts.inSubflow}/`;
2809
+ const idx = text.indexOf(prefix);
2810
+ if (idx !== -1) {
2811
+ text = text.slice(0, idx) + "[" + text.slice(idx + prefix.length);
2812
+ }
2754
2813
  }
2755
2814
  const isHeading = e.type === "stage" || e.type === "subflow" || e.type === "fork" || e.type === "selector";
2756
2815
  if (isHeading) {
@@ -4027,6 +4086,123 @@ var VLinePill = (0, import_react19.memo)(function VLinePill2({
4027
4086
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { flex: 1, width: 1, background: theme.border } })
4028
4087
  ] });
4029
4088
  });
4089
+ function detectKeyedSteps(data) {
4090
+ if (!data || typeof data !== "object") return null;
4091
+ const obj = data;
4092
+ for (const val of Object.values(obj)) {
4093
+ if (val && typeof val === "object" && !Array.isArray(val)) {
4094
+ const keys = Object.keys(val);
4095
+ if (keys.length > 0 && keys.every((k) => k.includes("#"))) {
4096
+ return val;
4097
+ }
4098
+ }
4099
+ }
4100
+ return null;
4101
+ }
4102
+ function findNumericField(entry) {
4103
+ for (const [k, v2] of Object.entries(entry)) {
4104
+ if (typeof v2 === "number") return { key: k, value: v2 };
4105
+ }
4106
+ return null;
4107
+ }
4108
+ function KeyedRecorderView({
4109
+ data,
4110
+ description,
4111
+ snapshots,
4112
+ selectedIndex
4113
+ }) {
4114
+ const [showAggregate, setShowAggregate] = (0, import_react19.useState)(false);
4115
+ const steps = (0, import_react19.useMemo)(() => detectKeyedSteps(data), [data]);
4116
+ const visibleIds = (0, import_react19.useMemo)(() => {
4117
+ const ids = /* @__PURE__ */ new Set();
4118
+ for (let i = 0; i <= selectedIndex && i < snapshots.length; i++) {
4119
+ const id = snapshots[i].runtimeStageId;
4120
+ if (id) ids.add(id);
4121
+ }
4122
+ return ids;
4123
+ }, [snapshots, selectedIndex]);
4124
+ const isAtEnd = selectedIndex >= snapshots.length - 1;
4125
+ if (!steps) {
4126
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { padding: 12, fontFamily: theme.fontMono, fontSize: 11, whiteSpace: "pre-wrap", overflow: "auto", height: "100%" }, children: typeof data === "string" ? data : JSON.stringify(data, null, 2) });
4127
+ }
4128
+ const allKeys = Object.keys(steps);
4129
+ const visibleEntries = allKeys.filter((k) => visibleIds.has(k));
4130
+ const numField = allKeys.length > 0 ? findNumericField(steps[allKeys[0]]) : null;
4131
+ const numFieldKey = numField?.key ?? "";
4132
+ let runningTotal = 0;
4133
+ if (numFieldKey) {
4134
+ for (const k of visibleEntries) {
4135
+ runningTotal += steps[k][numFieldKey] ?? 0;
4136
+ }
4137
+ }
4138
+ let grandTotal = 0;
4139
+ if (numFieldKey) {
4140
+ for (const entry of Object.values(steps)) {
4141
+ grandTotal += entry[numFieldKey] ?? 0;
4142
+ }
4143
+ }
4144
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { overflow: "auto", height: "100%", display: "flex", flexDirection: "column" }, children: [
4145
+ description && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { padding: "6px 12px", fontSize: 11, color: theme.textMuted, fontStyle: "italic", borderBottom: `1px solid ${theme.border}`, flexShrink: 0 }, children: description }),
4146
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { padding: 12, flex: 1, overflow: "auto" }, children: [
4147
+ visibleEntries.map((key) => {
4148
+ const entry = steps[key];
4149
+ const label = entry.stageName ?? key;
4150
+ const numVal = numFieldKey ? entry[numFieldKey] : void 0;
4151
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { display: "flex", alignItems: "center", padding: "4px 0", fontSize: 12, fontFamily: theme.fontMono, borderBottom: `1px solid ${theme.border}22` }, children: [
4152
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { style: { color: theme.textMuted, width: 140, flexShrink: 0, fontSize: 10 }, children: key }),
4153
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { style: { fontWeight: 600, flex: 1 }, children: label }),
4154
+ numVal !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { style: { color: theme.primary, fontWeight: 700, marginLeft: 8 }, children: numVal < 1 ? numVal.toFixed(3) : numVal.toFixed(1) })
4155
+ ] }, key);
4156
+ }),
4157
+ visibleEntries.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { color: theme.textMuted, fontSize: 11, fontStyle: "italic", padding: "8px 0" }, children: "Scrub the slider to reveal entries..." }),
4158
+ numFieldKey && visibleEntries.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { marginTop: 12, padding: "8px 12px", background: `color-mix(in srgb, ${theme.primary} 8%, transparent)`, borderRadius: 6, fontSize: 12 }, children: [
4159
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { style: { color: theme.textMuted }, children: [
4160
+ "Running total (",
4161
+ numFieldKey,
4162
+ "):"
4163
+ ] }),
4164
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { style: { fontWeight: 700, marginLeft: 8, color: theme.primary }, children: runningTotal < 1 ? runningTotal.toFixed(3) : runningTotal.toFixed(1) }),
4165
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { style: { color: theme.textMuted, marginLeft: 8, fontSize: 10 }, children: [
4166
+ "(",
4167
+ visibleEntries.length,
4168
+ " of ",
4169
+ allKeys.length,
4170
+ " steps)"
4171
+ ] })
4172
+ ] }),
4173
+ isAtEnd && numFieldKey && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { marginTop: 12 }, children: !showAggregate ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
4174
+ "button",
4175
+ {
4176
+ onClick: () => setShowAggregate(true),
4177
+ style: {
4178
+ background: theme.primary,
4179
+ color: "#fff",
4180
+ border: "none",
4181
+ borderRadius: 6,
4182
+ padding: "8px 16px",
4183
+ fontSize: 12,
4184
+ fontWeight: 600,
4185
+ cursor: "pointer",
4186
+ fontFamily: "inherit"
4187
+ },
4188
+ children: [
4189
+ "Aggregate (",
4190
+ numFieldKey,
4191
+ ")"
4192
+ ]
4193
+ }
4194
+ ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { padding: "10px 14px", background: `color-mix(in srgb, ${theme.success} 12%, transparent)`, borderRadius: 6, border: `1px solid ${theme.success}44` }, children: [
4195
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 4 }, children: "Aggregate \u2014 Grand Total" }),
4196
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { fontSize: 20, fontWeight: 700, color: theme.success }, children: grandTotal < 1 ? grandTotal.toFixed(3) : grandTotal.toFixed(1) }),
4197
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { fontSize: 10, color: theme.textMuted, marginTop: 2 }, children: [
4198
+ allKeys.length,
4199
+ " steps \xB7 ",
4200
+ numFieldKey
4201
+ ] })
4202
+ ] }) })
4203
+ ] })
4204
+ ] });
4205
+ }
4030
4206
  var DetailsContent = (0, import_react19.memo)(function DetailsContent2({
4031
4207
  snapshots,
4032
4208
  selectedIndex,
@@ -4050,6 +4226,12 @@ var DetailsContent = (0, import_react19.memo)(function DetailsContent2({
4050
4226
  ];
4051
4227
  const allViews = [...builtInViews, ...extraViews ?? []];
4052
4228
  const [activeViewId, setActiveViewId] = (0, import_react19.useState)(allViews[0]?.id ?? "memory");
4229
+ const viewIds = allViews.map((v2) => v2.id).join(",");
4230
+ (0, import_react19.useEffect)(() => {
4231
+ if (!allViews.find((v2) => v2.id === activeViewId)) {
4232
+ setActiveViewId(allViews[0]?.id ?? "memory");
4233
+ }
4234
+ }, [viewIds]);
4053
4235
  const activeView = allViews.find((v2) => v2.id === activeViewId) ?? allViews[0];
4054
4236
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }, children: [
4055
4237
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { display: "flex", borderBottom: `1px solid ${theme.border}`, flexShrink: 0, overflowX: "auto" }, children: allViews.map((view) => {
@@ -4100,25 +4282,6 @@ function resolveSubflowLevel(parentSpec, parentSnapshots, subflowNodeName, narra
4100
4282
  snapshots: sfSnapshots
4101
4283
  };
4102
4284
  }
4103
- function extractSubflowNarrative(entries, subflowId, subflowName) {
4104
- const prefix = subflowId + "/";
4105
- const byPrefix = entries.filter((e) => e.stageName?.startsWith(prefix));
4106
- if (byPrefix.length > 0) return byPrefix;
4107
- const byId = entries.filter((e) => e.subflowId === subflowId);
4108
- if (byId.length > 0) return byId;
4109
- const result = [];
4110
- const searchName = subflowName ?? subflowId;
4111
- let inside = false;
4112
- for (const entry of entries) {
4113
- if (entry.type === "subflow" && entry.text.includes(searchName) && entry.text.startsWith("Entering")) {
4114
- inside = true;
4115
- continue;
4116
- }
4117
- if (inside && entry.type === "subflow" && entry.text.includes(searchName) && entry.text.startsWith("Exiting")) break;
4118
- if (inside) result.push(entry);
4119
- }
4120
- return result;
4121
- }
4122
4285
  function findSubflowSpecNode(node, name) {
4123
4286
  if ((node.name === name || node.id === name) && node.isSubflowRoot) return node;
4124
4287
  if (node.children) {
@@ -4207,22 +4370,22 @@ function ExplainableShell({
4207
4370
  const recorders = runtimeSnapshot?.recorders;
4208
4371
  if (!recorders?.length) return [];
4209
4372
  const explicitIds = new Set((recorderViews ?? []).map((v2) => v2.id));
4210
- return recorders.filter((r) => !explicitIds.has(r.id)).map((r) => ({ id: r.id, name: r.name, data: r.data }));
4373
+ return recorders.filter((r) => !explicitIds.has(r.id)).map((r) => ({ id: r.id, name: r.name, description: r.description, data: r.data }));
4211
4374
  }, [runtimeSnapshot, recorderViews]);
4212
4375
  const hasNarrative = !!(narrative?.length || narrativeEntries?.length);
4213
4376
  const allTabs = (0, import_react19.useMemo)(() => {
4214
4377
  const tabs2 = [
4215
- { id: "result", name: "Result" },
4216
- { id: "memory", name: "Memory" }
4378
+ { id: "result", name: "Result", description: "Final output and console logs" },
4379
+ { id: "memory", name: "Memory", description: "Accumulator \u2014 progressive shared state at each stage" }
4217
4380
  ];
4218
4381
  if (hasNarrative) {
4219
- tabs2.push({ id: "narrative", name: "Narrative" });
4382
+ tabs2.push({ id: "narrative", name: "Narrative", description: "Translator (SequenceRecorder) \u2014 interleaved flow + data narrative per execution step" });
4220
4383
  }
4221
4384
  for (const v2 of recorderViews ?? []) {
4222
- tabs2.push({ id: v2.id, name: v2.name });
4385
+ tabs2.push({ id: v2.id, name: v2.name, description: v2.description });
4223
4386
  }
4224
4387
  for (const v2 of autoRecorderViews) {
4225
- tabs2.push({ id: v2.id, name: v2.name });
4388
+ tabs2.push({ id: v2.id, name: v2.name, description: v2.description });
4226
4389
  }
4227
4390
  const hideSet = new Set(hideTabsProp ?? []);
4228
4391
  return hideSet.size > 0 ? tabs2.filter((t) => !hideSet.has(t.id)) : tabs2;
@@ -4384,7 +4547,15 @@ function ExplainableShell({
4384
4547
  }
4385
4548
  const autoView = autoRecorderViews.find((v2) => v2.id === activeTab);
4386
4549
  if (autoView) {
4387
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { padding: 12, fontFamily: theme.fontMono, fontSize: 11, whiteSpace: "pre-wrap", overflow: "auto", height: "100%" }, children: typeof autoView.data === "string" ? autoView.data : JSON.stringify(autoView.data, null, 2) });
4550
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4551
+ KeyedRecorderView,
4552
+ {
4553
+ data: autoView.data,
4554
+ description: autoView.description,
4555
+ snapshots: activeSnapshots,
4556
+ selectedIndex: safeIdx
4557
+ }
4558
+ );
4388
4559
  }
4389
4560
  return null;
4390
4561
  }, [activeTab, resultData, logs, hideConsole, size, activeSnapshots, safeIdx, activeNarrativeEntries, activeNarrative, recorderViews, autoRecorderViews]);
@@ -4401,6 +4572,7 @@ function ExplainableShell({
4401
4572
  "button",
4402
4573
  {
4403
4574
  onClick: () => handleTabChange(tab.id),
4575
+ title: tab.description,
4404
4576
  style: {
4405
4577
  padding: "6px 14px",
4406
4578
  fontSize: 11,
@@ -4535,10 +4707,13 @@ function ExplainableShell({
4535
4707
  StoryNarrative,
4536
4708
  SubflowTree,
4537
4709
  TimeTravelControls,
4710
+ buildEntryRangeIndex,
4711
+ computeRevealedEntryCount,
4538
4712
  coolDark,
4539
4713
  coolLight,
4540
4714
  createSnapshots,
4541
4715
  defaultTokens,
4716
+ extractSubflowNarrative,
4542
4717
  rawDefaults,
4543
4718
  subflowResultToSnapshots,
4544
4719
  themePresets,