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
@@ -1,16 +1,29 @@
1
- import { useState, useEffect, type SetStateAction, type Dispatch } from 'react';
1
+ import {
2
+ useState,
3
+ useEffect,
4
+ type SetStateAction,
5
+ type Dispatch,
6
+ type MouseEvent,
7
+ } from 'react';
2
8
  import { useStore } from '@nanostores/react';
3
9
  import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
4
10
  import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
5
- import ArrowDownIcon from '@heroicons/react/24/outline/ArrowDownIcon';
6
- import ArrowUpIcon from '@heroicons/react/24/outline/ArrowUpIcon';
7
11
  import PaintBrushIcon from '@heroicons/react/24/outline/PaintBrushIcon';
12
+ import ArchiveBoxArrowDownIcon from '@heroicons/react/24/outline/ArchiveBoxArrowDownIcon';
13
+ import ArrowPathRoundedSquareIcon from '@heroicons/react/24/outline/ArrowPathRoundedSquareIcon';
14
+ import ArrowDownTrayIcon from '@heroicons/react/24/outline/ArrowDownTrayIcon';
15
+ import SparklesIcon from '@heroicons/react/24/solid/SparklesIcon';
8
16
  import {
9
17
  isContextPaneNode,
10
18
  hasBeliefPayload,
11
19
  } from '@/utils/compositor/typeGuards';
12
20
  import { settingsPanelStore, fullContentMapStore } from '@/stores/storykeep';
21
+ import { selectionStore } from '@/stores/selection';
13
22
  import { getCtx } from '@/stores/nodes';
23
+ import { copyPaneToClipboard } from '@/utils/compositor/designLibraryHelper';
24
+ import { SaveToLibraryModal } from '@/components/edit/state/SaveToLibraryModal';
25
+ import { RestylePaneModal } from '@/components/edit/pane/RestylePaneModal';
26
+ import { AiRestylePaneModal } from '@/components/edit/pane/AiRestylePaneModal';
14
27
  import PaneTitlePanel from './PanePanel_title';
15
28
  import PaneMagicPathPanel from './PanePanel_path';
16
29
  import PaneImpressionPanel from './PanePanel_impression';
@@ -18,18 +31,23 @@ import { PaneConfigMode, type PaneNode } from '@/types/compositorTypes';
18
31
 
19
32
  interface ConfigPanePanelProps {
20
33
  nodeId: string;
34
+ isHtmlAstPane: boolean;
35
+ isSandboxMode?: boolean;
21
36
  }
22
37
 
23
- const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
38
+ const ConfigPanePanel = ({
39
+ nodeId,
40
+ isHtmlAstPane,
41
+ isSandboxMode,
42
+ }: ConfigPanePanelProps) => {
24
43
  const ctx = getCtx();
25
44
  const isTemplate = useStore(ctx.isTemplate);
26
45
  const bgColorStyles = ctx.getNodeCSSPropertiesStyles(nodeId);
27
46
  const activePaneMode = useStore(ctx.activePaneMode);
28
- const toolMode = useStore(ctx.toolModeValStore);
29
- const reorderMode = toolMode.value === `move`;
30
47
  const isActiveMode =
31
48
  activePaneMode.panel === 'settings' && activePaneMode.paneId === nodeId;
32
49
  const $contentMap = useStore(fullContentMapStore);
50
+ const { isRestyleModalOpen, isAiRestyleModalOpen } = useStore(selectionStore);
33
51
 
34
52
  const allNodes = ctx.allNodes.get();
35
53
  const paneNode = allNodes.get(nodeId) as PaneNode;
@@ -49,6 +67,9 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
49
67
  : PaneConfigMode.DEFAULT
50
68
  );
51
69
 
70
+ const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
71
+ const [wasCopied, setWasCopied] = useState(false);
72
+
52
73
  useEffect(() => {
53
74
  if (isActiveMode && activePaneMode.mode) {
54
75
  setMode(activePaneMode.mode as PaneConfigMode);
@@ -65,7 +86,7 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
65
86
  };
66
87
 
67
88
  const handleCodeHookConfig = () => {
68
- ctx.toolModeValStore.set({ value: 'styles' });
89
+ ctx.toolModeValStore.set({ value: 'text' });
69
90
  settingsPanelStore.set({
70
91
  action: 'setup-codehook',
71
92
  nodeId: nodeId,
@@ -75,7 +96,7 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
75
96
 
76
97
  const handleEditStyles = () => {
77
98
  ctx.closeAllPanels();
78
- ctx.toolModeValStore.set({ value: 'styles' });
99
+ ctx.toolModeValStore.set({ value: 'text' });
79
100
  if (paneNode.isDecorative) {
80
101
  const childNodeIds = ctx.getChildNodeIDs(nodeId);
81
102
  const bgPaneId = childNodeIds.find((id) => {
@@ -99,6 +120,33 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
99
120
  }
100
121
  };
101
122
 
123
+ // Design Library Handlers
124
+ const handleRestyleClick = (e: MouseEvent) => {
125
+ e.stopPropagation();
126
+ selectionStore.setKey('paneToRestyleId', nodeId);
127
+ selectionStore.setKey('isRestyleModalOpen', true);
128
+ };
129
+
130
+ const handleAiRestyleClick = (e: MouseEvent) => {
131
+ e.stopPropagation();
132
+ selectionStore.setKey('paneToRestyleId', nodeId);
133
+ selectionStore.setKey('isAiRestyleModalOpen', true);
134
+ };
135
+
136
+ const handleSaveClick = (e: MouseEvent) => {
137
+ e.stopPropagation();
138
+ setIsSaveModalOpen(true);
139
+ };
140
+
141
+ const handleCopyToClipboard = async (e: MouseEvent) => {
142
+ e.stopPropagation();
143
+ const success = await copyPaneToClipboard(nodeId);
144
+ if (success) {
145
+ setWasCopied(true);
146
+ setTimeout(() => setWasCopied(false), 2000);
147
+ }
148
+ };
149
+
102
150
  if (mode === PaneConfigMode.TITLE) {
103
151
  return <PaneTitlePanel nodeId={nodeId} setMode={setSaveMode} />;
104
152
  } else if (mode === PaneConfigMode.PATH) {
@@ -112,8 +160,8 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
112
160
  className="border-t border-dotted border-mylightgrey bg-myoffwhite"
113
161
  style={bgColorStyles}
114
162
  >
115
- <div className="group w-full rounded-t-md px-1.5 pb-0.5 pt-1.5">
116
- <div className="flex flex-wrap gap-2">
163
+ <div className="group w-full rounded-t-md px-1.5 py-1.5">
164
+ <div className="flex flex-wrap items-center gap-2">
117
165
  <div className={`flex flex-wrap gap-2 transition-opacity`}>
118
166
  {paneNode.isDecorative ? (
119
167
  <>
@@ -142,24 +190,26 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
142
190
  ID
143
191
  </button>
144
192
  )}
145
- <button
146
- onClick={() => setSaveMode(PaneConfigMode.IMPRESSION)}
147
- className={buttonClass}
148
- >
149
- {impressionNodes.length ? (
150
- <>
151
- <CheckIcon className="inline h-4 w-4" />
152
- {` `}
153
- <span className="font-bold">Has Impression</span>
154
- </>
155
- ) : (
156
- <>
157
- <XMarkIcon className="inline h-4 w-4" />
158
- {` `}
159
- <span>No Impression</span>
160
- </>
161
- )}
162
- </button>
193
+ {!isHtmlAstPane && (
194
+ <button
195
+ onClick={() => setSaveMode(PaneConfigMode.IMPRESSION)}
196
+ className={buttonClass}
197
+ >
198
+ {impressionNodes.length ? (
199
+ <>
200
+ <CheckIcon className="inline h-4 w-4" />
201
+ {` `}
202
+ <span className="font-bold">Has Impression</span>
203
+ </>
204
+ ) : (
205
+ <>
206
+ <XMarkIcon className="inline h-4 w-4" />
207
+ {` `}
208
+ <span>No Impression</span>
209
+ </>
210
+ )}
211
+ </button>
212
+ )}
163
213
  </>
164
214
  )}
165
215
  {isCodeHook && (
@@ -170,7 +220,7 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
170
220
  Configure Code Hook
171
221
  </button>
172
222
  )}
173
- {!isCodeHook && (
223
+ {!isCodeHook && !isHtmlAstPane && (
174
224
  <button onClick={handleEditStyles} className={buttonClass}>
175
225
  <PaintBrushIcon className="inline h-4 w-4" />
176
226
  {` `}
@@ -179,7 +229,7 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
179
229
  )}
180
230
  </>
181
231
  )}
182
- {!isContextPane && !isTemplate && (
232
+ {!isContextPane && !isTemplate && !isHtmlAstPane && (
183
233
  <button
184
234
  onClick={() => setSaveMode(PaneConfigMode.PATH)}
185
235
  className={buttonClass}
@@ -199,29 +249,67 @@ const ConfigPanePanel = ({ nodeId }: ConfigPanePanelProps) => {
199
249
  )}
200
250
  </button>
201
251
  )}
202
- {reorderMode && (
203
- <div className="space-x-2">
204
- <button
205
- title="Move pane up"
206
- onClick={() => getCtx().moveNode(nodeId, 'after')}
207
- >
208
- <div className="inline-flex items-center rounded-b-md bg-gray-200 px-2 py-1 text-sm text-gray-800">
209
- <ArrowDownIcon className="mr-1 h-4 w-4" />
210
- </div>
211
- </button>
212
- <button
213
- title="Move pane down"
214
- onClick={() => getCtx().moveNode(nodeId, 'before')}
215
- >
216
- <div className="inline-flex items-center rounded-b-md bg-gray-200 px-2 py-1 text-sm text-gray-800">
217
- <ArrowUpIcon className="mr-1 h-4 w-4" />
218
- </div>
219
- </button>
220
- </div>
252
+ </div>
253
+
254
+ {/* Design Library Tools (Right Aligned) */}
255
+ <div className="ml-auto flex items-center gap-2 border-l border-gray-300 pl-2">
256
+ {!isHtmlAstPane && !isSandboxMode && (
257
+ <button
258
+ title="Save Pane to Design Library"
259
+ onClick={handleSaveClick}
260
+ className="flex h-7 w-7 items-center justify-center rounded-full bg-cyan-600 p-1 shadow-sm hover:bg-cyan-700"
261
+ >
262
+ <ArchiveBoxArrowDownIcon className="h-4 w-4 text-white" />
263
+ </button>
264
+ )}
265
+
266
+ <button
267
+ title={isHtmlAstPane ? 'Re-Style' : 'Re-Color'}
268
+ onClick={handleAiRestyleClick}
269
+ className="flex h-7 w-7 items-center justify-center rounded-full bg-purple-600 p-1 shadow-sm hover:bg-purple-700"
270
+ >
271
+ <SparklesIcon className="h-3.5 w-3.5 text-white" />
272
+ </button>
273
+
274
+ {!isHtmlAstPane && (
275
+ <button
276
+ title="Restyle Pane from Design Library"
277
+ onClick={handleRestyleClick}
278
+ className="flex h-7 w-7 items-center justify-center rounded-full bg-blue-600 p-1 shadow-sm hover:bg-blue-700"
279
+ >
280
+ <ArrowPathRoundedSquareIcon className="h-4 w-4 text-white" />
281
+ </button>
282
+ )}
283
+ {import.meta.env.DEV && (
284
+ <button
285
+ title="Copy Pane Design to Clipboard"
286
+ onClick={handleCopyToClipboard}
287
+ className={`flex h-7 w-7 items-center justify-center rounded-full p-1 shadow-sm transition-colors ${
288
+ wasCopied ? 'bg-green-500' : 'bg-gray-600 hover:bg-gray-700'
289
+ }`}
290
+ >
291
+ {wasCopied ? (
292
+ <CheckIcon className="h-4 w-4 text-white" />
293
+ ) : (
294
+ <ArrowDownTrayIcon className="h-4 w-4 text-white" />
295
+ )}
296
+ </button>
221
297
  )}
222
298
  </div>
223
299
  </div>
224
300
  </div>
301
+
302
+ {/* Modals */}
303
+ {isSaveModalOpen && (
304
+ <SaveToLibraryModal
305
+ paneId={nodeId}
306
+ onClose={() => setIsSaveModalOpen(false)}
307
+ />
308
+ )}
309
+ {isRestyleModalOpen && <RestylePaneModal />}
310
+ {isAiRestyleModalOpen && (
311
+ <AiRestylePaneModal isSandboxMode={isSandboxMode} />
312
+ )}
225
313
  </div>
226
314
  );
227
315
  };
@@ -450,7 +450,7 @@ export const RestylePaneModal = () => {
450
450
  <p className="text-gray-500">No designs found.</p>
451
451
  </div>
452
452
  ) : (
453
- <div className="lg:grid-cols-3 grid grid-cols-1 gap-6 md:grid-cols-2">
453
+ <div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
454
454
  {mergedTemplates.map(({ template }) => (
455
455
  <TemplatePreviewItem
456
456
  key={template.id}
@@ -0,0 +1,375 @@
1
+ import { useState } from 'react';
2
+ import SparklesIcon from '@heroicons/react/24/outline/SparklesIcon';
3
+ import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
4
+ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
5
+ import ArrowPathRoundedSquareIcon from '@heroicons/react/24/outline/ArrowPathRoundedSquareIcon';
6
+ import prompts from '@/constants/prompts.json';
7
+ import { getCtx } from '@/stores/nodes';
8
+ import { htmlToHtmlAst } from '@/utils/compositor/htmlAst';
9
+ import type { TemplatePane } from '@/types/compositorTypes';
10
+ import { callAskLemurAPI } from '@/utils/compositor/aiGeneration';
11
+ import BooleanToggle from '@/components/form/BooleanToggle';
12
+ import { AiDesignStep, type AiDesignConfig } from './AiDesignStep';
13
+
14
+ interface AiCreativeDesignStepProps {
15
+ onBack: () => void;
16
+ onSuccess: () => void;
17
+ onDirectInject: () => void;
18
+ onCreatePane: (template: TemplatePane) => void;
19
+ isSandboxMode?: boolean;
20
+ initialTopic?: string;
21
+ }
22
+
23
+ export const AiCreativeDesignStep = ({
24
+ onBack,
25
+ onSuccess,
26
+ onDirectInject,
27
+ onCreatePane,
28
+ isSandboxMode = false,
29
+ initialTopic = '',
30
+ }: AiCreativeDesignStepProps) => {
31
+ const [topic, setTopic] = useState(initialTopic);
32
+ const [designNotes, setDesignNotes] = useState('');
33
+ const [showColors, setShowColors] = useState(false);
34
+ const [aiDesignConfig, setAiDesignConfig] = useState<AiDesignConfig>({
35
+ harmony: 'Analogous',
36
+ baseColor: '',
37
+ accentColor: '',
38
+ theme: 'Light',
39
+ });
40
+
41
+ const [showAdvanced, setShowAdvanced] = useState(false);
42
+ const [customTemplate, setCustomTemplate] = useState(
43
+ prompts.aiPaneCreativePrompt.user_template
44
+ );
45
+
46
+ const [isGenerating, setIsGenerating] = useState(false);
47
+ const [error, setError] = useState<string | null>(null);
48
+
49
+ const [reviewMode, setReviewMode] = useState(false);
50
+ const [previewHtml, setPreviewHtml] = useState<string>('');
51
+ const [pendingTemplate, setPendingTemplate] = useState<TemplatePane | null>(
52
+ null
53
+ );
54
+
55
+ const handleGenerate = async () => {
56
+ if (!topic.trim()) {
57
+ setError('Please enter a topic.');
58
+ return;
59
+ }
60
+
61
+ setIsGenerating(true);
62
+ setError(null);
63
+
64
+ try {
65
+ const systemPrompt = prompts.aiPaneCreativePrompt.system;
66
+ let userPrompt = showAdvanced
67
+ ? customTemplate
68
+ : prompts.aiPaneCreativePrompt.user_template;
69
+
70
+ let colorContext = '';
71
+ if (showColors) {
72
+ colorContext = `Generate a design using a **${aiDesignConfig.harmony.toLowerCase()}** color scheme with a **${aiDesignConfig.theme.toLowerCase()}** theme.`;
73
+ if (aiDesignConfig.baseColor)
74
+ colorContext += ` Base the colors around **${aiDesignConfig.baseColor}**.`;
75
+ if (aiDesignConfig.accentColor)
76
+ colorContext += ` Use **${aiDesignConfig.accentColor}** as an accent color.`;
77
+ }
78
+
79
+ const combinedNotes = [
80
+ designNotes || 'Clean, modern, high contrast.',
81
+ colorContext,
82
+ ]
83
+ .filter(Boolean)
84
+ .join(' ');
85
+
86
+ userPrompt = userPrompt.replace('{{TOPIC}}', topic);
87
+ userPrompt = userPrompt.replace('{{DESIGN_NOTES}}', combinedNotes);
88
+
89
+ // Use shared infrastructure utility
90
+ const rawHtml = await callAskLemurAPI({
91
+ prompt: userPrompt,
92
+ context: systemPrompt,
93
+ expectJson: false,
94
+ isSandboxMode,
95
+ maxTokens: 6000,
96
+ temperature: 0.5,
97
+ });
98
+
99
+ const htmlAst = await htmlToHtmlAst(rawHtml, '');
100
+
101
+ const template: TemplatePane = {
102
+ id: '',
103
+ nodeType: 'Pane',
104
+ parentId: '',
105
+ title: `Creative: ${topic.slice(0, 20)}`,
106
+ slug: '',
107
+ isDecorative: false,
108
+ htmlAst,
109
+ markdown: {
110
+ id: '',
111
+ nodeType: 'Markdown',
112
+ parentId: '',
113
+ type: 'markdown',
114
+ markdownId: '',
115
+ defaultClasses: {},
116
+ parentClasses: [],
117
+ nodes: [],
118
+ },
119
+ };
120
+
121
+ const goBackend =
122
+ import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
123
+
124
+ const tenantId =
125
+ (window as any).TRACTSTACK_CONFIG?.tenantId ||
126
+ import.meta.env.PUBLIC_TENANTID ||
127
+ 'default';
128
+
129
+ const previewResponse = await fetch(
130
+ `${goBackend}/api/v1/fragments/ast-preview`,
131
+ {
132
+ method: 'POST',
133
+ headers: {
134
+ 'Content-Type': 'application/json',
135
+ 'X-Tenant-ID': tenantId,
136
+ },
137
+ body: JSON.stringify({
138
+ id: 'preview-temp',
139
+ title: 'Editor Preview',
140
+ tree: htmlAst.tree,
141
+ simulateFrontend: true,
142
+ }),
143
+ }
144
+ );
145
+
146
+ if (!previewResponse.ok) {
147
+ throw new Error('Failed to generate preview HTML');
148
+ }
149
+
150
+ const htmlString = await previewResponse.text();
151
+
152
+ setPendingTemplate(template);
153
+ setPreviewHtml(htmlString);
154
+ setReviewMode(true);
155
+ } catch (err: any) {
156
+ console.error('Creative Generation Error:', err);
157
+ setError(err.message || 'Failed to generate design.');
158
+ } finally {
159
+ setIsGenerating(false);
160
+ }
161
+ };
162
+
163
+ const handleAccept = () => {
164
+ if (pendingTemplate) {
165
+ onCreatePane(pendingTemplate);
166
+ const ctx = getCtx();
167
+ ctx.showSaveBypass.set(true);
168
+ onSuccess();
169
+ }
170
+ };
171
+
172
+ const handleCancel = () => {
173
+ setReviewMode(false);
174
+ setPendingTemplate(null);
175
+ setPreviewHtml('');
176
+ onBack();
177
+ };
178
+
179
+ const handleRedo = () => {
180
+ setReviewMode(false);
181
+ setPendingTemplate(null);
182
+ setPreviewHtml('');
183
+ };
184
+
185
+ if (isGenerating) {
186
+ return (
187
+ <div className="flex min-h-96 flex-col items-center justify-center space-y-4 p-8">
188
+ <div className="h-8 w-8 animate-spin rounded-full border-b-2 border-cyan-600"></div>
189
+ <div className="text-center">
190
+ <p className="font-bold text-gray-900">Generating Design...</p>
191
+ <p className="mt-1 text-sm text-gray-500">
192
+ This may take a few moments while AI codes your layout.
193
+ </p>
194
+ </div>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ if (reviewMode && previewHtml) {
200
+ return (
201
+ <div className="relative flex h-full flex-col">
202
+ <div className="absolute right-4 top-4 z-50 flex gap-2">
203
+ <button
204
+ onClick={handleCancel}
205
+ className="rounded-full border border-gray-200 bg-white p-2 text-red-500 shadow-md transition-colors hover:bg-gray-100"
206
+ title="Cancel"
207
+ >
208
+ <XMarkIcon className="h-6 w-6" />
209
+ </button>
210
+ <button
211
+ onClick={handleRedo}
212
+ className="rounded-full border border-gray-200 bg-white p-2 text-blue-500 shadow-md transition-colors hover:bg-gray-100"
213
+ title="Redo with same params"
214
+ >
215
+ <ArrowPathRoundedSquareIcon className="h-6 w-6" />
216
+ </button>
217
+ <button
218
+ onClick={handleAccept}
219
+ className="rounded-full border border-green-600 bg-green-500 p-2 text-white shadow-md transition-colors hover:bg-green-600"
220
+ title="Accept"
221
+ >
222
+ <CheckIcon className="h-6 w-6" />
223
+ </button>
224
+ </div>
225
+
226
+ <style
227
+ dangerouslySetInnerHTML={{
228
+ __html: pendingTemplate?.htmlAst?.css || '',
229
+ }}
230
+ />
231
+ <div
232
+ className="w-full flex-1 overflow-y-auto rounded-md border bg-gray-50"
233
+ dangerouslySetInnerHTML={{ __html: previewHtml }}
234
+ />
235
+ </div>
236
+ );
237
+ }
238
+
239
+ return (
240
+ <div className="space-y-6 p-4">
241
+ <div className="text-center">
242
+ <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-pink-50">
243
+ <SparklesIcon className="h-6 w-6 text-pink-600" aria-hidden="true" />
244
+ </div>
245
+ <h3 className="mt-2 text-lg font-bold text-gray-900">
246
+ Creative Design
247
+ </h3>
248
+ <p className="text-sm text-gray-500">
249
+ Describe what you want, and AI will code a unique HTML structure for
250
+ you.
251
+ </p>
252
+ </div>
253
+
254
+ <div className="space-y-4">
255
+ {!showAdvanced && (
256
+ <>
257
+ <div>
258
+ <label className="block text-sm font-bold text-gray-700">
259
+ Topic / Content Brief
260
+ </label>
261
+ <textarea
262
+ rows={6}
263
+ className="mt-1 block w-full rounded-md border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-pink-500 focus:ring-pink-500"
264
+ placeholder="e.g. A pricing table for a SaaS product..."
265
+ value={topic}
266
+ onChange={(e) => setTopic(e.target.value)}
267
+ disabled={isGenerating}
268
+ />
269
+ </div>
270
+
271
+ <div>
272
+ <label className="block text-sm font-bold text-gray-700">
273
+ Design Notes (Optional)
274
+ </label>
275
+ <textarea
276
+ rows={3}
277
+ className="mt-1 block w-full rounded-md border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-pink-500 focus:ring-pink-500"
278
+ placeholder="e.g. Dark mode, use rounded cards, neon accents..."
279
+ value={designNotes}
280
+ onChange={(e) => setDesignNotes(e.target.value)}
281
+ disabled={isGenerating}
282
+ />
283
+ </div>
284
+
285
+ <div className="my-4 flex items-center">
286
+ <BooleanToggle
287
+ label="Customize Colors"
288
+ value={showColors}
289
+ onChange={setShowColors}
290
+ size="sm"
291
+ />
292
+ </div>
293
+
294
+ {showColors && (
295
+ <div className="rounded-lg border border-gray-100 bg-gray-50 p-4">
296
+ <AiDesignStep
297
+ designConfig={aiDesignConfig}
298
+ onDesignConfigChange={setAiDesignConfig}
299
+ />
300
+ </div>
301
+ )}
302
+ </>
303
+ )}
304
+
305
+ <div className="my-4 flex items-center border-t border-gray-100 pt-4">
306
+ <BooleanToggle
307
+ label="Advanced: Edit Full Prompt"
308
+ value={showAdvanced}
309
+ onChange={setShowAdvanced}
310
+ size="sm"
311
+ />
312
+ </div>
313
+
314
+ {showAdvanced && (
315
+ <div className="rounded-md border border-yellow-200 bg-yellow-50 p-4">
316
+ <label className="mb-2 block text-sm font-bold text-gray-800">
317
+ Master Prompt Template
318
+ </label>
319
+ <p className="mb-3 text-xs text-gray-600">
320
+ This is the raw scaffolding ruleset. Leave{' '}
321
+ <code className="bg-gray-100 px-1 font-mono font-bold">
322
+ {`{{TOPIC}}`}
323
+ </code>{' '}
324
+ and{' '}
325
+ <code className="bg-gray-100 px-1 font-mono font-bold">
326
+ {`{{DESIGN_NOTES}}`}
327
+ </code>{' '}
328
+ as-is; they will be replaced by your inputs (which are hidden
329
+ while this mode is active).
330
+ </p>
331
+ <textarea
332
+ rows={12}
333
+ className="block w-full rounded-md border-gray-300 p-2 font-mono text-xs shadow-sm focus:border-pink-500 focus:ring-pink-500"
334
+ value={customTemplate}
335
+ onChange={(e) => setCustomTemplate(e.target.value)}
336
+ />
337
+ </div>
338
+ )}
339
+ </div>
340
+
341
+ {error && (
342
+ <div className="rounded-md bg-red-50 p-3">
343
+ <p className="text-sm text-red-700">{error}</p>
344
+ </div>
345
+ )}
346
+
347
+ <div className="text-center">
348
+ <button
349
+ onClick={onDirectInject}
350
+ className="text-xs text-gray-400 underline hover:text-gray-600"
351
+ >
352
+ Want to write raw HTML yourself? Use Direct Inject.
353
+ </button>
354
+ </div>
355
+
356
+ <div className="flex justify-between border-t border-gray-100 pt-4">
357
+ <button
358
+ onClick={onBack}
359
+ disabled={isGenerating}
360
+ className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 shadow-sm hover:bg-gray-50 disabled:opacity-50"
361
+ >
362
+ Cancel
363
+ </button>
364
+ <button
365
+ onClick={handleGenerate}
366
+ disabled={isGenerating || !topic.trim()}
367
+ className="flex items-center gap-2 rounded-md bg-pink-600 px-6 py-2 text-sm font-bold text-white shadow-sm hover:bg-pink-700 disabled:bg-gray-400"
368
+ >
369
+ <SparklesIcon className="h-4 w-4" />
370
+ Generate
371
+ </button>
372
+ </div>
373
+ </div>
374
+ );
375
+ };