astro-tractstack 2.0.29 → 2.0.30
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/edit/pane/AddPanePanel_new.tsx +323 -77
- package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +82 -9
- package/templates/src/components/edit/pane/steps/DirectInjectStep.tsx +41 -21
- package/templates/src/components/edit/state/SaveToLibraryModal.tsx +68 -34
- package/templates/src/constants/prompts.json +65 -36
- package/templates/src/constants.ts +3 -3
- package/templates/src/stores/nodes.ts +17 -4
- package/templates/src/types/compositorTypes.ts +3 -0
- package/templates/src/types/tractstack.ts +2 -0
- package/templates/src/utils/compositor/designLibraryHelper.ts +8 -3
- package/templates/src/utils/compositor/typeGuards.ts +0 -18
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { useState, useCallback } from 'react';
|
|
2
|
+
import { useState, useCallback, useMemo, useEffect } from 'react';
|
|
3
3
|
import DocumentPlusIcon from '@heroicons/react/24/outline/DocumentPlusIcon';
|
|
4
4
|
import SparklesIcon from '@heroicons/react/24/outline/SparklesIcon';
|
|
5
5
|
import SwatchIcon from '@heroicons/react/24/outline/SwatchIcon';
|
|
@@ -12,7 +12,7 @@ import prompts from '@/constants/prompts.json';
|
|
|
12
12
|
import type { DesignLibraryEntry } from '@/types/tractstack';
|
|
13
13
|
import { PaneAddMode, type TemplatePane } from '@/types/compositorTypes';
|
|
14
14
|
import { useStore } from '@nanostores/react';
|
|
15
|
-
import { CopyInputStep } from './steps/CopyInputStep';
|
|
15
|
+
import { CopyInputStep, type CopyMode } from './steps/CopyInputStep';
|
|
16
16
|
import { DesignLibraryStep } from './steps/DesignLibraryStep';
|
|
17
17
|
import { AiDesignStep, type AiDesignConfig } from './steps/AiDesignStep';
|
|
18
18
|
import { parseAiPane, parseAiCopyHtml } from '@/utils/compositor/aiPaneParser';
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
convertTemplateToAIShell,
|
|
22
22
|
} from '@/utils/compositor/designLibraryHelper';
|
|
23
23
|
import { DirectInjectStep } from './steps/DirectInjectStep';
|
|
24
|
+
import BooleanToggle from '@/components/form/BooleanToggle';
|
|
25
|
+
import EnumSelect from '@/components/form/EnumSelect';
|
|
24
26
|
|
|
25
27
|
type Step =
|
|
26
28
|
| 'initial'
|
|
@@ -33,7 +35,6 @@ type Step =
|
|
|
33
35
|
| 'directInject';
|
|
34
36
|
|
|
35
37
|
type InitialChoice = 'library' | 'ai' | 'blank';
|
|
36
|
-
type CopyMode = 'prompt' | 'raw';
|
|
37
38
|
type LayoutChoice = 'standard' | 'grid';
|
|
38
39
|
type ColumnPresetKey = 'left' | 'right';
|
|
39
40
|
|
|
@@ -140,12 +141,13 @@ const AddPaneNewPanel = ({
|
|
|
140
141
|
const [layoutChoice, setLayoutChoice] = useState<LayoutChoice>('standard');
|
|
141
142
|
const [error, setError] = useState<string | null>(null);
|
|
142
143
|
|
|
143
|
-
|
|
144
|
+
const [selectedPromptId, setSelectedPromptId] = useState<string>('');
|
|
145
|
+
const [isAiStyling, setIsAiStyling] = useState(false);
|
|
146
|
+
|
|
144
147
|
const [copyMode, setCopyMode] = useState<CopyMode>('prompt');
|
|
145
148
|
const [promptValue, setPromptValue] = useState('');
|
|
146
149
|
const [copyValue, setCopyValue] = useState('');
|
|
147
150
|
|
|
148
|
-
// Grid / 2-Column State (Strictly Prompt-Only)
|
|
149
151
|
const [overallPrompt, setOverallPrompt] = useState(
|
|
150
152
|
prompts.aiPaneCopyPrompt_2cols.presets.heroDefault.default
|
|
151
153
|
);
|
|
@@ -155,6 +157,8 @@ const AddPaneNewPanel = ({
|
|
|
155
157
|
const [promptValueCol2, setPromptValueCol2] = useState(
|
|
156
158
|
prompts.aiPaneCopyPrompt_2cols.presets.heroDefault.right.prompt
|
|
157
159
|
);
|
|
160
|
+
const [col1Copy, setCol1Copy] = useState('');
|
|
161
|
+
const [col2Copy, setCol2Copy] = useState('');
|
|
158
162
|
|
|
159
163
|
const [selectedLibraryEntry, setSelectedLibraryEntry] =
|
|
160
164
|
useState<DesignLibraryEntry | null>(null);
|
|
@@ -166,6 +170,52 @@ const AddPaneNewPanel = ({
|
|
|
166
170
|
additionalNotes: '',
|
|
167
171
|
});
|
|
168
172
|
|
|
173
|
+
const promptOptions = useMemo(() => {
|
|
174
|
+
return prompts.aiPromptsIndex
|
|
175
|
+
.filter((p) => p.layout === layoutChoice)
|
|
176
|
+
.map((p) => ({ label: p.label, value: p.id }));
|
|
177
|
+
}, [layoutChoice]);
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (promptOptions.length > 0) {
|
|
181
|
+
const currentValid = promptOptions.find(
|
|
182
|
+
(p) => p.value === selectedPromptId
|
|
183
|
+
);
|
|
184
|
+
if (!currentValid) {
|
|
185
|
+
setSelectedPromptId(promptOptions[0].value);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}, [promptOptions, selectedPromptId]);
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (!selectedPromptId) return;
|
|
192
|
+
|
|
193
|
+
const activeConfig = prompts.aiPromptsIndex.find(
|
|
194
|
+
(p) => p.id === selectedPromptId
|
|
195
|
+
);
|
|
196
|
+
if (!activeConfig) return;
|
|
197
|
+
|
|
198
|
+
const promptKey = activeConfig.prompts.copy;
|
|
199
|
+
const copyPromptGroup = (prompts as any)[promptKey];
|
|
200
|
+
if (!copyPromptGroup) return;
|
|
201
|
+
|
|
202
|
+
const variant = activeConfig.variants
|
|
203
|
+
? activeConfig.variants[0]
|
|
204
|
+
: 'default';
|
|
205
|
+
|
|
206
|
+
if (layoutChoice === 'standard') {
|
|
207
|
+
const newText = copyPromptGroup[variant] || '';
|
|
208
|
+
setPromptValue(newText);
|
|
209
|
+
} else if (layoutChoice === 'grid') {
|
|
210
|
+
const preset = copyPromptGroup.presets?.[variant];
|
|
211
|
+
if (preset) {
|
|
212
|
+
setOverallPrompt(preset.default || '');
|
|
213
|
+
setPromptValueCol1(preset.left?.prompt || '');
|
|
214
|
+
setPromptValueCol2(preset.right?.prompt || '');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}, [selectedPromptId, layoutChoice]);
|
|
218
|
+
|
|
169
219
|
const handleInitialChoice = (choice: InitialChoice) => {
|
|
170
220
|
setInitialChoice(choice);
|
|
171
221
|
setError(null);
|
|
@@ -279,6 +329,18 @@ const AddPaneNewPanel = ({
|
|
|
279
329
|
setLayoutChoice('standard');
|
|
280
330
|
}
|
|
281
331
|
|
|
332
|
+
if (entry.locked) {
|
|
333
|
+
const liveTemplate = convertStorageToLiveTemplate(entry.template);
|
|
334
|
+
handleApplyTemplate(liveTemplate);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (entry.retain) {
|
|
339
|
+
setCopyMode('original');
|
|
340
|
+
} else {
|
|
341
|
+
setCopyMode('prompt');
|
|
342
|
+
}
|
|
343
|
+
|
|
282
344
|
setStep('copyInput');
|
|
283
345
|
};
|
|
284
346
|
|
|
@@ -295,27 +357,60 @@ const AddPaneNewPanel = ({
|
|
|
295
357
|
const liveTemplate = convertStorageToLiveTemplate(
|
|
296
358
|
selectedLibraryEntry.template
|
|
297
359
|
);
|
|
360
|
+
|
|
361
|
+
if (copyMode === 'original') {
|
|
362
|
+
handleApplyTemplate(liveTemplate);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
298
366
|
const shellResult = convertTemplateToAIShell(liveTemplate);
|
|
299
367
|
const layout = 'Text Only';
|
|
300
368
|
|
|
301
369
|
if (layoutChoice === 'grid' && liveTemplate.gridLayout) {
|
|
370
|
+
if (copyMode === 'raw' && isAiStyling) {
|
|
371
|
+
const activeConfig =
|
|
372
|
+
prompts.aiPromptsIndex.find((p) => p.id === selectedPromptId) ||
|
|
373
|
+
prompts.aiPromptsIndex.find((p) => p.layout === 'grid') ||
|
|
374
|
+
prompts.aiPromptsIndex[0];
|
|
375
|
+
|
|
376
|
+
const stylePromptKey = activeConfig.prompts.style;
|
|
377
|
+
const stylePromptDetails = (prompts as any)[stylePromptKey];
|
|
378
|
+
|
|
379
|
+
const copyResults: string[] = [];
|
|
380
|
+
const rawContents = [col1Copy, col2Copy];
|
|
381
|
+
|
|
382
|
+
for (const rawContent of rawContents) {
|
|
383
|
+
const formattedStylePrompt = stylePromptDetails.user_template
|
|
384
|
+
.replace('{{SHELL_JSON}}', shellResult)
|
|
385
|
+
.replace('{{COPY_INPUT}}', rawContent);
|
|
386
|
+
|
|
387
|
+
const styledResult = await callAskLemurAPI(
|
|
388
|
+
formattedStylePrompt,
|
|
389
|
+
stylePromptDetails.system || '',
|
|
390
|
+
false,
|
|
391
|
+
isSandboxMode
|
|
392
|
+
);
|
|
393
|
+
copyResults.push(styledResult);
|
|
394
|
+
}
|
|
395
|
+
const finalPane = parseAiPane(shellResult, copyResults, layout);
|
|
396
|
+
handleApplyTemplate(finalPane);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (copyMode === 'raw') {
|
|
401
|
+
const nodes = liveTemplate.gridLayout.nodes;
|
|
402
|
+
if (nodes && nodes[0]) nodes[0].markdownBody = col1Copy;
|
|
403
|
+
if (nodes && nodes[1]) nodes[1].markdownBody = col2Copy;
|
|
404
|
+
handleApplyTemplate(liveTemplate);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
302
408
|
const copyPromptDetails = prompts.aiPaneCopyPrompt_2cols;
|
|
303
409
|
const preset = copyPromptDetails.presets.heroDefault;
|
|
304
410
|
const copyResults: string[] = [];
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
prompt: string;
|
|
309
|
-
presetKey: ColumnPresetKey;
|
|
310
|
-
}[] = [
|
|
311
|
-
{
|
|
312
|
-
prompt: promptValueCol1,
|
|
313
|
-
presetKey: 'left',
|
|
314
|
-
},
|
|
315
|
-
{
|
|
316
|
-
prompt: promptValueCol2,
|
|
317
|
-
presetKey: 'right',
|
|
318
|
-
},
|
|
411
|
+
const promptsToRun = [
|
|
412
|
+
{ prompt: promptValueCol1, presetKey: 'left' as ColumnPresetKey },
|
|
413
|
+
{ prompt: promptValueCol2, presetKey: 'right' as ColumnPresetKey },
|
|
319
414
|
];
|
|
320
415
|
|
|
321
416
|
for (const item of promptsToRun) {
|
|
@@ -339,16 +434,36 @@ const AddPaneNewPanel = ({
|
|
|
339
434
|
);
|
|
340
435
|
copyResults.push(copyResult);
|
|
341
436
|
}
|
|
342
|
-
|
|
343
437
|
const finalPane = parseAiPane(shellResult, copyResults, layout);
|
|
344
438
|
handleApplyTemplate(finalPane);
|
|
345
439
|
} else if (layoutChoice === 'standard' && liveTemplate.markdown) {
|
|
440
|
+
if (copyMode === 'raw' && isAiStyling) {
|
|
441
|
+
const activeConfig =
|
|
442
|
+
prompts.aiPromptsIndex.find((p) => p.id === selectedPromptId) ||
|
|
443
|
+
prompts.aiPromptsIndex[0];
|
|
444
|
+
const stylePromptKey = activeConfig.prompts.style;
|
|
445
|
+
const stylePromptDetails = (prompts as any)[stylePromptKey];
|
|
446
|
+
|
|
447
|
+
const formattedStylePrompt = stylePromptDetails.user_template
|
|
448
|
+
.replace('{{SHELL_JSON}}', shellResult)
|
|
449
|
+
.replace('{{COPY_INPUT}}', copyValue);
|
|
450
|
+
|
|
451
|
+
const styledResult = await callAskLemurAPI(
|
|
452
|
+
formattedStylePrompt,
|
|
453
|
+
stylePromptDetails.system || '',
|
|
454
|
+
false,
|
|
455
|
+
isSandboxMode
|
|
456
|
+
);
|
|
457
|
+
const finalPane = parseAiPane(shellResult, styledResult, layout);
|
|
458
|
+
handleApplyTemplate(finalPane);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
346
462
|
if (copyMode === 'raw') {
|
|
347
463
|
liveTemplate.markdown.markdownBody = copyValue;
|
|
348
464
|
handleApplyTemplate(liveTemplate);
|
|
349
465
|
return;
|
|
350
466
|
}
|
|
351
|
-
|
|
352
467
|
if (copyMode === 'prompt') {
|
|
353
468
|
if (!shellResult || shellResult === '{}') {
|
|
354
469
|
throw new Error(
|
|
@@ -387,6 +502,11 @@ const AddPaneNewPanel = ({
|
|
|
387
502
|
);
|
|
388
503
|
}
|
|
389
504
|
} else if (initialChoice === 'ai') {
|
|
505
|
+
const activeConfig = prompts.aiPromptsIndex.find(
|
|
506
|
+
(p) => p.id === selectedPromptId
|
|
507
|
+
);
|
|
508
|
+
if (!activeConfig) throw new Error('Selected prompt type not found.');
|
|
509
|
+
|
|
390
510
|
let designInput = `Generate a design using a **${aiDesignConfig.harmony.toLowerCase()}** color scheme with a **${aiDesignConfig.theme.toLowerCase()}** theme.`;
|
|
391
511
|
if (aiDesignConfig.baseColor)
|
|
392
512
|
designInput += ` Base the colors around **${aiDesignConfig.baseColor}**.`;
|
|
@@ -396,9 +516,11 @@ const AddPaneNewPanel = ({
|
|
|
396
516
|
designInput += ` Refine with these notes: "${aiDesignConfig.additionalNotes}"`;
|
|
397
517
|
|
|
398
518
|
const layout = 'Text Only';
|
|
519
|
+
const promptMap = prompts as any;
|
|
399
520
|
|
|
400
521
|
if (layoutChoice === 'standard') {
|
|
401
|
-
const
|
|
522
|
+
const shellPromptKey = activeConfig.prompts.shell;
|
|
523
|
+
const shellPromptDetails = promptMap[shellPromptKey];
|
|
402
524
|
const formattedShellPrompt = shellPromptDetails.user_template
|
|
403
525
|
.replace('{{DESIGN_INPUT}}', designInput)
|
|
404
526
|
.replace('{{LAYOUT_TYPE}}', layout);
|
|
@@ -410,7 +532,8 @@ const AddPaneNewPanel = ({
|
|
|
410
532
|
isSandboxMode
|
|
411
533
|
);
|
|
412
534
|
|
|
413
|
-
const
|
|
535
|
+
const copyPromptKey = activeConfig.prompts.copy;
|
|
536
|
+
const copyPromptDetails = promptMap[copyPromptKey];
|
|
414
537
|
const copyInputContent =
|
|
415
538
|
copyMode === 'prompt' ? promptValue : copyValue;
|
|
416
539
|
const formattedCopyPrompt = copyPromptDetails.user_template
|
|
@@ -428,7 +551,8 @@ const AddPaneNewPanel = ({
|
|
|
428
551
|
const finalPane = parseAiPane(shellResult, copyResult, layout);
|
|
429
552
|
handleApplyTemplate(finalPane);
|
|
430
553
|
} else if (layoutChoice === 'grid') {
|
|
431
|
-
const
|
|
554
|
+
const shellPromptKey = activeConfig.prompts.shell;
|
|
555
|
+
const shellPromptDetails = promptMap[shellPromptKey];
|
|
432
556
|
const formattedShellPrompt = shellPromptDetails.user_template
|
|
433
557
|
.replace('{{COPY_INPUT}}', overallPrompt)
|
|
434
558
|
.replace('{{DESIGN_INPUT}}', designInput);
|
|
@@ -440,23 +564,16 @@ const AddPaneNewPanel = ({
|
|
|
440
564
|
isSandboxMode
|
|
441
565
|
);
|
|
442
566
|
|
|
443
|
-
const
|
|
444
|
-
const
|
|
567
|
+
const copyPromptKey = activeConfig.prompts.copy;
|
|
568
|
+
const copyPromptDetails = promptMap[copyPromptKey];
|
|
569
|
+
const preset =
|
|
570
|
+
copyPromptDetails.presets?.[activeConfig.variants[0]] ||
|
|
571
|
+
copyPromptDetails.presets?.heroDefault;
|
|
445
572
|
const copyResults: string[] = [];
|
|
446
573
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
prompt:
|
|
450
|
-
presetKey: ColumnPresetKey;
|
|
451
|
-
}[] = [
|
|
452
|
-
{
|
|
453
|
-
prompt: promptValueCol1,
|
|
454
|
-
presetKey: 'left',
|
|
455
|
-
},
|
|
456
|
-
{
|
|
457
|
-
prompt: promptValueCol2,
|
|
458
|
-
presetKey: 'right',
|
|
459
|
-
},
|
|
574
|
+
const promptsToRun = [
|
|
575
|
+
{ prompt: promptValueCol1, presetKey: 'left' as ColumnPresetKey },
|
|
576
|
+
{ prompt: promptValueCol2, presetKey: 'right' as ColumnPresetKey },
|
|
460
577
|
];
|
|
461
578
|
|
|
462
579
|
for (const item of promptsToRun) {
|
|
@@ -491,6 +608,8 @@ const AddPaneNewPanel = ({
|
|
|
491
608
|
copyMode,
|
|
492
609
|
promptValue,
|
|
493
610
|
copyValue,
|
|
611
|
+
col1Copy,
|
|
612
|
+
col2Copy,
|
|
494
613
|
overallPrompt,
|
|
495
614
|
promptValueCol1,
|
|
496
615
|
promptValueCol2,
|
|
@@ -499,6 +618,8 @@ const AddPaneNewPanel = ({
|
|
|
499
618
|
layoutChoice,
|
|
500
619
|
selectedLibraryEntry,
|
|
501
620
|
handleApplyTemplate,
|
|
621
|
+
selectedPromptId,
|
|
622
|
+
isAiStyling,
|
|
502
623
|
]);
|
|
503
624
|
|
|
504
625
|
const renderInitialStep = () => (
|
|
@@ -584,50 +705,160 @@ const AddPaneNewPanel = ({
|
|
|
584
705
|
const renderContentStep = () => {
|
|
585
706
|
if (layoutChoice === 'grid') {
|
|
586
707
|
const isGenerateDisabled =
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
708
|
+
copyMode === 'prompt'
|
|
709
|
+
? !overallPrompt.trim() ||
|
|
710
|
+
!promptValueCol1.trim() ||
|
|
711
|
+
!promptValueCol2.trim()
|
|
712
|
+
: copyMode === 'raw'
|
|
713
|
+
? !col1Copy.trim() || !col2Copy.trim()
|
|
714
|
+
: false;
|
|
590
715
|
|
|
591
716
|
return (
|
|
592
717
|
<div className="space-y-4 p-4">
|
|
593
|
-
<
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
This context is applied to both columns.
|
|
605
|
-
</p>
|
|
606
|
-
</div>
|
|
607
|
-
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
608
|
-
<div>
|
|
609
|
-
<label className="mb-2 block text-sm font-bold text-gray-700">
|
|
610
|
-
Left Column Prompt
|
|
611
|
-
</label>
|
|
612
|
-
<textarea
|
|
613
|
-
value={promptValueCol1}
|
|
614
|
-
onChange={(e) => setPromptValueCol1(e.target.value)}
|
|
615
|
-
rows={4}
|
|
616
|
-
className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
|
|
718
|
+
<label className="block text-lg font-bold text-gray-800">
|
|
719
|
+
1. Provide Content
|
|
720
|
+
</label>
|
|
721
|
+
|
|
722
|
+
<div className="my-2 flex flex-wrap gap-4">
|
|
723
|
+
<div className="flex items-center space-x-2">
|
|
724
|
+
<input
|
|
725
|
+
type="radio"
|
|
726
|
+
checked={copyMode === 'prompt'}
|
|
727
|
+
onChange={() => setCopyMode('prompt')}
|
|
728
|
+
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
617
729
|
/>
|
|
730
|
+
<label className="text-sm font-bold text-gray-700">Prompt</label>
|
|
618
731
|
</div>
|
|
619
|
-
<div>
|
|
620
|
-
<
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
onChange={(e) => setPromptValueCol2(e.target.value)}
|
|
626
|
-
rows={4}
|
|
627
|
-
className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
|
|
732
|
+
<div className="flex items-center space-x-2">
|
|
733
|
+
<input
|
|
734
|
+
type="radio"
|
|
735
|
+
checked={copyMode === 'raw'}
|
|
736
|
+
onChange={() => setCopyMode('raw')}
|
|
737
|
+
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
628
738
|
/>
|
|
739
|
+
<label className="text-sm font-bold text-gray-700">
|
|
740
|
+
Manual Markdown
|
|
741
|
+
</label>
|
|
629
742
|
</div>
|
|
743
|
+
{selectedLibraryEntry?.retain && (
|
|
744
|
+
<div className="flex items-center space-x-2">
|
|
745
|
+
<input
|
|
746
|
+
type="radio"
|
|
747
|
+
checked={copyMode === 'original'}
|
|
748
|
+
onChange={() => setCopyMode('original')}
|
|
749
|
+
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
750
|
+
/>
|
|
751
|
+
<label className="text-sm font-bold text-gray-700">
|
|
752
|
+
Use Original
|
|
753
|
+
</label>
|
|
754
|
+
</div>
|
|
755
|
+
)}
|
|
630
756
|
</div>
|
|
757
|
+
|
|
758
|
+
{copyMode === 'raw' && initialChoice === 'library' && (
|
|
759
|
+
<div className="mb-4 flex items-center justify-between rounded-lg border border-gray-100 bg-gray-50 p-2">
|
|
760
|
+
<span className="text-sm text-gray-600">
|
|
761
|
+
Style this content with AI?
|
|
762
|
+
</span>
|
|
763
|
+
<div className="flex items-center">
|
|
764
|
+
<BooleanToggle
|
|
765
|
+
label="AI Styles"
|
|
766
|
+
value={isAiStyling}
|
|
767
|
+
onChange={setIsAiStyling}
|
|
768
|
+
size="sm"
|
|
769
|
+
/>
|
|
770
|
+
</div>
|
|
771
|
+
</div>
|
|
772
|
+
)}
|
|
773
|
+
|
|
774
|
+
{copyMode === 'prompt' && (
|
|
775
|
+
<>
|
|
776
|
+
<div className="mb-4">
|
|
777
|
+
<EnumSelect
|
|
778
|
+
label="Section Type"
|
|
779
|
+
value={selectedPromptId}
|
|
780
|
+
onChange={setSelectedPromptId}
|
|
781
|
+
options={promptOptions}
|
|
782
|
+
placeholder="Select a section type..."
|
|
783
|
+
className="w-full"
|
|
784
|
+
/>
|
|
785
|
+
</div>
|
|
786
|
+
<div>
|
|
787
|
+
<label className="mb-2 block text-sm font-bold text-gray-700">
|
|
788
|
+
Overall Component Brief
|
|
789
|
+
</label>
|
|
790
|
+
<textarea
|
|
791
|
+
value={overallPrompt}
|
|
792
|
+
onChange={(e) => setOverallPrompt(e.target.value)}
|
|
793
|
+
rows={3}
|
|
794
|
+
className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
|
|
795
|
+
/>
|
|
796
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
797
|
+
This context is applied to both columns.
|
|
798
|
+
</p>
|
|
799
|
+
</div>
|
|
800
|
+
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
801
|
+
<div>
|
|
802
|
+
<label className="mb-2 block text-sm font-bold text-gray-700">
|
|
803
|
+
Left Column Prompt
|
|
804
|
+
</label>
|
|
805
|
+
<textarea
|
|
806
|
+
value={promptValueCol1}
|
|
807
|
+
onChange={(e) => setPromptValueCol1(e.target.value)}
|
|
808
|
+
rows={4}
|
|
809
|
+
className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
|
|
810
|
+
/>
|
|
811
|
+
</div>
|
|
812
|
+
<div>
|
|
813
|
+
<label className="mb-2 block text-sm font-bold text-gray-700">
|
|
814
|
+
Right Column Prompt
|
|
815
|
+
</label>
|
|
816
|
+
<textarea
|
|
817
|
+
value={promptValueCol2}
|
|
818
|
+
onChange={(e) => setPromptValueCol2(e.target.value)}
|
|
819
|
+
rows={4}
|
|
820
|
+
className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
|
|
821
|
+
/>
|
|
822
|
+
</div>
|
|
823
|
+
</div>
|
|
824
|
+
</>
|
|
825
|
+
)}
|
|
826
|
+
|
|
827
|
+
{copyMode === 'raw' && (
|
|
828
|
+
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
829
|
+
<div>
|
|
830
|
+
<label className="mb-2 block text-sm font-bold text-gray-700">
|
|
831
|
+
Left Column Markdown
|
|
832
|
+
</label>
|
|
833
|
+
<textarea
|
|
834
|
+
value={col1Copy}
|
|
835
|
+
onChange={(e) => setCol1Copy(e.target.value)}
|
|
836
|
+
rows={8}
|
|
837
|
+
className="block w-full rounded-md border-gray-300 p-2 font-mono text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
|
|
838
|
+
/>
|
|
839
|
+
</div>
|
|
840
|
+
<div>
|
|
841
|
+
<label className="mb-2 block text-sm font-bold text-gray-700">
|
|
842
|
+
Right Column Markdown
|
|
843
|
+
</label>
|
|
844
|
+
<textarea
|
|
845
|
+
value={col2Copy}
|
|
846
|
+
onChange={(e) => setCol2Copy(e.target.value)}
|
|
847
|
+
rows={8}
|
|
848
|
+
className="block w-full rounded-md border-gray-300 p-2 font-mono text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
|
|
849
|
+
/>
|
|
850
|
+
</div>
|
|
851
|
+
</div>
|
|
852
|
+
)}
|
|
853
|
+
|
|
854
|
+
{copyMode === 'original' && (
|
|
855
|
+
<div className="rounded-md border border-blue-200 bg-blue-50 p-4 text-blue-700">
|
|
856
|
+
<p className="text-sm">
|
|
857
|
+
The original text saved with this design will be used.
|
|
858
|
+
</p>
|
|
859
|
+
</div>
|
|
860
|
+
)}
|
|
861
|
+
|
|
631
862
|
<div className="flex justify-between">
|
|
632
863
|
<button
|
|
633
864
|
onClick={handleBack}
|
|
@@ -656,11 +887,18 @@ const AddPaneNewPanel = ({
|
|
|
656
887
|
onPromptValueChange={setPromptValue}
|
|
657
888
|
copyValue={copyValue}
|
|
658
889
|
onCopyValueChange={setCopyValue}
|
|
890
|
+
hasRetainedContent={selectedLibraryEntry?.retain}
|
|
659
891
|
defaultPrompt={
|
|
660
892
|
first
|
|
661
893
|
? prompts.aiPaneCopyPrompt.heroDefault
|
|
662
894
|
: prompts.aiPaneCopyPrompt.contentDefault
|
|
663
895
|
}
|
|
896
|
+
promptOptions={promptOptions}
|
|
897
|
+
selectedPromptId={selectedPromptId}
|
|
898
|
+
onSelectedPromptIdChange={setSelectedPromptId}
|
|
899
|
+
isAiStyling={isAiStyling}
|
|
900
|
+
onIsAiStylingChange={setIsAiStyling}
|
|
901
|
+
showStyleToggle={initialChoice === 'library'}
|
|
664
902
|
/>
|
|
665
903
|
<div className="flex justify-between">
|
|
666
904
|
<button
|
|
@@ -672,7 +910,11 @@ const AddPaneNewPanel = ({
|
|
|
672
910
|
<button
|
|
673
911
|
onClick={handleFinalGenerate}
|
|
674
912
|
disabled={
|
|
675
|
-
copyMode === 'prompt'
|
|
913
|
+
copyMode === 'prompt'
|
|
914
|
+
? !promptValue.trim()
|
|
915
|
+
: copyMode === 'raw'
|
|
916
|
+
? !copyValue.trim()
|
|
917
|
+
: false
|
|
676
918
|
}
|
|
677
919
|
className="rounded-md bg-cyan-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-700 disabled:cursor-not-allowed disabled:bg-gray-400"
|
|
678
920
|
>
|
|
@@ -732,7 +974,11 @@ const AddPaneNewPanel = ({
|
|
|
732
974
|
);
|
|
733
975
|
|
|
734
976
|
const renderDirectInjectStep = () => (
|
|
735
|
-
<DirectInjectStep
|
|
977
|
+
<DirectInjectStep
|
|
978
|
+
onBack={handleBack}
|
|
979
|
+
onCreatePane={handleApplyTemplate}
|
|
980
|
+
layout={layoutChoice}
|
|
981
|
+
/>
|
|
736
982
|
);
|
|
737
983
|
|
|
738
984
|
const renderLoading = () => (
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
|
+
import BooleanToggle from '@/components/form/BooleanToggle';
|
|
3
|
+
import EnumSelect from '@/components/form/EnumSelect';
|
|
2
4
|
|
|
3
|
-
type CopyMode = 'prompt' | 'raw';
|
|
5
|
+
export type CopyMode = 'prompt' | 'raw' | 'original';
|
|
6
|
+
|
|
7
|
+
interface PromptOption {
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
4
11
|
|
|
5
12
|
interface CopyInputStepProps {
|
|
6
13
|
copyMode: CopyMode;
|
|
@@ -10,6 +17,13 @@ interface CopyInputStepProps {
|
|
|
10
17
|
copyValue: string;
|
|
11
18
|
onCopyValueChange: (value: string) => void;
|
|
12
19
|
defaultPrompt?: string;
|
|
20
|
+
hasRetainedContent?: boolean;
|
|
21
|
+
promptOptions: PromptOption[];
|
|
22
|
+
selectedPromptId: string;
|
|
23
|
+
onSelectedPromptIdChange: (id: string) => void;
|
|
24
|
+
isAiStyling: boolean;
|
|
25
|
+
onIsAiStylingChange: (checked: boolean) => void;
|
|
26
|
+
showStyleToggle?: boolean;
|
|
13
27
|
}
|
|
14
28
|
|
|
15
29
|
export const CopyInputStep = ({
|
|
@@ -20,9 +34,15 @@ export const CopyInputStep = ({
|
|
|
20
34
|
copyValue,
|
|
21
35
|
onCopyValueChange,
|
|
22
36
|
defaultPrompt,
|
|
37
|
+
hasRetainedContent = false,
|
|
38
|
+
promptOptions,
|
|
39
|
+
selectedPromptId,
|
|
40
|
+
onSelectedPromptIdChange,
|
|
41
|
+
isAiStyling,
|
|
42
|
+
onIsAiStylingChange,
|
|
43
|
+
showStyleToggle = true,
|
|
23
44
|
}: CopyInputStepProps) => {
|
|
24
45
|
useEffect(() => {
|
|
25
|
-
// Pre-populate the prompt field if a default is provided and the field is empty
|
|
26
46
|
if (defaultPrompt && !promptValue) {
|
|
27
47
|
onPromptValueChange(defaultPrompt);
|
|
28
48
|
}
|
|
@@ -33,7 +53,7 @@ export const CopyInputStep = ({
|
|
|
33
53
|
<label className="block text-lg font-bold text-gray-800">
|
|
34
54
|
1. Provide Content
|
|
35
55
|
</label>
|
|
36
|
-
<div className="my-2 flex
|
|
56
|
+
<div className="my-2 flex flex-wrap gap-4">
|
|
37
57
|
<div className="flex items-center space-x-2">
|
|
38
58
|
<input
|
|
39
59
|
type="radio"
|
|
@@ -68,9 +88,41 @@ export const CopyInputStep = ({
|
|
|
68
88
|
Provide Copy (Markdown)
|
|
69
89
|
</label>
|
|
70
90
|
</div>
|
|
91
|
+
{hasRetainedContent && (
|
|
92
|
+
<div className="flex items-center space-x-2">
|
|
93
|
+
<input
|
|
94
|
+
type="radio"
|
|
95
|
+
id="copy-original-mode"
|
|
96
|
+
name="copyModeOptions"
|
|
97
|
+
value="original"
|
|
98
|
+
checked={copyMode === 'original'}
|
|
99
|
+
onChange={(e) => onCopyModeChange(e.target.value as CopyMode)}
|
|
100
|
+
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
101
|
+
/>
|
|
102
|
+
<label
|
|
103
|
+
htmlFor="copy-original-mode"
|
|
104
|
+
className="text-sm font-bold text-gray-700"
|
|
105
|
+
>
|
|
106
|
+
Use Original
|
|
107
|
+
</label>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
71
110
|
</div>
|
|
72
111
|
|
|
73
|
-
{copyMode === 'prompt'
|
|
112
|
+
{(copyMode === 'prompt' || (copyMode === 'raw' && isAiStyling)) && (
|
|
113
|
+
<div className="mb-4">
|
|
114
|
+
<EnumSelect
|
|
115
|
+
label="Section Type"
|
|
116
|
+
value={selectedPromptId}
|
|
117
|
+
onChange={onSelectedPromptIdChange}
|
|
118
|
+
options={promptOptions}
|
|
119
|
+
placeholder="Select a type..."
|
|
120
|
+
className="w-full"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{copyMode === 'prompt' && (
|
|
74
126
|
<>
|
|
75
127
|
<p className="mb-2 text-sm text-gray-500">
|
|
76
128
|
Let the AI write the copy based on your prompt.
|
|
@@ -84,12 +136,25 @@ export const CopyInputStep = ({
|
|
|
84
136
|
className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
|
|
85
137
|
/>
|
|
86
138
|
</>
|
|
87
|
-
)
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{copyMode === 'raw' && (
|
|
88
142
|
<>
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
143
|
+
<div className="mb-2 flex items-center justify-between">
|
|
144
|
+
<p className="text-sm text-gray-500">
|
|
145
|
+
Provide your raw copy here. Use Markdown.
|
|
146
|
+
</p>
|
|
147
|
+
{showStyleToggle && (
|
|
148
|
+
<div className="flex items-center">
|
|
149
|
+
<BooleanToggle
|
|
150
|
+
label="Style with AI"
|
|
151
|
+
value={isAiStyling}
|
|
152
|
+
onChange={onIsAiStylingChange}
|
|
153
|
+
size="sm"
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
93
158
|
<textarea
|
|
94
159
|
id="raw-copy"
|
|
95
160
|
value={copyValue}
|
|
@@ -100,6 +165,14 @@ export const CopyInputStep = ({
|
|
|
100
165
|
/>
|
|
101
166
|
</>
|
|
102
167
|
)}
|
|
168
|
+
|
|
169
|
+
{copyMode === 'original' && (
|
|
170
|
+
<div className="rounded-md border border-blue-200 bg-blue-50 p-4 text-blue-700">
|
|
171
|
+
<p className="text-sm">
|
|
172
|
+
The original text saved with this design will be used.
|
|
173
|
+
</p>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
103
176
|
</div>
|
|
104
177
|
);
|
|
105
178
|
};
|
|
@@ -5,25 +5,41 @@ import type { TemplatePane } from '@/types/compositorTypes';
|
|
|
5
5
|
interface DirectInjectStepProps {
|
|
6
6
|
onBack: () => void;
|
|
7
7
|
onCreatePane: (template: TemplatePane) => void;
|
|
8
|
+
layout: 'standard' | 'grid';
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export const DirectInjectStep = ({
|
|
11
12
|
onBack,
|
|
12
13
|
onCreatePane,
|
|
14
|
+
layout,
|
|
13
15
|
}: DirectInjectStepProps) => {
|
|
14
16
|
const [shellJson, setShellJson] = useState('');
|
|
15
|
-
const [
|
|
17
|
+
const [columnContent, setColumnContent] = useState<string[]>(
|
|
18
|
+
layout === 'grid' ? ['', ''] : ['']
|
|
19
|
+
);
|
|
16
20
|
const [error, setError] = useState<string | null>(null);
|
|
17
21
|
|
|
22
|
+
const handleContentChange = (index: number, value: string) => {
|
|
23
|
+
const newContent = [...columnContent];
|
|
24
|
+
newContent[index] = value;
|
|
25
|
+
setColumnContent(newContent);
|
|
26
|
+
};
|
|
27
|
+
|
|
18
28
|
const handleCreate = () => {
|
|
19
29
|
setError(null);
|
|
20
|
-
if (!shellJson.trim()
|
|
21
|
-
setError('
|
|
30
|
+
if (!shellJson.trim()) {
|
|
31
|
+
setError('Shell JSON must be provided.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (columnContent.some((c) => !c.trim())) {
|
|
35
|
+
setError('All content fields must be filled.');
|
|
22
36
|
return;
|
|
23
37
|
}
|
|
24
38
|
|
|
25
39
|
try {
|
|
26
|
-
const
|
|
40
|
+
const contentPayload =
|
|
41
|
+
layout === 'standard' ? columnContent[0] : columnContent;
|
|
42
|
+
const finalPane = parseAiPane(shellJson, contentPayload, 'DirectInject');
|
|
27
43
|
onCreatePane(finalPane);
|
|
28
44
|
} catch (err: any) {
|
|
29
45
|
console.error('Direct Inject Error:', err);
|
|
@@ -52,22 +68,27 @@ export const DirectInjectStep = ({
|
|
|
52
68
|
placeholder={`{ "bgColour": "#ffffff", "parentClasses": [...], "defaultClasses": {...} }`}
|
|
53
69
|
/>
|
|
54
70
|
</div>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
|
|
72
|
+
{columnContent.map((content, index) => (
|
|
73
|
+
<div key={index}>
|
|
74
|
+
<label
|
|
75
|
+
htmlFor={`copyHtml-${index}`}
|
|
76
|
+
className="block text-sm font-bold text-gray-700"
|
|
77
|
+
>
|
|
78
|
+
{layout === 'grid'
|
|
79
|
+
? `Inner HTML (Column ${index + 1})`
|
|
80
|
+
: 'Inner HTML'}
|
|
81
|
+
</label>
|
|
82
|
+
<textarea
|
|
83
|
+
id={`copyHtml-${index}`}
|
|
84
|
+
rows={layout === 'grid' ? 6 : 10}
|
|
85
|
+
value={content}
|
|
86
|
+
onChange={(e) => handleContentChange(index, e.target.value)}
|
|
87
|
+
className="mt-1 block w-full rounded-md border-gray-300 p-2 font-mono text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
|
|
88
|
+
placeholder={`<h2 class="...">...</h2>\n<p class="...">...</p>`}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
71
92
|
</div>
|
|
72
93
|
|
|
73
94
|
{error && (
|
|
@@ -85,7 +106,6 @@ export const DirectInjectStep = ({
|
|
|
85
106
|
</button>
|
|
86
107
|
<button
|
|
87
108
|
onClick={handleCreate}
|
|
88
|
-
disabled={!shellJson.trim() || !copyHtml.trim()}
|
|
89
109
|
className="rounded-md bg-cyan-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-700 disabled:cursor-not-allowed disabled:bg-gray-400"
|
|
90
110
|
>
|
|
91
111
|
Create Pane
|
|
@@ -4,6 +4,8 @@ import { savePaneToLibrary } from '@/utils/compositor/designLibraryHelper';
|
|
|
4
4
|
import { convertToBackendFormat } from '@/utils/api/brandHelpers';
|
|
5
5
|
import StringInput from '@/components/form/StringInput';
|
|
6
6
|
import { brandConfigStore } from '@/stores/storykeep';
|
|
7
|
+
import { getCtx } from '@/stores/nodes';
|
|
8
|
+
import type { FlatNode } from '@/types/compositorTypes';
|
|
7
9
|
|
|
8
10
|
interface SaveToLibraryModalProps {
|
|
9
11
|
paneId: string;
|
|
@@ -42,6 +44,7 @@ export function SaveToLibraryModal({
|
|
|
42
44
|
const [selectedCategory, setSelectedCategory] = useState(OTHER_CATEGORY);
|
|
43
45
|
const [customCategory, setCustomCategory] = useState('');
|
|
44
46
|
const [copyMode, setCopyMode] = useState<CopyMode>('retain');
|
|
47
|
+
const [locked, setLocked] = useState(false);
|
|
45
48
|
const [saveState, setSaveState] = useState<SaveState>('idle');
|
|
46
49
|
const [error, setError] = useState('');
|
|
47
50
|
|
|
@@ -54,6 +57,34 @@ export function SaveToLibraryModal({
|
|
|
54
57
|
return [...cats, OTHER_CATEGORY];
|
|
55
58
|
}, [designLibrary]);
|
|
56
59
|
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const ctx = getCtx();
|
|
62
|
+
const childIds = ctx.getChildNodeIDs(paneId);
|
|
63
|
+
|
|
64
|
+
const hasWidget = (ids: string[]): boolean => {
|
|
65
|
+
for (const id of ids) {
|
|
66
|
+
const node = ctx.allNodes.get().get(id) as FlatNode;
|
|
67
|
+
if (!node) continue;
|
|
68
|
+
|
|
69
|
+
// Strict check for widget based on tagName being 'code'
|
|
70
|
+
if (node.tagName === 'code') {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const children = ctx.getChildNodeIDs(id);
|
|
75
|
+
if (children.length > 0 && hasWidget(children)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (hasWidget(childIds)) {
|
|
83
|
+
setLocked(true);
|
|
84
|
+
setCopyMode('retain');
|
|
85
|
+
}
|
|
86
|
+
}, [paneId]);
|
|
87
|
+
|
|
57
88
|
useEffect(() => {
|
|
58
89
|
if (saveState === 'saved') {
|
|
59
90
|
const timer = setTimeout(() => {
|
|
@@ -77,6 +108,7 @@ export function SaveToLibraryModal({
|
|
|
77
108
|
title: title,
|
|
78
109
|
category: finalCategory,
|
|
79
110
|
copyMode: copyMode,
|
|
111
|
+
locked: locked,
|
|
80
112
|
};
|
|
81
113
|
const brandConfig = brandConfigStore.get();
|
|
82
114
|
|
|
@@ -90,7 +122,7 @@ export function SaveToLibraryModal({
|
|
|
90
122
|
if (newBrandConfig) {
|
|
91
123
|
const backendDTO = convertToBackendFormat(newBrandConfig);
|
|
92
124
|
brandConfigStore.set({
|
|
93
|
-
...backendDTO,
|
|
125
|
+
...backendDTO,
|
|
94
126
|
TENANT_ID: brandConfig.TENANT_ID,
|
|
95
127
|
});
|
|
96
128
|
setSaveState('saved');
|
|
@@ -106,7 +138,7 @@ export function SaveToLibraryModal({
|
|
|
106
138
|
|
|
107
139
|
return (
|
|
108
140
|
<div
|
|
109
|
-
className="z-105 fixed inset-0 flex items-center justify-center bg-black
|
|
141
|
+
className="z-105 fixed inset-0 flex items-center justify-center bg-black bg-opacity-75"
|
|
110
142
|
onClick={saveState === 'idle' ? onClose : undefined}
|
|
111
143
|
>
|
|
112
144
|
<div
|
|
@@ -151,38 +183,40 @@ export function SaveToLibraryModal({
|
|
|
151
183
|
)}
|
|
152
184
|
</div>
|
|
153
185
|
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
+
{!locked && (
|
|
187
|
+
<div>
|
|
188
|
+
<label className="block text-sm font-bold text-gray-700">
|
|
189
|
+
Content Mode
|
|
190
|
+
</label>
|
|
191
|
+
<fieldset className="mt-2">
|
|
192
|
+
<legend className="sr-only">Copy Mode</legend>
|
|
193
|
+
<div className="space-y-2">
|
|
194
|
+
{copyOptions.map((option) => (
|
|
195
|
+
<div key={option.id} className="flex items-center">
|
|
196
|
+
<input
|
|
197
|
+
id={option.id}
|
|
198
|
+
name="copy-mode"
|
|
199
|
+
type="radio"
|
|
200
|
+
value={option.id}
|
|
201
|
+
checked={copyMode === option.id}
|
|
202
|
+
onChange={() => setCopyMode(option.id)}
|
|
203
|
+
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
204
|
+
/>
|
|
205
|
+
<label
|
|
206
|
+
htmlFor={option.id}
|
|
207
|
+
className="ml-3 block text-sm font-bold text-gray-700"
|
|
208
|
+
>
|
|
209
|
+
{option.title}
|
|
210
|
+
<p className="text-xs text-gray-500">
|
|
211
|
+
{option.description}
|
|
212
|
+
</p>
|
|
213
|
+
</label>
|
|
214
|
+
</div>
|
|
215
|
+
))}
|
|
216
|
+
</div>
|
|
217
|
+
</fieldset>
|
|
218
|
+
</div>
|
|
219
|
+
)}
|
|
186
220
|
|
|
187
221
|
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
188
222
|
</div>
|
|
@@ -1,43 +1,69 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"title": "Landing Page",
|
|
15
|
-
"description": "Headline, value prop, CTA"
|
|
16
|
-
},
|
|
17
|
-
"feature": {
|
|
18
|
-
"title": "Feature Section",
|
|
19
|
-
"description": "Describe a product feature"
|
|
20
|
-
},
|
|
21
|
-
"about": {
|
|
22
|
-
"title": "About Us",
|
|
23
|
-
"description": "Company mission/values"
|
|
2
|
+
"aiPromptsIndex": [
|
|
3
|
+
{
|
|
4
|
+
"id": "hero_standard",
|
|
5
|
+
"label": "Hero Section (Standard)",
|
|
6
|
+
"layout": "standard",
|
|
7
|
+
"columns": 1,
|
|
8
|
+
"prompts": {
|
|
9
|
+
"shell": "aiPaneShellPrompt",
|
|
10
|
+
"copy": "aiPaneCopyPrompt",
|
|
11
|
+
"style": "aiPaneStyleOnlyPrompt"
|
|
12
|
+
},
|
|
13
|
+
"variants": ["heroDefault"]
|
|
24
14
|
},
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
15
|
+
{
|
|
16
|
+
"id": "hero_2col",
|
|
17
|
+
"label": "Hero Section (2-Column)",
|
|
18
|
+
"layout": "grid",
|
|
19
|
+
"columns": 2,
|
|
20
|
+
"prompts": {
|
|
21
|
+
"shell": "aiPaneShellPrompt_2cols",
|
|
22
|
+
"copy": "aiPaneCopyPrompt_2cols",
|
|
23
|
+
"style": "aiPaneStyleOnlyPrompt"
|
|
24
|
+
},
|
|
25
|
+
"variants": ["heroDefault"]
|
|
28
26
|
},
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
"
|
|
27
|
+
{
|
|
28
|
+
"id": "article_intro",
|
|
29
|
+
"label": "Article Intro (Standard)",
|
|
30
|
+
"layout": "standard",
|
|
31
|
+
"columns": 1,
|
|
32
|
+
"prompts": {
|
|
33
|
+
"shell": "aiPaneShellPrompt",
|
|
34
|
+
"copy": "aiPaneCopyPrompt",
|
|
35
|
+
"style": "aiPaneStyleOnlyPrompt"
|
|
36
|
+
},
|
|
37
|
+
"variants": ["articleIntro"]
|
|
32
38
|
},
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
"
|
|
39
|
+
{
|
|
40
|
+
"id": "article_body",
|
|
41
|
+
"label": "Article Body (Standard)",
|
|
42
|
+
"layout": "standard",
|
|
43
|
+
"columns": 1,
|
|
44
|
+
"prompts": {
|
|
45
|
+
"shell": "aiPaneShellPrompt",
|
|
46
|
+
"copy": "aiPaneCopyPrompt",
|
|
47
|
+
"style": "aiPaneStyleOnlyPrompt"
|
|
48
|
+
},
|
|
49
|
+
"variants": ["articleBody"]
|
|
36
50
|
},
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
51
|
+
{
|
|
52
|
+
"id": "section_header",
|
|
53
|
+
"label": "Section Header (Standard)",
|
|
54
|
+
"layout": "standard",
|
|
55
|
+
"columns": 1,
|
|
56
|
+
"prompts": {
|
|
57
|
+
"shell": "aiPaneShellPrompt",
|
|
58
|
+
"copy": "aiPaneCopyPrompt",
|
|
59
|
+
"style": "aiPaneStyleOnlyPrompt"
|
|
60
|
+
},
|
|
61
|
+
"variants": ["sectionHeader"]
|
|
40
62
|
}
|
|
63
|
+
],
|
|
64
|
+
"aiPaneStyleOnlyPrompt": {
|
|
65
|
+
"system": "You are an expert **frontend developer**. Your task is to convert **raw Markdown** text into semantic HTML with Tailwind CSS classes based on a provided design theme. **CRITICAL: DO NOT REWRITE, SUMMARIZE, OR CHANGE THE TEXT CONTENT.** Your job is purely structural translation (Markdown -> HTML) and aesthetic formatting.",
|
|
66
|
+
"user_template": "Here is the design 'shell' and 'theme' (bgColour, parentClasses, and defaultClasses) you must use. Use the `defaultClasses` as your base theme for styling:\n{{SHELL_JSON}}\n\nHere is the **RAW MARKDOWN** content you must style:\n\"{{COPY_INPUT}}\"\n\nCRITICAL RULES:\n1. **Parse the Markdown:** Convert headings (`##`, `###`) to tags (`<h2>`, `<h3>`), lists (`-`, `1.`) to (`<ul>`, `<ol>`), and formatting (`**`, `*`) to (`<strong>`, `<em>`). **DO NOT use `<h1>` tags.**\n2. **Apply Theme:** Add the Tailwind classes from the Shell's `defaultClasses` to the corresponding HTML tags.\n3. **Visual Rhythm:** 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 to ensure they do not touch.\n4. **Responsive Styles:** For responsive styles, you *must* only use `md:` and `xl:` prefixes.\n5. **Interactive Elements:** If the markdown contains a link `[text](url)` or a clear Call-to-Action phrase, style it as a `<button>` or styled `<a>` tag using the theme's button styles (if available) or high-contrast styling.\n6. **Block Wrapping:** **All text** must be wrapped in a block element like `<p>`, `<h2>`, `<h3>`, or `<li>`.\n7. **Contrast:** Verify that **all text elements** maintain **high contrast** against the `bgColour` provided in the `SHELL_JSON`. Prioritize readability.\n8. **OUTPUT ONLY THE RAW HTML.**"
|
|
41
67
|
},
|
|
42
68
|
"aiPaneShellPrompt": {
|
|
43
69
|
"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.",
|
|
@@ -47,11 +73,14 @@
|
|
|
47
73
|
"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
74
|
"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-bold 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
75
|
"heroDefault": "A compelling hero section for a website about [topic]. It should have a strong, attention-grabbing headline, a brief paragraph explaining the core value proposition, and a clear call-to-action.",
|
|
50
|
-
"contentDefault": "A content section that follows a hero. It should elaborate on a key feature or benefit related to [topic]. Include a sub-headline and a descriptive paragraph."
|
|
76
|
+
"contentDefault": "A content section that follows a hero. It should elaborate on a key feature or benefit related to [topic]. Include a sub-headline and a descriptive paragraph.",
|
|
77
|
+
"articleIntro": "A powerful introductory paragraph (Lede) for an article about [topic]. Use large, legible typography (e.g. text-xl or text-2xl) to hook the reader immediately.",
|
|
78
|
+
"articleBody": "A semantic article body section about [topic]. Use H3 subheadings, prose-style paragraphs, and unordered lists to structure the content for high readability.",
|
|
79
|
+
"sectionHeader": "A distinct section header (sub-hero) for [topic]. Use a high-contrast background, a prominent H2 title, and a short descriptive lead paragraph to act as a visual divider."
|
|
51
80
|
},
|
|
52
81
|
"aiPaneShellPrompt_2cols": {
|
|
53
82
|
"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.",
|
|
54
|
-
"user_template": "Generate the design JSON for the following component. The component is a 2-column grid layout where the columns stack vertically on mobile. Your task is to design the outer container shell (`parentClasses`), the shared typography theme (`defaultClasses`), and the specific styles for the individual columns (`columns`).\n\nComponent Brief:
|
|
83
|
+
"user_template": "Generate the design JSON for the following component. The component is a 2-column grid layout where the columns stack vertically on mobile. Your task is to design the outer container shell (`parentClasses`), the shared typography theme (`defaultClasses`), and the specific styles for the individual columns (`columns`).\n\nComponent Brief: {{COPY_INPUT}}\n\nDesign Style: {{DESIGN_INPUT}}\n\nCRITICAL RULES:\n1. You must respond with a JSON object with the top-level keys: `bgColour`, `parentClasses`, `defaultClasses`, and `columns`.\n2. The `parentClasses` value is for the OUTER container's spacing and width ONLY (e.g. `max-w-7xl`, `py-24`). It is **FORBIDDEN** to include `grid`, `grid-cols-*`, or `gap-*` properties in `parentClasses`. The application will handle the grid creation. Any response violating this rule will be rejected. The `parentClasses` value *must* be an ARRAY of objects.\n3. The `defaultClasses` value defines the theme and *must* be structured with responsive keys (`mobile`, `tablet`, `desktop`) containing Tailwind class strings.\n4. The `columns` key *must* be an array containing exactly **two** objects. Each object represents an individual column and must have a `gridClasses` key. The value for `gridClasses` is a responsive object where each key's value is a string of Tailwind classes. This is used to style the column's wrapper. Crucially, you must remember that Tailwind is mobile-first, so you must reset styles at larger breakpoints if needed. For example, to add spacing for the mobile stack that is removed on larger screens, you would use `{ \"mobile\": \"mt-12\", \"tablet\": \"mt-0\" }`.\n5. Ensure the selected `bgColour` provides **high contrast** (meeting at least WCAG AA standards) with the primary text colors defined in `defaultClasses`. Prioritize readability.\n\nEXAMPLE:\n{\n \"bgColour\": \"#0d1117\",\n \"parentClasses\": [\n { \n \"mobile\": \"mx-auto max-w-7xl\"\n },\n {\n \"mobile\": \"px-6 py-24\",\n \"tablet\": \"px-8 py-32\"\n }\n ],\n \"defaultClasses\": {\n \"h2\": {\n \"mobile\": \"text-4xl font-bold tracking-tight text-white\",\n \"tablet\": \"text-6xl\"\n },\n \"p\": {\n \"mobile\": \"text-lg leading-8 text-gray-300 mt-6\"\n }\n },\n \"columns\": [\n {\n \"gridClasses\": {\n \"mobile\": \"text-center\",\n \"tablet\": \"text-left\"\n }\n },\n {\n \"gridClasses\": {\n \"mobile\": \"flex flex-col items-center mt-12\",\n \"tablet\": \"items-start mt-0\"\n }\n }\n ]\n}"
|
|
55
84
|
},
|
|
56
85
|
"aiPaneCopyPrompt_2cols": {
|
|
57
86
|
"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**.",
|
|
@@ -141,6 +141,9 @@ export const toolAddModes = [
|
|
|
141
141
|
//"aside",
|
|
142
142
|
] as const;
|
|
143
143
|
|
|
144
|
+
export const regexpHook =
|
|
145
|
+
/^(identifyAs|youtube|bunny|bunnyContext|toggle|resource|belief|interactiveDisclosure|signup)\((.*)\)$/;
|
|
146
|
+
|
|
144
147
|
export const toolAddModeTitles: Record<ToolAddMode, string> = {
|
|
145
148
|
p: 'Paragraph',
|
|
146
149
|
h2: 'Heading 2',
|
|
@@ -293,9 +296,6 @@ export const contactPersona = [
|
|
|
293
296
|
},
|
|
294
297
|
];
|
|
295
298
|
|
|
296
|
-
export const regexpHook =
|
|
297
|
-
/^(identifyAs|youtube|bunny|bunnyContext|toggle|resource|belief|interactiveDisclosure|signup)\((.*)\)$/;
|
|
298
|
-
|
|
299
299
|
export const biIcons = [
|
|
300
300
|
'0-circle',
|
|
301
301
|
'0-circle-fill',
|
|
@@ -3199,6 +3199,9 @@ export class NodesContext {
|
|
|
3199
3199
|
const allOriginalNodes: TemplateNode[] = [];
|
|
3200
3200
|
const columnNodes: TemplateMarkdown[] = [];
|
|
3201
3201
|
|
|
3202
|
+
// Instantiate generator for column markdown parsing
|
|
3203
|
+
const markdownGen = new MarkdownGenerator(this);
|
|
3204
|
+
|
|
3202
3205
|
duplicatedGrid.nodes?.forEach((originalColumn) => {
|
|
3203
3206
|
const newColumn = cloneDeep(originalColumn);
|
|
3204
3207
|
newColumn.id = ulid();
|
|
@@ -3206,12 +3209,22 @@ export class NodesContext {
|
|
|
3206
3209
|
oldToNewIdMap.set(originalColumn.id, newColumn.id);
|
|
3207
3210
|
columnNodes.push(newColumn);
|
|
3208
3211
|
|
|
3209
|
-
originalColumn.
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
+
if (originalColumn.markdownBody) {
|
|
3213
|
+
const columnContentNodes = markdownGen.markdownToFlatNodes(
|
|
3214
|
+
originalColumn.markdownBody,
|
|
3215
|
+
newColumn.id
|
|
3216
|
+
) as TemplateNode[];
|
|
3217
|
+
// Add generated nodes directly to allNodes
|
|
3218
|
+
allNodes.push(...columnContentNodes);
|
|
3219
|
+
} else {
|
|
3220
|
+
// Standard flow: collect existing nodes for remapping
|
|
3221
|
+
originalColumn.nodes?.forEach((colNode) => {
|
|
3222
|
+
allOriginalNodes.push(colNode);
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3212
3225
|
});
|
|
3213
3226
|
|
|
3214
|
-
// Second pass: Clone all descendant nodes
|
|
3227
|
+
// Second pass: Clone all descendant nodes (only those from the standard flow)
|
|
3215
3228
|
const allClonedDescendants = allOriginalNodes.map((originalNode) => {
|
|
3216
3229
|
const newNode = cloneDeep(originalNode);
|
|
3217
3230
|
newNode.id = ulid();
|
|
@@ -281,12 +281,15 @@ export interface MarkdownPaneFragmentNode extends PaneFragmentNode {
|
|
|
281
281
|
parentClasses?: ParentClassesPayload;
|
|
282
282
|
parentCss?: string[];
|
|
283
283
|
gridClasses?: DefaultClassValue;
|
|
284
|
+
gridCss?: string;
|
|
284
285
|
}
|
|
285
286
|
|
|
286
287
|
export interface GridLayoutNode extends PaneFragmentNode {
|
|
287
288
|
nodeType: 'GridLayoutNode';
|
|
288
289
|
type: 'grid-layout';
|
|
289
290
|
parentClasses?: ParentClassesPayload;
|
|
291
|
+
parentCss?: string;
|
|
292
|
+
gridCss?: string;
|
|
290
293
|
defaultClasses?: Record<
|
|
291
294
|
string,
|
|
292
295
|
{
|
|
@@ -62,7 +62,7 @@ function convertLiveNodeToStorageNode(
|
|
|
62
62
|
fileId: copyMode === 'retain' ? node.fileId : undefined,
|
|
63
63
|
buttonPayload: copyMode === 'retain' ? node.buttonPayload : undefined,
|
|
64
64
|
codeHookParams: copyMode === 'retain' ? node.codeHookParams : undefined,
|
|
65
|
-
|
|
65
|
+
copy: copyMode === 'retain' ? node.copy : undefined,
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
const childIds = ctx.getChildNodeIDs(node.id);
|
|
@@ -500,7 +500,8 @@ function convertLivePaneToStoragePane(
|
|
|
500
500
|
: [],
|
|
501
501
|
};
|
|
502
502
|
} else if (gridLayoutNode) {
|
|
503
|
-
const { id, parentId, isChanged, ...restOfGrid } =
|
|
503
|
+
const { id, parentId, isChanged, parentCss, gridCss, ...restOfGrid } =
|
|
504
|
+
gridLayoutNode;
|
|
504
505
|
storageGridLayout = {
|
|
505
506
|
...restOfGrid,
|
|
506
507
|
nodes: ctx
|
|
@@ -517,6 +518,7 @@ function convertLivePaneToStoragePane(
|
|
|
517
518
|
isChanged,
|
|
518
519
|
markdownId,
|
|
519
520
|
parentCss,
|
|
521
|
+
gridCss,
|
|
520
522
|
...restOfColumn
|
|
521
523
|
} = columnNode;
|
|
522
524
|
|
|
@@ -584,10 +586,11 @@ export async function savePaneToLibrary(
|
|
|
584
586
|
title: string;
|
|
585
587
|
category: string;
|
|
586
588
|
copyMode: CopyMode;
|
|
589
|
+
locked?: boolean;
|
|
587
590
|
}
|
|
588
591
|
): Promise<BrandConfigState | null> {
|
|
589
592
|
const ctx = getCtx();
|
|
590
|
-
const { title, category, copyMode } = formData;
|
|
593
|
+
const { title, category, copyMode, locked } = formData;
|
|
591
594
|
|
|
592
595
|
const newStoragePane = convertLivePaneToStoragePane(paneId, ctx, {
|
|
593
596
|
title,
|
|
@@ -613,6 +616,8 @@ export async function savePaneToLibrary(
|
|
|
613
616
|
title: title,
|
|
614
617
|
markdownCount: actualMarkdownCount,
|
|
615
618
|
template: newStoragePane,
|
|
619
|
+
retain: copyMode === 'retain',
|
|
620
|
+
locked: !!locked,
|
|
616
621
|
};
|
|
617
622
|
|
|
618
623
|
const currentState: BrandConfigState = convertToLocalState(config);
|
|
@@ -93,24 +93,6 @@ export const isMarkdownPaneFragmentNode = (
|
|
|
93
93
|
);
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
-
interface WidgetNode extends FlatNode {
|
|
97
|
-
tagName: 'code';
|
|
98
|
-
codeHookParams: (string | string[])[];
|
|
99
|
-
copy: string;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export const isWidgetNode = (
|
|
103
|
-
node: BaseNode | FlatNode | null
|
|
104
|
-
): node is WidgetNode => {
|
|
105
|
-
return (
|
|
106
|
-
node !== null &&
|
|
107
|
-
'tagName' in node &&
|
|
108
|
-
node.tagName === 'code' &&
|
|
109
|
-
'codeHookParams' in node &&
|
|
110
|
-
Array.isArray(node.codeHookParams)
|
|
111
|
-
);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
96
|
export function hasTagName(
|
|
115
97
|
node: BaseNode | null | undefined
|
|
116
98
|
): node is FlatNode {
|