astro-tractstack 2.0.11 → 2.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +8 -14
- package/package.json +1 -1
- package/templates/css/storykeep.css +1 -92872
- package/templates/src/components/compositor/Compositor.tsx +14 -6
- package/templates/src/components/compositor/Node.tsx +42 -2
- package/templates/src/components/compositor/nodes/Pane.tsx +0 -5
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +2 -1
- package/templates/src/components/edit/pane/AddPanePanel.tsx +5 -1
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +140 -116
- package/templates/src/components/edit/pane/AiPaneGenerator.tsx +575 -0
- package/templates/src/components/edit/pane/AiPanePreview.tsx +107 -0
- package/templates/src/components/edit/pane/PageGenSelector.tsx +10 -3
- package/templates/src/constants/prompts.json +35 -40
- package/templates/src/stores/nodes.ts +18 -15
- package/templates/src/types/compositorTypes.ts +27 -13
- package/templates/src/utils/compositor/aiPaneParser.ts +613 -0
- package/templates/src/utils/compositor/allowInsert.ts +1 -3
- package/templates/src/utils/compositor/nodesHelper.ts +61 -42
- package/templates/src/utils/compositor/tailwindClasses.ts +200 -70
- package/utils/inject-files.ts +8 -14
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy.tsx +0 -107
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +0 -217
- package/templates/src/components/edit/pane/AddPanePanel_newCopyMode.tsx +0 -109
|
@@ -0,0 +1,575 @@
|
|
|
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: 'anthropic/claude-3-5-sonnet',
|
|
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
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import type { TemplatePane } from '@/types/compositorTypes';
|
|
3
|
+
import { parseAiPane } from '@/utils/compositor/aiPaneParser';
|
|
4
|
+
|
|
5
|
+
interface AiPanePreviewProps {
|
|
6
|
+
shellJson: string;
|
|
7
|
+
copyHtml: string;
|
|
8
|
+
layout: string;
|
|
9
|
+
ownerId: string;
|
|
10
|
+
onComplete: (pane: TemplatePane) => void;
|
|
11
|
+
onBack: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function AiPanePreview({
|
|
15
|
+
shellJson,
|
|
16
|
+
copyHtml,
|
|
17
|
+
layout,
|
|
18
|
+
onComplete,
|
|
19
|
+
onBack,
|
|
20
|
+
}: AiPanePreviewProps) {
|
|
21
|
+
const [error, setError] = useState<string | null>(null);
|
|
22
|
+
const [hasCompleted, setHasCompleted] = useState<boolean>(false);
|
|
23
|
+
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setError(null);
|
|
27
|
+
setHasCompleted(false);
|
|
28
|
+
setIsLoading(true);
|
|
29
|
+
let isActive = true;
|
|
30
|
+
|
|
31
|
+
if (shellJson && copyHtml) {
|
|
32
|
+
try {
|
|
33
|
+
const pane = parseAiPane(shellJson, copyHtml, layout);
|
|
34
|
+
if (isActive && !hasCompleted) {
|
|
35
|
+
onComplete(pane);
|
|
36
|
+
setHasCompleted(true);
|
|
37
|
+
setIsLoading(false);
|
|
38
|
+
}
|
|
39
|
+
} catch (err: any) {
|
|
40
|
+
console.error('Error parsing AI Pane:', err);
|
|
41
|
+
if (isActive) {
|
|
42
|
+
setError(err.message || 'Failed to parse generated content.');
|
|
43
|
+
setIsLoading(false);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
// Handle case where inputs might be initially empty
|
|
48
|
+
setIsLoading(false);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
isActive = false;
|
|
53
|
+
};
|
|
54
|
+
}, [shellJson, copyHtml, layout, onComplete, hasCompleted]);
|
|
55
|
+
|
|
56
|
+
const displayContent = useMemo(() => {
|
|
57
|
+
if (isLoading) {
|
|
58
|
+
return (
|
|
59
|
+
<div className="p-4 text-center text-gray-500">
|
|
60
|
+
<div className="mx-auto mb-2 h-8 w-8 animate-spin rounded-full border-b-2 border-gray-400"></div>
|
|
61
|
+
<p className="text-sm">Processing...</p>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (error) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="p-4 text-center text-red-600">
|
|
68
|
+
<p className="font-semibold">Error:</p>
|
|
69
|
+
<p className="mt-1 text-sm">{error}</p>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (hasCompleted) {
|
|
74
|
+
return (
|
|
75
|
+
<div className="p-4 text-center text-green-700">
|
|
76
|
+
<p className="font-semibold">Pane Applied Successfully!</p>
|
|
77
|
+
<p className="mt-1 text-sm">
|
|
78
|
+
You can now go back or continue editing.
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
// Fallback/initial state before useEffect runs if needed
|
|
84
|
+
return (
|
|
85
|
+
<div className="p-4 text-center text-gray-500">
|
|
86
|
+
<p className="text-sm">Preparing...</p>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}, [isLoading, error, hasCompleted]);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="flex h-full flex-col p-4">
|
|
93
|
+
<div className="relative mb-4 flex min-h-[200px] flex-grow items-center justify-center overflow-auto rounded border bg-gray-50">
|
|
94
|
+
{displayContent}
|
|
95
|
+
</div>
|
|
96
|
+
<div className="flex flex-shrink-0 justify-start">
|
|
97
|
+
<button
|
|
98
|
+
onClick={onBack}
|
|
99
|
+
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
|
100
|
+
type="button"
|
|
101
|
+
>
|
|
102
|
+
Back
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|