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
@@ -6,7 +6,6 @@ export interface AiDesignConfig {
6
6
  baseColor: string;
7
7
  accentColor: string;
8
8
  theme: string;
9
- additionalNotes: string;
10
9
  }
11
10
 
12
11
  interface AiDesignStepProps {
@@ -78,7 +77,7 @@ export const AiDesignStep = ({
78
77
  </div>
79
78
  </div>
80
79
 
81
- <div className="sm:grid-cols-2 grid grid-cols-1 gap-4">
80
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
82
81
  <div>
83
82
  <ColorPickerCombo
84
83
  title="Base Color (Optional)"
@@ -123,27 +122,6 @@ export const AiDesignStep = ({
123
122
  ))}
124
123
  </div>
125
124
  </div>
126
-
127
- <div>
128
- <label
129
- htmlFor={`${idPrefix}additional-notes`}
130
- className="block text-base font-bold text-gray-800"
131
- >
132
- Additional Design Notes (Optional)
133
- </label>
134
- <p className="mb-2 mt-1 text-sm text-gray-500">
135
- Add specific requests like "use rounded corners", "add subtle
136
- texture".
137
- </p>
138
- <textarea
139
- id={`${idPrefix}additional-notes`}
140
- value={designConfig.additionalNotes}
141
- onChange={(e) => updateField('additionalNotes', e.target.value)}
142
- placeholder="Enter additional notes..."
143
- rows={3}
144
- className="sm:text-sm block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
145
- />
146
- </div>
147
125
  </div>
148
126
  );
149
127
  };
@@ -0,0 +1,327 @@
1
+ import { useState, useMemo, useEffect } from 'react';
2
+ import { cloneDeep } from '@/utils/helpers';
3
+ import prompts from '@/constants/prompts.json';
4
+ import {
5
+ convertStorageToLiveTemplate,
6
+ convertTemplateToAIShell,
7
+ } from '@/utils/compositor/designLibraryHelper';
8
+ import { parseAiPane, parseAiCopyHtml } from '@/utils/compositor/aiPaneParser';
9
+ import { callAskLemurAPI } from '@/utils/compositor/aiGeneration';
10
+ import { CopyInputStep, type CopyMode } from './CopyInputStep';
11
+ import type { DesignLibraryEntry } from '@/types/tractstack';
12
+ import type { TemplatePane } from '@/types/compositorTypes';
13
+
14
+ interface AiLibraryCopyStepProps {
15
+ entry: DesignLibraryEntry;
16
+ onBack: () => void;
17
+ onCreatePane: (template: TemplatePane) => void;
18
+ isSandboxMode?: boolean;
19
+ }
20
+
21
+ type ColumnPresetKey = 'left' | 'right';
22
+
23
+ export const AiLibraryCopyStep = ({
24
+ entry,
25
+ onBack,
26
+ onCreatePane,
27
+ isSandboxMode = false,
28
+ }: AiLibraryCopyStepProps) => {
29
+ const [isGenerating, setIsGenerating] = useState(false);
30
+ const [error, setError] = useState<string | null>(null);
31
+
32
+ const [copyMode, setCopyMode] = useState<CopyMode>(
33
+ entry.retain ? 'original' : 'prompt'
34
+ );
35
+
36
+ const [topic, setTopic] = useState('');
37
+ const [promptValue, setPromptValue] = useState('');
38
+ const [copyValue, setCopyValue] = useState('');
39
+
40
+ const [overallPrompt, setOverallPrompt] = useState('');
41
+ const [promptValueCol1, setPromptValueCol1] = useState('');
42
+ const [promptValueCol2, setPromptValueCol2] = useState('');
43
+ const [col1Copy, setCol1Copy] = useState('');
44
+ const [col2Copy, setCol2Copy] = useState('');
45
+
46
+ const [showAdvancedPrompts, setShowAdvancedPrompts] = useState(false);
47
+ const [selectedPromptId, setSelectedPromptId] = useState('');
48
+ const [isAiStyling, setIsAiStyling] = useState(false);
49
+
50
+ const liveTemplate = useMemo(() => {
51
+ return convertStorageToLiveTemplate(entry.template);
52
+ }, [entry]);
53
+
54
+ const isGrid = !!liveTemplate.gridLayout;
55
+ const layoutChoice = isGrid ? 'grid' : 'standard';
56
+
57
+ const promptOptions = useMemo(() => {
58
+ return prompts.aiPromptsIndex
59
+ .filter((p) => p.layout === layoutChoice)
60
+ .map((p) => ({ label: p.label, value: p.id }));
61
+ }, [layoutChoice]);
62
+
63
+ useEffect(() => {
64
+ if (promptOptions.length > 0) {
65
+ const currentValid = promptOptions.find(
66
+ (p) => p.value === selectedPromptId
67
+ );
68
+ if (!currentValid) {
69
+ setSelectedPromptId(promptOptions[0].value);
70
+ }
71
+ }
72
+ }, [promptOptions, selectedPromptId]);
73
+
74
+ useEffect(() => {
75
+ if (!selectedPromptId) return;
76
+
77
+ const activeConfig = prompts.aiPromptsIndex.find(
78
+ (p) => p.id === selectedPromptId
79
+ );
80
+ if (!activeConfig) return;
81
+
82
+ const promptKey = activeConfig.prompts.copy;
83
+ const copyPromptGroup = (prompts as any)[promptKey];
84
+ if (!copyPromptGroup) return;
85
+
86
+ const variant = activeConfig.variants
87
+ ? activeConfig.variants[0]
88
+ : 'default';
89
+
90
+ if (layoutChoice === 'standard') {
91
+ const baseText = copyPromptGroup[variant] || '';
92
+ setPromptValue(baseText);
93
+ } else if (layoutChoice === 'grid') {
94
+ const preset = copyPromptGroup.presets?.[variant];
95
+ if (preset) {
96
+ setOverallPrompt(preset.default || '');
97
+ setPromptValueCol1(preset.left?.prompt || '');
98
+ setPromptValueCol2(preset.right?.prompt || '');
99
+ }
100
+ }
101
+ }, [selectedPromptId, layoutChoice]);
102
+
103
+ const handleGenerate = async () => {
104
+ setError(null);
105
+ setIsGenerating(true);
106
+
107
+ try {
108
+ if (copyMode === 'original') {
109
+ onCreatePane(liveTemplate);
110
+ return;
111
+ }
112
+
113
+ const shellResult = convertTemplateToAIShell(liveTemplate);
114
+ const layoutType = 'Text Only';
115
+
116
+ const activeConfig = prompts.aiPromptsIndex.find(
117
+ (p) => p.id === selectedPromptId
118
+ );
119
+ if (!activeConfig) throw new Error('Selected prompt type not found.');
120
+
121
+ const injectTopic = (text: string) => {
122
+ return text.replace('{{TOPIC}}', topic.trim());
123
+ };
124
+
125
+ if (isGrid && liveTemplate.gridLayout) {
126
+ if (copyMode === 'raw') {
127
+ const nodes = liveTemplate.gridLayout.nodes;
128
+ if (nodes && nodes[0]) nodes[0].markdownBody = col1Copy;
129
+ if (nodes && nodes[1]) nodes[1].markdownBody = col2Copy;
130
+ onCreatePane(liveTemplate);
131
+ return;
132
+ }
133
+
134
+ if (copyMode === 'prompt') {
135
+ const copyPromptKey = activeConfig.prompts.copy;
136
+ const copyPromptDetails = (prompts as any)[copyPromptKey];
137
+
138
+ const preset =
139
+ copyPromptDetails.presets?.[activeConfig.variants[0]] ||
140
+ copyPromptDetails.presets?.heroDefault;
141
+
142
+ const copyResults: string[] = [];
143
+
144
+ const promptsToRun = [
145
+ { prompt: promptValueCol1, presetKey: 'left' as ColumnPresetKey },
146
+ { prompt: promptValueCol2, presetKey: 'right' as ColumnPresetKey },
147
+ ];
148
+
149
+ for (const item of promptsToRun) {
150
+ const columnPreset = preset[item.presetKey];
151
+ const formattedCopyPrompt = copyPromptDetails.user_template
152
+ .replace('{{SHELL_JSON}}', shellResult)
153
+ .replace('{{COPY_INPUT}}', injectTopic(overallPrompt))
154
+ .replace('{{COLUMN_PROMPT}}', item.prompt)
155
+ .replace(
156
+ '{{DESIGN_INPUT}}',
157
+ "N/A - Use the provided Shell JSON's design."
158
+ )
159
+ .replace('{{LAYOUT_TYPE}}', layoutType)
160
+ .replace('{{COLUMN_EXAMPLE}}', columnPreset.example);
161
+
162
+ const copyResult = await callAskLemurAPI({
163
+ prompt: formattedCopyPrompt,
164
+ context: copyPromptDetails.system || '',
165
+ expectJson: false,
166
+ isSandboxMode,
167
+ maxTokens: 2000,
168
+ });
169
+ copyResults.push(copyResult);
170
+ }
171
+
172
+ const finalPane = parseAiPane(shellResult, copyResults, layoutType);
173
+ onCreatePane(finalPane);
174
+ return;
175
+ }
176
+ }
177
+
178
+ if (!isGrid && liveTemplate.markdown) {
179
+ if (copyMode === 'raw') {
180
+ liveTemplate.markdown.markdownBody = copyValue;
181
+ onCreatePane(liveTemplate);
182
+ return;
183
+ }
184
+
185
+ if (copyMode === 'prompt') {
186
+ if (!shellResult || shellResult === '{}') {
187
+ throw new Error(
188
+ 'Could not generate a valid AI shell from this design.'
189
+ );
190
+ }
191
+
192
+ const copyPromptKey = activeConfig.prompts.copy;
193
+ const copyPromptDetails = (prompts as any)[copyPromptKey];
194
+
195
+ const formattedCopyPrompt = copyPromptDetails.user_template
196
+ .replace('{{COPY_INPUT}}', injectTopic(promptValue))
197
+ .replace(
198
+ '{{DESIGN_INPUT}}',
199
+ "N/A - Use the provided Shell JSON's design."
200
+ )
201
+ .replace('{{LAYOUT_TYPE}}', layoutType)
202
+ .replace('{{SHELL_JSON}}', shellResult);
203
+
204
+ const copyResult = await callAskLemurAPI({
205
+ prompt: formattedCopyPrompt,
206
+ context: copyPromptDetails.system || '',
207
+ expectJson: false,
208
+ isSandboxMode,
209
+ maxTokens: 2000,
210
+ });
211
+
212
+ const newNodes = parseAiCopyHtml(
213
+ copyResult,
214
+ liveTemplate.markdown.id
215
+ );
216
+ const finalPane = cloneDeep(liveTemplate);
217
+ if (finalPane.markdown) {
218
+ finalPane.markdown.nodes = newNodes;
219
+ }
220
+ onCreatePane(finalPane);
221
+ return;
222
+ }
223
+ }
224
+
225
+ throw new Error(
226
+ 'Template configuration mismatch. Please try a different design.'
227
+ );
228
+ } catch (err: any) {
229
+ console.error('Library Generation Error:', err);
230
+ setError(err.message || 'Failed to generate content for this design.');
231
+ } finally {
232
+ setIsGenerating(false);
233
+ }
234
+ };
235
+
236
+ if (isGenerating) {
237
+ return (
238
+ <div className="flex min-h-96 flex-col items-center justify-center space-y-4 p-6">
239
+ <div className="h-8 w-8 animate-spin rounded-full border-b-2 border-cyan-600"></div>
240
+ <p className="text-sm text-gray-600">Writing Content...</p>
241
+ <p className="text-xs text-gray-500">
242
+ Adapting the design to your text.
243
+ </p>
244
+ </div>
245
+ );
246
+ }
247
+
248
+ return (
249
+ <div className="space-y-6 p-4">
250
+ <div className="flex items-center justify-between border-b pb-4">
251
+ <div>
252
+ <h3 className="font-bold text-gray-800">Customize Design</h3>
253
+ <p className="text-xs text-gray-500">
254
+ Selected: <span className="font-bold">{entry.title}</span>
255
+ </p>
256
+ </div>
257
+ <button
258
+ onClick={onBack}
259
+ className="text-xs text-gray-500 hover:text-gray-700"
260
+ >
261
+ Change Design
262
+ </button>
263
+ </div>
264
+
265
+ <CopyInputStep
266
+ layoutChoice={layoutChoice}
267
+ copyMode={copyMode}
268
+ onCopyModeChange={setCopyMode}
269
+ topic={topic}
270
+ onTopicChange={setTopic}
271
+ showAdvancedPrompts={showAdvancedPrompts}
272
+ onShowAdvancedPromptsChange={setShowAdvancedPrompts}
273
+ promptValue={promptValue}
274
+ onPromptValueChange={setPromptValue}
275
+ copyValue={copyValue}
276
+ onCopyValueChange={setCopyValue}
277
+ overallPrompt={overallPrompt}
278
+ onOverallPromptChange={setOverallPrompt}
279
+ promptValueCol1={promptValueCol1}
280
+ onPromptValueCol1Change={setPromptValueCol1}
281
+ promptValueCol2={promptValueCol2}
282
+ onPromptValueCol2Change={setPromptValueCol2}
283
+ col1Copy={col1Copy}
284
+ onCol1CopyChange={setCol1Copy}
285
+ col2Copy={col2Copy}
286
+ onCol2CopyChange={setCol2Copy}
287
+ hasRetainedContent={entry.retain}
288
+ showStyleToggle={false}
289
+ promptOptions={promptOptions}
290
+ selectedPromptId={selectedPromptId}
291
+ onSelectedPromptIdChange={setSelectedPromptId}
292
+ isAiStyling={isAiStyling}
293
+ onIsAiStylingChange={setIsAiStyling}
294
+ />
295
+
296
+ {error && (
297
+ <div className="rounded-md bg-red-50 p-3 text-sm text-red-700">
298
+ {error}
299
+ </div>
300
+ )}
301
+
302
+ <div className="mt-4 flex justify-end border-t pt-4">
303
+ <button
304
+ onClick={handleGenerate}
305
+ disabled={
306
+ copyMode === 'prompt'
307
+ ? isGrid
308
+ ? !promptValueCol1.trim() &&
309
+ !promptValueCol2.trim() &&
310
+ !overallPrompt.trim()
311
+ : !promptValue.trim()
312
+ : copyMode === 'raw'
313
+ ? isGrid
314
+ ? !col1Copy.trim() || !col2Copy.trim()
315
+ : !copyValue.trim()
316
+ : false
317
+ }
318
+ className="rounded-md bg-cyan-600 px-6 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-700 disabled:cursor-not-allowed disabled:bg-gray-400"
319
+ >
320
+ {copyMode === 'original'
321
+ ? 'Use Original Content'
322
+ : '✨ Generate Pane'}
323
+ </button>
324
+ </div>
325
+ </div>
326
+ );
327
+ };
@@ -0,0 +1,267 @@
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 { htmlToHtmlAst } from '@/utils/compositor/htmlAst';
8
+ import { callAskLemurAPI } from '@/utils/compositor/aiGeneration';
9
+ import type { TemplatePane } from '@/types/compositorTypes';
10
+
11
+ interface AiRefineDesignStepProps {
12
+ onBack: () => void;
13
+ onSuccess: () => void;
14
+ onUpdatePane: (template: TemplatePane) => void;
15
+ isSandboxMode?: boolean;
16
+ initialHtml: string;
17
+ initialCss: string;
18
+ }
19
+
20
+ export const AiRefineDesignStep = ({
21
+ onBack,
22
+ onSuccess,
23
+ onUpdatePane,
24
+ isSandboxMode = false,
25
+ initialHtml,
26
+ initialCss,
27
+ }: AiRefineDesignStepProps) => {
28
+ const [prompt, setPrompt] = useState('');
29
+ const [isGenerating, setIsGenerating] = useState(false);
30
+ const [error, setError] = useState<string | null>(null);
31
+
32
+ const [reviewMode, setReviewMode] = useState(false);
33
+ const [previewHtml, setPreviewHtml] = useState<string>('');
34
+ const [pendingTemplate, setPendingTemplate] = useState<TemplatePane | null>(
35
+ null
36
+ );
37
+
38
+ const handleGenerate = async () => {
39
+ if (!prompt.trim()) {
40
+ setError('Please enter a refinement request.');
41
+ return;
42
+ }
43
+
44
+ setIsGenerating(true);
45
+ setError(null);
46
+
47
+ try {
48
+ const promptConfig = prompts.aiPaneCreativeRefinePrompt;
49
+ const systemPrompt = promptConfig.system;
50
+ let userPrompt = promptConfig.user_template;
51
+
52
+ userPrompt = userPrompt.replace('{{DESIGN_NOTES}}', prompt);
53
+ userPrompt = userPrompt.replace('{{CSS_INPUT}}', initialCss);
54
+ userPrompt = userPrompt.replace('{{HTML_INPUT}}', initialHtml);
55
+
56
+ // 1. Get RAW output from AI
57
+ const resultHtml = await callAskLemurAPI({
58
+ prompt: userPrompt,
59
+ context: systemPrompt,
60
+ expectJson: false,
61
+ isSandboxMode,
62
+ maxTokens: 4000,
63
+ temperature: 0.5,
64
+ });
65
+
66
+ // 2. Generate AST immediately
67
+ const htmlAst = await htmlToHtmlAst(resultHtml, '');
68
+
69
+ // 3. Construct the candidate TemplatePane
70
+ const candidateTemplate: TemplatePane = {
71
+ id: '',
72
+ nodeType: 'Pane',
73
+ parentId: '',
74
+ title: 'Refined Pane',
75
+ slug: '',
76
+ isDecorative: false,
77
+ htmlAst,
78
+ markdown: {
79
+ id: '',
80
+ nodeType: 'Markdown',
81
+ parentId: '',
82
+ type: 'markdown',
83
+ markdownId: '',
84
+ defaultClasses: {},
85
+ parentClasses: [],
86
+ nodes: [],
87
+ },
88
+ };
89
+
90
+ const tenantId =
91
+ (window as any).TRACTSTACK_CONFIG?.tenantId ||
92
+ import.meta.env.PUBLIC_TENANTID ||
93
+ 'default';
94
+
95
+ const goBackend =
96
+ import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
97
+
98
+ // 4. Send AST to backend to get SAFE/Sanitized HTML for preview
99
+ const response = await fetch(
100
+ `${goBackend}/api/v1/fragments/ast-preview`,
101
+ {
102
+ method: 'POST',
103
+ headers: {
104
+ 'Content-Type': 'application/json',
105
+ 'X-Tenant-ID': tenantId,
106
+ },
107
+ body: JSON.stringify({
108
+ id: 'temp-refine-preview',
109
+ title: 'Refine Preview',
110
+ tree: htmlAst.tree,
111
+ simulateFrontend: true,
112
+ }),
113
+ }
114
+ );
115
+
116
+ if (!response.ok) {
117
+ const text = await response.text();
118
+ throw new Error(
119
+ text || `Preview generation failed: ${response.status}`
120
+ );
121
+ }
122
+
123
+ const safeHtml = await response.text();
124
+
125
+ // 5. Store SAFE HTML for display and valid Template for acceptance
126
+ setPendingTemplate(candidateTemplate);
127
+ setPreviewHtml(safeHtml);
128
+ setReviewMode(true);
129
+ } catch (err: any) {
130
+ console.error('Refine Generation Error:', err);
131
+ setError(err.message || 'Failed to refine design.');
132
+ } finally {
133
+ setIsGenerating(false);
134
+ }
135
+ };
136
+
137
+ const handleAccept = () => {
138
+ if (pendingTemplate) {
139
+ onUpdatePane(pendingTemplate);
140
+ onSuccess();
141
+ }
142
+ };
143
+
144
+ const handleCancel = () => {
145
+ setReviewMode(false);
146
+ setPendingTemplate(null);
147
+ setPreviewHtml('');
148
+ onBack();
149
+ };
150
+
151
+ const handleRedo = () => {
152
+ setReviewMode(false);
153
+ setPendingTemplate(null);
154
+ setPreviewHtml('');
155
+ };
156
+
157
+ if (isGenerating) {
158
+ return (
159
+ <div className="flex min-h-96 flex-col items-center justify-center space-y-4 p-8">
160
+ <div className="h-8 w-8 animate-spin rounded-full border-b-2 border-cyan-600"></div>
161
+ <div className="text-center">
162
+ <p className="font-bold text-gray-900">
163
+ {reviewMode ? 'Compiling Design...' : 'Refining Design...'}
164
+ </p>
165
+ <p className="mt-1 text-sm text-gray-500">
166
+ AI is refactoring your component.
167
+ </p>
168
+ </div>
169
+ </div>
170
+ );
171
+ }
172
+
173
+ if (reviewMode && previewHtml) {
174
+ return (
175
+ <div className="relative flex h-full flex-col">
176
+ <div className="absolute right-4 top-4 z-50 flex gap-2">
177
+ <button
178
+ onClick={handleCancel}
179
+ className="rounded-full border border-gray-200 bg-white p-2 text-red-500 shadow-md transition-colors hover:bg-gray-100"
180
+ title="Cancel"
181
+ >
182
+ <XMarkIcon className="h-6 w-6" />
183
+ </button>
184
+ <button
185
+ onClick={handleRedo}
186
+ className="rounded-full border border-gray-200 bg-white p-2 text-blue-500 shadow-md transition-colors hover:bg-gray-100"
187
+ title="Redo with same prompt"
188
+ >
189
+ <ArrowPathRoundedSquareIcon className="h-6 w-6" />
190
+ </button>
191
+ <button
192
+ onClick={handleAccept}
193
+ className="rounded-full border border-green-600 bg-green-500 p-2 text-white shadow-md transition-colors hover:bg-green-600"
194
+ title="Accept"
195
+ >
196
+ <CheckIcon className="h-6 w-6" />
197
+ </button>
198
+ </div>
199
+
200
+ <style
201
+ dangerouslySetInnerHTML={{
202
+ __html: pendingTemplate?.htmlAst?.css || '',
203
+ }}
204
+ />
205
+ <div
206
+ className="w-full flex-1 overflow-y-auto rounded-md border bg-gray-50 p-4"
207
+ dangerouslySetInnerHTML={{ __html: previewHtml }}
208
+ />
209
+ </div>
210
+ );
211
+ }
212
+
213
+ return (
214
+ <div className="space-y-6 p-4">
215
+ <div className="text-center">
216
+ <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-purple-50">
217
+ <SparklesIcon
218
+ className="h-6 w-6 text-purple-600"
219
+ aria-hidden="true"
220
+ />
221
+ </div>
222
+ <h3 className="mt-2 text-lg font-bold text-gray-900">Refine Design</h3>
223
+ <p className="text-sm text-gray-500">
224
+ Modify the existing design using AI instructions.
225
+ </p>
226
+ </div>
227
+
228
+ <div>
229
+ <label className="block text-sm font-bold text-gray-700">
230
+ Refinement Instructions
231
+ </label>
232
+ <textarea
233
+ rows={6}
234
+ className="mt-1 block w-full rounded-md border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-purple-500 focus:ring-purple-500"
235
+ placeholder="e.g. Change the background to dark blue, make the title larger, and switch the button color to yellow."
236
+ value={prompt}
237
+ onChange={(e) => setPrompt(e.target.value)}
238
+ disabled={isGenerating}
239
+ />
240
+ </div>
241
+
242
+ {error && (
243
+ <div className="rounded-md bg-red-50 p-3">
244
+ <p className="text-sm text-red-700">{error}</p>
245
+ </div>
246
+ )}
247
+
248
+ <div className="flex justify-between border-t border-gray-100 pt-4">
249
+ <button
250
+ onClick={onBack}
251
+ disabled={isGenerating}
252
+ 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"
253
+ >
254
+ Cancel
255
+ </button>
256
+ <button
257
+ onClick={handleGenerate}
258
+ disabled={isGenerating || !prompt.trim()}
259
+ className="flex items-center gap-2 rounded-md bg-purple-600 px-6 py-2 text-sm font-bold text-white shadow-sm hover:bg-purple-700 disabled:bg-gray-400"
260
+ >
261
+ <SparklesIcon className="h-4 w-4" />
262
+ Refine
263
+ </button>
264
+ </div>
265
+ </div>
266
+ );
267
+ };