astro-tractstack 2.0.28 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.28",
3
+ "version": "2.0.30",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -5,22 +5,48 @@ import XMarkIcon from '@heroicons/react/24/solid/XMarkIcon';
5
5
  import { ProfileStorage } from '@/utils/profileStorage';
6
6
  import SandboxRegisterForm from '@/components/codehooks/SandboxRegisterForm';
7
7
 
8
- export default function SandboxAuthWrapper() {
8
+ interface SandboxAuthWrapperProps {
9
+ isServerSideAuthenticated: boolean;
10
+ }
11
+
12
+ export default function SandboxAuthWrapper({
13
+ isServerSideAuthenticated,
14
+ }: SandboxAuthWrapperProps) {
9
15
  const [profileExists, setProfileExists] = useState<boolean | null>(null);
10
16
 
11
17
  useEffect(() => {
12
- setProfileExists(ProfileStorage.hasProfile());
13
- }, []);
18
+ const hasLocalProfile = ProfileStorage.hasProfile();
19
+
20
+ if (hasLocalProfile && !isServerSideAuthenticated) {
21
+ const token = localStorage.getItem('tractstack_profile_token');
22
+
23
+ if (token) {
24
+ ProfileStorage.storeProfileToken(token);
25
+ window.location.reload();
26
+ return;
27
+ } else {
28
+ ProfileStorage.clearProfile();
29
+ setProfileExists(false);
30
+ }
31
+ } else {
32
+ setProfileExists(hasLocalProfile);
33
+ }
34
+ }, [isServerSideAuthenticated]);
14
35
 
15
36
  const handleRegistrationSuccess = () => {
16
37
  setProfileExists(true);
38
+ window.location.reload();
17
39
  };
18
40
 
19
41
  const handleClose = () => {
20
42
  window.location.href = '/';
21
43
  };
22
44
 
23
- if (profileExists === null || profileExists === true) {
45
+ if (profileExists === true && isServerSideAuthenticated) {
46
+ return null;
47
+ }
48
+
49
+ if (profileExists === null) {
24
50
  return null;
25
51
  }
26
52
 
@@ -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
- // Standard / Single Column State
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
- // Only prompt mode supported for grid now
307
- const promptsToRun: {
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 shellPromptDetails = prompts.aiPaneShellPrompt;
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 copyPromptDetails = prompts.aiPaneCopyPrompt;
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 shellPromptDetails = prompts.aiPaneShellPrompt_2cols;
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 copyPromptDetails = prompts.aiPaneCopyPrompt_2cols;
444
- const preset = copyPromptDetails.presets.heroDefault;
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
- // Grid is strictly prompt-based
448
- const promptsToRun: {
449
- prompt: string;
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
- !overallPrompt.trim() ||
588
- !promptValueCol1.trim() ||
589
- !promptValueCol2.trim();
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
- <div>
594
- <label className="mb-2 block text-sm font-bold text-gray-700">
595
- Overall Component Brief
596
- </label>
597
- <textarea
598
- value={overallPrompt}
599
- onChange={(e) => setOverallPrompt(e.target.value)}
600
- rows={3}
601
- className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
602
- />
603
- <p className="mt-1 text-xs text-gray-500">
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
- <label className="mb-2 block text-sm font-bold text-gray-700">
621
- Right Column Prompt
622
- </label>
623
- <textarea
624
- value={promptValueCol2}
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' ? !promptValue.trim() : !copyValue.trim()
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 onBack={handleBack} onCreatePane={handleApplyTemplate} />
977
+ <DirectInjectStep
978
+ onBack={handleBack}
979
+ onCreatePane={handleApplyTemplate}
980
+ layout={layoutChoice}
981
+ />
736
982
  );
737
983
 
738
984
  const renderLoading = () => (