@almadar/ui 5.146.3 → 5.148.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { OrbitalSchema, SExpr, Effect, OrbitalVerificationAPI, TraitStateSnapshot, EventPayload, BusEvent, VerificationCheck, BridgeHealth, VerificationSnapshot, VerificationSummary, TransitionTrace, ServerResponseTrace, CheckStatus, AssetLoadStatus } from '@almadar/core';
2
+ import React__default from 'react';
2
3
  import { D as DrawableNode } from './paintDispatch-Cb_hQj4Y.js';
3
4
  import { ClassValue } from 'clsx';
4
5
 
@@ -34,7 +35,7 @@ type ContentSegment = {
34
35
  * Splits markdown into segments of plain markdown and fenced code blocks.
35
36
  * JSON/orb code blocks containing orbital schemas are tagged as 'orbital'.
36
37
  */
37
- declare function parseMarkdownWithCodeBlocks(content: string | undefined | null): ContentSegment[];
38
+ declare function parseMarkdownWithCodeBlocks$1(content: string | undefined | null): ContentSegment[];
38
39
  /**
39
40
  * Parse content to extract all segments including quiz tags and code blocks.
40
41
  *
@@ -45,6 +46,95 @@ declare function parseMarkdownWithCodeBlocks(content: string | undefined | null)
45
46
  */
46
47
  declare function parseContentSegments(content: string | undefined | null): ContentSegment[];
47
48
 
49
+ /**
50
+ * BloomQuizBlock Molecule Component
51
+ *
52
+ * Practice Q&A with Bloom's Taxonomy level badge. Emits `UI:ANSWER_BLOOM { index, level }`.
53
+ *
54
+ * Event Contract:
55
+ * - Emits: UI:ANSWER_BLOOM { index, level }
56
+ * - entityAware: false
57
+ */
58
+
59
+ type BloomLevel = 'remember' | 'understand' | 'apply' | 'analyze' | 'evaluate' | 'create';
60
+ interface BloomQuizBlockProps {
61
+ level: BloomLevel;
62
+ question: string;
63
+ answer: string;
64
+ /** Zero-based index (used in the emitted event payload) */
65
+ index?: number;
66
+ /** Whether the learner has already answered */
67
+ isAnswered?: boolean;
68
+ /** Event name emitted on first reveal (as `UI:<answerEvent>`). Defaults to 'ANSWER_BLOOM'. */
69
+ answerEvent?: string;
70
+ /** Additional CSS classes */
71
+ className?: string;
72
+ }
73
+ declare const BloomQuizBlock: React__default.FC<BloomQuizBlockProps>;
74
+
75
+ /**
76
+ * Utility for lesson-segment parsing: splits a markdown string into alternating
77
+ * markdown and fenced-code-block segments. Exported so SegmentRenderer and
78
+ * BloomQuizBlock can reuse without circular imports.
79
+ */
80
+ type MarkdownSegment = {
81
+ type: 'markdown';
82
+ content: string;
83
+ };
84
+ type CodeSegment = {
85
+ type: 'code';
86
+ language: string;
87
+ content: string;
88
+ runnable?: boolean;
89
+ };
90
+ type MixedSegment = MarkdownSegment | CodeSegment;
91
+ /** Splits markdown content into markdown and fenced-code-block segments. */
92
+ declare function parseMarkdownWithCodeBlocks(content: string): MixedSegment[];
93
+
94
+ /**
95
+ * parseLessonSegments — parses lesson markdown with embedded learning-science
96
+ * XML-style tags into a typed Segment array.
97
+ *
98
+ * Supported tags: <activate>, <connect>, <reflect>, <bloom level="...">,
99
+ * <question>/<answer>, <visualize type="..." description="..." />.
100
+ *
101
+ * Tags may be open (unclosed) — the parser falls back to consuming content
102
+ * until the next recognised tag, a section heading (`\n\n#`), or end-of-input.
103
+ */
104
+
105
+ type InteractiveOrbitalType = 'algorithms' | 'math' | 'physics' | 'biology' | 'chemistry' | 'probability';
106
+ type LessonSegment = MixedSegment | {
107
+ type: 'quiz';
108
+ question: string;
109
+ answer: string;
110
+ } | {
111
+ type: 'activate';
112
+ question: string;
113
+ } | {
114
+ type: 'connect';
115
+ content: string;
116
+ } | {
117
+ type: 'reflect';
118
+ prompt: string;
119
+ } | {
120
+ type: 'bloom';
121
+ level: BloomLevel;
122
+ question: string;
123
+ answer: string;
124
+ } | {
125
+ type: 'visualization';
126
+ visualizationType: InteractiveOrbitalType;
127
+ description: string;
128
+ };
129
+ /** User progress state passed into SegmentRenderer. */
130
+ interface LessonUserProgress {
131
+ activationResponse?: string;
132
+ reflectionNotes?: string[];
133
+ bloomAnswered?: Record<number, boolean>;
134
+ }
135
+ /** Parse a lesson string into typed segments. Returns `[]` when input is empty. */
136
+ declare function parseLessonSegments(lesson: string | undefined): LessonSegment[];
137
+
48
138
  /**
49
139
  * Orbital State Machine Visualizer
50
140
  *
@@ -292,4 +382,4 @@ declare function clearVerification(): void;
292
382
  */
293
383
  declare function cn(...inputs: ClassValue[]): string;
294
384
 
295
- export { parseMarkdownWithCodeBlocks as A, recordServerResponse as B, type ContentSegment as C, DEFAULT_CONFIG as D, type EntityDefinition as E, recordTransition as F, registerCheck as G, registerTraitSnapshot as H, renderStateMachineToDomData as I, renderStateMachineToSvg as J, subscribeToVerification as K, updateAssetStatus as L, updateBridgeHealth as M, updateCheck as N, waitForTransition as O, type RenderOptions as R, type StateDefinition as S, type TraitSnapshotGetter as T, type VisualizerConfig as V, type DomEntityBox as a, type DomLayoutData as b, type DomOutputsBox as c, type DomStateNode as d, type DomTransitionLabel as e, type DomTransitionPath as f, type StateMachineDefinition as g, type TransitionDefinition as h, bindCanvasCapture as i, bindEventBus as j, bindLastDrawables as k, bindTraitStateGetter as l, clearVerification as m, cn as n, extractOutputsFromTransitions as o, extractStateMachine as p, formatGuard as q, getAllChecks as r, getBridgeHealth as s, getEffectSummary as t, getSnapshot as u, getSummary as v, getTraitSnapshots as w, getTransitions as x, getTransitionsForTrait as y, parseContentSegments as z };
385
+ export { parseMarkdownWithCodeBlocks as $, parseLessonSegments as A, parseMarkdownWithCodeBlocks$1 as B, type ContentSegment as C, DEFAULT_CONFIG as D, type EntityDefinition as E, recordServerResponse as F, recordTransition as G, registerCheck as H, registerTraitSnapshot as I, renderStateMachineToDomData as J, renderStateMachineToSvg as K, type LessonSegment as L, subscribeToVerification as M, updateAssetStatus as N, updateBridgeHealth as O, updateCheck as P, waitForTransition as Q, type RenderOptions as R, type StateDefinition as S, type TraitSnapshotGetter as T, type LessonUserProgress as U, type VisualizerConfig as V, type InteractiveOrbitalType as W, type BloomLevel as X, BloomQuizBlock as Y, type BloomQuizBlockProps as Z, type MixedSegment as _, type DomEntityBox as a, type DomLayoutData as b, type DomOutputsBox as c, type DomStateNode as d, type DomTransitionLabel as e, type DomTransitionPath as f, type StateMachineDefinition as g, type TransitionDefinition as h, bindCanvasCapture as i, bindEventBus as j, bindLastDrawables as k, bindTraitStateGetter as l, clearVerification as m, cn as n, extractOutputsFromTransitions as o, extractStateMachine as p, formatGuard as q, getAllChecks as r, getBridgeHealth as s, getEffectSummary as t, getSnapshot as u, getSummary as v, getTraitSnapshots as w, getTransitions as x, getTransitionsForTrait as y, parseContentSegments as z };
@@ -8690,7 +8690,10 @@ var init_LearningCanvas = __esm({
8690
8690
  ctx.fillRect(0, 0, width, height);
8691
8691
  }
8692
8692
  for (const shape of shapes) {
8693
- drawShape(ctx, shape, width, height);
8693
+ if (shape.type !== "text") drawShape(ctx, shape, width, height);
8694
+ }
8695
+ for (const shape of shapes) {
8696
+ if (shape.type === "text") drawShape(ctx, shape, width, height);
8694
8697
  }
8695
8698
  }, [width, height, backgroundColor, shapes]);
8696
8699
  React77.useEffect(() => {
@@ -8771,7 +8774,7 @@ var init_AlgorithmCanvas = __esm({
8771
8774
  DEFAULT_CELL_COLOR = "#e5e7eb";
8772
8775
  DEFAULT_POINTER_COLOR = "#dc2626";
8773
8776
  POINTER_BAND = 34;
8774
- TOP_PAD = 12;
8777
+ TOP_PAD = 26;
8775
8778
  exports.AlgorithmCanvas = ({
8776
8779
  className,
8777
8780
  width = 600,
@@ -23756,6 +23759,7 @@ var init_FilterGroup = __esm({
23756
23759
  init_Badge();
23757
23760
  init_Stack();
23758
23761
  init_Icon();
23762
+ init_RangeSlider();
23759
23763
  init_useEventBus();
23760
23764
  init_useQuerySingleton();
23761
23765
  resolveFilterType = (filter) => filter.filterType ?? filter.type;
@@ -23950,6 +23954,35 @@ var init_FilterGroup = __esm({
23950
23954
  onClear: () => handleFilterSelect(`${filter.field}_to`, null)
23951
23955
  }
23952
23956
  )
23957
+ ] }) : resolveFilterType(filter) === "numberrange" || resolveFilterType(filter) === "number-range" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [
23958
+ /* @__PURE__ */ jsxRuntime.jsx(
23959
+ exports.RangeSlider,
23960
+ {
23961
+ min: filter.min ?? 0,
23962
+ max: filter.max ?? 100,
23963
+ step: filter.step ?? 1,
23964
+ value: Number(
23965
+ selectedValues[`${filter.field}_min`] ?? filter.min ?? 0
23966
+ ),
23967
+ onChange: (v) => handleFilterSelect(`${filter.field}_min`, String(v)),
23968
+ showTooltip: true,
23969
+ "aria-label": t("filterGroup.from")
23970
+ }
23971
+ ),
23972
+ /* @__PURE__ */ jsxRuntime.jsx(
23973
+ exports.RangeSlider,
23974
+ {
23975
+ min: filter.min ?? 0,
23976
+ max: filter.max ?? 100,
23977
+ step: filter.step ?? 1,
23978
+ value: Number(
23979
+ selectedValues[`${filter.field}_max`] ?? filter.max ?? 100
23980
+ ),
23981
+ onChange: (v) => handleFilterSelect(`${filter.field}_max`, String(v)),
23982
+ showTooltip: true,
23983
+ "aria-label": t("filterGroup.to")
23984
+ }
23985
+ )
23953
23986
  ] }) : resolveFilterType(filter) === "text" ? /* @__PURE__ */ jsxRuntime.jsx(
23954
23987
  exports.Input,
23955
23988
  {
@@ -24032,6 +24065,36 @@ var init_FilterGroup = __esm({
24032
24065
  className: "text-sm min-w-[100px]"
24033
24066
  }
24034
24067
  )
24068
+ ] }) : resolveFilterType(filter) === "numberrange" || resolveFilterType(filter) === "number-range" ? /* @__PURE__ */ jsxRuntime.jsxs(exports.HStack, { gap: "xs", align: "center", children: [
24069
+ /* @__PURE__ */ jsxRuntime.jsx(
24070
+ exports.RangeSlider,
24071
+ {
24072
+ min: filter.min ?? 0,
24073
+ max: filter.max ?? 100,
24074
+ step: filter.step ?? 1,
24075
+ value: Number(
24076
+ selectedValues[`${filter.field}_min`] ?? filter.min ?? 0
24077
+ ),
24078
+ onChange: (v) => handleFilterSelect(`${filter.field}_min`, String(v)),
24079
+ className: "min-w-[100px]",
24080
+ "aria-label": t("filterGroup.from")
24081
+ }
24082
+ ),
24083
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "-" }),
24084
+ /* @__PURE__ */ jsxRuntime.jsx(
24085
+ exports.RangeSlider,
24086
+ {
24087
+ min: filter.min ?? 0,
24088
+ max: filter.max ?? 100,
24089
+ step: filter.step ?? 1,
24090
+ value: Number(
24091
+ selectedValues[`${filter.field}_max`] ?? filter.max ?? 100
24092
+ ),
24093
+ onChange: (v) => handleFilterSelect(`${filter.field}_max`, String(v)),
24094
+ className: "min-w-[100px]",
24095
+ "aria-label": t("filterGroup.to")
24096
+ }
24097
+ )
24035
24098
  ] }) : resolveFilterType(filter) === "text" ? /* @__PURE__ */ jsxRuntime.jsx(
24036
24099
  exports.Input,
24037
24100
  {
@@ -24150,6 +24213,38 @@ var init_FilterGroup = __esm({
24150
24213
  className: "min-w-[130px]"
24151
24214
  }
24152
24215
  )
24216
+ ] }) : resolveFilterType(filter) === "numberrange" || resolveFilterType(filter) === "number-range" ? /* @__PURE__ */ jsxRuntime.jsxs(exports.HStack, { gap: "xs", align: "center", children: [
24217
+ /* @__PURE__ */ jsxRuntime.jsx(
24218
+ exports.RangeSlider,
24219
+ {
24220
+ min: filter.min ?? 0,
24221
+ max: filter.max ?? 100,
24222
+ step: filter.step ?? 1,
24223
+ value: Number(
24224
+ selectedValues[`${filter.field}_min`] ?? filter.min ?? 0
24225
+ ),
24226
+ onChange: (v) => handleFilterSelect(`${filter.field}_min`, String(v)),
24227
+ showTooltip: true,
24228
+ className: "min-w-[130px]",
24229
+ "aria-label": t("filterGroup.from")
24230
+ }
24231
+ ),
24232
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "-" }),
24233
+ /* @__PURE__ */ jsxRuntime.jsx(
24234
+ exports.RangeSlider,
24235
+ {
24236
+ min: filter.min ?? 0,
24237
+ max: filter.max ?? 100,
24238
+ step: filter.step ?? 1,
24239
+ value: Number(
24240
+ selectedValues[`${filter.field}_max`] ?? filter.max ?? 100
24241
+ ),
24242
+ onChange: (v) => handleFilterSelect(`${filter.field}_max`, String(v)),
24243
+ showTooltip: true,
24244
+ className: "min-w-[130px]",
24245
+ "aria-label": t("filterGroup.to")
24246
+ }
24247
+ )
24153
24248
  ] }) : resolveFilterType(filter) === "text" ? /* @__PURE__ */ jsxRuntime.jsx(
24154
24249
  exports.Input,
24155
24250
  {
@@ -26299,7 +26394,12 @@ function daysAgo(n) {
26299
26394
  d.setDate(d.getDate() - n);
26300
26395
  return d;
26301
26396
  }
26302
- var DEFAULT_PRESETS; exports.DateRangePicker = void 0;
26397
+ function resolvePresetRange(preset) {
26398
+ if (typeof preset.range === "function") return preset.range();
26399
+ if (preset.range) return preset.range;
26400
+ return TOKEN_RANGES[preset.value]?.() ?? null;
26401
+ }
26402
+ var TOKEN_RANGES, DEFAULT_PRESETS; exports.DateRangePicker = void 0;
26303
26403
  var init_DateRangePicker = __esm({
26304
26404
  "components/core/molecules/DateRangePicker.tsx"() {
26305
26405
  "use client";
@@ -26309,32 +26409,19 @@ var init_DateRangePicker = __esm({
26309
26409
  init_Stack();
26310
26410
  init_Typography();
26311
26411
  init_useEventBus();
26412
+ TOKEN_RANGES = {
26413
+ "7d": () => ({ from: toISODate(daysAgo(7)), to: toISODate(/* @__PURE__ */ new Date()) }),
26414
+ "30d": () => ({ from: toISODate(daysAgo(30)), to: toISODate(/* @__PURE__ */ new Date()) }),
26415
+ month: () => ({ from: toISODate(startOfMonth(/* @__PURE__ */ new Date())), to: toISODate(/* @__PURE__ */ new Date()) }),
26416
+ quarter: () => ({ from: toISODate(startOfQuarter(/* @__PURE__ */ new Date())), to: toISODate(/* @__PURE__ */ new Date()) }),
26417
+ ytd: () => ({ from: toISODate(startOfYear(/* @__PURE__ */ new Date())), to: toISODate(/* @__PURE__ */ new Date()) })
26418
+ };
26312
26419
  DEFAULT_PRESETS = [
26313
- {
26314
- label: "Last 7 days",
26315
- value: "7d",
26316
- range: () => ({ from: toISODate(daysAgo(7)), to: toISODate(/* @__PURE__ */ new Date()) })
26317
- },
26318
- {
26319
- label: "Last 30 days",
26320
- value: "30d",
26321
- range: () => ({ from: toISODate(daysAgo(30)), to: toISODate(/* @__PURE__ */ new Date()) })
26322
- },
26323
- {
26324
- label: "This Month",
26325
- value: "month",
26326
- range: () => ({ from: toISODate(startOfMonth(/* @__PURE__ */ new Date())), to: toISODate(/* @__PURE__ */ new Date()) })
26327
- },
26328
- {
26329
- label: "This Quarter",
26330
- value: "quarter",
26331
- range: () => ({ from: toISODate(startOfQuarter(/* @__PURE__ */ new Date())), to: toISODate(/* @__PURE__ */ new Date()) })
26332
- },
26333
- {
26334
- label: "YTD",
26335
- value: "ytd",
26336
- range: () => ({ from: toISODate(startOfYear(/* @__PURE__ */ new Date())), to: toISODate(/* @__PURE__ */ new Date()) })
26337
- }
26420
+ { label: "Last 7 days", value: "7d" },
26421
+ { label: "Last 30 days", value: "30d" },
26422
+ { label: "This Month", value: "month" },
26423
+ { label: "This Quarter", value: "quarter" },
26424
+ { label: "YTD", value: "ytd" }
26338
26425
  ];
26339
26426
  exports.DateRangePicker = ({
26340
26427
  from: fromProp,
@@ -26375,7 +26462,8 @@ var init_DateRangePicker = __esm({
26375
26462
  );
26376
26463
  const handlePreset = React77.useCallback(
26377
26464
  (preset) => {
26378
- const range = preset.range();
26465
+ const range = resolvePresetRange(preset);
26466
+ if (range === null) return;
26379
26467
  setFrom(range.from);
26380
26468
  setTo(range.to);
26381
26469
  setActivePreset(preset.value);
@@ -26383,8 +26471,12 @@ var init_DateRangePicker = __esm({
26383
26471
  },
26384
26472
  [emit]
26385
26473
  );
26474
+ const renderablePresets = React77.useMemo(
26475
+ () => presets.filter((p) => p.range !== void 0 || TOKEN_RANGES[p.value] !== void 0),
26476
+ [presets]
26477
+ );
26386
26478
  const presetButtons = React77.useMemo(
26387
- () => presets.map((preset) => /* @__PURE__ */ jsxRuntime.jsx(
26479
+ () => renderablePresets.map((preset) => /* @__PURE__ */ jsxRuntime.jsx(
26388
26480
  exports.Button,
26389
26481
  {
26390
26482
  variant: activePreset === preset.value ? "primary" : "ghost",
@@ -26394,7 +26486,7 @@ var init_DateRangePicker = __esm({
26394
26486
  },
26395
26487
  preset.value
26396
26488
  )),
26397
- [presets, activePreset, handlePreset]
26489
+ [renderablePresets, activePreset, handlePreset]
26398
26490
  );
26399
26491
  return /* @__PURE__ */ jsxRuntime.jsxs(exports.VStack, { gap: "sm", className: cn(className), children: [
26400
26492
  /* @__PURE__ */ jsxRuntime.jsxs(exports.HStack, { gap: "md", align: "end", children: [
@@ -26421,7 +26513,7 @@ var init_DateRangePicker = __esm({
26421
26513
  )
26422
26514
  ] })
26423
26515
  ] }),
26424
- presets.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(exports.HStack, { gap: "xs", wrap: true, children: presetButtons })
26516
+ renderablePresets.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(exports.HStack, { gap: "xs", wrap: true, children: presetButtons })
26425
26517
  ] });
26426
26518
  };
26427
26519
  exports.DateRangePicker.displayName = "DateRangePicker";
@@ -29297,6 +29389,208 @@ var init_PhysicsCanvas = __esm({
29297
29389
  };
29298
29390
  }
29299
29391
  });
29392
+
29393
+ // lib/graphViewLayouts.ts
29394
+ function buildAdjacency(nodeIds, edges) {
29395
+ const known = new Set(nodeIds);
29396
+ const out = /* @__PURE__ */ new Map();
29397
+ const inMap = /* @__PURE__ */ new Map();
29398
+ for (const id of nodeIds) {
29399
+ out.set(id, []);
29400
+ inMap.set(id, []);
29401
+ }
29402
+ for (const edge of edges) {
29403
+ if (!known.has(edge.source) || !known.has(edge.target)) continue;
29404
+ out.get(edge.source)?.push(edge.target);
29405
+ inMap.get(edge.target)?.push(edge.source);
29406
+ }
29407
+ return { out, in: inMap };
29408
+ }
29409
+ function findRoots(nodeIds, adjacency) {
29410
+ return nodeIds.filter((id) => (adjacency.in.get(id)?.length ?? 0) === 0);
29411
+ }
29412
+ function assignLayers(nodeIds, adjacency, roots) {
29413
+ const layer = new Map(nodeIds.map((id) => [id, 0]));
29414
+ const onStack = /* @__PURE__ */ new Set();
29415
+ const visit = (id) => {
29416
+ onStack.add(id);
29417
+ const currentLayer = layer.get(id) ?? 0;
29418
+ for (const next of adjacency.out.get(id) ?? []) {
29419
+ if (onStack.has(next)) continue;
29420
+ const candidate = currentLayer + 1;
29421
+ if (candidate > (layer.get(next) ?? 0)) {
29422
+ layer.set(next, candidate);
29423
+ }
29424
+ visit(next);
29425
+ }
29426
+ onStack.delete(id);
29427
+ };
29428
+ for (const root of roots) visit(root);
29429
+ return layer;
29430
+ }
29431
+ function computeVisitOrder(nodeIds, adjacency, roots) {
29432
+ const order = /* @__PURE__ */ new Map();
29433
+ const visited = /* @__PURE__ */ new Set();
29434
+ let counter = 0;
29435
+ const visit = (id) => {
29436
+ if (visited.has(id)) return;
29437
+ visited.add(id);
29438
+ order.set(id, counter++);
29439
+ for (const next of adjacency.out.get(id) ?? []) visit(next);
29440
+ };
29441
+ for (const root of roots) visit(root);
29442
+ for (const id of nodeIds) {
29443
+ if (!visited.has(id)) order.set(id, counter++);
29444
+ }
29445
+ return order;
29446
+ }
29447
+ function bfsDepthAndOrder(nodeIds, adjacency, roots) {
29448
+ const depth = new Map(nodeIds.map((id) => [id, 0]));
29449
+ const visitOrder = /* @__PURE__ */ new Map();
29450
+ const visited = /* @__PURE__ */ new Set();
29451
+ const queue = [];
29452
+ let counter = 0;
29453
+ for (const root of roots) {
29454
+ if (visited.has(root)) continue;
29455
+ visited.add(root);
29456
+ depth.set(root, 0);
29457
+ visitOrder.set(root, counter++);
29458
+ queue.push(root);
29459
+ }
29460
+ let head = 0;
29461
+ while (head < queue.length) {
29462
+ const id = queue[head++];
29463
+ const d = depth.get(id) ?? 0;
29464
+ for (const next of adjacency.out.get(id) ?? []) {
29465
+ if (visited.has(next)) continue;
29466
+ visited.add(next);
29467
+ depth.set(next, d + 1);
29468
+ visitOrder.set(next, counter++);
29469
+ queue.push(next);
29470
+ }
29471
+ }
29472
+ for (const id of nodeIds) {
29473
+ if (!visited.has(id)) visitOrder.set(id, counter++);
29474
+ }
29475
+ return { depth, visitOrder };
29476
+ }
29477
+ function edgeWalkOrder(nodeIds, adjacency) {
29478
+ const visited = /* @__PURE__ */ new Set();
29479
+ const result = [];
29480
+ for (const start of nodeIds) {
29481
+ if (visited.has(start)) continue;
29482
+ let current = start;
29483
+ while (current !== void 0 && !visited.has(current)) {
29484
+ visited.add(current);
29485
+ result.push(current);
29486
+ const outs = adjacency.out.get(current) ?? [];
29487
+ current = outs.find((next) => !visited.has(next));
29488
+ }
29489
+ }
29490
+ return result;
29491
+ }
29492
+ function groupByTier(nodeIds, tierOf, maxTier) {
29493
+ const groups = Array.from({ length: maxTier + 1 }, () => []);
29494
+ for (const id of nodeIds) {
29495
+ groups[tierOf.get(id) ?? 0].push(id);
29496
+ }
29497
+ return groups;
29498
+ }
29499
+ function distributeAxis(count, start, end) {
29500
+ if (count <= 0) return [];
29501
+ const slot = (end - start) / count;
29502
+ return Array.from({ length: count }, (_, i) => start + slot * (i + 0.5));
29503
+ }
29504
+ function orderByParentPositionThenInput(group, adjacency, positioned, inputIndex) {
29505
+ const withKey = group.map((id) => {
29506
+ const parentValues = (adjacency.in.get(id) ?? []).map((p) => positioned.get(p)).filter((v) => v !== void 0);
29507
+ const avg = parentValues.length > 0 ? parentValues.reduce((a, b) => a + b, 0) / parentValues.length : Number.POSITIVE_INFINITY;
29508
+ return { id, avg, idx: inputIndex.get(id) ?? 0 };
29509
+ });
29510
+ withKey.sort((a, b) => a.avg !== b.avg ? a.avg - b.avg : a.idx - b.idx);
29511
+ return withKey.map((k) => k.id);
29512
+ }
29513
+ function layoutFlow(nodeIds, adjacency, roots, width, height, margin) {
29514
+ const inputIndex = new Map(nodeIds.map((id, i) => [id, i]));
29515
+ const layers = assignLayers(nodeIds, adjacency, roots);
29516
+ const maxLayer = Math.max(...Array.from(layers.values()));
29517
+ const layerGroups = groupByTier(nodeIds, layers, maxLayer);
29518
+ const positions = /* @__PURE__ */ new Map();
29519
+ const yById = /* @__PURE__ */ new Map();
29520
+ for (let l = 0; l <= maxLayer; l++) {
29521
+ const x = margin + l * (width - 2 * margin) / Math.max(1, maxLayer);
29522
+ const ordered2 = orderByParentPositionThenInput(layerGroups[l], adjacency, yById, inputIndex);
29523
+ const ys = distributeAxis(ordered2.length, margin, height - margin);
29524
+ ordered2.forEach((id, i) => {
29525
+ positions.set(id, { id, x, y: ys[i] });
29526
+ yById.set(id, ys[i]);
29527
+ });
29528
+ }
29529
+ return nodeIds.map((id) => positions.get(id));
29530
+ }
29531
+ function layoutTree(nodeIds, adjacency, roots, width, height, margin) {
29532
+ const effectiveRoots = roots.length > 0 ? roots : [nodeIds[0]];
29533
+ const layers = assignLayers(nodeIds, adjacency, effectiveRoots);
29534
+ const maxLayer = Math.max(...Array.from(layers.values()));
29535
+ const visitOrder = computeVisitOrder(nodeIds, adjacency, effectiveRoots);
29536
+ const layerGroups = groupByTier(nodeIds, layers, maxLayer);
29537
+ const positions = /* @__PURE__ */ new Map();
29538
+ for (let l = 0; l <= maxLayer; l++) {
29539
+ const ordered2 = [...layerGroups[l]].sort(
29540
+ (a, b) => (visitOrder.get(a) ?? 0) - (visitOrder.get(b) ?? 0)
29541
+ );
29542
+ const y = margin + l * (height - 2 * margin) / Math.max(1, maxLayer);
29543
+ const xs = distributeAxis(ordered2.length, margin, width - margin);
29544
+ ordered2.forEach((id, i) => positions.set(id, { id, x: xs[i], y }));
29545
+ }
29546
+ return nodeIds.map((id) => positions.get(id));
29547
+ }
29548
+ function layoutRadial(nodeIds, adjacency, roots, width, height, margin) {
29549
+ const cx = width / 2;
29550
+ const cy = height / 2;
29551
+ const maxRadius = Math.min(width, height) / 2 - margin;
29552
+ if (roots.length === 0) {
29553
+ const order = edgeWalkOrder(nodeIds, adjacency);
29554
+ const k = order.length;
29555
+ const positions2 = /* @__PURE__ */ new Map();
29556
+ order.forEach((id, i) => {
29557
+ const angle = i / k * 2 * Math.PI;
29558
+ positions2.set(id, { id, x: cx + maxRadius * Math.cos(angle), y: cy + maxRadius * Math.sin(angle) });
29559
+ });
29560
+ return nodeIds.map((id) => positions2.get(id));
29561
+ }
29562
+ const { depth, visitOrder } = bfsDepthAndOrder(nodeIds, adjacency, roots);
29563
+ const maxDepth = Math.max(...Array.from(depth.values()));
29564
+ const ringGroups = groupByTier(nodeIds, depth, maxDepth);
29565
+ const positions = /* @__PURE__ */ new Map();
29566
+ for (let d = 0; d <= maxDepth; d++) {
29567
+ const ordered2 = [...ringGroups[d]].sort(
29568
+ (a, b) => (visitOrder.get(a) ?? 0) - (visitOrder.get(b) ?? 0)
29569
+ );
29570
+ const radius = d * maxRadius / Math.max(1, maxDepth);
29571
+ const k = ordered2.length;
29572
+ ordered2.forEach((id, i) => {
29573
+ const angle = i / k * 2 * Math.PI;
29574
+ positions.set(id, { id, x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) });
29575
+ });
29576
+ }
29577
+ return nodeIds.map((id) => positions.get(id));
29578
+ }
29579
+ function computeStaticLayout(mode, input) {
29580
+ const { nodeIds, edges, width, height } = input;
29581
+ const margin = input.margin ?? 40;
29582
+ if (nodeIds.length === 0) return [];
29583
+ if (nodeIds.length === 1) return [{ id: nodeIds[0], x: width / 2, y: height / 2 }];
29584
+ const adjacency = buildAdjacency(nodeIds, edges);
29585
+ const roots = findRoots(nodeIds, adjacency);
29586
+ if (mode === "flow") return layoutFlow(nodeIds, adjacency, roots, width, height, margin);
29587
+ if (mode === "tree") return layoutTree(nodeIds, adjacency, roots, width, height, margin);
29588
+ return layoutRadial(nodeIds, adjacency, roots, width, height, margin);
29589
+ }
29590
+ var init_graphViewLayouts = __esm({
29591
+ "lib/graphViewLayouts.ts"() {
29592
+ }
29593
+ });
29300
29594
  function resolveNodeColor(node, groups) {
29301
29595
  if (node.color) return node.color;
29302
29596
  if (node.group) {
@@ -29311,6 +29605,7 @@ var init_GraphView = __esm({
29311
29605
  "use client";
29312
29606
  init_cn();
29313
29607
  init_atoms();
29608
+ init_graphViewLayouts();
29314
29609
  GROUP_COLORS = [
29315
29610
  "#3b82f6",
29316
29611
  // blue-500
@@ -29341,11 +29636,13 @@ var init_GraphView = __esm({
29341
29636
  height: propHeight,
29342
29637
  className,
29343
29638
  showLabels = true,
29344
- zoomToFit = true
29639
+ zoomToFit = true,
29640
+ layout = "force"
29345
29641
  }) => {
29346
29642
  const { t } = hooks.useTranslate();
29347
29643
  const containerRef = React77.useRef(null);
29348
29644
  const animRef = React77.useRef(0);
29645
+ const arrowMarkerId = React77.useId();
29349
29646
  const [simNodes, setSimNodes] = React77.useState([]);
29350
29647
  const [settled, setSettled] = React77.useState(false);
29351
29648
  const [hoveredId, setHoveredId] = React77.useState(null);
@@ -29402,6 +29699,22 @@ var init_GraphView = __esm({
29402
29699
  fy: 0
29403
29700
  };
29404
29701
  });
29702
+ if (layout !== "force") {
29703
+ const points = computeStaticLayout(layout, {
29704
+ nodeIds: nodes.map((n) => n.id),
29705
+ edges,
29706
+ width: w,
29707
+ height: h
29708
+ });
29709
+ const pointById = new Map(points.map((p) => [p.id, p]));
29710
+ const laidOut = initialNodes.map((node) => {
29711
+ const point = pointById.get(node.id);
29712
+ return point ? { ...node, x: point.x, y: point.y } : node;
29713
+ });
29714
+ setSimNodes(laidOut);
29715
+ setSettled(true);
29716
+ return;
29717
+ }
29405
29718
  let iterations = 0;
29406
29719
  const maxIterations = 120;
29407
29720
  let currentNodes = initialNodes;
@@ -29470,7 +29783,7 @@ var init_GraphView = __esm({
29470
29783
  return () => {
29471
29784
  cancelAnimationFrame(animRef.current);
29472
29785
  };
29473
- }, [nodes, edges, w, h, groups]);
29786
+ }, [nodes, edges, w, h, groups, layout]);
29474
29787
  const viewBox = React77.useMemo(() => {
29475
29788
  if (!zoomToFit || !settled || simNodes.length === 0) {
29476
29789
  return `0 0 ${w} ${h}`;
@@ -29545,6 +29858,19 @@ var init_GraphView = __esm({
29545
29858
  viewBox,
29546
29859
  preserveAspectRatio: "xMidYMid meet",
29547
29860
  children: [
29861
+ layout !== "force" && /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsx(
29862
+ "marker",
29863
+ {
29864
+ id: arrowMarkerId,
29865
+ viewBox: "0 0 10 10",
29866
+ refX: "9",
29867
+ refY: "5",
29868
+ markerWidth: "6",
29869
+ markerHeight: "6",
29870
+ orient: "auto-start-reverse",
29871
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M0,0 L10,5 L0,10 z", fill: "currentColor" })
29872
+ }
29873
+ ) }),
29548
29874
  edges.map((edge, idx) => {
29549
29875
  const source = nodeMap.get(edge.source);
29550
29876
  const target = nodeMap.get(edge.target);
@@ -29559,8 +29885,10 @@ var init_GraphView = __esm({
29559
29885
  x2: target.x,
29560
29886
  y2: target.y,
29561
29887
  stroke: edge.color ?? DEFAULT_EDGE_COLOR,
29888
+ color: edge.color ?? DEFAULT_EDGE_COLOR,
29562
29889
  strokeWidth: 1.5,
29563
- opacity: isHighlighted ? 0.8 : 0.15
29890
+ opacity: isHighlighted ? 0.8 : 0.15,
29891
+ markerEnd: layout !== "force" ? `url(#${arrowMarkerId})` : void 0
29564
29892
  }
29565
29893
  ),
29566
29894
  showLabels && edge.label && /* @__PURE__ */ jsxRuntime.jsx(
@@ -39351,7 +39679,7 @@ function parseLessonSegments(lesson) {
39351
39679
  content = content.replace(connectResult.fullMatch, "").trim();
39352
39680
  }
39353
39681
  const tagRegex = new RegExp(
39354
- '(?<reflect><reflect>(?<reflectClosed>[\\s\\S]*?)<\\/reflect>)|(?<reflectUnclosed><reflect>(?<reflectOpen>[\\s\\S]*?)(?=<(?:activate|connect|reflect|bloom|prq|question|answer|visualize)|\\n\\n#|$))|(?<bloom><bloom\\s+level="(?<bloomLevel>remember|understand|apply|analyze|evaluate|create)">(?<bloomClosed>[\\s\\S]*?)<\\/bloom>)|(?<bloomUnclosed><bloom\\s+level="(?<bloomLevelUn>remember|understand|apply|analyze|evaluate|create)">(?<bloomOpen>[\\s\\S]*?)(?=<(?:activate|connect|reflect|bloom|prq|question|answer|visualize)|\\n\\n#|$))|(?<quiz><question>(?<quizQuestion>[\\s\\S]*?)<\\/question>\\s*<answer>(?<quizAnswer>[\\s\\S]*?)<\\/answer>)|(?<visualize><visualize\\s+type="(?<vizType>algorithms|math|physics|biology|chemistry|probability|freeform)"\\s+description="(?<vizDesc>[^"]*?)"\\s*\\/?>)',
39682
+ '(?<reflect><reflect>(?<reflectClosed>[\\s\\S]*?)<\\/reflect>)|(?<reflectUnclosed><reflect>(?<reflectOpen>[\\s\\S]*?)(?=<(?:activate|connect|reflect|bloom|prq|question|answer|visualize)|\\n\\n#|$))|(?<bloom><bloom\\s+level="(?<bloomLevel>remember|understand|apply|analyze|evaluate|create)">(?<bloomClosed>[\\s\\S]*?)<\\/bloom>)|(?<bloomUnclosed><bloom\\s+level="(?<bloomLevelUn>remember|understand|apply|analyze|evaluate|create)">(?<bloomOpen>[\\s\\S]*?)(?=<(?:activate|connect|reflect|bloom|prq|question|answer|visualize)|\\n\\n#|$))|(?<quiz><question>(?<quizQuestion>[\\s\\S]*?)<\\/question>\\s*<answer>(?<quizAnswer>[\\s\\S]*?)<\\/answer>)|(?<visualize><visualize\\s+type="(?<vizType>algorithms|math|physics|biology|chemistry|probability)"\\s+description="(?<vizDesc>[^"]*?)"\\s*\\/?>)',
39355
39683
  "gi"
39356
39684
  );
39357
39685
  let lastIndex = 0;