astro-tractstack 2.1.2 → 2.2.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.
Files changed (131) hide show
  1. package/README.md +54 -266
  2. package/bin/create-tractstack.js +9 -6
  3. package/dist/index.js +109 -71
  4. package/package.json +4 -2
  5. package/templates/css/custom.css +5 -0
  6. package/templates/custom/minimal/CodeHook.astro +1 -0
  7. package/templates/custom/with-examples/CodeHook.astro +1 -0
  8. package/templates/icons/code.svg +18 -0
  9. package/templates/icons/li.svg +4 -0
  10. package/templates/icons/link.svg +22 -0
  11. package/templates/icons/p.svg +3 -0
  12. package/templates/src/client/app.js +80 -1
  13. package/templates/src/components/Footer.astro +1 -1
  14. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +6 -6
  15. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +3 -3
  16. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +1 -1
  17. package/templates/src/components/codehooks/ListContentSetup.tsx +2 -2
  18. package/templates/src/components/codehooks/ProductCardSetup.tsx +1 -1
  19. package/templates/src/components/codehooks/ProductGridSetup.tsx +2 -2
  20. package/templates/src/components/codehooks/SandboxRegisterForm.tsx +3 -3
  21. package/templates/src/components/compositor/Compositor.tsx +25 -9
  22. package/templates/src/components/compositor/Node.tsx +168 -496
  23. package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +1 -0
  24. package/templates/src/components/compositor/elements/SignUp.tsx +1 -1
  25. package/templates/src/components/compositor/elements/YouTubeWrapper.tsx +2 -0
  26. package/templates/src/components/compositor/nodes/CreativePane.tsx +262 -0
  27. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +4 -6
  28. package/templates/src/components/compositor/nodes/GridLayout.tsx +4 -2
  29. package/templates/src/components/compositor/nodes/Markdown.tsx +18 -3
  30. package/templates/src/components/compositor/nodes/Pane.tsx +11 -5
  31. package/templates/src/components/compositor/nodes/RenderChildren.tsx +1 -1
  32. package/templates/src/components/compositor/nodes/tagElements/NodeAnchorComponent.tsx +5 -5
  33. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +90 -42
  34. package/templates/src/components/compositor/nodes/tagElements/NodeImg.tsx +2 -0
  35. package/templates/src/components/compositor/nodes/tagElements/NodeText.tsx +27 -1
  36. package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +10 -8
  37. package/templates/src/components/compositor/tools/NodeOverlay.tsx +224 -0
  38. package/templates/src/components/compositor/tools/PaneOverlay.tsx +122 -0
  39. package/templates/src/components/edit/Header.tsx +68 -9
  40. package/templates/src/components/edit/PanelSwitch.tsx +42 -4
  41. package/templates/src/components/edit/SettingsPanel.tsx +2 -3
  42. package/templates/src/components/edit/ToolMode.tsx +1 -31
  43. package/templates/src/components/edit/pane/AddPanePanel_break.tsx +2 -2
  44. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +1 -1
  45. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +193 -659
  46. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +15 -82
  47. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +95 -45
  48. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +137 -49
  49. package/templates/src/components/edit/pane/RestylePaneModal.tsx +1 -1
  50. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +375 -0
  51. package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +1 -23
  52. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +327 -0
  53. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +267 -0
  54. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +371 -0
  55. package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +201 -76
  56. package/templates/src/components/edit/pane/steps/CreativeInjectStep.tsx +141 -0
  57. package/templates/src/components/edit/panels/CreativeImagePanel.tsx +435 -0
  58. package/templates/src/components/edit/panels/CreativeLinkPanel.tsx +110 -0
  59. package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +1 -1
  60. package/templates/src/components/edit/panels/StyleParentPanel.tsx +118 -126
  61. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +3 -2
  62. package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +1 -0
  63. package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +3 -1
  64. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +3 -1
  65. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +1 -1
  66. package/templates/src/components/edit/state/SaveModal.tsx +19 -787
  67. package/templates/src/components/edit/state/SaveToLibraryModal.tsx +2 -2
  68. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +1 -1
  69. package/templates/src/components/edit/widgets/BunnyWidget.tsx +5 -5
  70. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +1 -1
  71. package/templates/src/components/edit/widgets/SignupWidget.tsx +1 -1
  72. package/templates/src/components/fields/ActionBuilderTimeSelector.tsx +1 -1
  73. package/templates/src/components/fields/ArtpackImage.tsx +11 -3
  74. package/templates/src/components/fields/BackgroundImage.tsx +8 -0
  75. package/templates/src/components/fields/BackgroundImageWrapper.tsx +15 -9
  76. package/templates/src/components/fields/ImageUpload.tsx +6 -0
  77. package/templates/src/components/form/ActionBuilderField.tsx +15 -5
  78. package/templates/src/components/form/ActionBuilderSlugSelector.tsx +1 -1
  79. package/templates/src/components/form/ColorPicker.tsx +1 -1
  80. package/templates/src/components/form/EnumSelect.tsx +1 -1
  81. package/templates/src/components/form/NumberInput.tsx +1 -1
  82. package/templates/src/components/form/StringArrayInput.tsx +1 -1
  83. package/templates/src/components/form/StringInput.tsx +1 -1
  84. package/templates/src/components/form/UnsavedChangesBar.tsx +1 -1
  85. package/templates/src/components/form/advanced/APIConfigSection.tsx +2 -2
  86. package/templates/src/components/form/advanced/AuthConfigSection.tsx +2 -2
  87. package/templates/src/components/profile/ProfileCreate.tsx +1 -1
  88. package/templates/src/components/profile/ProfileEdit.tsx +1 -1
  89. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +2 -2
  90. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +1 -1
  91. package/templates/src/components/storykeep/controls/content/ContentSummary.tsx +2 -2
  92. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +1 -1
  93. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +6 -6
  94. package/templates/src/components/storykeep/controls/content/MenuForm.tsx +1 -1
  95. package/templates/src/components/storykeep/controls/content/PaneTable.tsx +358 -0
  96. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +1 -1
  97. package/templates/src/constants/prompts.json +18 -10
  98. package/templates/src/constants.ts +3 -0
  99. package/templates/src/hooks/usePaneFragments.ts +60 -0
  100. package/templates/src/lib/session.ts +71 -16
  101. package/templates/src/pages/[...slug].astro +5 -46
  102. package/templates/src/pages/api/css.ts +149 -0
  103. package/templates/src/pages/context/[...contextSlug].astro +1 -0
  104. package/templates/src/pages/maint.astro +1 -1
  105. package/templates/src/pages/storykeep/login.astro +2 -2
  106. package/templates/src/stores/nodes.ts +162 -49
  107. package/templates/src/stores/orphanAnalysis.ts +6 -30
  108. package/templates/src/stores/previews.ts +7 -0
  109. package/templates/src/stores/storykeep.ts +0 -8
  110. package/templates/src/types/compositorTypes.ts +53 -10
  111. package/templates/src/utils/compositor/aiGeneration.ts +93 -0
  112. package/templates/src/utils/compositor/allowInsert.ts +2 -0
  113. package/templates/src/utils/compositor/htmlAst.ts +704 -0
  114. package/templates/src/utils/compositor/nodesHelper.ts +281 -102
  115. package/templates/src/utils/compositor/savePipeline.ts +893 -0
  116. package/templates/src/utils/etl/index.ts +3 -0
  117. package/templates/src/utils/etl/transformer.ts +10 -0
  118. package/templates/src/utils/helpers.ts +101 -0
  119. package/utils/inject-files.ts +100 -62
  120. package/templates/icons/text.svg +0 -6
  121. package/templates/src/components/compositor/NodeWithGuid.tsx +0 -69
  122. package/templates/src/components/compositor/nodes/GridLayout_eraser.tsx +0 -33
  123. package/templates/src/components/compositor/nodes/Markdown_eraser.tsx +0 -56
  124. package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +0 -269
  125. package/templates/src/components/compositor/nodes/Pane_eraser.tsx +0 -186
  126. package/templates/src/components/compositor/nodes/Pane_layout.tsx +0 -79
  127. package/templates/src/components/compositor/nodes/tagElements/NodeA_eraser.tsx +0 -26
  128. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_eraser.tsx +0 -61
  129. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_insert.tsx +0 -120
  130. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_settings.tsx +0 -62
  131. package/templates/src/components/compositor/nodes/tagElements/NodeButton_eraser.tsx +0 -26
@@ -9,6 +9,11 @@ import allowInsert from '@/utils/compositor/allowInsert';
9
9
  import { reservedSlugs } from '@/constants';
10
10
  import { NodesHistory, PatchOp } from '@/stores/nodesHistory';
11
11
  import { moveNodeAtLocationInContext } from '@/utils/compositor/nodesHelper';
12
+ import {
13
+ rehydrateChildrenFromHtml,
14
+ regenerateCreativePane,
15
+ extractFileIdsFromAst,
16
+ } from '@/utils/compositor/htmlAst';
12
17
  import { MarkdownGenerator } from '@/utils/compositor/nodesMarkdownGenerator';
13
18
  import {
14
19
  hasButtonPayload,
@@ -19,6 +24,8 @@ import {
19
24
  toTag,
20
25
  } from '@/utils/compositor/typeGuards';
21
26
  import { startLoadingAnimation } from '@/utils/helpers';
27
+ import { lispLexer } from '@/utils/actions/lispLexer';
28
+ import { preParseAction } from '@/utils/actions/preParse_Action';
22
29
  import { settingsPanelStore } from '@/stores/storykeep';
23
30
  import {
24
31
  PaneAddMode,
@@ -26,6 +33,7 @@ import {
26
33
  ContextPaneMode,
27
34
  } from '@/types/compositorTypes';
28
35
  import type {
36
+ EditableElementMetadata,
29
37
  PanelState,
30
38
  BaseNode,
31
39
  FlatNode,
@@ -55,7 +63,6 @@ import type {
55
63
  } from '@/types/compositorTypes';
56
64
  import type { NodeProps, WidgetProps } from '@/types/nodeProps';
57
65
  import type { CSSProperties } from 'react';
58
- import { selectionStore } from '@/stores/selection';
59
66
  import type { SelectionRange, SelectionStoreState } from '@/stores/selection';
60
67
  import type { CompositorProps } from '@/components/compositor/Compositor';
61
68
 
@@ -83,6 +90,7 @@ export class NodesContext {
83
90
  allNodes = atom<Map<string, BaseNode>>(new Map<string, BaseNode>());
84
91
  impressionNodes = atom<Set<ImpressionNode>>(new Set<ImpressionNode>());
85
92
  parentNodes = atom<Map<string, string[]>>(new Map<string, string[]>());
93
+ showSaveBypass = atom<boolean>(false);
86
94
  hasTitle = atom<boolean>(false);
87
95
  hasPanes = atom<boolean>(false);
88
96
  isTemplate = atom<boolean>(false);
@@ -342,15 +350,6 @@ export class NodesContext {
342
350
 
343
351
  // click handler based on toolModeVal
344
352
  switch (toolModeVal) {
345
- case `styles`:
346
- const selection = selectionStore.get();
347
- if (!selection.isActive)
348
- handleClickEventDefault(
349
- node,
350
- dblClick,
351
- this.clickedParentLayer.get()
352
- );
353
- break;
354
353
  case `text`:
355
354
  if (
356
355
  dblClick &&
@@ -358,7 +357,6 @@ export class NodesContext {
358
357
  'tagName' in node &&
359
358
  (node.tagName === 'a' || node.tagName === 'button')
360
359
  ) {
361
- this.toolModeValStore.set({ value: 'styles' });
362
360
  handleClickEventDefault(
363
361
  node,
364
362
  dblClick,
@@ -366,7 +364,6 @@ export class NodesContext {
366
364
  );
367
365
  }
368
366
  if (dblClick && ![`Markdown`].includes(node.nodeType)) {
369
- this.toolModeValStore.set({ value: 'styles' });
370
367
  handleClickEventDefault(
371
368
  node,
372
369
  dblClick,
@@ -374,10 +371,6 @@ export class NodesContext {
374
371
  );
375
372
  }
376
373
  break;
377
- case `eraser`:
378
- this.handleEraseEvent(node.id);
379
- this.deleteNode(node.id);
380
- break;
381
374
  default:
382
375
  }
383
376
  // reset on parentLayer
@@ -562,34 +555,38 @@ export class NodesContext {
562
555
 
563
556
  applyShellToPane(paneId: string, template: TemplatePane) {
564
557
  const allNodes = new Map(this.allNodes.get());
565
- const paneNode = allNodes.get(paneId) as PaneNode;
566
- if (!paneNode) return;
558
+ const originalPane = allNodes.get(paneId);
559
+ if (!originalPane) return;
560
+
561
+ const paneNode = cloneDeep(originalPane) as PaneNode;
562
+ paneNode.isChanged = true;
567
563
 
568
564
  if (template.bgColour) {
569
565
  paneNode.bgColour = template.bgColour;
570
- paneNode.isChanged = true;
566
+ }
567
+ if (template.htmlAst) {
568
+ paneNode.htmlAst = template.htmlAst;
571
569
  }
572
570
 
571
+ allNodes.set(paneId, paneNode);
572
+
573
573
  const childrenIds = this.getChildNodeIDs(paneId);
574
574
 
575
- const gridLayoutNode = childrenIds
575
+ const gridNodeRaw = childrenIds
576
576
  .map((id) => allNodes.get(id))
577
- .find((n) => n?.nodeType === 'GridLayoutNode') as
578
- | GridLayoutNode
579
- | undefined;
577
+ .find((n) => n?.nodeType === 'GridLayoutNode');
580
578
 
581
- const markdownNodes = childrenIds
582
- .map((id) => allNodes.get(id))
583
- .filter((n) => n?.nodeType === 'Markdown') as MarkdownPaneFragmentNode[];
579
+ if (gridNodeRaw && template.gridLayout) {
580
+ const gridLayoutNode = cloneDeep(gridNodeRaw) as GridLayoutNode;
581
+ gridLayoutNode.isChanged = true;
584
582
 
585
- if (gridLayoutNode && template.gridLayout) {
586
583
  if (template.gridLayout.parentClasses) {
587
584
  gridLayoutNode.parentClasses = template.gridLayout.parentClasses;
588
585
  }
589
586
  if (template.gridLayout.defaultClasses) {
590
587
  gridLayoutNode.defaultClasses = template.gridLayout.defaultClasses;
591
588
  }
592
- gridLayoutNode.isChanged = true;
589
+ allNodes.set(gridLayoutNode.id, gridLayoutNode);
593
590
 
594
591
  if (
595
592
  template.gridLayout.nodes &&
@@ -599,30 +596,134 @@ export class NodesContext {
599
596
 
600
597
  columnIds.forEach((colId, index) => {
601
598
  const templateCol = template.gridLayout!.nodes![index];
602
- if (templateCol && templateCol.gridClasses) {
603
- const liveColNode = allNodes.get(colId) as MarkdownPaneFragmentNode;
604
- if (liveColNode) {
605
- liveColNode.gridClasses = templateCol.gridClasses;
606
- liveColNode.isChanged = true;
607
- }
599
+ const colNodeRaw = allNodes.get(colId);
600
+ if (templateCol && colNodeRaw) {
601
+ const liveColNode = cloneDeep(
602
+ colNodeRaw
603
+ ) as MarkdownPaneFragmentNode;
604
+ liveColNode.gridClasses = templateCol.gridClasses;
605
+ liveColNode.isChanged = true;
606
+ allNodes.set(colId, liveColNode);
608
607
  }
609
608
  });
610
609
  }
611
- } else if (markdownNodes.length > 0 && template.markdown) {
612
- const primaryMarkdown = markdownNodes[0];
613
-
614
- if (template.markdown.parentClasses) {
615
- primaryMarkdown.parentClasses = template.markdown.parentClasses;
616
- }
617
- if (template.markdown.defaultClasses) {
618
- primaryMarkdown.defaultClasses = template.markdown.defaultClasses;
610
+ } else {
611
+ const markdownNodes = childrenIds
612
+ .map((id) => allNodes.get(id))
613
+ .filter(
614
+ (n) => n?.nodeType === 'Markdown'
615
+ ) as MarkdownPaneFragmentNode[];
616
+
617
+ if (markdownNodes.length > 0 && template.markdown) {
618
+ const primaryMarkdown = cloneDeep(markdownNodes[0]);
619
+ primaryMarkdown.isChanged = true;
620
+
621
+ if (template.markdown.parentClasses) {
622
+ primaryMarkdown.parentClasses = template.markdown.parentClasses;
623
+ }
624
+ if (template.markdown.defaultClasses) {
625
+ primaryMarkdown.defaultClasses = template.markdown.defaultClasses;
626
+ }
627
+ allNodes.set(primaryMarkdown.id, primaryMarkdown);
619
628
  }
620
- primaryMarkdown.isChanged = true;
621
629
  }
622
630
 
623
631
  this.allNodes.set(allNodes);
624
632
  this.notifyNode(paneId);
625
633
  this.notifyNode('root');
634
+ this.showSaveBypass.set(true);
635
+ }
636
+
637
+ async updateCreativeAsset(
638
+ paneId: string,
639
+ astId: string,
640
+ updates: Partial<EditableElementMetadata>
641
+ ) {
642
+ const allNodes = new Map(this.allNodes.get());
643
+ const originalPane = allNodes.get(paneId);
644
+
645
+ if (!originalPane || originalPane.nodeType !== 'Pane') return;
646
+
647
+ const paneNode = cloneDeep(originalPane) as PaneNode;
648
+ if (!paneNode.htmlAst) return;
649
+
650
+ if (updates.tagName === 'a' && updates.buttonPayload?.callbackPayload) {
651
+ try {
652
+ const config = (window as any).TRACTSTACK_CONFIG || {};
653
+ const lexed = lispLexer(updates.buttonPayload.callbackPayload);
654
+
655
+ const resolvedHref = preParseAction(
656
+ lexed,
657
+ paneNode.slug,
658
+ !!paneNode.isContextPane,
659
+ config
660
+ );
661
+
662
+ if (resolvedHref) {
663
+ updates.href = resolvedHref;
664
+ }
665
+ } catch (e) {
666
+ console.warn('[Nodes] Failed to resolve href from ActionLisp:', e);
667
+ }
668
+ }
669
+
670
+ let newHtmlAst = await regenerateCreativePane(
671
+ paneNode.htmlAst,
672
+ astId,
673
+ updates
674
+ );
675
+
676
+ if (newHtmlAst.editableElements[astId]) {
677
+ newHtmlAst.editableElements[astId] = {
678
+ ...newHtmlAst.editableElements[astId],
679
+ ...updates,
680
+ };
681
+ }
682
+
683
+ paneNode.htmlAst = newHtmlAst;
684
+ paneNode.isChanged = true;
685
+
686
+ this.modifyNodes([paneNode]);
687
+ }
688
+
689
+ updateCreativePane(paneId: string, containerId: string, htmlContent: string) {
690
+ const allNodes = new Map(this.allNodes.get());
691
+ const originalPane = allNodes.get(paneId);
692
+
693
+ // 1. Validation and Clone (matching applyShellToPane pattern)
694
+ if (!originalPane || originalPane.nodeType !== 'Pane') return;
695
+
696
+ // Deep clone ensures we don't mutate state outside the atom update
697
+ const paneNode = cloneDeep(originalPane) as PaneNode;
698
+
699
+ // Guard: Ensure we are in HTML mode
700
+ if (!('htmlAst' in paneNode) || !paneNode.htmlAst) return;
701
+
702
+ // 2. Logic: Rehydrate and Patch
703
+ const newChildren = rehydrateChildrenFromHtml(htmlContent);
704
+
705
+ // Recursive patcher to find the container in the cloned tree
706
+ const patchNode = (nodes: any[]): boolean => {
707
+ for (const node of nodes) {
708
+ if (node.id === containerId) {
709
+ node.children = newChildren;
710
+ return true;
711
+ }
712
+ if (node.children && node.children.length > 0) {
713
+ if (patchNode(node.children)) return true;
714
+ }
715
+ }
716
+ return false;
717
+ };
718
+
719
+ // 3. Commit
720
+ if (patchNode(paneNode.htmlAst.tree)) {
721
+ paneNode.isChanged = true;
722
+ allNodes.set(paneId, paneNode);
723
+ this.allNodes.set(allNodes);
724
+ this.notifyNode(paneId);
725
+ this.showSaveBypass.set(true);
726
+ }
626
727
  }
627
728
 
628
729
  /**
@@ -2001,9 +2102,7 @@ export class NodesContext {
2001
2102
  });
2002
2103
  break;
2003
2104
  }
2004
- if ([`p`, `h2`, `h3`, `h4`, `li`].includes(tagName))
2005
- this.toolModeValStore.set({ value: 'text' });
2006
- else this.toolModeValStore.set({ value: 'styles' });
2105
+ this.toolModeValStore.set({ value: 'text' });
2007
2106
  this.notifyNode('root');
2008
2107
  }
2009
2108
 
@@ -2585,7 +2684,15 @@ export class NodesContext {
2585
2684
  getPaneImageFileIds(paneId: string): string[] {
2586
2685
  const paneNode = this.allNodes.get().get(paneId);
2587
2686
  if (!paneNode || paneNode.nodeType !== 'Pane') return [];
2687
+ const pane = paneNode as PaneNode;
2688
+
2689
+ // 1. Extract from Creative AST (if present)
2690
+ let creativeFileIds: string[] = [];
2691
+ if (pane.htmlAst) {
2692
+ creativeFileIds = extractFileIdsFromAst(pane.htmlAst);
2693
+ }
2588
2694
 
2695
+ // 2. Extract from Standard Nodes (TagElement, BgPane)
2589
2696
  const allNodes = this.getNodesRecursively(paneNode);
2590
2697
 
2591
2698
  const embeddedFileIds = allNodes
@@ -2612,7 +2719,10 @@ export class NodesContext {
2612
2719
  .map((node) => node.fileId)
2613
2720
  .filter((id): id is string => id !== undefined);
2614
2721
 
2615
- return [...embeddedFileIds, ...bgFileIds];
2722
+ // 3. Merge unique IDs
2723
+ return Array.from(
2724
+ new Set([...embeddedFileIds, ...bgFileIds, ...creativeFileIds])
2725
+ );
2616
2726
  }
2617
2727
 
2618
2728
  getPaneImagesMap(): Record<string, string[]> {
@@ -3193,6 +3303,11 @@ export class NodesContext {
3193
3303
  paneTemplate: TemplatePane,
3194
3304
  newPaneId: string
3195
3305
  ): BaseNode[] {
3306
+ if (paneTemplate.htmlAst) {
3307
+ // No nodes when in htmlAst mode
3308
+ return [];
3309
+ }
3310
+
3196
3311
  let allNodes: BaseNode[] = [];
3197
3312
 
3198
3313
  // 1. Process Markdown Content
@@ -3350,8 +3465,6 @@ export class NodesContext {
3350
3465
  };
3351
3466
  allNodes.push(bgPaneNode);
3352
3467
  }
3353
- // This helper only processes nodes, it doesn't modify the paneTemplate.
3354
- // The deletion of `duplicatedPane.bgPane` will remain in `addTemplatePane`.
3355
3468
  }
3356
3469
 
3357
3470
  return allNodes;
@@ -1,4 +1,4 @@
1
- import { atom } from 'nanostores';
1
+ import { atom, computed } from 'nanostores';
2
2
 
3
3
  export interface OrphanAnalysisData {
4
4
  storyFragments: Record<string, string[]>;
@@ -39,35 +39,11 @@ const defaultOrphanState: OrphanAnalysisState = {
39
39
  lastFetched: null,
40
40
  };
41
41
 
42
- const createOrphanAnalysisStore = () => {
43
- const store = {
44
- get: () => {
45
- const tenantId = getCurrentTenantId();
46
- return tenantOrphanAnalysis.get()[tenantId] || defaultOrphanState;
47
- },
48
-
49
- subscribe: (callback: (value: OrphanAnalysisState) => void) => {
50
- const tenantId = getCurrentTenantId();
51
- return tenantOrphanAnalysis.subscribe((analysis) => {
52
- callback(analysis[tenantId] || defaultOrphanState);
53
- });
54
- },
55
- lc: 0,
56
- listen: function (callback: any) {
57
- return this.subscribe(callback);
58
- },
59
- notify: function () {},
60
- off: function () {},
61
- get value() {
62
- return this.get();
63
- },
64
- set: function () {}, // Orphan store is read-only for components
65
- };
66
-
67
- return store;
68
- };
69
-
70
- export const orphanAnalysisStore = createOrphanAnalysisStore();
42
+ // Computed store that slices the state for the current tenant
43
+ export const orphanAnalysisStore = computed(tenantOrphanAnalysis, (states) => {
44
+ const tenantId = getCurrentTenantId();
45
+ return states[tenantId] || defaultOrphanState;
46
+ });
71
47
 
72
48
  function updateTenantState(
73
49
  tenantId: string,
@@ -0,0 +1,7 @@
1
+ import { map } from 'nanostores';
2
+
3
+ export const renderedPreviews = map<Record<string, string>>({});
4
+
5
+ export const updatePreview = (paneId: string, html: string) => {
6
+ renderedPreviews.setKey(paneId, html);
7
+ };
@@ -32,14 +32,6 @@ export const pendingHomePageSlugStore = atom<string | null>(null);
32
32
  export const saasModalOpenStore = atom<boolean>(false);
33
33
  export const sandboxTokenStore = atom<string | null>(null);
34
34
 
35
- export type ToolModeVal =
36
- | 'styles'
37
- | 'text'
38
- | 'insert'
39
- | 'eraser'
40
- | 'move'
41
- | 'debug';
42
-
43
35
  export type ToolAddMode =
44
36
  | 'p'
45
37
  | 'h2'
@@ -4,15 +4,7 @@ export type LispToken = string | number | LispToken[];
4
4
 
5
5
  export type ViewportKey = 'mobile' | 'tablet' | 'desktop' | 'auto';
6
6
  export type ViewportAuto = 'mobile' | 'tablet' | 'desktop';
7
- export type ToolModeVal =
8
- | 'styles'
9
- | 'text'
10
- | 'insert'
11
- | 'eraser'
12
- | 'move'
13
- | 'layout'
14
- | 'designLibrary'
15
- | 'debug';
7
+ export type ToolModeVal = 'text' | 'insert';
16
8
 
17
9
  export const toolAddModes = [
18
10
  'p',
@@ -27,6 +19,7 @@ export const toolAddModes = [
27
19
  'interactiveDisclosure',
28
20
  'identify',
29
21
  'toggle',
22
+ 'span',
30
23
  //"aside",
31
24
  ] as const;
32
25
  export type ToolAddMode = (typeof toolAddModes)[number];
@@ -55,6 +48,51 @@ export enum PaneConfigMode {
55
48
  CODEHOOK = 'CODEHOOK',
56
49
  }
57
50
 
51
+ export interface HtmlAstNode {
52
+ tag: string;
53
+ attrs?: Record<string, string>;
54
+ children?: HtmlAstNode[];
55
+ text?: string;
56
+ id?: string;
57
+ }
58
+
59
+ export type CreativeButtonPayload = {
60
+ callbackPayload: string;
61
+ isExternalUrl?: boolean;
62
+ bunnyPayload?: {
63
+ t: string;
64
+ videoId: string | null;
65
+ slug?: string;
66
+ isContext?: boolean;
67
+ };
68
+ };
69
+
70
+ export interface EditableElementMetadata {
71
+ astId: string;
72
+ tagName: string;
73
+ src?: string;
74
+ srcSet?: string;
75
+ fileId?: string;
76
+ base64Data?: string;
77
+ alt?: string;
78
+ href?: string;
79
+ buttonPayload?: CreativeButtonPayload;
80
+ isCssBackground?: boolean;
81
+ collection?: string;
82
+ image?: string;
83
+ }
84
+
85
+ export interface CreativePanePayload {
86
+ css: string;
87
+ viewportCss: {
88
+ xs: string;
89
+ md: string;
90
+ xl: string;
91
+ };
92
+ tree: HtmlAstNode[];
93
+ editableElements: Record<string, EditableElementMetadata>;
94
+ }
95
+
58
96
  export enum StoryFragmentMode {
59
97
  DEFAULT = 'DEFAULT',
60
98
  SLUG = 'SLUG',
@@ -73,6 +111,7 @@ export type SettingsPanelSignal = {
73
111
  nodeId: string;
74
112
  childId?: string;
75
113
  layer?: number;
114
+ view?: string;
76
115
  className?: string;
77
116
  minimized?: boolean;
78
117
  expanded?: boolean;
@@ -146,7 +185,8 @@ export type Tag =
146
185
  | 'belief'
147
186
  | 'identify'
148
187
  | 'toggle'
149
- | 'code';
188
+ | 'code'
189
+ | 'span';
150
190
 
151
191
  export const tagTitles: Record<Tag, string> = {
152
192
  p: 'Paragraph',
@@ -168,6 +208,7 @@ export const tagTitles: Record<Tag, string> = {
168
208
  belief: 'Belief Select Widget',
169
209
  toggle: 'Belief Toggle Widget',
170
210
  identify: 'Identify As Widget',
211
+ span: 'Creative Span',
171
212
  };
172
213
 
173
214
  export type NodeType =
@@ -232,6 +273,7 @@ export interface PaneNode extends BaseNode {
232
273
  codeHookPayload?: {
233
274
  [key: string]: string;
234
275
  };
276
+ htmlAst?: CreativePanePayload;
235
277
  heldBeliefs?: BeliefDatum;
236
278
  withheldBeliefs?: BeliefDatum;
237
279
  }
@@ -566,6 +608,7 @@ export type ParentBasePanelProps = {
566
608
  parentNode?: FlatNode | PaneNode;
567
609
  config?: BrandConfig | null;
568
610
  layer?: number;
611
+ view?: string;
569
612
  className?: string;
570
613
  childId?: string;
571
614
  availableCodeHooks?: string[];
@@ -0,0 +1,93 @@
1
+ import { TractStackAPI } from '@/utils/api';
2
+ import { sandboxTokenStore } from '@/stores/storykeep';
3
+
4
+ interface AiGenerationOptions {
5
+ prompt: string;
6
+ context: string;
7
+ expectJson: boolean;
8
+ isSandboxMode: boolean;
9
+ maxTokens?: number;
10
+ temperature?: number;
11
+ }
12
+
13
+ export const callAskLemurAPI = async ({
14
+ prompt,
15
+ context,
16
+ expectJson,
17
+ isSandboxMode,
18
+ maxTokens = 2000,
19
+ temperature = 0.5,
20
+ }: AiGenerationOptions): Promise<string> => {
21
+ const tenantId =
22
+ (window as any).TRACTSTACK_CONFIG?.tenantId ||
23
+ import.meta.env.PUBLIC_TENANTID ||
24
+ 'default';
25
+
26
+ const requestBody = {
27
+ prompt,
28
+ input_text: context,
29
+ final_model: '',
30
+ temperature,
31
+ max_tokens: maxTokens,
32
+ };
33
+
34
+ let resultData: any;
35
+
36
+ if (isSandboxMode) {
37
+ const token = sandboxTokenStore.get();
38
+ const response = await fetch(`/api/sandbox`, {
39
+ method: 'POST',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ 'X-Tenant-ID': tenantId,
43
+ 'X-Sandbox-Token': token || '',
44
+ },
45
+ credentials: 'include',
46
+ body: JSON.stringify({ action: 'askLemur', payload: requestBody }),
47
+ });
48
+
49
+ if (!response.ok) {
50
+ const errorText = await response.text();
51
+ throw new Error(`Sandbox API failed: ${response.status} ${errorText}`);
52
+ }
53
+
54
+ const json = await response.json();
55
+ if (!json.success) {
56
+ throw new Error(json.error || 'Sandbox generation failed');
57
+ }
58
+ resultData = json.data;
59
+ } else {
60
+ const api = new TractStackAPI(tenantId);
61
+ const response = await api.post('/api/v1/aai/askLemur', requestBody);
62
+
63
+ if (!response.success) {
64
+ throw new Error(
65
+ response.error || 'Generation failed to return valid response.'
66
+ );
67
+ }
68
+ resultData = response.data;
69
+ }
70
+
71
+ if (!resultData?.response) {
72
+ throw new Error('Generation failed to return a response object.');
73
+ }
74
+
75
+ const rawResponseData = resultData.response;
76
+
77
+ if (expectJson && typeof rawResponseData === 'object') {
78
+ return JSON.stringify(rawResponseData);
79
+ }
80
+
81
+ if (typeof rawResponseData === 'string') {
82
+ let responseString = rawResponseData;
83
+ // Clean up markdown code blocks if present to ensure clean parsing by caller
84
+ if (responseString.startsWith('```json')) {
85
+ responseString = responseString.slice(7, -3).trim();
86
+ } else if (responseString.startsWith('```html')) {
87
+ responseString = responseString.slice(7, -3).trim();
88
+ }
89
+ return responseString;
90
+ }
91
+
92
+ throw new Error('Unexpected response format received from API.');
93
+ };
@@ -16,6 +16,8 @@ const allowInsert = (
16
16
  case 'ol':
17
17
  {
18
18
  switch (tagNameNew) {
19
+ case 'span':
20
+ return false;
19
21
  case 'bunny':
20
22
  case 'yt':
21
23
  case 'interactiveDisclosure':