astro-tractstack 2.0.15 → 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.
- package/dist/index.js +27 -13
- package/package.json +1 -1
- package/templates/custom/with-examples/CodeHook.astro +4 -0
- package/templates/custom/with-examples/SandboxLauncher.tsx +65 -0
- package/templates/env.example +3 -0
- package/templates/src/components/codehooks/SandboxAuthWrapper.tsx +75 -0
- package/templates/src/components/codehooks/SandboxRegisterForm.tsx +202 -0
- package/templates/src/components/compositor/Compositor.tsx +2 -0
- package/templates/src/components/compositor/Node.tsx +6 -1
- package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +13 -11
- package/templates/src/components/compositor/nodes/Pane_layout.tsx +16 -14
- package/templates/src/components/edit/Header.tsx +8 -2
- package/templates/src/components/edit/PanelSwitch.tsx +4 -4
- package/templates/src/components/edit/pane/AddPanePanel.tsx +3 -0
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +28 -24
- package/templates/src/components/edit/panels/StyleImagePanel.tsx +10 -8
- package/templates/src/components/edit/state/SaveModal.tsx +41 -0
- package/templates/src/pages/api/sandbox.ts +86 -0
- package/templates/src/pages/sandbox.astro +137 -0
- package/templates/src/types/nodeProps.ts +1 -0
- package/templates/src/utils/profileStorage.ts +13 -0
- package/utils/inject-files.ts +27 -14
- package/templates/src/components/edit/pane/AiPaneGenerator.tsx +0 -512
- package/templates/src/components/edit/pane/AiPanePreview.tsx +0 -107
- package/templates/src/utils/aai/getTitleSlug.ts +0 -72
|
@@ -1,512 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react';
|
|
2
|
-
import prompts from '@/constants/prompts.json';
|
|
3
|
-
import ColorPickerCombo from '@/components/fields/ColorPickerCombo';
|
|
4
|
-
import { AiPanePreview } from './AiPanePreview';
|
|
5
|
-
import { CopyInputStep } from './steps/CopyInputStep';
|
|
6
|
-
import { parseAiPane } from '@/utils/compositor/aiPaneParser';
|
|
7
|
-
import { classNames } from '@/utils/helpers';
|
|
8
|
-
import type { TemplatePane } from '@/types/compositorTypes';
|
|
9
|
-
import type { BrandConfig } from '@/types/tractstack';
|
|
10
|
-
|
|
11
|
-
interface AiPaneGeneratorProps {
|
|
12
|
-
ownerId: string;
|
|
13
|
-
onComplete: (pane: TemplatePane) => void;
|
|
14
|
-
onCancel: () => void;
|
|
15
|
-
config?: BrandConfig;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type GenerationStep = 'input' | 'preview' | 'loading';
|
|
19
|
-
type CopyMode = 'prompt' | 'raw';
|
|
20
|
-
|
|
21
|
-
interface GenerationResponse {
|
|
22
|
-
success: boolean;
|
|
23
|
-
data?: {
|
|
24
|
-
response: string | object;
|
|
25
|
-
};
|
|
26
|
-
error?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const harmonyOptions = [
|
|
30
|
-
'Analogous',
|
|
31
|
-
'Monochromatic',
|
|
32
|
-
'Complementary',
|
|
33
|
-
'Triadic',
|
|
34
|
-
];
|
|
35
|
-
const themeOptions = ['Light', 'Dark', 'Bright', 'Muted', 'Pastel', 'Earthy'];
|
|
36
|
-
|
|
37
|
-
export function AiPaneGenerator({
|
|
38
|
-
ownerId,
|
|
39
|
-
onComplete,
|
|
40
|
-
onCancel,
|
|
41
|
-
config,
|
|
42
|
-
}: AiPaneGeneratorProps) {
|
|
43
|
-
const [currentStep, setCurrentStep] = useState<GenerationStep>('input');
|
|
44
|
-
const [selectedLayout] = useState<string>('Text Only');
|
|
45
|
-
const [copyMode, setCopyMode] = useState<CopyMode>('prompt');
|
|
46
|
-
const [copyPrompt, setCopyPrompt] = useState('');
|
|
47
|
-
const [rawCopy, setRawCopy] = useState('');
|
|
48
|
-
const [generatedShell, setGeneratedShell] = useState<string | null>(null);
|
|
49
|
-
const [generatedCopy, setGeneratedCopy] = useState<string | null>(null);
|
|
50
|
-
const [error, setError] = useState<string | null>(null);
|
|
51
|
-
|
|
52
|
-
const [selectedHarmony, setSelectedHarmony] = useState<string>(
|
|
53
|
-
harmonyOptions[0]
|
|
54
|
-
);
|
|
55
|
-
const [baseColor, setBaseColor] = useState<string>('');
|
|
56
|
-
const [accentColor, setAccentColor] = useState<string>('');
|
|
57
|
-
const [selectedTheme, setSelectedTheme] = useState<string>(themeOptions[0]);
|
|
58
|
-
const [additionalNotes, setAdditionalNotes] = useState<string>('');
|
|
59
|
-
|
|
60
|
-
const [isInjectMode, setIsInjectMode] = useState(false);
|
|
61
|
-
const [injectShell, setInjectShell] = useState('');
|
|
62
|
-
const [injectCopy, setInjectCopy] = useState('');
|
|
63
|
-
|
|
64
|
-
const callAskLemurAPI = useCallback(
|
|
65
|
-
async (
|
|
66
|
-
prompt: string,
|
|
67
|
-
context: string,
|
|
68
|
-
expectJson: boolean
|
|
69
|
-
): Promise<string> => {
|
|
70
|
-
const goBackend =
|
|
71
|
-
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
72
|
-
const tenantId = import.meta.env.PUBLIC_TENANTID || 'default';
|
|
73
|
-
|
|
74
|
-
const requestBody = {
|
|
75
|
-
prompt: prompt,
|
|
76
|
-
input_text: context,
|
|
77
|
-
final_model: '',
|
|
78
|
-
temperature: 0.5,
|
|
79
|
-
max_tokens: 2000,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const response = await fetch(`${goBackend}/api/v1/aai/askLemur`, {
|
|
83
|
-
method: 'POST',
|
|
84
|
-
headers: {
|
|
85
|
-
'Content-Type': 'application/json',
|
|
86
|
-
'X-Tenant-ID': tenantId,
|
|
87
|
-
},
|
|
88
|
-
credentials: 'include',
|
|
89
|
-
body: JSON.stringify(requestBody),
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
if (!response.ok) {
|
|
93
|
-
const errorText = await response.text();
|
|
94
|
-
console.error('AskLemur API Error Response:', errorText);
|
|
95
|
-
let backendError = `API call failed: ${response.status} ${response.statusText}`;
|
|
96
|
-
try {
|
|
97
|
-
const errorJson = JSON.parse(errorText);
|
|
98
|
-
if (errorJson && errorJson.error) {
|
|
99
|
-
backendError = errorJson.error;
|
|
100
|
-
}
|
|
101
|
-
} catch (e) {
|
|
102
|
-
/* Ignore */
|
|
103
|
-
}
|
|
104
|
-
throw new Error(backendError);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const result = (await response.json()) as GenerationResponse;
|
|
108
|
-
|
|
109
|
-
if (!result.success || !result.data?.response) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
result.error || 'Generation failed to return valid response.'
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
let rawResponseData = result.data.response;
|
|
116
|
-
|
|
117
|
-
if (expectJson && typeof rawResponseData === 'object') {
|
|
118
|
-
return JSON.stringify(rawResponseData);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (typeof rawResponseData === 'string') {
|
|
122
|
-
let responseString = rawResponseData;
|
|
123
|
-
try {
|
|
124
|
-
if (
|
|
125
|
-
responseString.startsWith('```json') &&
|
|
126
|
-
responseString.endsWith('```')
|
|
127
|
-
) {
|
|
128
|
-
responseString = responseString.slice(7, -3).trim();
|
|
129
|
-
} else if (
|
|
130
|
-
responseString.startsWith('```html') &&
|
|
131
|
-
responseString.endsWith('```')
|
|
132
|
-
) {
|
|
133
|
-
responseString = responseString.slice(7, -3).trim();
|
|
134
|
-
}
|
|
135
|
-
} catch (e) {
|
|
136
|
-
/* Ignore stripping errors */
|
|
137
|
-
}
|
|
138
|
-
return responseString;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
throw new Error('Unexpected response format received from API.');
|
|
142
|
-
},
|
|
143
|
-
[]
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
const handleGenerate = useCallback(async () => {
|
|
147
|
-
setError(null);
|
|
148
|
-
setCurrentStep('loading');
|
|
149
|
-
setGeneratedShell(null);
|
|
150
|
-
setGeneratedCopy(null);
|
|
151
|
-
|
|
152
|
-
let designInput = `Generate a design using a **${selectedHarmony.toLowerCase()}** color scheme with a **${selectedTheme.toLowerCase()}** theme.`;
|
|
153
|
-
if (baseColor) {
|
|
154
|
-
designInput += ` Base the colors around **${baseColor}**.`;
|
|
155
|
-
}
|
|
156
|
-
if (accentColor) {
|
|
157
|
-
designInput += ` Use **${accentColor}** as an accent color.`;
|
|
158
|
-
}
|
|
159
|
-
if (additionalNotes) {
|
|
160
|
-
designInput += ` Refine the design with these additional notes: "${additionalNotes}"`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const shellPromptDetails = prompts.aiPaneShellPrompt;
|
|
165
|
-
const copyPromptDetails = prompts.aiPaneCopyPrompt;
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
!shellPromptDetails?.user_template ||
|
|
169
|
-
!copyPromptDetails?.user_template
|
|
170
|
-
) {
|
|
171
|
-
throw new Error('AI prompts not found or incomplete in prompts.json');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const formattedShellPrompt = shellPromptDetails.user_template
|
|
175
|
-
.replace('{{DESIGN_INPUT}}', designInput)
|
|
176
|
-
.replace('{{LAYOUT_TYPE}}', selectedLayout);
|
|
177
|
-
|
|
178
|
-
const shellResult = await callAskLemurAPI(
|
|
179
|
-
formattedShellPrompt,
|
|
180
|
-
shellPromptDetails.system || '',
|
|
181
|
-
true
|
|
182
|
-
);
|
|
183
|
-
setGeneratedShell(shellResult);
|
|
184
|
-
|
|
185
|
-
const copyInputContent = copyMode === 'prompt' ? copyPrompt : rawCopy;
|
|
186
|
-
const formattedCopyPrompt = copyPromptDetails.user_template
|
|
187
|
-
.replace('{{COPY_INPUT}}', copyInputContent)
|
|
188
|
-
.replace('{{DESIGN_INPUT}}', designInput)
|
|
189
|
-
.replace('{{LAYOUT_TYPE}}', selectedLayout)
|
|
190
|
-
.replace('{{SHELL_JSON}}', shellResult);
|
|
191
|
-
|
|
192
|
-
const copyResult = await callAskLemurAPI(
|
|
193
|
-
formattedCopyPrompt,
|
|
194
|
-
copyPromptDetails.system || '',
|
|
195
|
-
false
|
|
196
|
-
);
|
|
197
|
-
setGeneratedCopy(copyResult);
|
|
198
|
-
|
|
199
|
-
setCurrentStep('preview');
|
|
200
|
-
} catch (err: any) {
|
|
201
|
-
console.error('AI Pane Generation Error:', err);
|
|
202
|
-
setError(err.message || 'Failed to generate AI pane.');
|
|
203
|
-
setCurrentStep('input');
|
|
204
|
-
}
|
|
205
|
-
}, [
|
|
206
|
-
selectedHarmony,
|
|
207
|
-
baseColor,
|
|
208
|
-
accentColor,
|
|
209
|
-
selectedTheme,
|
|
210
|
-
additionalNotes,
|
|
211
|
-
selectedLayout,
|
|
212
|
-
copyMode,
|
|
213
|
-
copyPrompt,
|
|
214
|
-
rawCopy,
|
|
215
|
-
callAskLemurAPI,
|
|
216
|
-
]);
|
|
217
|
-
|
|
218
|
-
const handleInject = useCallback(() => {
|
|
219
|
-
setError(null);
|
|
220
|
-
if (!injectShell || !injectCopy) {
|
|
221
|
-
setError('Both Shell JSON and Copy HTML must be provided.');
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
const shellResponse = JSON.parse(injectShell);
|
|
226
|
-
const copyResponse = JSON.parse(injectCopy);
|
|
227
|
-
|
|
228
|
-
const shellPayloadString = JSON.stringify(shellResponse?.data?.response);
|
|
229
|
-
const copyPayloadString = copyResponse?.data?.response;
|
|
230
|
-
|
|
231
|
-
if (
|
|
232
|
-
!shellPayloadString ||
|
|
233
|
-
shellPayloadString === 'null' ||
|
|
234
|
-
typeof copyPayloadString !== 'string'
|
|
235
|
-
) {
|
|
236
|
-
throw new Error(
|
|
237
|
-
'Payloads are in an unexpected format. Could not find "data.response".'
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const pane = parseAiPane(
|
|
242
|
-
shellPayloadString,
|
|
243
|
-
copyPayloadString,
|
|
244
|
-
selectedLayout
|
|
245
|
-
);
|
|
246
|
-
onComplete(pane);
|
|
247
|
-
} catch (err: any) {
|
|
248
|
-
console.error('Payload Injection Error:', err);
|
|
249
|
-
setError(err.message || 'Failed to parse payloads. Check JSON format.');
|
|
250
|
-
}
|
|
251
|
-
}, [injectShell, injectCopy, selectedLayout, onComplete]);
|
|
252
|
-
|
|
253
|
-
const handleBack = () => {
|
|
254
|
-
setError(null);
|
|
255
|
-
if (currentStep === 'preview') {
|
|
256
|
-
setCurrentStep('input');
|
|
257
|
-
} else if (currentStep === 'input') {
|
|
258
|
-
if (isInjectMode) {
|
|
259
|
-
setIsInjectMode(false);
|
|
260
|
-
} else {
|
|
261
|
-
onCancel();
|
|
262
|
-
}
|
|
263
|
-
} else if (currentStep === 'loading') {
|
|
264
|
-
setCurrentStep('input');
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
if (currentStep === 'loading') {
|
|
269
|
-
return (
|
|
270
|
-
<div className="flex min-h-[200px] flex-col items-center justify-center space-y-4 p-6">
|
|
271
|
-
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-gray-400"></div>
|
|
272
|
-
<p className="text-sm text-gray-500">Generating AI Pane...</p>
|
|
273
|
-
<button
|
|
274
|
-
type="button"
|
|
275
|
-
onClick={handleBack}
|
|
276
|
-
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"
|
|
277
|
-
>
|
|
278
|
-
Cancel
|
|
279
|
-
</button>
|
|
280
|
-
</div>
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (currentStep === 'preview' && generatedShell && generatedCopy) {
|
|
285
|
-
return (
|
|
286
|
-
<AiPanePreview
|
|
287
|
-
shellJson={generatedShell}
|
|
288
|
-
copyHtml={generatedCopy}
|
|
289
|
-
layout={selectedLayout}
|
|
290
|
-
ownerId={ownerId}
|
|
291
|
-
onComplete={onComplete}
|
|
292
|
-
onBack={handleBack}
|
|
293
|
-
/>
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (currentStep === 'input') {
|
|
298
|
-
if (isInjectMode) {
|
|
299
|
-
return (
|
|
300
|
-
<div className="space-y-6 p-4">
|
|
301
|
-
<div>
|
|
302
|
-
<label
|
|
303
|
-
htmlFor="shell-json"
|
|
304
|
-
className="block text-lg font-bold text-gray-800"
|
|
305
|
-
>
|
|
306
|
-
Shell JSON Payload
|
|
307
|
-
</label>
|
|
308
|
-
<textarea
|
|
309
|
-
id="shell-json"
|
|
310
|
-
value={injectShell}
|
|
311
|
-
onChange={(e) => setInjectShell(e.target.value)}
|
|
312
|
-
placeholder="Paste raw API response for ShellJson here..."
|
|
313
|
-
rows={8}
|
|
314
|
-
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"
|
|
315
|
-
/>
|
|
316
|
-
</div>
|
|
317
|
-
|
|
318
|
-
<div>
|
|
319
|
-
<label
|
|
320
|
-
htmlFor="copy-html"
|
|
321
|
-
className="block text-lg font-bold text-gray-800"
|
|
322
|
-
>
|
|
323
|
-
Copy HTML Payload
|
|
324
|
-
</label>
|
|
325
|
-
<textarea
|
|
326
|
-
id="copy-html"
|
|
327
|
-
value={injectCopy}
|
|
328
|
-
onChange={(e) => setInjectCopy(e.target.value)}
|
|
329
|
-
placeholder="Paste raw API response for copyHtml here..."
|
|
330
|
-
rows={8}
|
|
331
|
-
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"
|
|
332
|
-
/>
|
|
333
|
-
</div>
|
|
334
|
-
|
|
335
|
-
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
336
|
-
|
|
337
|
-
<div className="flex justify-between pt-4">
|
|
338
|
-
<button
|
|
339
|
-
type="button"
|
|
340
|
-
onClick={handleBack}
|
|
341
|
-
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"
|
|
342
|
-
>
|
|
343
|
-
Back to Generator
|
|
344
|
-
</button>
|
|
345
|
-
<button
|
|
346
|
-
type="button"
|
|
347
|
-
onClick={handleInject}
|
|
348
|
-
disabled={!injectShell || !injectCopy}
|
|
349
|
-
className={`rounded-md border border-transparent px-4 py-2 text-sm font-bold text-white shadow-sm ${
|
|
350
|
-
!injectShell || !injectCopy
|
|
351
|
-
? 'cursor-not-allowed bg-gray-400'
|
|
352
|
-
: 'bg-cyan-600 hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2'
|
|
353
|
-
}`}
|
|
354
|
-
>
|
|
355
|
-
Create from Payloads
|
|
356
|
-
</button>
|
|
357
|
-
</div>
|
|
358
|
-
</div>
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return (
|
|
363
|
-
<div className="space-y-6 p-4">
|
|
364
|
-
<div>
|
|
365
|
-
<label className="block text-lg font-bold text-gray-800">
|
|
366
|
-
Color Harmony
|
|
367
|
-
</label>
|
|
368
|
-
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-2">
|
|
369
|
-
{harmonyOptions.map((option) => (
|
|
370
|
-
<div key={option} className="flex items-center space-x-2">
|
|
371
|
-
<input
|
|
372
|
-
type="radio"
|
|
373
|
-
id={`harmony-${option}`}
|
|
374
|
-
name="harmonyOptions"
|
|
375
|
-
value={option}
|
|
376
|
-
checked={selectedHarmony === option}
|
|
377
|
-
onChange={(e) => setSelectedHarmony(e.target.value)}
|
|
378
|
-
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
379
|
-
/>
|
|
380
|
-
<label
|
|
381
|
-
htmlFor={`harmony-${option}`}
|
|
382
|
-
className="text-sm font-bold text-gray-700"
|
|
383
|
-
>
|
|
384
|
-
{option}
|
|
385
|
-
</label>
|
|
386
|
-
</div>
|
|
387
|
-
))}
|
|
388
|
-
</div>
|
|
389
|
-
</div>
|
|
390
|
-
|
|
391
|
-
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
392
|
-
<div>
|
|
393
|
-
<ColorPickerCombo
|
|
394
|
-
title="Base Color (Optional)"
|
|
395
|
-
config={config!}
|
|
396
|
-
defaultColor={baseColor}
|
|
397
|
-
onColorChange={setBaseColor}
|
|
398
|
-
allowNull={true}
|
|
399
|
-
/>
|
|
400
|
-
</div>
|
|
401
|
-
<div>
|
|
402
|
-
<ColorPickerCombo
|
|
403
|
-
title="Accent Color (Optional)"
|
|
404
|
-
config={config!}
|
|
405
|
-
defaultColor={accentColor}
|
|
406
|
-
onColorChange={setAccentColor}
|
|
407
|
-
allowNull={true}
|
|
408
|
-
/>
|
|
409
|
-
</div>
|
|
410
|
-
</div>
|
|
411
|
-
|
|
412
|
-
<div>
|
|
413
|
-
<label className="block text-lg font-bold text-gray-800">
|
|
414
|
-
Theme / Mood
|
|
415
|
-
</label>
|
|
416
|
-
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-2">
|
|
417
|
-
{themeOptions.map((option) => (
|
|
418
|
-
<div key={option} className="flex items-center space-x-2">
|
|
419
|
-
<input
|
|
420
|
-
type="radio"
|
|
421
|
-
id={`theme-${option}`}
|
|
422
|
-
name="themeOptions"
|
|
423
|
-
value={option}
|
|
424
|
-
checked={selectedTheme === option}
|
|
425
|
-
onChange={(e) => setSelectedTheme(e.target.value)}
|
|
426
|
-
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
427
|
-
/>
|
|
428
|
-
<label
|
|
429
|
-
htmlFor={`theme-${option}`}
|
|
430
|
-
className="text-sm font-bold text-gray-700"
|
|
431
|
-
>
|
|
432
|
-
{option}
|
|
433
|
-
</label>
|
|
434
|
-
</div>
|
|
435
|
-
))}
|
|
436
|
-
</div>
|
|
437
|
-
</div>
|
|
438
|
-
|
|
439
|
-
<div>
|
|
440
|
-
<label
|
|
441
|
-
htmlFor="additional-notes"
|
|
442
|
-
className="block text-lg font-bold text-gray-800"
|
|
443
|
-
>
|
|
444
|
-
Additional Design Notes (Optional)
|
|
445
|
-
</label>
|
|
446
|
-
<p className="mb-2 mt-1 text-sm text-gray-500">
|
|
447
|
-
Add specific requests like "use rounded corners", "add subtle
|
|
448
|
-
texture".
|
|
449
|
-
</p>
|
|
450
|
-
<textarea
|
|
451
|
-
id="additional-notes"
|
|
452
|
-
value={additionalNotes}
|
|
453
|
-
onChange={(e) => setAdditionalNotes(e.target.value)}
|
|
454
|
-
placeholder="Enter additional notes..."
|
|
455
|
-
rows={3}
|
|
456
|
-
className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
|
|
457
|
-
/>
|
|
458
|
-
</div>
|
|
459
|
-
|
|
460
|
-
<CopyInputStep
|
|
461
|
-
copyMode={copyMode}
|
|
462
|
-
onCopyModeChange={setCopyMode}
|
|
463
|
-
promptValue={copyPrompt}
|
|
464
|
-
onPromptValueChange={setCopyPrompt}
|
|
465
|
-
copyValue={rawCopy}
|
|
466
|
-
onCopyValueChange={setRawCopy}
|
|
467
|
-
/>
|
|
468
|
-
|
|
469
|
-
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
470
|
-
|
|
471
|
-
<div className="flex justify-between pt-4">
|
|
472
|
-
<button
|
|
473
|
-
type="button"
|
|
474
|
-
onClick={handleBack}
|
|
475
|
-
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"
|
|
476
|
-
>
|
|
477
|
-
Cancel
|
|
478
|
-
</button>
|
|
479
|
-
<button
|
|
480
|
-
type="button"
|
|
481
|
-
onClick={handleGenerate}
|
|
482
|
-
disabled={copyMode === 'prompt' ? !copyPrompt : !rawCopy}
|
|
483
|
-
className={classNames(
|
|
484
|
-
`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`,
|
|
485
|
-
(copyMode === 'prompt' && !copyPrompt) ||
|
|
486
|
-
(copyMode === `raw` && !rawCopy)
|
|
487
|
-
? 'cursor-not-allowed bg-gray-300 text-gray-500'
|
|
488
|
-
: 'bg-cyan-600 text-white hover:bg-cyan-700'
|
|
489
|
-
)}
|
|
490
|
-
>
|
|
491
|
-
Generate Pane
|
|
492
|
-
</button>
|
|
493
|
-
</div>
|
|
494
|
-
|
|
495
|
-
<div className="border-t border-gray-200 pt-4 text-center">
|
|
496
|
-
<button
|
|
497
|
-
type="button"
|
|
498
|
-
onClick={() => {
|
|
499
|
-
setError(null);
|
|
500
|
-
setIsInjectMode(true);
|
|
501
|
-
}}
|
|
502
|
-
className="text-sm text-cyan-600 hover:text-cyan-800 hover:underline"
|
|
503
|
-
>
|
|
504
|
-
Direct Inject Payload
|
|
505
|
-
</button>
|
|
506
|
-
</div>
|
|
507
|
-
</div>
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
return null;
|
|
512
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { findUniqueSlug } from '@/utils/helpers';
|
|
2
|
-
|
|
3
|
-
interface TitleSlugResponse {
|
|
4
|
-
title: string;
|
|
5
|
-
slug: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export async function getTitleSlug(
|
|
9
|
-
markdownContent: string,
|
|
10
|
-
existingSlugs: string[]
|
|
11
|
-
): Promise<TitleSlugResponse | null> {
|
|
12
|
-
if (
|
|
13
|
-
!markdownContent ||
|
|
14
|
-
markdownContent.trim() === '...' ||
|
|
15
|
-
markdownContent.trim().length === 0
|
|
16
|
-
) {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
const backendUrl =
|
|
22
|
-
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
23
|
-
const tenantId = import.meta.env.PUBLIC_TENANTID || 'default';
|
|
24
|
-
|
|
25
|
-
const response = await fetch(`${backendUrl}/api/v1/aai/askLemur`, {
|
|
26
|
-
method: 'POST',
|
|
27
|
-
headers: {
|
|
28
|
-
'Content-Type': 'application/json',
|
|
29
|
-
'X-Tenant-ID': tenantId,
|
|
30
|
-
},
|
|
31
|
-
credentials: 'include',
|
|
32
|
-
body: JSON.stringify({
|
|
33
|
-
prompt: `Generate a concise title (maximum 40-50 characters) and a URL-friendly slug (lowercase, only letters, numbers, and dashes, no spaces) that captures the essence of this markdown content. Return only a JSON object with "title" and "slug" keys.
|
|
34
|
-
|
|
35
|
-
Example response format:
|
|
36
|
-
{
|
|
37
|
-
"title": "Short Descriptive Title",
|
|
38
|
-
"slug": "short-descriptive-title"
|
|
39
|
-
}`,
|
|
40
|
-
input_text: markdownContent,
|
|
41
|
-
final_model: '',
|
|
42
|
-
}),
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
if (response.ok) {
|
|
46
|
-
const data = await response.json();
|
|
47
|
-
if (data.success && data.data?.response) {
|
|
48
|
-
let titleData;
|
|
49
|
-
try {
|
|
50
|
-
if (typeof data.data.response === 'string') {
|
|
51
|
-
titleData = JSON.parse(data.data.response);
|
|
52
|
-
} else {
|
|
53
|
-
titleData = data.data.response;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (titleData.title && titleData.slug) {
|
|
57
|
-
return {
|
|
58
|
-
title: findUniqueSlug(titleData.title, existingSlugs),
|
|
59
|
-
slug: findUniqueSlug(titleData.slug, existingSlugs),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
} catch (parseError) {
|
|
63
|
-
console.error('Error parsing title data:', parseError);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
} catch (error) {
|
|
68
|
-
console.error('Error generating title:', error);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return null;
|
|
72
|
-
}
|