astro-tractstack 2.0.12 → 2.0.14
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/dist/index.js +22 -0
- package/package.json +1 -1
- package/templates/src/client/view.js +5 -0
- package/templates/src/components/compositor/Compositor.tsx +3 -2
- package/templates/src/components/compositor/Node.tsx +18 -2
- package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +105 -0
- package/templates/src/components/edit/ToolMode.tsx +7 -0
- package/templates/src/components/edit/pane/AddPanePanel.tsx +5 -1
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +4 -1
- package/templates/src/components/edit/pane/AiPaneGenerator.tsx +264 -94
- package/templates/src/components/edit/pane/AiPanePreview.tsx +60 -210
- package/templates/src/components/edit/pane/PageGen.tsx +1 -1
- package/templates/src/components/edit/pane/PageGenSelector.tsx +4 -0
- package/templates/src/components/edit/pane/RestylePaneModal.tsx +573 -0
- package/templates/src/components/edit/state/SaveToLibraryModal.tsx +205 -0
- package/templates/src/constants/prompts.json +3 -3
- package/templates/src/stores/selection.ts +4 -0
- package/templates/src/types/compositorTypes.ts +51 -1
- package/templates/src/types/tractstack.ts +36 -31
- package/templates/src/utils/aai/getTitleSlug.ts +1 -1
- package/templates/src/utils/api/brandConfig.ts +8 -2
- package/templates/src/utils/api/brandHelpers.ts +4 -0
- package/templates/src/utils/compositor/aiPaneParser.ts +39 -13
- package/templates/src/utils/compositor/designLibraryHelper.ts +331 -0
- package/templates/src/utils/compositor/processMarkdown.ts +1 -1
- package/utils/inject-files.ts +22 -0
|
@@ -2,15 +2,19 @@ import { useState, useCallback } from 'react';
|
|
|
2
2
|
import { AiPanePreview } from './AiPanePreview';
|
|
3
3
|
import type { TemplatePane } from '@/types/compositorTypes';
|
|
4
4
|
import prompts from '@/constants/prompts.json';
|
|
5
|
-
import
|
|
5
|
+
import ColorPickerCombo from '@/components/fields/ColorPickerCombo';
|
|
6
|
+
import { parseAiPane } from '@/utils/compositor/aiPaneParser';
|
|
7
|
+
import { classNames } from '@/utils/helpers';
|
|
8
|
+
import type { BrandConfig } from '@/types/tractstack';
|
|
6
9
|
|
|
7
10
|
interface AiPaneGeneratorProps {
|
|
8
11
|
ownerId: string;
|
|
9
12
|
onComplete: (pane: TemplatePane) => void;
|
|
10
13
|
onCancel: () => void;
|
|
14
|
+
config?: BrandConfig;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
type GenerationStep = '
|
|
17
|
+
type GenerationStep = 'input' | 'preview' | 'loading';
|
|
14
18
|
type CopyMode = 'prompt' | 'raw';
|
|
15
19
|
|
|
16
20
|
interface GenerationResponse {
|
|
@@ -21,25 +25,41 @@ interface GenerationResponse {
|
|
|
21
25
|
error?: string;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
const
|
|
28
|
+
const harmonyOptions = [
|
|
29
|
+
'Analogous',
|
|
30
|
+
'Monochromatic',
|
|
31
|
+
'Complementary',
|
|
32
|
+
'Triadic',
|
|
33
|
+
];
|
|
34
|
+
const themeOptions = ['Light', 'Dark', 'Bright', 'Muted', 'Pastel', 'Earthy'];
|
|
25
35
|
|
|
26
36
|
export function AiPaneGenerator({
|
|
27
37
|
ownerId,
|
|
28
38
|
onComplete,
|
|
29
39
|
onCancel,
|
|
40
|
+
config,
|
|
30
41
|
}: AiPaneGeneratorProps) {
|
|
31
|
-
const [currentStep, setCurrentStep] = useState<GenerationStep>('
|
|
32
|
-
const [selectedLayout
|
|
33
|
-
layoutOptions[0]
|
|
34
|
-
);
|
|
42
|
+
const [currentStep, setCurrentStep] = useState<GenerationStep>('input');
|
|
43
|
+
const [selectedLayout] = useState<string>('Text Only');
|
|
35
44
|
const [copyMode, setCopyMode] = useState<CopyMode>('prompt');
|
|
36
45
|
const [copyPrompt, setCopyPrompt] = useState('');
|
|
37
46
|
const [rawCopy, setRawCopy] = useState('');
|
|
38
|
-
const [designPrompt, setDesignPrompt] = useState('');
|
|
39
47
|
const [generatedShell, setGeneratedShell] = useState<string | null>(null);
|
|
40
48
|
const [generatedCopy, setGeneratedCopy] = useState<string | null>(null);
|
|
41
49
|
const [error, setError] = useState<string | null>(null);
|
|
42
50
|
|
|
51
|
+
const [selectedHarmony, setSelectedHarmony] = useState<string>(
|
|
52
|
+
harmonyOptions[0]
|
|
53
|
+
);
|
|
54
|
+
const [baseColor, setBaseColor] = useState<string>('');
|
|
55
|
+
const [accentColor, setAccentColor] = useState<string>('');
|
|
56
|
+
const [selectedTheme, setSelectedTheme] = useState<string>(themeOptions[0]);
|
|
57
|
+
const [additionalNotes, setAdditionalNotes] = useState<string>('');
|
|
58
|
+
|
|
59
|
+
const [isInjectMode, setIsInjectMode] = useState(false);
|
|
60
|
+
const [injectShell, setInjectShell] = useState('');
|
|
61
|
+
const [injectCopy, setInjectCopy] = useState('');
|
|
62
|
+
|
|
43
63
|
const callAskLemurAPI = useCallback(
|
|
44
64
|
async (
|
|
45
65
|
prompt: string,
|
|
@@ -53,7 +73,7 @@ export function AiPaneGenerator({
|
|
|
53
73
|
const requestBody = {
|
|
54
74
|
prompt: prompt,
|
|
55
75
|
input_text: context,
|
|
56
|
-
final_model: '
|
|
76
|
+
final_model: '',
|
|
57
77
|
temperature: 0.5,
|
|
58
78
|
max_tokens: 2000,
|
|
59
79
|
};
|
|
@@ -93,12 +113,10 @@ export function AiPaneGenerator({
|
|
|
93
113
|
|
|
94
114
|
let rawResponseData = result.data.response;
|
|
95
115
|
|
|
96
|
-
// Handle case where API returns JSON object for shell
|
|
97
116
|
if (expectJson && typeof rawResponseData === 'object') {
|
|
98
|
-
return JSON.stringify(rawResponseData);
|
|
117
|
+
return JSON.stringify(rawResponseData);
|
|
99
118
|
}
|
|
100
119
|
|
|
101
|
-
// Handle case where API returns string (potentially wrapped)
|
|
102
120
|
if (typeof rawResponseData === 'string') {
|
|
103
121
|
let responseString = rawResponseData;
|
|
104
122
|
try {
|
|
@@ -116,10 +134,9 @@ export function AiPaneGenerator({
|
|
|
116
134
|
} catch (e) {
|
|
117
135
|
/* Ignore stripping errors */
|
|
118
136
|
}
|
|
119
|
-
return responseString;
|
|
137
|
+
return responseString;
|
|
120
138
|
}
|
|
121
139
|
|
|
122
|
-
// Fallback if response is neither expected string nor object
|
|
123
140
|
throw new Error('Unexpected response format received from API.');
|
|
124
141
|
},
|
|
125
142
|
[]
|
|
@@ -131,6 +148,17 @@ export function AiPaneGenerator({
|
|
|
131
148
|
setGeneratedShell(null);
|
|
132
149
|
setGeneratedCopy(null);
|
|
133
150
|
|
|
151
|
+
let designInput = `Generate a design using a **${selectedHarmony.toLowerCase()}** color scheme with a **${selectedTheme.toLowerCase()}** theme.`;
|
|
152
|
+
if (baseColor) {
|
|
153
|
+
designInput += ` Base the colors around **${baseColor}**.`;
|
|
154
|
+
}
|
|
155
|
+
if (accentColor) {
|
|
156
|
+
designInput += ` Use **${accentColor}** as an accent color.`;
|
|
157
|
+
}
|
|
158
|
+
if (additionalNotes) {
|
|
159
|
+
designInput += ` Refine the design with these additional notes: "${additionalNotes}"`;
|
|
160
|
+
}
|
|
161
|
+
|
|
134
162
|
try {
|
|
135
163
|
const shellPromptDetails = prompts.aiPaneShellPrompt;
|
|
136
164
|
const copyPromptDetails = prompts.aiPaneCopyPrompt;
|
|
@@ -142,11 +170,8 @@ export function AiPaneGenerator({
|
|
|
142
170
|
throw new Error('AI prompts not found or incomplete in prompts.json');
|
|
143
171
|
}
|
|
144
172
|
|
|
145
|
-
// --- This is the updated (sequential) logic ---
|
|
146
|
-
|
|
147
|
-
// 1. Prepare and call the Shell API first
|
|
148
173
|
const formattedShellPrompt = shellPromptDetails.user_template
|
|
149
|
-
.replace('{{DESIGN_INPUT}}',
|
|
174
|
+
.replace('{{DESIGN_INPUT}}', designInput)
|
|
150
175
|
.replace('{{LAYOUT_TYPE}}', selectedLayout);
|
|
151
176
|
|
|
152
177
|
const shellResult = await callAskLemurAPI(
|
|
@@ -154,35 +179,34 @@ export function AiPaneGenerator({
|
|
|
154
179
|
shellPromptDetails.system || '',
|
|
155
180
|
true
|
|
156
181
|
);
|
|
157
|
-
setGeneratedShell(shellResult);
|
|
182
|
+
setGeneratedShell(shellResult);
|
|
158
183
|
|
|
159
|
-
// 2. NOW, create the copy prompt, injecting the shellResult
|
|
160
184
|
const copyInputContent = copyMode === 'prompt' ? copyPrompt : rawCopy;
|
|
161
|
-
// Note: Assumes prompts.json's aiPaneCopyPrompt.user_template now includes {{SHELL_JSON}}
|
|
162
185
|
const formattedCopyPrompt = copyPromptDetails.user_template
|
|
163
186
|
.replace('{{COPY_INPUT}}', copyInputContent)
|
|
164
|
-
.replace('{{DESIGN_INPUT}}',
|
|
187
|
+
.replace('{{DESIGN_INPUT}}', designInput)
|
|
165
188
|
.replace('{{LAYOUT_TYPE}}', selectedLayout)
|
|
166
|
-
.replace('{{SHELL_JSON}}', shellResult);
|
|
189
|
+
.replace('{{SHELL_JSON}}', shellResult);
|
|
167
190
|
|
|
168
|
-
// 3. Call Copy API second, using the fully-formed prompt
|
|
169
191
|
const copyResult = await callAskLemurAPI(
|
|
170
192
|
formattedCopyPrompt,
|
|
171
193
|
copyPromptDetails.system || '',
|
|
172
194
|
false
|
|
173
195
|
);
|
|
174
|
-
setGeneratedCopy(copyResult);
|
|
196
|
+
setGeneratedCopy(copyResult);
|
|
175
197
|
|
|
176
198
|
setCurrentStep('preview');
|
|
177
|
-
|
|
178
|
-
// --- End of updated logic ---
|
|
179
199
|
} catch (err: any) {
|
|
180
200
|
console.error('AI Pane Generation Error:', err);
|
|
181
201
|
setError(err.message || 'Failed to generate AI pane.');
|
|
182
202
|
setCurrentStep('input');
|
|
183
203
|
}
|
|
184
204
|
}, [
|
|
185
|
-
|
|
205
|
+
selectedHarmony,
|
|
206
|
+
baseColor,
|
|
207
|
+
accentColor,
|
|
208
|
+
selectedTheme,
|
|
209
|
+
additionalNotes,
|
|
186
210
|
selectedLayout,
|
|
187
211
|
copyMode,
|
|
188
212
|
copyPrompt,
|
|
@@ -190,12 +214,51 @@ export function AiPaneGenerator({
|
|
|
190
214
|
callAskLemurAPI,
|
|
191
215
|
]);
|
|
192
216
|
|
|
217
|
+
const handleInject = useCallback(() => {
|
|
218
|
+
setError(null);
|
|
219
|
+
if (!injectShell || !injectCopy) {
|
|
220
|
+
setError('Both Shell JSON and Copy HTML must be provided.');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
const shellResponse = JSON.parse(injectShell);
|
|
225
|
+
const copyResponse = JSON.parse(injectCopy);
|
|
226
|
+
|
|
227
|
+
const shellPayloadString = JSON.stringify(shellResponse?.data?.response);
|
|
228
|
+
const copyPayloadString = copyResponse?.data?.response;
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
!shellPayloadString ||
|
|
232
|
+
shellPayloadString === 'null' ||
|
|
233
|
+
typeof copyPayloadString !== 'string'
|
|
234
|
+
) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
'Payloads are in an unexpected format. Could not find "data.response".'
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const pane = parseAiPane(
|
|
241
|
+
shellPayloadString,
|
|
242
|
+
copyPayloadString,
|
|
243
|
+
selectedLayout
|
|
244
|
+
);
|
|
245
|
+
onComplete(pane);
|
|
246
|
+
} catch (err: any) {
|
|
247
|
+
console.error('Payload Injection Error:', err);
|
|
248
|
+
setError(err.message || 'Failed to parse payloads. Check JSON format.');
|
|
249
|
+
}
|
|
250
|
+
}, [injectShell, injectCopy, selectedLayout, onComplete]);
|
|
251
|
+
|
|
193
252
|
const handleBack = () => {
|
|
194
253
|
setError(null);
|
|
195
254
|
if (currentStep === 'preview') {
|
|
196
255
|
setCurrentStep('input');
|
|
197
256
|
} else if (currentStep === 'input') {
|
|
198
|
-
|
|
257
|
+
if (isInjectMode) {
|
|
258
|
+
setIsInjectMode(false);
|
|
259
|
+
} else {
|
|
260
|
+
onCancel();
|
|
261
|
+
}
|
|
199
262
|
} else if (currentStep === 'loading') {
|
|
200
263
|
setCurrentStep('input');
|
|
201
264
|
}
|
|
@@ -230,72 +293,166 @@ export function AiPaneGenerator({
|
|
|
230
293
|
);
|
|
231
294
|
}
|
|
232
295
|
|
|
233
|
-
if (currentStep === '
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
296
|
+
if (currentStep === 'input') {
|
|
297
|
+
if (isInjectMode) {
|
|
298
|
+
return (
|
|
299
|
+
<div className="space-y-6 p-4">
|
|
300
|
+
<div>
|
|
301
|
+
<label
|
|
302
|
+
htmlFor="shell-json"
|
|
303
|
+
className="block text-lg font-semibold text-gray-800"
|
|
304
|
+
>
|
|
305
|
+
Shell JSON Payload
|
|
306
|
+
</label>
|
|
307
|
+
<textarea
|
|
308
|
+
id="shell-json"
|
|
309
|
+
value={injectShell}
|
|
310
|
+
onChange={(e) => setInjectShell(e.target.value)}
|
|
311
|
+
placeholder="Paste raw API response for ShellJson here..."
|
|
312
|
+
rows={8}
|
|
313
|
+
className="mt-2 block w-full rounded-md border-gray-300 p-2 font-mono text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
|
|
314
|
+
/>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div>
|
|
318
|
+
<label
|
|
319
|
+
htmlFor="copy-html"
|
|
320
|
+
className="block text-lg font-semibold text-gray-800"
|
|
321
|
+
>
|
|
322
|
+
Copy HTML Payload
|
|
323
|
+
</label>
|
|
324
|
+
<textarea
|
|
325
|
+
id="copy-html"
|
|
326
|
+
value={injectCopy}
|
|
327
|
+
onChange={(e) => setInjectCopy(e.target.value)}
|
|
328
|
+
placeholder="Paste raw API response for copyHtml here..."
|
|
329
|
+
rows={8}
|
|
330
|
+
className="mt-2 block w-full rounded-md border-gray-300 p-2 font-mono text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
|
|
331
|
+
/>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
335
|
+
|
|
336
|
+
<div className="flex justify-between pt-4">
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
onClick={handleBack}
|
|
340
|
+
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2"
|
|
341
|
+
>
|
|
342
|
+
Back to Generator
|
|
343
|
+
</button>
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
onClick={handleInject}
|
|
347
|
+
disabled={!injectShell || !injectCopy}
|
|
348
|
+
className={`rounded-md border border-transparent px-4 py-2 text-sm font-bold text-white shadow-sm ${
|
|
349
|
+
!injectShell || !injectCopy
|
|
350
|
+
? 'cursor-not-allowed bg-gray-400'
|
|
351
|
+
: 'bg-cyan-600 hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2'
|
|
352
|
+
}`}
|
|
353
|
+
>
|
|
354
|
+
Create from Payloads
|
|
355
|
+
</button>
|
|
356
|
+
</div>
|
|
275
357
|
</div>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
358
|
+
);
|
|
359
|
+
}
|
|
279
360
|
|
|
280
|
-
if (currentStep === 'input') {
|
|
281
361
|
return (
|
|
282
362
|
<div className="space-y-6 p-4">
|
|
363
|
+
<div>
|
|
364
|
+
<label className="block text-lg font-semibold text-gray-800">
|
|
365
|
+
Color Harmony
|
|
366
|
+
</label>
|
|
367
|
+
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-2">
|
|
368
|
+
{harmonyOptions.map((option) => (
|
|
369
|
+
<div key={option} className="flex items-center space-x-2">
|
|
370
|
+
<input
|
|
371
|
+
type="radio"
|
|
372
|
+
id={`harmony-${option}`}
|
|
373
|
+
name="harmonyOptions"
|
|
374
|
+
value={option}
|
|
375
|
+
checked={selectedHarmony === option}
|
|
376
|
+
onChange={(e) => setSelectedHarmony(e.target.value)}
|
|
377
|
+
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
378
|
+
/>
|
|
379
|
+
<label
|
|
380
|
+
htmlFor={`harmony-${option}`}
|
|
381
|
+
className="text-sm font-medium text-gray-700"
|
|
382
|
+
>
|
|
383
|
+
{option}
|
|
384
|
+
</label>
|
|
385
|
+
</div>
|
|
386
|
+
))}
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
391
|
+
<div>
|
|
392
|
+
<ColorPickerCombo
|
|
393
|
+
title="Base Color (Optional)"
|
|
394
|
+
config={config!}
|
|
395
|
+
defaultColor={baseColor}
|
|
396
|
+
onColorChange={setBaseColor}
|
|
397
|
+
allowNull={true}
|
|
398
|
+
/>
|
|
399
|
+
</div>
|
|
400
|
+
<div>
|
|
401
|
+
<ColorPickerCombo
|
|
402
|
+
title="Accent Color (Optional)"
|
|
403
|
+
config={config!}
|
|
404
|
+
defaultColor={accentColor}
|
|
405
|
+
onColorChange={setAccentColor}
|
|
406
|
+
allowNull={true}
|
|
407
|
+
/>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
<div>
|
|
412
|
+
<label className="block text-lg font-semibold text-gray-800">
|
|
413
|
+
Theme / Mood
|
|
414
|
+
</label>
|
|
415
|
+
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-2">
|
|
416
|
+
{themeOptions.map((option) => (
|
|
417
|
+
<div key={option} className="flex items-center space-x-2">
|
|
418
|
+
<input
|
|
419
|
+
type="radio"
|
|
420
|
+
id={`theme-${option}`}
|
|
421
|
+
name="themeOptions"
|
|
422
|
+
value={option}
|
|
423
|
+
checked={selectedTheme === option}
|
|
424
|
+
onChange={(e) => setSelectedTheme(e.target.value)}
|
|
425
|
+
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
426
|
+
/>
|
|
427
|
+
<label
|
|
428
|
+
htmlFor={`theme-${option}`}
|
|
429
|
+
className="text-sm font-medium text-gray-700"
|
|
430
|
+
>
|
|
431
|
+
{option}
|
|
432
|
+
</label>
|
|
433
|
+
</div>
|
|
434
|
+
))}
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
283
438
|
<div>
|
|
284
439
|
<label
|
|
285
|
-
htmlFor="
|
|
440
|
+
htmlFor="additional-notes"
|
|
286
441
|
className="block text-lg font-semibold text-gray-800"
|
|
287
442
|
>
|
|
288
|
-
|
|
443
|
+
Additional Design Notes (Optional)
|
|
289
444
|
</label>
|
|
290
445
|
<p className="mb-2 mt-1 text-sm text-gray-500">
|
|
291
|
-
|
|
446
|
+
Add specific requests like "use rounded corners", "add subtle
|
|
447
|
+
texture".
|
|
292
448
|
</p>
|
|
293
|
-
<
|
|
294
|
-
id="
|
|
295
|
-
value={
|
|
296
|
-
onChange={
|
|
297
|
-
placeholder="Enter
|
|
298
|
-
|
|
449
|
+
<textarea
|
|
450
|
+
id="additional-notes"
|
|
451
|
+
value={additionalNotes}
|
|
452
|
+
onChange={(e) => setAdditionalNotes(e.target.value)}
|
|
453
|
+
placeholder="Enter additional notes..."
|
|
454
|
+
rows={3}
|
|
455
|
+
className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
|
|
299
456
|
/>
|
|
300
457
|
</div>
|
|
301
458
|
|
|
@@ -380,21 +537,34 @@ export function AiPaneGenerator({
|
|
|
380
537
|
onClick={handleBack}
|
|
381
538
|
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2"
|
|
382
539
|
>
|
|
383
|
-
|
|
540
|
+
Cancel
|
|
384
541
|
</button>
|
|
385
542
|
<button
|
|
386
543
|
type="button"
|
|
387
544
|
onClick={handleGenerate}
|
|
388
|
-
disabled={
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
? 'cursor-not-allowed bg-gray-
|
|
394
|
-
: 'bg-cyan-600 hover:bg-cyan-700
|
|
395
|
-
}
|
|
545
|
+
disabled={copyMode === 'prompt' ? !copyPrompt : !rawCopy}
|
|
546
|
+
className={classNames(
|
|
547
|
+
`rounded-md border border-transparent px-4 py-2 text-sm font-bold shadow-sm transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2`,
|
|
548
|
+
(copyMode === 'prompt' && !copyPrompt) ||
|
|
549
|
+
(copyMode === `raw` && !rawCopy)
|
|
550
|
+
? 'cursor-not-allowed bg-gray-300 text-gray-500'
|
|
551
|
+
: 'bg-cyan-600 text-white hover:bg-cyan-700'
|
|
552
|
+
)}
|
|
553
|
+
>
|
|
554
|
+
Generate Pane
|
|
555
|
+
</button>
|
|
556
|
+
</div>
|
|
557
|
+
|
|
558
|
+
<div className="border-t border-gray-200 pt-4 text-center">
|
|
559
|
+
<button
|
|
560
|
+
type="button"
|
|
561
|
+
onClick={() => {
|
|
562
|
+
setError(null);
|
|
563
|
+
setIsInjectMode(true);
|
|
564
|
+
}}
|
|
565
|
+
className="text-sm text-cyan-600 hover:text-cyan-800 hover:underline"
|
|
396
566
|
>
|
|
397
|
-
|
|
567
|
+
Direct Inject Payload
|
|
398
568
|
</button>
|
|
399
569
|
</div>
|
|
400
570
|
</div>
|