astro-tractstack 2.0.12 → 2.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/src/components/compositor/Node.tsx +14 -2
- package/templates/src/components/edit/pane/AddPanePanel.tsx +5 -1
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +4 -1
- package/templates/src/components/edit/pane/AiPaneGenerator.tsx +263 -93
- package/templates/src/components/edit/pane/AiPanePreview.tsx +60 -210
- package/templates/src/components/edit/pane/PageGenSelector.tsx +4 -0
- package/templates/src/constants/prompts.json +3 -3
- package/templates/src/utils/compositor/aiPaneParser.ts +39 -13
package/package.json
CHANGED
|
@@ -155,6 +155,7 @@ const getElement = (
|
|
|
155
155
|
nodeId={props.nodeId}
|
|
156
156
|
ctx={getCtx(props)}
|
|
157
157
|
isTemplate={isTemplate}
|
|
158
|
+
config={props.config!}
|
|
158
159
|
/>
|
|
159
160
|
) : (
|
|
160
161
|
<>
|
|
@@ -215,6 +216,7 @@ const getElement = (
|
|
|
215
216
|
nodeId={node.id}
|
|
216
217
|
first={true}
|
|
217
218
|
ctx={getCtx(props)}
|
|
219
|
+
config={props.config!}
|
|
218
220
|
isContextPane={true}
|
|
219
221
|
/>
|
|
220
222
|
</PanelVisibilityWrapper>
|
|
@@ -242,7 +244,12 @@ const getElement = (
|
|
|
242
244
|
panelType="add"
|
|
243
245
|
ctx={getCtx(props)}
|
|
244
246
|
>
|
|
245
|
-
<AddPanePanel
|
|
247
|
+
<AddPanePanel
|
|
248
|
+
nodeId={node.id}
|
|
249
|
+
first={true}
|
|
250
|
+
ctx={getCtx(props)}
|
|
251
|
+
config={props.config!}
|
|
252
|
+
/>
|
|
246
253
|
</PanelVisibilityWrapper>
|
|
247
254
|
)}
|
|
248
255
|
<div className="py-0.5">
|
|
@@ -266,7 +273,12 @@ const getElement = (
|
|
|
266
273
|
panelType="add"
|
|
267
274
|
ctx={getCtx(props)}
|
|
268
275
|
>
|
|
269
|
-
<AddPanePanel
|
|
276
|
+
<AddPanePanel
|
|
277
|
+
nodeId={node.id}
|
|
278
|
+
first={false}
|
|
279
|
+
ctx={getCtx(props)}
|
|
280
|
+
config={props.config!}
|
|
281
|
+
/>
|
|
270
282
|
</PanelVisibilityWrapper>
|
|
271
283
|
</>
|
|
272
284
|
);
|
|
@@ -5,8 +5,9 @@ import AddPaneNewPanel from './AddPanePanel_new';
|
|
|
5
5
|
import AddPaneBreakPanel from './AddPanePanel_break';
|
|
6
6
|
import AddPaneReUsePanel from './AddPanePanel_reuse';
|
|
7
7
|
import AddPaneCodeHookPanel from './AddPanePanel_codehook';
|
|
8
|
-
import { NodesContext, ROOT_NODE_NAME, getCtx } from '@/stores/nodes';
|
|
8
|
+
import { NodesContext, ROOT_NODE_NAME, getCtx } from '@/stores/nodes';
|
|
9
9
|
import { PaneAddMode } from '@/types/compositorTypes';
|
|
10
|
+
import type { BrandConfig } from '@/types/tractstack';
|
|
10
11
|
|
|
11
12
|
interface AddPanePanelProps {
|
|
12
13
|
nodeId: string;
|
|
@@ -14,6 +15,7 @@ interface AddPanePanelProps {
|
|
|
14
15
|
ctx?: NodesContext;
|
|
15
16
|
isStoryFragment?: boolean;
|
|
16
17
|
isContextPane?: boolean;
|
|
18
|
+
config?: BrandConfig;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
const AddPanePanel = ({
|
|
@@ -22,6 +24,7 @@ const AddPanePanel = ({
|
|
|
22
24
|
ctx,
|
|
23
25
|
isStoryFragment = false,
|
|
24
26
|
isContextPane = false,
|
|
27
|
+
config,
|
|
25
28
|
}: AddPanePanelProps) => {
|
|
26
29
|
const [reset, setReset] = useState(false);
|
|
27
30
|
const lookup = first ? `${nodeId}-0` : nodeId;
|
|
@@ -62,6 +65,7 @@ const AddPanePanel = ({
|
|
|
62
65
|
ctx={nodesCtx}
|
|
63
66
|
isStoryFragment={isStoryFragment}
|
|
64
67
|
isContextPane={isContextPane}
|
|
68
|
+
config={config!}
|
|
65
69
|
/>
|
|
66
70
|
) : mode === PaneAddMode.BREAK && !isContextPane ? (
|
|
67
71
|
<AddPaneBreakPanel
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
import { templateCategories } from '@/utils/compositor/templateMarkdownStyles';
|
|
27
27
|
import { AiPaneGenerator } from './AiPaneGenerator';
|
|
28
28
|
import { AddPaneNewCustomCopy } from './AddPanePanel_newCustomCopy';
|
|
29
|
-
import { themes, type Theme } from '@/types/tractstack';
|
|
29
|
+
import { themes, type Theme, type BrandConfig } from '@/types/tractstack';
|
|
30
30
|
import { PaneAddMode, type TemplatePane } from '@/types/compositorTypes';
|
|
31
31
|
import { useStore } from '@nanostores/react';
|
|
32
32
|
|
|
@@ -37,6 +37,7 @@ interface AddPaneNewPanelProps {
|
|
|
37
37
|
ctx?: NodesContext;
|
|
38
38
|
isStoryFragment?: boolean;
|
|
39
39
|
isContextPane?: boolean;
|
|
40
|
+
config?: BrandConfig;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
interface PreviewPane {
|
|
@@ -65,6 +66,7 @@ const AddPaneNewPanel = ({
|
|
|
65
66
|
ctx,
|
|
66
67
|
isStoryFragment = false,
|
|
67
68
|
isContextPane = false,
|
|
69
|
+
config,
|
|
68
70
|
}: AddPaneNewPanelProps) => {
|
|
69
71
|
const brand = useStore(brandColourStore);
|
|
70
72
|
const hasAssemblyAI = useStore(hasAssemblyAIStore);
|
|
@@ -384,6 +386,7 @@ const AddPaneNewPanel = ({
|
|
|
384
386
|
ownerId={nodeId}
|
|
385
387
|
onComplete={handleApplyGeneratedPane}
|
|
386
388
|
onCancel={() => setMode('template')}
|
|
389
|
+
config={config!}
|
|
387
390
|
/>
|
|
388
391
|
</div>
|
|
389
392
|
)}
|
|
@@ -2,15 +2,19 @@ import { useState, useCallback } from 'react';
|
|
|
2
2
|
import { AiPanePreview } from './AiPanePreview';
|
|
3
3
|
import type { TemplatePane } from '@/types/compositorTypes';
|
|
4
4
|
import prompts from '@/constants/prompts.json';
|
|
5
|
-
import
|
|
5
|
+
import ColorPickerCombo from '@/components/fields/ColorPickerCombo';
|
|
6
|
+
import { parseAiPane } from '@/utils/compositor/aiPaneParser';
|
|
7
|
+
import { classNames } from '@/utils/helpers';
|
|
8
|
+
import type { BrandConfig } from '@/types/tractstack';
|
|
6
9
|
|
|
7
10
|
interface AiPaneGeneratorProps {
|
|
8
11
|
ownerId: string;
|
|
9
12
|
onComplete: (pane: TemplatePane) => void;
|
|
10
13
|
onCancel: () => void;
|
|
14
|
+
config?: BrandConfig;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
type GenerationStep = '
|
|
17
|
+
type GenerationStep = 'input' | 'preview' | 'loading';
|
|
14
18
|
type CopyMode = 'prompt' | 'raw';
|
|
15
19
|
|
|
16
20
|
interface GenerationResponse {
|
|
@@ -21,25 +25,41 @@ interface GenerationResponse {
|
|
|
21
25
|
error?: string;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
const
|
|
28
|
+
const harmonyOptions = [
|
|
29
|
+
'Analogous',
|
|
30
|
+
'Monochromatic',
|
|
31
|
+
'Complementary',
|
|
32
|
+
'Triadic',
|
|
33
|
+
];
|
|
34
|
+
const themeOptions = ['Light', 'Dark', 'Bright', 'Muted', 'Pastel', 'Earthy'];
|
|
25
35
|
|
|
26
36
|
export function AiPaneGenerator({
|
|
27
37
|
ownerId,
|
|
28
38
|
onComplete,
|
|
29
39
|
onCancel,
|
|
40
|
+
config,
|
|
30
41
|
}: AiPaneGeneratorProps) {
|
|
31
|
-
const [currentStep, setCurrentStep] = useState<GenerationStep>('
|
|
32
|
-
const [selectedLayout
|
|
33
|
-
layoutOptions[0]
|
|
34
|
-
);
|
|
42
|
+
const [currentStep, setCurrentStep] = useState<GenerationStep>('input');
|
|
43
|
+
const [selectedLayout] = useState<string>('Text Only');
|
|
35
44
|
const [copyMode, setCopyMode] = useState<CopyMode>('prompt');
|
|
36
45
|
const [copyPrompt, setCopyPrompt] = useState('');
|
|
37
46
|
const [rawCopy, setRawCopy] = useState('');
|
|
38
|
-
const [designPrompt, setDesignPrompt] = useState('');
|
|
39
47
|
const [generatedShell, setGeneratedShell] = useState<string | null>(null);
|
|
40
48
|
const [generatedCopy, setGeneratedCopy] = useState<string | null>(null);
|
|
41
49
|
const [error, setError] = useState<string | null>(null);
|
|
42
50
|
|
|
51
|
+
const [selectedHarmony, setSelectedHarmony] = useState<string>(
|
|
52
|
+
harmonyOptions[0]
|
|
53
|
+
);
|
|
54
|
+
const [baseColor, setBaseColor] = useState<string>('');
|
|
55
|
+
const [accentColor, setAccentColor] = useState<string>('');
|
|
56
|
+
const [selectedTheme, setSelectedTheme] = useState<string>(themeOptions[0]);
|
|
57
|
+
const [additionalNotes, setAdditionalNotes] = useState<string>('');
|
|
58
|
+
|
|
59
|
+
const [isInjectMode, setIsInjectMode] = useState(false);
|
|
60
|
+
const [injectShell, setInjectShell] = useState('');
|
|
61
|
+
const [injectCopy, setInjectCopy] = useState('');
|
|
62
|
+
|
|
43
63
|
const callAskLemurAPI = useCallback(
|
|
44
64
|
async (
|
|
45
65
|
prompt: string,
|
|
@@ -93,12 +113,10 @@ export function AiPaneGenerator({
|
|
|
93
113
|
|
|
94
114
|
let rawResponseData = result.data.response;
|
|
95
115
|
|
|
96
|
-
// Handle case where API returns JSON object for shell
|
|
97
116
|
if (expectJson && typeof rawResponseData === 'object') {
|
|
98
|
-
return JSON.stringify(rawResponseData);
|
|
117
|
+
return JSON.stringify(rawResponseData);
|
|
99
118
|
}
|
|
100
119
|
|
|
101
|
-
// Handle case where API returns string (potentially wrapped)
|
|
102
120
|
if (typeof rawResponseData === 'string') {
|
|
103
121
|
let responseString = rawResponseData;
|
|
104
122
|
try {
|
|
@@ -116,10 +134,9 @@ export function AiPaneGenerator({
|
|
|
116
134
|
} catch (e) {
|
|
117
135
|
/* Ignore stripping errors */
|
|
118
136
|
}
|
|
119
|
-
return responseString;
|
|
137
|
+
return responseString;
|
|
120
138
|
}
|
|
121
139
|
|
|
122
|
-
// Fallback if response is neither expected string nor object
|
|
123
140
|
throw new Error('Unexpected response format received from API.');
|
|
124
141
|
},
|
|
125
142
|
[]
|
|
@@ -131,6 +148,17 @@ export function AiPaneGenerator({
|
|
|
131
148
|
setGeneratedShell(null);
|
|
132
149
|
setGeneratedCopy(null);
|
|
133
150
|
|
|
151
|
+
let designInput = `Generate a design using a **${selectedHarmony.toLowerCase()}** color scheme with a **${selectedTheme.toLowerCase()}** theme.`;
|
|
152
|
+
if (baseColor) {
|
|
153
|
+
designInput += ` Base the colors around **${baseColor}**.`;
|
|
154
|
+
}
|
|
155
|
+
if (accentColor) {
|
|
156
|
+
designInput += ` Use **${accentColor}** as an accent color.`;
|
|
157
|
+
}
|
|
158
|
+
if (additionalNotes) {
|
|
159
|
+
designInput += ` Refine the design with these additional notes: "${additionalNotes}"`;
|
|
160
|
+
}
|
|
161
|
+
|
|
134
162
|
try {
|
|
135
163
|
const shellPromptDetails = prompts.aiPaneShellPrompt;
|
|
136
164
|
const copyPromptDetails = prompts.aiPaneCopyPrompt;
|
|
@@ -142,11 +170,8 @@ export function AiPaneGenerator({
|
|
|
142
170
|
throw new Error('AI prompts not found or incomplete in prompts.json');
|
|
143
171
|
}
|
|
144
172
|
|
|
145
|
-
// --- This is the updated (sequential) logic ---
|
|
146
|
-
|
|
147
|
-
// 1. Prepare and call the Shell API first
|
|
148
173
|
const formattedShellPrompt = shellPromptDetails.user_template
|
|
149
|
-
.replace('{{DESIGN_INPUT}}',
|
|
174
|
+
.replace('{{DESIGN_INPUT}}', designInput)
|
|
150
175
|
.replace('{{LAYOUT_TYPE}}', selectedLayout);
|
|
151
176
|
|
|
152
177
|
const shellResult = await callAskLemurAPI(
|
|
@@ -154,35 +179,34 @@ export function AiPaneGenerator({
|
|
|
154
179
|
shellPromptDetails.system || '',
|
|
155
180
|
true
|
|
156
181
|
);
|
|
157
|
-
setGeneratedShell(shellResult);
|
|
182
|
+
setGeneratedShell(shellResult);
|
|
158
183
|
|
|
159
|
-
// 2. NOW, create the copy prompt, injecting the shellResult
|
|
160
184
|
const copyInputContent = copyMode === 'prompt' ? copyPrompt : rawCopy;
|
|
161
|
-
// Note: Assumes prompts.json's aiPaneCopyPrompt.user_template now includes {{SHELL_JSON}}
|
|
162
185
|
const formattedCopyPrompt = copyPromptDetails.user_template
|
|
163
186
|
.replace('{{COPY_INPUT}}', copyInputContent)
|
|
164
|
-
.replace('{{DESIGN_INPUT}}',
|
|
187
|
+
.replace('{{DESIGN_INPUT}}', designInput)
|
|
165
188
|
.replace('{{LAYOUT_TYPE}}', selectedLayout)
|
|
166
|
-
.replace('{{SHELL_JSON}}', shellResult);
|
|
189
|
+
.replace('{{SHELL_JSON}}', shellResult);
|
|
167
190
|
|
|
168
|
-
// 3. Call Copy API second, using the fully-formed prompt
|
|
169
191
|
const copyResult = await callAskLemurAPI(
|
|
170
192
|
formattedCopyPrompt,
|
|
171
193
|
copyPromptDetails.system || '',
|
|
172
194
|
false
|
|
173
195
|
);
|
|
174
|
-
setGeneratedCopy(copyResult);
|
|
196
|
+
setGeneratedCopy(copyResult);
|
|
175
197
|
|
|
176
198
|
setCurrentStep('preview');
|
|
177
|
-
|
|
178
|
-
// --- End of updated logic ---
|
|
179
199
|
} catch (err: any) {
|
|
180
200
|
console.error('AI Pane Generation Error:', err);
|
|
181
201
|
setError(err.message || 'Failed to generate AI pane.');
|
|
182
202
|
setCurrentStep('input');
|
|
183
203
|
}
|
|
184
204
|
}, [
|
|
185
|
-
|
|
205
|
+
selectedHarmony,
|
|
206
|
+
baseColor,
|
|
207
|
+
accentColor,
|
|
208
|
+
selectedTheme,
|
|
209
|
+
additionalNotes,
|
|
186
210
|
selectedLayout,
|
|
187
211
|
copyMode,
|
|
188
212
|
copyPrompt,
|
|
@@ -190,12 +214,51 @@ export function AiPaneGenerator({
|
|
|
190
214
|
callAskLemurAPI,
|
|
191
215
|
]);
|
|
192
216
|
|
|
217
|
+
const handleInject = useCallback(() => {
|
|
218
|
+
setError(null);
|
|
219
|
+
if (!injectShell || !injectCopy) {
|
|
220
|
+
setError('Both Shell JSON and Copy HTML must be provided.');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
const shellResponse = JSON.parse(injectShell);
|
|
225
|
+
const copyResponse = JSON.parse(injectCopy);
|
|
226
|
+
|
|
227
|
+
const shellPayloadString = JSON.stringify(shellResponse?.data?.response);
|
|
228
|
+
const copyPayloadString = copyResponse?.data?.response;
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
!shellPayloadString ||
|
|
232
|
+
shellPayloadString === 'null' ||
|
|
233
|
+
typeof copyPayloadString !== 'string'
|
|
234
|
+
) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
'Payloads are in an unexpected format. Could not find "data.response".'
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const pane = parseAiPane(
|
|
241
|
+
shellPayloadString,
|
|
242
|
+
copyPayloadString,
|
|
243
|
+
selectedLayout
|
|
244
|
+
);
|
|
245
|
+
onComplete(pane);
|
|
246
|
+
} catch (err: any) {
|
|
247
|
+
console.error('Payload Injection Error:', err);
|
|
248
|
+
setError(err.message || 'Failed to parse payloads. Check JSON format.');
|
|
249
|
+
}
|
|
250
|
+
}, [injectShell, injectCopy, selectedLayout, onComplete]);
|
|
251
|
+
|
|
193
252
|
const handleBack = () => {
|
|
194
253
|
setError(null);
|
|
195
254
|
if (currentStep === 'preview') {
|
|
196
255
|
setCurrentStep('input');
|
|
197
256
|
} else if (currentStep === 'input') {
|
|
198
|
-
|
|
257
|
+
if (isInjectMode) {
|
|
258
|
+
setIsInjectMode(false);
|
|
259
|
+
} else {
|
|
260
|
+
onCancel();
|
|
261
|
+
}
|
|
199
262
|
} else if (currentStep === 'loading') {
|
|
200
263
|
setCurrentStep('input');
|
|
201
264
|
}
|
|
@@ -230,72 +293,166 @@ export function AiPaneGenerator({
|
|
|
230
293
|
);
|
|
231
294
|
}
|
|
232
295
|
|
|
233
|
-
if (currentStep === '
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
296
|
+
if (currentStep === 'input') {
|
|
297
|
+
if (isInjectMode) {
|
|
298
|
+
return (
|
|
299
|
+
<div className="space-y-6 p-4">
|
|
300
|
+
<div>
|
|
301
|
+
<label
|
|
302
|
+
htmlFor="shell-json"
|
|
303
|
+
className="block text-lg font-semibold text-gray-800"
|
|
304
|
+
>
|
|
305
|
+
Shell JSON Payload
|
|
306
|
+
</label>
|
|
307
|
+
<textarea
|
|
308
|
+
id="shell-json"
|
|
309
|
+
value={injectShell}
|
|
310
|
+
onChange={(e) => setInjectShell(e.target.value)}
|
|
311
|
+
placeholder="Paste raw API response for ShellJson here..."
|
|
312
|
+
rows={8}
|
|
313
|
+
className="mt-2 block w-full rounded-md border-gray-300 p-2 font-mono text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
|
|
314
|
+
/>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div>
|
|
318
|
+
<label
|
|
319
|
+
htmlFor="copy-html"
|
|
320
|
+
className="block text-lg font-semibold text-gray-800"
|
|
321
|
+
>
|
|
322
|
+
Copy HTML Payload
|
|
323
|
+
</label>
|
|
324
|
+
<textarea
|
|
325
|
+
id="copy-html"
|
|
326
|
+
value={injectCopy}
|
|
327
|
+
onChange={(e) => setInjectCopy(e.target.value)}
|
|
328
|
+
placeholder="Paste raw API response for copyHtml here..."
|
|
329
|
+
rows={8}
|
|
330
|
+
className="mt-2 block w-full rounded-md border-gray-300 p-2 font-mono text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
|
|
331
|
+
/>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
335
|
+
|
|
336
|
+
<div className="flex justify-between pt-4">
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
onClick={handleBack}
|
|
340
|
+
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2"
|
|
341
|
+
>
|
|
342
|
+
Back to Generator
|
|
343
|
+
</button>
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
onClick={handleInject}
|
|
347
|
+
disabled={!injectShell || !injectCopy}
|
|
348
|
+
className={`rounded-md border border-transparent px-4 py-2 text-sm font-bold text-white shadow-sm ${
|
|
349
|
+
!injectShell || !injectCopy
|
|
350
|
+
? 'cursor-not-allowed bg-gray-400'
|
|
351
|
+
: 'bg-cyan-600 hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2'
|
|
352
|
+
}`}
|
|
353
|
+
>
|
|
354
|
+
Create from Payloads
|
|
355
|
+
</button>
|
|
356
|
+
</div>
|
|
275
357
|
</div>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
358
|
+
);
|
|
359
|
+
}
|
|
279
360
|
|
|
280
|
-
if (currentStep === 'input') {
|
|
281
361
|
return (
|
|
282
362
|
<div className="space-y-6 p-4">
|
|
363
|
+
<div>
|
|
364
|
+
<label className="block text-lg font-semibold text-gray-800">
|
|
365
|
+
Color Harmony
|
|
366
|
+
</label>
|
|
367
|
+
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-2">
|
|
368
|
+
{harmonyOptions.map((option) => (
|
|
369
|
+
<div key={option} className="flex items-center space-x-2">
|
|
370
|
+
<input
|
|
371
|
+
type="radio"
|
|
372
|
+
id={`harmony-${option}`}
|
|
373
|
+
name="harmonyOptions"
|
|
374
|
+
value={option}
|
|
375
|
+
checked={selectedHarmony === option}
|
|
376
|
+
onChange={(e) => setSelectedHarmony(e.target.value)}
|
|
377
|
+
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
378
|
+
/>
|
|
379
|
+
<label
|
|
380
|
+
htmlFor={`harmony-${option}`}
|
|
381
|
+
className="text-sm font-medium text-gray-700"
|
|
382
|
+
>
|
|
383
|
+
{option}
|
|
384
|
+
</label>
|
|
385
|
+
</div>
|
|
386
|
+
))}
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
391
|
+
<div>
|
|
392
|
+
<ColorPickerCombo
|
|
393
|
+
title="Base Color (Optional)"
|
|
394
|
+
config={config!}
|
|
395
|
+
defaultColor={baseColor}
|
|
396
|
+
onColorChange={setBaseColor}
|
|
397
|
+
allowNull={true}
|
|
398
|
+
/>
|
|
399
|
+
</div>
|
|
400
|
+
<div>
|
|
401
|
+
<ColorPickerCombo
|
|
402
|
+
title="Accent Color (Optional)"
|
|
403
|
+
config={config!}
|
|
404
|
+
defaultColor={accentColor}
|
|
405
|
+
onColorChange={setAccentColor}
|
|
406
|
+
allowNull={true}
|
|
407
|
+
/>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
<div>
|
|
412
|
+
<label className="block text-lg font-semibold text-gray-800">
|
|
413
|
+
Theme / Mood
|
|
414
|
+
</label>
|
|
415
|
+
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-2">
|
|
416
|
+
{themeOptions.map((option) => (
|
|
417
|
+
<div key={option} className="flex items-center space-x-2">
|
|
418
|
+
<input
|
|
419
|
+
type="radio"
|
|
420
|
+
id={`theme-${option}`}
|
|
421
|
+
name="themeOptions"
|
|
422
|
+
value={option}
|
|
423
|
+
checked={selectedTheme === option}
|
|
424
|
+
onChange={(e) => setSelectedTheme(e.target.value)}
|
|
425
|
+
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
426
|
+
/>
|
|
427
|
+
<label
|
|
428
|
+
htmlFor={`theme-${option}`}
|
|
429
|
+
className="text-sm font-medium text-gray-700"
|
|
430
|
+
>
|
|
431
|
+
{option}
|
|
432
|
+
</label>
|
|
433
|
+
</div>
|
|
434
|
+
))}
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
283
438
|
<div>
|
|
284
439
|
<label
|
|
285
|
-
htmlFor="
|
|
440
|
+
htmlFor="additional-notes"
|
|
286
441
|
className="block text-lg font-semibold text-gray-800"
|
|
287
442
|
>
|
|
288
|
-
|
|
443
|
+
Additional Design Notes (Optional)
|
|
289
444
|
</label>
|
|
290
445
|
<p className="mb-2 mt-1 text-sm text-gray-500">
|
|
291
|
-
|
|
446
|
+
Add specific requests like "use rounded corners", "add subtle
|
|
447
|
+
texture".
|
|
292
448
|
</p>
|
|
293
|
-
<
|
|
294
|
-
id="
|
|
295
|
-
value={
|
|
296
|
-
onChange={
|
|
297
|
-
placeholder="Enter
|
|
298
|
-
|
|
449
|
+
<textarea
|
|
450
|
+
id="additional-notes"
|
|
451
|
+
value={additionalNotes}
|
|
452
|
+
onChange={(e) => setAdditionalNotes(e.target.value)}
|
|
453
|
+
placeholder="Enter additional notes..."
|
|
454
|
+
rows={3}
|
|
455
|
+
className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
|
|
299
456
|
/>
|
|
300
457
|
</div>
|
|
301
458
|
|
|
@@ -380,21 +537,34 @@ export function AiPaneGenerator({
|
|
|
380
537
|
onClick={handleBack}
|
|
381
538
|
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2"
|
|
382
539
|
>
|
|
383
|
-
|
|
540
|
+
Cancel
|
|
384
541
|
</button>
|
|
385
542
|
<button
|
|
386
543
|
type="button"
|
|
387
544
|
onClick={handleGenerate}
|
|
388
|
-
disabled={
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
? 'cursor-not-allowed bg-gray-
|
|
394
|
-
: 'bg-cyan-600 hover:bg-cyan-700
|
|
395
|
-
}
|
|
545
|
+
disabled={copyMode === 'prompt' ? !copyPrompt : !rawCopy}
|
|
546
|
+
className={classNames(
|
|
547
|
+
`rounded-md border border-transparent px-4 py-2 text-sm font-bold shadow-sm transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2`,
|
|
548
|
+
(copyMode === 'prompt' && !copyPrompt) ||
|
|
549
|
+
(copyMode === `raw` && !rawCopy)
|
|
550
|
+
? 'cursor-not-allowed bg-gray-300 text-gray-500'
|
|
551
|
+
: 'bg-cyan-600 text-white hover:bg-cyan-700'
|
|
552
|
+
)}
|
|
553
|
+
>
|
|
554
|
+
Generate Pane
|
|
555
|
+
</button>
|
|
556
|
+
</div>
|
|
557
|
+
|
|
558
|
+
<div className="border-t border-gray-200 pt-4 text-center">
|
|
559
|
+
<button
|
|
560
|
+
type="button"
|
|
561
|
+
onClick={() => {
|
|
562
|
+
setError(null);
|
|
563
|
+
setIsInjectMode(true);
|
|
564
|
+
}}
|
|
565
|
+
className="text-sm text-cyan-600 hover:text-cyan-800 hover:underline"
|
|
396
566
|
>
|
|
397
|
-
|
|
567
|
+
Direct Inject Payload
|
|
398
568
|
</button>
|
|
399
569
|
</div>
|
|
400
570
|
</div>
|
|
@@ -1,23 +1,6 @@
|
|
|
1
|
-
import { useState, useEffect,
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
2
2
|
import type { TemplatePane } from '@/types/compositorTypes';
|
|
3
3
|
import { parseAiPane } from '@/utils/compositor/aiPaneParser';
|
|
4
|
-
import {
|
|
5
|
-
PaneSnapshotGenerator,
|
|
6
|
-
type SnapshotData,
|
|
7
|
-
} from '@/components/compositor/preview/PaneSnapshotGenerator';
|
|
8
|
-
import { ulid } from 'ulid';
|
|
9
|
-
|
|
10
|
-
type LLMShellLayer = {
|
|
11
|
-
mobile?: Record<string, string>;
|
|
12
|
-
tablet?: Record<string, string>;
|
|
13
|
-
desktop?: Record<string, string>;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
type ShellJson = {
|
|
17
|
-
bgColour: string;
|
|
18
|
-
parentClasses: LLMShellLayer[];
|
|
19
|
-
defaultClasses: Record<string, any>;
|
|
20
|
-
};
|
|
21
4
|
|
|
22
5
|
interface AiPanePreviewProps {
|
|
23
6
|
shellJson: string;
|
|
@@ -28,49 +11,6 @@ interface AiPanePreviewProps {
|
|
|
28
11
|
onBack: () => void;
|
|
29
12
|
}
|
|
30
13
|
|
|
31
|
-
function convertObjectToTailwindString(
|
|
32
|
-
styleObj: Record<string, string> | undefined
|
|
33
|
-
): string {
|
|
34
|
-
if (!styleObj) return '';
|
|
35
|
-
return Object.entries(styleObj)
|
|
36
|
-
.map(([key, value]) => {
|
|
37
|
-
// Basic mapping, might need adjustment based on tailwindClasses structure if prefixes differ
|
|
38
|
-
const prefixMap: Record<string, string> = {
|
|
39
|
-
mx: 'mx',
|
|
40
|
-
my: 'my',
|
|
41
|
-
px: 'px',
|
|
42
|
-
py: 'py',
|
|
43
|
-
textALIGN: 'text',
|
|
44
|
-
textSIZE: 'text',
|
|
45
|
-
textCOLOR: 'text',
|
|
46
|
-
fontWEIGHT: 'font',
|
|
47
|
-
fontFACE: 'font',
|
|
48
|
-
letterSPACING: 'tracking',
|
|
49
|
-
lineHEIGHT: 'leading',
|
|
50
|
-
bgCOLOR: 'bg',
|
|
51
|
-
rounded: 'rounded',
|
|
52
|
-
shadow: 'shadow',
|
|
53
|
-
maxW: 'max-w',
|
|
54
|
-
// Add other mappings as needed based on keys used in compositorTypes vs tailwindClasses
|
|
55
|
-
};
|
|
56
|
-
const prefix = prefixMap[key] || key.toLowerCase();
|
|
57
|
-
if (value === '') return key; // Handle boolean classes like 'relative', 'flex'
|
|
58
|
-
return `${prefix}-${value}`;
|
|
59
|
-
})
|
|
60
|
-
.join(' ');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function getPreviewClasses(classes: LLMShellLayer | undefined): string {
|
|
64
|
-
if (!classes) return '';
|
|
65
|
-
|
|
66
|
-
const mobileStyles = convertObjectToTailwindString(classes.mobile);
|
|
67
|
-
const tabletStyles = convertObjectToTailwindString(classes.tablet);
|
|
68
|
-
const desktopStyles = convertObjectToTailwindString(classes.desktop);
|
|
69
|
-
|
|
70
|
-
const combined = `${mobileStyles} ${tabletStyles ? `md:${tabletStyles.split(' ').join(' md:')}` : ''} ${desktopStyles ? `xl:${desktopStyles.split(' ').join(' xl:')}` : ''}`;
|
|
71
|
-
return combined.replace(/\s+/g, ' ').trim();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
14
|
export function AiPanePreview({
|
|
75
15
|
shellJson,
|
|
76
16
|
copyHtml,
|
|
@@ -78,152 +18,82 @@ export function AiPanePreview({
|
|
|
78
18
|
onComplete,
|
|
79
19
|
onBack,
|
|
80
20
|
}: AiPanePreviewProps) {
|
|
81
|
-
const [parsedPaneForApply, setParsedPaneForApply] =
|
|
82
|
-
useState<TemplatePane | null>(null);
|
|
83
21
|
const [error, setError] = useState<string | null>(null);
|
|
84
|
-
const [
|
|
85
|
-
const [
|
|
86
|
-
useState<boolean>(false);
|
|
87
|
-
const previewId = useMemo(() => `ai-preview-${ulid()}`, []);
|
|
22
|
+
const [hasCompleted, setHasCompleted] = useState<boolean>(false);
|
|
23
|
+
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
88
24
|
|
|
89
25
|
useEffect(() => {
|
|
90
|
-
let isActive = true;
|
|
91
26
|
setError(null);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const pane = parseAiPane(shellJson, copyHtml, layout);
|
|
97
|
-
if (isActive) {
|
|
98
|
-
setParsedPaneForApply(pane);
|
|
99
|
-
}
|
|
100
|
-
} catch (err: any) {
|
|
101
|
-
console.error('Error parsing AI Pane for apply:', err);
|
|
102
|
-
if (isActive) {
|
|
103
|
-
setError(
|
|
104
|
-
err.message || 'Failed to parse generated content for application.'
|
|
105
|
-
);
|
|
106
|
-
setParsedPaneForApply(null);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return () => {
|
|
110
|
-
isActive = false;
|
|
111
|
-
};
|
|
112
|
-
}, [shellJson, copyHtml, layout]);
|
|
113
|
-
|
|
114
|
-
const previewHtmlString = useMemo(() => {
|
|
115
|
-
try {
|
|
116
|
-
if (!shellJson || !copyHtml) return '';
|
|
117
|
-
const shell: ShellJson = JSON.parse(shellJson);
|
|
118
|
-
|
|
119
|
-
let currentHtml = copyHtml;
|
|
120
|
-
if (shell.parentClasses && shell.parentClasses.length > 0) {
|
|
121
|
-
[...shell.parentClasses].reverse().forEach((layer) => {
|
|
122
|
-
const layerClasses = getPreviewClasses(layer);
|
|
123
|
-
currentHtml = `<div class="${layerClasses}">${currentHtml}</div>`;
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const outerStyle = shell.bgColour
|
|
128
|
-
? `background-color: ${shell.bgColour};`
|
|
129
|
-
: '';
|
|
130
|
-
// Wrap in a div that sets width similar to snapshot generator default for better preview consistency
|
|
131
|
-
return `<div style="${outerStyle} width: 800px; padding: 1px; margin: auto;">${currentHtml}</div>`;
|
|
132
|
-
} catch (err: any) {
|
|
133
|
-
console.error('Error constructing preview HTML string:', err);
|
|
134
|
-
setError(err.message || 'Failed to construct preview HTML.');
|
|
135
|
-
return '';
|
|
136
|
-
}
|
|
137
|
-
}, [shellJson, copyHtml]);
|
|
138
|
-
|
|
139
|
-
const handleSnapshotComplete = useCallback(
|
|
140
|
-
(id: string, data: SnapshotData) => {
|
|
141
|
-
if (id === previewId) {
|
|
142
|
-
setSnapshotData(data);
|
|
143
|
-
setIsGeneratingSnapshot(false);
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
[previewId]
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
const handleSnapshotError = useCallback(
|
|
150
|
-
(id: string, errorMsg: string) => {
|
|
151
|
-
if (id === previewId) {
|
|
152
|
-
console.error(`Snapshot generation failed for ${id}:`, errorMsg);
|
|
153
|
-
setError(`Snapshot generation failed: ${errorMsg}`);
|
|
154
|
-
setIsGeneratingSnapshot(false);
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
[previewId]
|
|
158
|
-
);
|
|
27
|
+
setHasCompleted(false);
|
|
28
|
+
setIsLoading(true);
|
|
29
|
+
let isActive = true;
|
|
159
30
|
|
|
160
|
-
|
|
161
|
-
if (parsedPaneForApply) {
|
|
162
|
-
onComplete(parsedPaneForApply);
|
|
163
|
-
console.log('FINAL TEMPLATE PANE PAYLOAD:', parsedPaneForApply);
|
|
164
|
-
} else if (!error) {
|
|
165
|
-
// Attempt parsing again if it failed silently initially
|
|
31
|
+
if (shellJson && copyHtml) {
|
|
166
32
|
try {
|
|
167
33
|
const pane = parseAiPane(shellJson, copyHtml, layout);
|
|
168
|
-
|
|
34
|
+
if (isActive && !hasCompleted) {
|
|
35
|
+
onComplete(pane);
|
|
36
|
+
setHasCompleted(true);
|
|
37
|
+
setIsLoading(false);
|
|
38
|
+
}
|
|
169
39
|
} catch (err: any) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
40
|
+
console.error('Error parsing AI Pane:', err);
|
|
41
|
+
if (isActive) {
|
|
42
|
+
setError(err.message || 'Failed to parse generated content.');
|
|
43
|
+
setIsLoading(false);
|
|
44
|
+
}
|
|
173
45
|
}
|
|
46
|
+
} else {
|
|
47
|
+
// Handle case where inputs might be initially empty
|
|
48
|
+
setIsLoading(false);
|
|
174
49
|
}
|
|
175
|
-
};
|
|
176
50
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
51
|
+
return () => {
|
|
52
|
+
isActive = false;
|
|
53
|
+
};
|
|
54
|
+
}, [shellJson, copyHtml, layout, onComplete, hasCompleted]);
|
|
55
|
+
|
|
56
|
+
const displayContent = useMemo(() => {
|
|
57
|
+
if (isLoading) {
|
|
58
|
+
return (
|
|
59
|
+
<div className="p-4 text-center text-gray-500">
|
|
60
|
+
<div className="mx-auto mb-2 h-8 w-8 animate-spin rounded-full border-b-2 border-gray-400"></div>
|
|
61
|
+
<p className="text-sm">Processing...</p>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
180
64
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
65
|
+
if (error) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="p-4 text-center text-red-600">
|
|
68
|
+
<p className="font-semibold">Error:</p>
|
|
69
|
+
<p className="mt-1 text-sm">{error}</p>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (hasCompleted) {
|
|
74
|
+
return (
|
|
75
|
+
<div className="p-4 text-center text-green-700">
|
|
76
|
+
<p className="font-semibold">Pane Applied Successfully!</p>
|
|
77
|
+
<p className="mt-1 text-sm">
|
|
78
|
+
You can now go back or continue editing.
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
// Fallback/initial state before useEffect runs if needed
|
|
84
|
+
return (
|
|
85
|
+
<div className="p-4 text-center text-gray-500">
|
|
86
|
+
<p className="text-sm">Preparing...</p>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}, [isLoading, error, hasCompleted]);
|
|
188
90
|
|
|
189
91
|
return (
|
|
190
92
|
<div className="flex h-full flex-col p-4">
|
|
191
93
|
<div className="relative mb-4 flex min-h-[200px] flex-grow items-center justify-center overflow-auto rounded border bg-gray-50">
|
|
192
|
-
{
|
|
193
|
-
<div className="p-4 text-center text-red-600">
|
|
194
|
-
<p className="font-semibold">Error:</p>
|
|
195
|
-
<p className="mt-1 text-sm">{error}</p>
|
|
196
|
-
</div>
|
|
197
|
-
)}
|
|
198
|
-
{showLoading && !showError && (
|
|
199
|
-
<div className="p-4 text-center text-gray-500">
|
|
200
|
-
<div className="mx-auto mb-2 h-8 w-8 animate-spin rounded-full border-b-2 border-gray-400"></div>
|
|
201
|
-
<p className="text-sm">
|
|
202
|
-
{isGeneratingSnapshot
|
|
203
|
-
? 'Generating Snapshot...'
|
|
204
|
-
: 'Constructing Preview...'}
|
|
205
|
-
</p>
|
|
206
|
-
</div>
|
|
207
|
-
)}
|
|
208
|
-
{isGeneratingSnapshot && previewHtmlString && (
|
|
209
|
-
<div className="pointer-events-none absolute left-[-9999px] top-[-9999px] w-[800px] opacity-0">
|
|
210
|
-
<PaneSnapshotGenerator
|
|
211
|
-
id={previewId}
|
|
212
|
-
htmlString={previewHtmlString}
|
|
213
|
-
onComplete={handleSnapshotComplete}
|
|
214
|
-
onError={handleSnapshotError}
|
|
215
|
-
/>
|
|
216
|
-
</div>
|
|
217
|
-
)}
|
|
218
|
-
{showPreview && snapshotData && (
|
|
219
|
-
<img
|
|
220
|
-
src={snapshotData.imageData}
|
|
221
|
-
alt="AI Pane Preview"
|
|
222
|
-
className="block h-auto max-w-full"
|
|
223
|
-
/>
|
|
224
|
-
)}
|
|
94
|
+
{displayContent}
|
|
225
95
|
</div>
|
|
226
|
-
<div className="flex flex-shrink-0 justify-
|
|
96
|
+
<div className="flex flex-shrink-0 justify-start">
|
|
227
97
|
<button
|
|
228
98
|
onClick={onBack}
|
|
229
99
|
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
|
@@ -231,26 +101,6 @@ export function AiPanePreview({
|
|
|
231
101
|
>
|
|
232
102
|
Back
|
|
233
103
|
</button>
|
|
234
|
-
<button
|
|
235
|
-
onClick={handleApply}
|
|
236
|
-
disabled={
|
|
237
|
-
!parsedPaneForApply ||
|
|
238
|
-
!!error ||
|
|
239
|
-
!snapshotData ||
|
|
240
|
-
isGeneratingSnapshot
|
|
241
|
-
}
|
|
242
|
-
className={`rounded-md border border-transparent px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors duration-150 ${
|
|
243
|
-
!parsedPaneForApply ||
|
|
244
|
-
!!error ||
|
|
245
|
-
!snapshotData ||
|
|
246
|
-
isGeneratingSnapshot
|
|
247
|
-
? 'cursor-not-allowed bg-gray-400'
|
|
248
|
-
: 'bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2'
|
|
249
|
-
}`}
|
|
250
|
-
type="button"
|
|
251
|
-
>
|
|
252
|
-
Apply Pane
|
|
253
|
-
</button>
|
|
254
104
|
</div>
|
|
255
105
|
</div>
|
|
256
106
|
);
|
|
@@ -12,11 +12,13 @@ import {
|
|
|
12
12
|
/* hasAssemblyAIStore,*/ fullContentMapStore,
|
|
13
13
|
} from '@/stores/storykeep';
|
|
14
14
|
import type { NodesContext } from '@/stores/nodes';
|
|
15
|
+
import type { BrandConfig } from '@/types/tractstack';
|
|
15
16
|
|
|
16
17
|
interface PageCreationSelectorProps {
|
|
17
18
|
nodeId: string;
|
|
18
19
|
ctx: NodesContext;
|
|
19
20
|
isTemplate?: boolean;
|
|
21
|
+
config?: BrandConfig;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
type CreationMode = {
|
|
@@ -33,6 +35,7 @@ export const PageCreationSelector = ({
|
|
|
33
35
|
nodeId,
|
|
34
36
|
ctx,
|
|
35
37
|
isTemplate = false,
|
|
38
|
+
config,
|
|
36
39
|
}: PageCreationSelectorProps) => {
|
|
37
40
|
const [selectedMode, setSelectedMode] =
|
|
38
41
|
useState<CreationMode['id']>('design');
|
|
@@ -137,6 +140,7 @@ export const PageCreationSelector = ({
|
|
|
137
140
|
first={true}
|
|
138
141
|
ctx={ctx}
|
|
139
142
|
isStoryFragment={true}
|
|
143
|
+
config={config!}
|
|
140
144
|
/>
|
|
141
145
|
);
|
|
142
146
|
else if (showGen) return <PageCreationGen nodeId={nodeId} ctx={ctx} />;
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
},
|
|
42
42
|
"aiPaneShellPrompt": {
|
|
43
43
|
"system": "You are an expert web designer. Your task is to generate the structural design for a component as a single JSON object. Respond *only* with the JSON.",
|
|
44
|
-
"user_template": "Generate the design JSON for a component with the following characteristics:\n\nDesign Style: \"{{DESIGN_INPUT}}\"\nLayout Type: \"{{LAYOUT_TYPE}}\"\n\nCRITICAL RULES:\n1. You must respond with a JSON object with keys: `bgColour`, `parentClasses`, `defaultClasses`.\n2. The `parentClasses` value *must* be structured with our internal responsive object schema (e.g., `[ { \"mobile\": { \"px\": \"4\", \"py\": \"12\" } } ]`).\n3. The `defaultClasses` value *must* be structured with responsive keys containing
|
|
44
|
+
"user_template": "Generate the design JSON for a component with the following characteristics:\n\nDesign Style: \"{{DESIGN_INPUT}}\"\nLayout Type: \"{{LAYOUT_TYPE}}\"\n\nCRITICAL RULES:\n1. You must respond with a JSON object with keys: `bgColour`, `parentClasses`, `defaultClasses`.\n2. The `parentClasses` value *must* be structured with our internal responsive object schema (e.g., `[ { \"mobile\": { \"px\": \"4\", \"py\": \"12\" } } ]`).\n3. The `defaultClasses` value *must* be structured with responsive keys (`mobile`, `tablet`, `desktop`) containing Tailwind class strings.\n4. Ensure the selected `bgColour` provides **high contrast** (meeting at least WCAG AA standards - 4.5:1 for normal text, 3:1 for large text) with the primary text colors defined in `defaultClasses`.\n\nEXAMPLE:\n{\n \"bgColour\": \"#050710\",\n \"parentClasses\": [\n { \"mobile\": { \"px\": \"6\", \"py\": \"24\" }, \"tablet\": { \"px\": \"8\", \"py\": \"32\" } },\n { \"mobile\": { \"mx\": \"auto\", \"maxW\": \"2xl\", \"textALIGN\": \"center\" }, \"tablet\": { \"maxW\": \"4xl\" } }\n ],\n \"defaultClasses\": {\n \"h2\": { \"mobile\": \"text-4xl font-bold tracking-tight text-white mt-4\", \"tablet\": \"text-6xl\", \"desktop\": \"text-7xl\" },\n \"p\": { \"mobile\": \"text-lg leading-8 text-gray-300 mt-6\", \"tablet\": \"text-xl\", \"desktop\": \"\" }\n }\n}"
|
|
45
45
|
},
|
|
46
46
|
"aiPaneCopyPrompt": {
|
|
47
|
-
"system": "You are an expert **web designer and copywriter**. Your task is to generate a single, visually compelling block of HTML content. You must ensure the content is well-written, engaging, and **
|
|
48
|
-
"user_template": "Here is the design 'shell' and 'theme' (bgColour, parentClasses, and defaultClasses) you must write your HTML for. Your HTML will be placed *inside* this shell. Use the `defaultClasses` as your base theme for styling:\n{{SHELL_JSON}}\n\nNow, generate the HTML content based on these inputs:\n\nContent Prompt: \"{{COPY_INPUT}}\"\nDesign Style: **strictly for visual reference** when choosing element styles **DO NOT** include any words or concepts from the `Design Style` input in the written copy text itself.\"{{DESIGN_INPUT}}\"\nLayout Type: \"{{LAYOUT_TYPE}}\"\n\nCRITICAL RULES:\n1. You are responsible for the **inner layout and visual rhythm**. You MUST add appropriate vertical margins (e.g., `mt-4`, `mt-6`, `mt-8`) directly to any HTML block elements that *deviate* from the default spacing. **Elements must not touch.**\n2. You **MUST NOT** use `<h1>` tags. You must use `<h2>`, `<h3>`, and `<p>` tags for all text content.\n3. For responsive styles, you *must* only use `md:` and `xl:` prefixes.\n4. To make headlines pop, you MUST wrap key words in `<span>` tags with creative classes (e.g., gradient text, different colors).\n5. **All text**, even short links or phrases (like 'Learn more →'), **must** be wrapped in a block element like `<p>` or `<button>`.\n6. You MUST include at least one `<button>` tag for the primary call-to-action.\n7. Respond *only* with the raw HTML.\n\nEXAMPLE of a good, well-spaced response:\n<h2 class=\"text-4xl font-bold tracking-tight text-white md:text-6xl\"><span class=\"bg-gradient-to-r from-purple-500 to-indigo-400 bg-clip-text text-transparent\">Own the Art.</span> Possess the Reality.</h2>\n<p class=\"mt-6 text-lg leading-8 text-gray-300 md:text-xl\">Every Sneaky Productions NFT is your key. This is where digital rarity meets tangible legacy.</p>\n<button class=\"mt-8 rounded-md bg-indigo-600 px-5 py-3 text-base font-semibold text-white shadow-sm hover:bg-indigo-500\">Secure Your Drop</button>\n<p class=\"mt-4 text-sm text-gray-400\">Learn more <span>→</span></p>"
|
|
47
|
+
"system": "You are an expert **web designer and copywriter**. Your task is to generate a single, visually compelling block of HTML content. You must ensure the content is well-written, engaging, **beautifully spaced**, and **highly readable**.",
|
|
48
|
+
"user_template": "Here is the design 'shell' and 'theme' (bgColour, parentClasses, and defaultClasses) you must write your HTML for. Your HTML will be placed *inside* this shell. Use the `defaultClasses` as your base theme for styling:\n{{SHELL_JSON}}\n\nNow, generate the HTML content based on these inputs:\n\nContent Prompt: \"{{COPY_INPUT}}\"\nDesign Style: **strictly for visual reference** when choosing element styles **DO NOT** include any words or concepts from the `Design Style` input in the written copy text itself.\"{{DESIGN_INPUT}}\"\nLayout Type: \"{{LAYOUT_TYPE}}\"\n\nCRITICAL RULES:\n1. You are responsible for the **inner layout and visual rhythm**. You MUST add appropriate vertical margins (e.g., `mt-4`, `mt-6`, `mt-8`) directly to any HTML block elements that *deviate* from the default spacing. **Elements must not touch.**\n2. You **MUST NOT** use `<h1>` tags. You must use `<h2>`, `<h3>`, and `<p>` tags for all text content.\n3. For responsive styles, you *must* only use `md:` and `xl:` prefixes.\n4. To make headlines pop, you MUST wrap key words in `<span>` tags with creative classes (e.g., gradient text, different colors).\n5. **All text**, even short links or phrases (like 'Learn more →'), **must** be wrapped in a block element like `<p>` or `<button>`.\n6. You MUST include at least one `<button>` tag for the primary call-to-action.\n7. Verify that **all text elements**, including text within `<span>` tags, `<button>` elements, and any elements using override classes, maintain **high contrast** (meeting at least WCAG AA standards - 4.5:1 for normal text, 3:1 for large text) against the `bgColour` provided in the `SHELL_JSON`. **Prioritize readability above all else**.\n8. Respond *only* with the raw HTML.\n\nEXAMPLE of a good, well-spaced response:\n<h2 class=\"text-4xl font-bold tracking-tight text-white md:text-6xl\"><span class=\"bg-gradient-to-r from-purple-500 to-indigo-400 bg-clip-text text-transparent\">Own the Art.</span> Possess the Reality.</h2>\n<p class=\"mt-6 text-lg leading-8 text-gray-300 md:text-xl\">Every Sneaky Productions NFT is your key. This is where digital rarity meets tangible legacy.</p>\n<button class=\"mt-8 rounded-md bg-indigo-600 px-5 py-3 text-base font-semibold text-white shadow-sm hover:bg-indigo-500\">Secure Your Drop</button>\n<p class=\"mt-4 text-sm text-gray-400\">Learn more <span>→</span></p>"
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -250,7 +250,12 @@ function sanitizeButtonClasses(
|
|
|
250
250
|
return buttonPayload;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
function walkDom(
|
|
253
|
+
function walkDom(
|
|
254
|
+
domNode: Node,
|
|
255
|
+
parentId: string,
|
|
256
|
+
parsedNodes: ParsedNode[],
|
|
257
|
+
markdownId: string
|
|
258
|
+
) {
|
|
254
259
|
if (domNode.nodeType === Node.TEXT_NODE) {
|
|
255
260
|
const copy = domNode.textContent || '';
|
|
256
261
|
// Preserve leading/trailing spaces unless the *entire* content is just whitespace.
|
|
@@ -305,17 +310,37 @@ function walkDom(domNode: Node, parentId: string, parsedNodes: ParsedNode[]) {
|
|
|
305
310
|
const tagName = el.tagName.toLowerCase();
|
|
306
311
|
|
|
307
312
|
if (!ALLOWED_TAGS.has(tagName)) {
|
|
308
|
-
el.childNodes.forEach((child) =>
|
|
313
|
+
el.childNodes.forEach((child) =>
|
|
314
|
+
walkDom(child, parentId, parsedNodes, markdownId)
|
|
315
|
+
);
|
|
309
316
|
return;
|
|
310
317
|
}
|
|
311
318
|
|
|
312
319
|
if (tagName === 'button') {
|
|
320
|
+
let finalParentId = parentId;
|
|
321
|
+
|
|
322
|
+
if (parentId === markdownId) {
|
|
323
|
+
const pNodeId = ulid();
|
|
324
|
+
const pNode: TemplateNode = {
|
|
325
|
+
id: pNodeId,
|
|
326
|
+
nodeType: 'TagElement',
|
|
327
|
+
parentId: parentId,
|
|
328
|
+
tagName: 'p',
|
|
329
|
+
overrideClasses: {},
|
|
330
|
+
};
|
|
331
|
+
parsedNodes.push({
|
|
332
|
+
flatNode: pNode,
|
|
333
|
+
responsiveClasses: {},
|
|
334
|
+
});
|
|
335
|
+
finalParentId = pNodeId;
|
|
336
|
+
}
|
|
337
|
+
|
|
313
338
|
const buttonPayload = sanitizeButtonClasses(el.getAttribute('class'));
|
|
314
339
|
|
|
315
340
|
const flatNode: TemplateNode = {
|
|
316
341
|
id: ulid(),
|
|
317
342
|
nodeType: 'TagElement',
|
|
318
|
-
parentId:
|
|
343
|
+
parentId: finalParentId,
|
|
319
344
|
tagName: 'a',
|
|
320
345
|
overrideClasses: {},
|
|
321
346
|
href: '#',
|
|
@@ -330,7 +355,9 @@ function walkDom(domNode: Node, parentId: string, parsedNodes: ParsedNode[]) {
|
|
|
330
355
|
responsiveClasses: {},
|
|
331
356
|
});
|
|
332
357
|
|
|
333
|
-
el.childNodes.forEach((child) =>
|
|
358
|
+
el.childNodes.forEach((child) =>
|
|
359
|
+
walkDom(child, flatNode.id, parsedNodes, markdownId)
|
|
360
|
+
);
|
|
334
361
|
return;
|
|
335
362
|
}
|
|
336
363
|
|
|
@@ -353,7 +380,9 @@ function walkDom(domNode: Node, parentId: string, parsedNodes: ParsedNode[]) {
|
|
|
353
380
|
responsiveClasses: responsive,
|
|
354
381
|
});
|
|
355
382
|
|
|
356
|
-
el.childNodes.forEach((child) =>
|
|
383
|
+
el.childNodes.forEach((child) =>
|
|
384
|
+
walkDom(child, flatNode.id, parsedNodes, markdownId)
|
|
385
|
+
);
|
|
357
386
|
}
|
|
358
387
|
|
|
359
388
|
function findMostCommonClasses(nodes: ParsedNode[]): ResponsiveClasses {
|
|
@@ -476,18 +505,15 @@ export const parseAiPane = (
|
|
|
476
505
|
const paneId = ulid();
|
|
477
506
|
const markdownId = ulid();
|
|
478
507
|
|
|
479
|
-
// --- MODIFICATION START ---
|
|
480
|
-
// Normalize the keys within parentClasses using the new helper
|
|
481
508
|
const transformedParentClasses: ParentClassesPayload = (
|
|
482
509
|
shell.parentClasses || []
|
|
483
510
|
).map(
|
|
484
511
|
(layer): ParentClassLayer => ({
|
|
485
|
-
mobile: normalizeKeys(layer.mobile),
|
|
486
|
-
tablet: normalizeKeys(layer.tablet),
|
|
487
|
-
desktop: normalizeKeys(layer.desktop),
|
|
512
|
+
mobile: normalizeKeys(layer.mobile),
|
|
513
|
+
tablet: normalizeKeys(layer.tablet),
|
|
514
|
+
desktop: normalizeKeys(layer.desktop),
|
|
488
515
|
})
|
|
489
516
|
);
|
|
490
|
-
// --- MODIFICATION END ---
|
|
491
517
|
|
|
492
518
|
const shellDefaults = parseDefaultClassesFromShell(shell.defaultClasses);
|
|
493
519
|
|
|
@@ -497,12 +523,12 @@ export const parseAiPane = (
|
|
|
497
523
|
parentId: paneId,
|
|
498
524
|
type: 'markdown',
|
|
499
525
|
markdownId: ulid(),
|
|
500
|
-
parentClasses: transformedParentClasses,
|
|
526
|
+
parentClasses: transformedParentClasses,
|
|
501
527
|
defaultClasses: shellDefaults,
|
|
502
528
|
};
|
|
503
529
|
|
|
504
530
|
const allParsedNodes: ParsedNode[] = [];
|
|
505
|
-
walkDom(doc.body, markdownId, allParsedNodes);
|
|
531
|
+
walkDom(doc.body, markdownId, allParsedNodes, markdownId);
|
|
506
532
|
|
|
507
533
|
const templateNodes: TemplateNode[] = [];
|
|
508
534
|
const nodesByTag = new Map<string, ParsedNode[]>();
|