astro-tractstack 2.0.14 → 2.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/index.js +41 -9
  2. package/package.json +1 -1
  3. package/templates/custom/with-examples/CodeHook.astro +4 -0
  4. package/templates/custom/with-examples/SandboxLauncher.tsx +65 -0
  5. package/templates/env.example +3 -0
  6. package/templates/src/components/codehooks/SandboxAuthWrapper.tsx +75 -0
  7. package/templates/src/components/codehooks/SandboxRegisterForm.tsx +202 -0
  8. package/templates/src/components/compositor/Compositor.tsx +2 -0
  9. package/templates/src/components/compositor/Node.tsx +27 -9
  10. package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +13 -11
  11. package/templates/src/components/compositor/nodes/Pane_layout.tsx +16 -14
  12. package/templates/src/components/edit/Header.tsx +8 -2
  13. package/templates/src/components/edit/PanelSwitch.tsx +4 -4
  14. package/templates/src/components/edit/pane/AddPanePanel.tsx +3 -0
  15. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +463 -561
  16. package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +140 -0
  17. package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +105 -0
  18. package/templates/src/components/edit/pane/steps/DesignLibraryStep.tsx +395 -0
  19. package/templates/src/components/edit/panels/StyleImagePanel.tsx +10 -8
  20. package/templates/src/components/edit/state/SaveModal.tsx +41 -0
  21. package/templates/src/constants/prompts.json +3 -1
  22. package/templates/src/pages/api/sandbox.ts +86 -0
  23. package/templates/src/pages/sandbox.astro +137 -0
  24. package/templates/src/types/nodeProps.ts +1 -0
  25. package/templates/src/utils/compositor/aiPaneParser.ts +32 -84
  26. package/templates/src/utils/compositor/designLibraryHelper.ts +87 -2
  27. package/templates/src/utils/profileStorage.ts +13 -0
  28. package/utils/inject-files.ts +41 -10
  29. package/templates/src/components/edit/pane/AiPaneGenerator.tsx +0 -575
  30. package/templates/src/components/edit/pane/AiPanePreview.tsx +0 -107
  31. package/templates/src/components/edit/pane/PageGen.tsx +0 -485
  32. package/templates/src/components/edit/pane/PageGenSelector.tsx +0 -245
  33. package/templates/src/components/edit/pane/PageGenSpecial.tsx +0 -339
  34. package/templates/src/utils/aai/getTitleSlug.ts +0 -72
@@ -139,6 +139,18 @@ export async function injectTemplateFiles(
139
139
  ),
140
140
  dest: 'src/components/compositor/nodes/Pane_layout.tsx',
141
141
  },
142
+ {
143
+ src: resolve(
144
+ '../templates/src/components/codehooks/SandboxAuthWrapper.tsx'
145
+ ),
146
+ dest: 'src/components/codehooks/SandboxAuthWrapper.tsx',
147
+ },
148
+ {
149
+ src: resolve(
150
+ '../templates/src/components/codehooks/SandboxRegisterForm.tsx'
151
+ ),
152
+ dest: 'src/components/codehooks/SandboxRegisterForm.tsx',
153
+ },
142
154
  {
143
155
  src: resolve('../templates/src/components/compositor/nodes/Markdown.tsx'),
144
156
  dest: 'src/components/compositor/nodes/Markdown.tsx',
@@ -442,12 +454,22 @@ export async function injectTemplateFiles(
442
454
  dest: 'src/components/edit/pane/RestylePaneModal.tsx',
443
455
  },
444
456
  {
445
- src: resolve('../templates/src/components/edit/pane/AiPaneGenerator.tsx'),
446
- dest: 'src/components/edit/pane/AiPaneGenerator.tsx',
457
+ src: resolve(
458
+ '../templates/src/components/edit/pane/steps/CopyInputStep.tsx'
459
+ ),
460
+ dest: 'src/components/edit/pane/steps/CopyInputStep.tsx',
461
+ },
462
+ {
463
+ src: resolve(
464
+ '../templates/src/components/edit/pane/steps/DesignLibraryStep.tsx'
465
+ ),
466
+ dest: 'src/components/edit/pane/steps/DesignLibraryStep.tsx',
447
467
  },
448
468
  {
449
- src: resolve('../templates/src/components/edit/pane/AiPanePreview.tsx'),
450
- dest: 'src/components/edit/pane/AiPanePreview.tsx',
469
+ src: resolve(
470
+ '../templates/src/components/edit/pane/steps/AiDesignStep.tsx'
471
+ ),
472
+ dest: 'src/components/edit/pane/steps/AiDesignStep.tsx',
451
473
  },
452
474
  {
453
475
  src: resolve(
@@ -585,12 +607,6 @@ export async function injectTemplateFiles(
585
607
  dest: 'src/stores/selection.ts',
586
608
  },
587
609
 
588
- // AAI utils
589
- {
590
- src: resolve('../templates/src/utils/aai/getTitleSlug.ts'),
591
- dest: 'src/utils/aai/getTitleSlug.ts',
592
- },
593
-
594
610
  // Compositor utils - etl
595
611
  {
596
612
  src: resolve('../templates/src/utils/etl/index.ts'),
@@ -826,6 +842,10 @@ export async function injectTemplateFiles(
826
842
  ),
827
843
  dest: 'src/pages/context/[...contextSlug]/edit.astro',
828
844
  },
845
+ {
846
+ src: resolve('../templates/src/pages/sandbox.astro'),
847
+ dest: 'src/pages/sandbox.astro',
848
+ },
829
849
  {
830
850
  src: resolve('../templates/src/pages/storykeep.astro'),
831
851
  dest: 'src/pages/storykeep.astro',
@@ -870,6 +890,10 @@ export async function injectTemplateFiles(
870
890
  src: resolve('../templates/src/pages/api/tailwind.ts'),
871
891
  dest: 'src/pages/api/tailwind.ts',
872
892
  },
893
+ {
894
+ src: resolve('../templates/src/pages/api/sandbox.ts'),
895
+ dest: 'src/pages/api/sandbox.ts',
896
+ },
873
897
 
874
898
  // Authentication Pages
875
899
  {
@@ -2138,6 +2162,13 @@ export async function injectTemplateFiles(
2138
2162
  // Example Components (Conditional)
2139
2163
  ...(config?.includeExamples
2140
2164
  ? [
2165
+ {
2166
+ src: resolve(
2167
+ '../templates/custom/with-examples/SandboxLauncher.tsx'
2168
+ ),
2169
+ dest: 'src/custom/SandboxLauncher.tsx',
2170
+ protected: true,
2171
+ },
2141
2172
  {
2142
2173
  src: resolve('../templates/custom/with-examples/CustomHero.astro'),
2143
2174
  dest: 'src/custom/CustomHero.astro',
@@ -1,575 +0,0 @@
1
- import { useState, useCallback } from 'react';
2
- import { AiPanePreview } from './AiPanePreview';
3
- import type { TemplatePane } from '@/types/compositorTypes';
4
- import prompts from '@/constants/prompts.json';
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';
9
-
10
- interface AiPaneGeneratorProps {
11
- ownerId: string;
12
- onComplete: (pane: TemplatePane) => void;
13
- onCancel: () => void;
14
- config?: BrandConfig;
15
- }
16
-
17
- type GenerationStep = 'input' | 'preview' | 'loading';
18
- type CopyMode = 'prompt' | 'raw';
19
-
20
- interface GenerationResponse {
21
- success: boolean;
22
- data?: {
23
- response: string | object;
24
- };
25
- error?: string;
26
- }
27
-
28
- const harmonyOptions = [
29
- 'Analogous',
30
- 'Monochromatic',
31
- 'Complementary',
32
- 'Triadic',
33
- ];
34
- const themeOptions = ['Light', 'Dark', 'Bright', 'Muted', 'Pastel', 'Earthy'];
35
-
36
- export function AiPaneGenerator({
37
- ownerId,
38
- onComplete,
39
- onCancel,
40
- config,
41
- }: AiPaneGeneratorProps) {
42
- const [currentStep, setCurrentStep] = useState<GenerationStep>('input');
43
- const [selectedLayout] = useState<string>('Text Only');
44
- const [copyMode, setCopyMode] = useState<CopyMode>('prompt');
45
- const [copyPrompt, setCopyPrompt] = useState('');
46
- const [rawCopy, setRawCopy] = useState('');
47
- const [generatedShell, setGeneratedShell] = useState<string | null>(null);
48
- const [generatedCopy, setGeneratedCopy] = useState<string | null>(null);
49
- const [error, setError] = useState<string | null>(null);
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
-
63
- const callAskLemurAPI = useCallback(
64
- async (
65
- prompt: string,
66
- context: string,
67
- expectJson: boolean
68
- ): Promise<string> => {
69
- const goBackend =
70
- import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
71
- const tenantId = import.meta.env.PUBLIC_TENANTID || 'default';
72
-
73
- const requestBody = {
74
- prompt: prompt,
75
- input_text: context,
76
- final_model: '',
77
- temperature: 0.5,
78
- max_tokens: 2000,
79
- };
80
-
81
- const response = await fetch(`${goBackend}/api/v1/aai/askLemur`, {
82
- method: 'POST',
83
- headers: {
84
- 'Content-Type': 'application/json',
85
- 'X-Tenant-ID': tenantId,
86
- },
87
- credentials: 'include',
88
- body: JSON.stringify(requestBody),
89
- });
90
-
91
- if (!response.ok) {
92
- const errorText = await response.text();
93
- console.error('AskLemur API Error Response:', errorText);
94
- let backendError = `API call failed: ${response.status} ${response.statusText}`;
95
- try {
96
- const errorJson = JSON.parse(errorText);
97
- if (errorJson && errorJson.error) {
98
- backendError = errorJson.error;
99
- }
100
- } catch (e) {
101
- /* Ignore */
102
- }
103
- throw new Error(backendError);
104
- }
105
-
106
- const result = (await response.json()) as GenerationResponse;
107
-
108
- if (!result.success || !result.data?.response) {
109
- throw new Error(
110
- result.error || 'Generation failed to return valid response.'
111
- );
112
- }
113
-
114
- let rawResponseData = result.data.response;
115
-
116
- if (expectJson && typeof rawResponseData === 'object') {
117
- return JSON.stringify(rawResponseData);
118
- }
119
-
120
- if (typeof rawResponseData === 'string') {
121
- let responseString = rawResponseData;
122
- try {
123
- if (
124
- responseString.startsWith('```json') &&
125
- responseString.endsWith('```')
126
- ) {
127
- responseString = responseString.slice(7, -3).trim();
128
- } else if (
129
- responseString.startsWith('```html') &&
130
- responseString.endsWith('```')
131
- ) {
132
- responseString = responseString.slice(7, -3).trim();
133
- }
134
- } catch (e) {
135
- /* Ignore stripping errors */
136
- }
137
- return responseString;
138
- }
139
-
140
- throw new Error('Unexpected response format received from API.');
141
- },
142
- []
143
- );
144
-
145
- const handleGenerate = useCallback(async () => {
146
- setError(null);
147
- setCurrentStep('loading');
148
- setGeneratedShell(null);
149
- setGeneratedCopy(null);
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
-
162
- try {
163
- const shellPromptDetails = prompts.aiPaneShellPrompt;
164
- const copyPromptDetails = prompts.aiPaneCopyPrompt;
165
-
166
- if (
167
- !shellPromptDetails?.user_template ||
168
- !copyPromptDetails?.user_template
169
- ) {
170
- throw new Error('AI prompts not found or incomplete in prompts.json');
171
- }
172
-
173
- const formattedShellPrompt = shellPromptDetails.user_template
174
- .replace('{{DESIGN_INPUT}}', designInput)
175
- .replace('{{LAYOUT_TYPE}}', selectedLayout);
176
-
177
- const shellResult = await callAskLemurAPI(
178
- formattedShellPrompt,
179
- shellPromptDetails.system || '',
180
- true
181
- );
182
- setGeneratedShell(shellResult);
183
-
184
- const copyInputContent = copyMode === 'prompt' ? copyPrompt : rawCopy;
185
- const formattedCopyPrompt = copyPromptDetails.user_template
186
- .replace('{{COPY_INPUT}}', copyInputContent)
187
- .replace('{{DESIGN_INPUT}}', designInput)
188
- .replace('{{LAYOUT_TYPE}}', selectedLayout)
189
- .replace('{{SHELL_JSON}}', shellResult);
190
-
191
- const copyResult = await callAskLemurAPI(
192
- formattedCopyPrompt,
193
- copyPromptDetails.system || '',
194
- false
195
- );
196
- setGeneratedCopy(copyResult);
197
-
198
- setCurrentStep('preview');
199
- } catch (err: any) {
200
- console.error('AI Pane Generation Error:', err);
201
- setError(err.message || 'Failed to generate AI pane.');
202
- setCurrentStep('input');
203
- }
204
- }, [
205
- selectedHarmony,
206
- baseColor,
207
- accentColor,
208
- selectedTheme,
209
- additionalNotes,
210
- selectedLayout,
211
- copyMode,
212
- copyPrompt,
213
- rawCopy,
214
- callAskLemurAPI,
215
- ]);
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
-
252
- const handleBack = () => {
253
- setError(null);
254
- if (currentStep === 'preview') {
255
- setCurrentStep('input');
256
- } else if (currentStep === 'input') {
257
- if (isInjectMode) {
258
- setIsInjectMode(false);
259
- } else {
260
- onCancel();
261
- }
262
- } else if (currentStep === 'loading') {
263
- setCurrentStep('input');
264
- }
265
- };
266
-
267
- if (currentStep === 'loading') {
268
- return (
269
- <div className="flex min-h-[200px] flex-col items-center justify-center space-y-4 p-6">
270
- <div className="h-8 w-8 animate-spin rounded-full border-b-2 border-gray-400"></div>
271
- <p className="text-sm text-gray-500">Generating AI Pane...</p>
272
- <button
273
- type="button"
274
- onClick={handleBack}
275
- className="mt-2 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"
276
- >
277
- Cancel
278
- </button>
279
- </div>
280
- );
281
- }
282
-
283
- if (currentStep === 'preview' && generatedShell && generatedCopy) {
284
- return (
285
- <AiPanePreview
286
- shellJson={generatedShell}
287
- copyHtml={generatedCopy}
288
- layout={selectedLayout}
289
- ownerId={ownerId}
290
- onComplete={onComplete}
291
- onBack={handleBack}
292
- />
293
- );
294
- }
295
-
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>
357
- </div>
358
- );
359
- }
360
-
361
- return (
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
-
438
- <div>
439
- <label
440
- htmlFor="additional-notes"
441
- className="block text-lg font-semibold text-gray-800"
442
- >
443
- Additional Design Notes (Optional)
444
- </label>
445
- <p className="mb-2 mt-1 text-sm text-gray-500">
446
- Add specific requests like "use rounded corners", "add subtle
447
- texture".
448
- </p>
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"
456
- />
457
- </div>
458
-
459
- <div>
460
- <label className="block text-lg font-semibold text-gray-800">
461
- Provide Content
462
- </label>
463
- <div className="my-2 flex space-x-4">
464
- <div className="flex items-center space-x-2">
465
- <input
466
- type="radio"
467
- id="copy-prompt-mode"
468
- name="copyModeOptions"
469
- value="prompt"
470
- checked={copyMode === 'prompt'}
471
- onChange={(e) => setCopyMode(e.target.value as CopyMode)}
472
- className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
473
- />
474
- <label
475
- htmlFor="copy-prompt-mode"
476
- className="text-sm font-medium text-gray-700"
477
- >
478
- Write a prompt
479
- </label>
480
- </div>
481
- <div className="flex items-center space-x-2">
482
- <input
483
- type="radio"
484
- id="copy-raw-mode"
485
- name="copyModeOptions"
486
- value="raw"
487
- checked={copyMode === 'raw'}
488
- onChange={(e) => setCopyMode(e.target.value as CopyMode)}
489
- className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
490
- />
491
- <label
492
- htmlFor="copy-raw-mode"
493
- className="text-sm font-medium text-gray-700"
494
- >
495
- Provide Copy
496
- </label>
497
- </div>
498
- </div>
499
-
500
- {copyMode === 'prompt' ? (
501
- <>
502
- <p className="mb-2 text-sm text-gray-500">
503
- Let the AI write the copy based on your prompt.
504
- </p>
505
- <textarea
506
- id="copy-prompt"
507
- value={copyPrompt}
508
- onChange={(e) => setCopyPrompt(e.target.value)}
509
- placeholder="Enter copy prompt..."
510
- rows={4}
511
- className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
512
- />
513
- </>
514
- ) : (
515
- <>
516
- <p className="mb-2 text-sm text-gray-500">
517
- Provide your raw copy text here. The AI will structure and style
518
- it.
519
- </p>
520
- <textarea
521
- id="raw-copy"
522
- value={rawCopy}
523
- onChange={(e) => setRawCopy(e.target.value)}
524
- placeholder="Paste or type your copy text..."
525
- rows={6}
526
- className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
527
- />
528
- </>
529
- )}
530
- </div>
531
-
532
- {error && <p className="text-sm text-red-600">{error}</p>}
533
-
534
- <div className="flex justify-between pt-4">
535
- <button
536
- type="button"
537
- onClick={handleBack}
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"
539
- >
540
- Cancel
541
- </button>
542
- <button
543
- type="button"
544
- onClick={handleGenerate}
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"
566
- >
567
- Direct Inject Payload
568
- </button>
569
- </div>
570
- </div>
571
- );
572
- }
573
-
574
- return null;
575
- }