astro-tractstack 2.0.10 → 2.0.12
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 +28 -0
- 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_new.tsx +136 -115
- package/templates/src/components/edit/pane/AiPaneGenerator.tsx +405 -0
- package/templates/src/components/edit/pane/AiPanePreview.tsx +257 -0
- package/templates/src/components/edit/pane/PageGenSelector.tsx +6 -3
- package/templates/src/constants/prompts.json +35 -40
- package/templates/src/pages/sitemap.xml.ts +38 -8
- package/templates/src/stores/nodes.ts +18 -15
- package/templates/src/types/compositorTypes.ts +27 -13
- package/templates/src/utils/compositor/aiPaneParser.ts +587 -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,405 @@
|
|
|
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 StringInput from '@/components/form/StringInput';
|
|
6
|
+
|
|
7
|
+
interface AiPaneGeneratorProps {
|
|
8
|
+
ownerId: string;
|
|
9
|
+
onComplete: (pane: TemplatePane) => void;
|
|
10
|
+
onCancel: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type GenerationStep = 'layout' | 'input' | 'preview' | 'loading';
|
|
14
|
+
type CopyMode = 'prompt' | 'raw';
|
|
15
|
+
|
|
16
|
+
interface GenerationResponse {
|
|
17
|
+
success: boolean;
|
|
18
|
+
data?: {
|
|
19
|
+
response: string | object;
|
|
20
|
+
};
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const layoutOptions = ['Text Only', 'Text + Image Left', 'Text + Image Right'];
|
|
25
|
+
|
|
26
|
+
export function AiPaneGenerator({
|
|
27
|
+
ownerId,
|
|
28
|
+
onComplete,
|
|
29
|
+
onCancel,
|
|
30
|
+
}: AiPaneGeneratorProps) {
|
|
31
|
+
const [currentStep, setCurrentStep] = useState<GenerationStep>('layout');
|
|
32
|
+
const [selectedLayout, setSelectedLayout] = useState<string>(
|
|
33
|
+
layoutOptions[0]
|
|
34
|
+
);
|
|
35
|
+
const [copyMode, setCopyMode] = useState<CopyMode>('prompt');
|
|
36
|
+
const [copyPrompt, setCopyPrompt] = useState('');
|
|
37
|
+
const [rawCopy, setRawCopy] = useState('');
|
|
38
|
+
const [designPrompt, setDesignPrompt] = useState('');
|
|
39
|
+
const [generatedShell, setGeneratedShell] = useState<string | null>(null);
|
|
40
|
+
const [generatedCopy, setGeneratedCopy] = useState<string | null>(null);
|
|
41
|
+
const [error, setError] = useState<string | null>(null);
|
|
42
|
+
|
|
43
|
+
const callAskLemurAPI = useCallback(
|
|
44
|
+
async (
|
|
45
|
+
prompt: string,
|
|
46
|
+
context: string,
|
|
47
|
+
expectJson: boolean
|
|
48
|
+
): Promise<string> => {
|
|
49
|
+
const goBackend =
|
|
50
|
+
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
51
|
+
const tenantId = import.meta.env.PUBLIC_TENANTID || 'default';
|
|
52
|
+
|
|
53
|
+
const requestBody = {
|
|
54
|
+
prompt: prompt,
|
|
55
|
+
input_text: context,
|
|
56
|
+
final_model: 'anthropic/claude-3-5-sonnet',
|
|
57
|
+
temperature: 0.5,
|
|
58
|
+
max_tokens: 2000,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const response = await fetch(`${goBackend}/api/v1/aai/askLemur`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
'Content-Type': 'application/json',
|
|
65
|
+
'X-Tenant-ID': tenantId,
|
|
66
|
+
},
|
|
67
|
+
credentials: 'include',
|
|
68
|
+
body: JSON.stringify(requestBody),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
const errorText = await response.text();
|
|
73
|
+
console.error('AskLemur API Error Response:', errorText);
|
|
74
|
+
let backendError = `API call failed: ${response.status} ${response.statusText}`;
|
|
75
|
+
try {
|
|
76
|
+
const errorJson = JSON.parse(errorText);
|
|
77
|
+
if (errorJson && errorJson.error) {
|
|
78
|
+
backendError = errorJson.error;
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
/* Ignore */
|
|
82
|
+
}
|
|
83
|
+
throw new Error(backendError);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = (await response.json()) as GenerationResponse;
|
|
87
|
+
|
|
88
|
+
if (!result.success || !result.data?.response) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
result.error || 'Generation failed to return valid response.'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let rawResponseData = result.data.response;
|
|
95
|
+
|
|
96
|
+
// Handle case where API returns JSON object for shell
|
|
97
|
+
if (expectJson && typeof rawResponseData === 'object') {
|
|
98
|
+
return JSON.stringify(rawResponseData); // Return as string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Handle case where API returns string (potentially wrapped)
|
|
102
|
+
if (typeof rawResponseData === 'string') {
|
|
103
|
+
let responseString = rawResponseData;
|
|
104
|
+
try {
|
|
105
|
+
if (
|
|
106
|
+
responseString.startsWith('```json') &&
|
|
107
|
+
responseString.endsWith('```')
|
|
108
|
+
) {
|
|
109
|
+
responseString = responseString.slice(7, -3).trim();
|
|
110
|
+
} else if (
|
|
111
|
+
responseString.startsWith('```html') &&
|
|
112
|
+
responseString.endsWith('```')
|
|
113
|
+
) {
|
|
114
|
+
responseString = responseString.slice(7, -3).trim();
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
/* Ignore stripping errors */
|
|
118
|
+
}
|
|
119
|
+
return responseString; // Return string directly
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Fallback if response is neither expected string nor object
|
|
123
|
+
throw new Error('Unexpected response format received from API.');
|
|
124
|
+
},
|
|
125
|
+
[]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const handleGenerate = useCallback(async () => {
|
|
129
|
+
setError(null);
|
|
130
|
+
setCurrentStep('loading');
|
|
131
|
+
setGeneratedShell(null);
|
|
132
|
+
setGeneratedCopy(null);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const shellPromptDetails = prompts.aiPaneShellPrompt;
|
|
136
|
+
const copyPromptDetails = prompts.aiPaneCopyPrompt;
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
!shellPromptDetails?.user_template ||
|
|
140
|
+
!copyPromptDetails?.user_template
|
|
141
|
+
) {
|
|
142
|
+
throw new Error('AI prompts not found or incomplete in prompts.json');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- This is the updated (sequential) logic ---
|
|
146
|
+
|
|
147
|
+
// 1. Prepare and call the Shell API first
|
|
148
|
+
const formattedShellPrompt = shellPromptDetails.user_template
|
|
149
|
+
.replace('{{DESIGN_INPUT}}', designPrompt)
|
|
150
|
+
.replace('{{LAYOUT_TYPE}}', selectedLayout);
|
|
151
|
+
|
|
152
|
+
const shellResult = await callAskLemurAPI(
|
|
153
|
+
formattedShellPrompt,
|
|
154
|
+
shellPromptDetails.system || '',
|
|
155
|
+
true
|
|
156
|
+
);
|
|
157
|
+
setGeneratedShell(shellResult); // Set this for the previewer
|
|
158
|
+
|
|
159
|
+
// 2. NOW, create the copy prompt, injecting the shellResult
|
|
160
|
+
const copyInputContent = copyMode === 'prompt' ? copyPrompt : rawCopy;
|
|
161
|
+
// Note: Assumes prompts.json's aiPaneCopyPrompt.user_template now includes {{SHELL_JSON}}
|
|
162
|
+
const formattedCopyPrompt = copyPromptDetails.user_template
|
|
163
|
+
.replace('{{COPY_INPUT}}', copyInputContent)
|
|
164
|
+
.replace('{{DESIGN_INPUT}}', designPrompt)
|
|
165
|
+
.replace('{{LAYOUT_TYPE}}', selectedLayout)
|
|
166
|
+
.replace('{{SHELL_JSON}}', shellResult); // <-- This is the new, critical part
|
|
167
|
+
|
|
168
|
+
// 3. Call Copy API second, using the fully-formed prompt
|
|
169
|
+
const copyResult = await callAskLemurAPI(
|
|
170
|
+
formattedCopyPrompt,
|
|
171
|
+
copyPromptDetails.system || '',
|
|
172
|
+
false
|
|
173
|
+
);
|
|
174
|
+
setGeneratedCopy(copyResult); // Should be an HTML string
|
|
175
|
+
|
|
176
|
+
setCurrentStep('preview');
|
|
177
|
+
|
|
178
|
+
// --- End of updated logic ---
|
|
179
|
+
} catch (err: any) {
|
|
180
|
+
console.error('AI Pane Generation Error:', err);
|
|
181
|
+
setError(err.message || 'Failed to generate AI pane.');
|
|
182
|
+
setCurrentStep('input');
|
|
183
|
+
}
|
|
184
|
+
}, [
|
|
185
|
+
designPrompt,
|
|
186
|
+
selectedLayout,
|
|
187
|
+
copyMode,
|
|
188
|
+
copyPrompt,
|
|
189
|
+
rawCopy,
|
|
190
|
+
callAskLemurAPI,
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
const handleBack = () => {
|
|
194
|
+
setError(null);
|
|
195
|
+
if (currentStep === 'preview') {
|
|
196
|
+
setCurrentStep('input');
|
|
197
|
+
} else if (currentStep === 'input') {
|
|
198
|
+
setCurrentStep('layout');
|
|
199
|
+
} else if (currentStep === 'loading') {
|
|
200
|
+
setCurrentStep('input');
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (currentStep === 'loading') {
|
|
205
|
+
return (
|
|
206
|
+
<div className="flex min-h-[200px] flex-col items-center justify-center space-y-4 p-6">
|
|
207
|
+
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-gray-400"></div>
|
|
208
|
+
<p className="text-sm text-gray-500">Generating AI Pane...</p>
|
|
209
|
+
<button
|
|
210
|
+
type="button"
|
|
211
|
+
onClick={handleBack}
|
|
212
|
+
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"
|
|
213
|
+
>
|
|
214
|
+
Cancel
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (currentStep === 'preview' && generatedShell && generatedCopy) {
|
|
221
|
+
return (
|
|
222
|
+
<AiPanePreview
|
|
223
|
+
shellJson={generatedShell}
|
|
224
|
+
copyHtml={generatedCopy}
|
|
225
|
+
layout={selectedLayout}
|
|
226
|
+
ownerId={ownerId}
|
|
227
|
+
onComplete={onComplete}
|
|
228
|
+
onBack={handleBack}
|
|
229
|
+
/>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (currentStep === 'layout') {
|
|
234
|
+
return (
|
|
235
|
+
<div className="space-y-4 p-4">
|
|
236
|
+
<label className="mb-2 block text-lg font-semibold text-gray-800">
|
|
237
|
+
Choose a Layout
|
|
238
|
+
</label>
|
|
239
|
+
<div className="space-y-2">
|
|
240
|
+
{layoutOptions.map((layout) => (
|
|
241
|
+
<div key={layout} className="flex items-center space-x-2">
|
|
242
|
+
<input
|
|
243
|
+
type="radio"
|
|
244
|
+
id={`layout-${layout}`}
|
|
245
|
+
name="layoutOptions"
|
|
246
|
+
value={layout}
|
|
247
|
+
checked={selectedLayout === layout}
|
|
248
|
+
onChange={(e) => setSelectedLayout(e.target.value)}
|
|
249
|
+
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
250
|
+
/>
|
|
251
|
+
<label
|
|
252
|
+
htmlFor={`layout-${layout}`}
|
|
253
|
+
className="text-sm font-medium text-gray-700"
|
|
254
|
+
>
|
|
255
|
+
{layout}
|
|
256
|
+
</label>
|
|
257
|
+
</div>
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
<div className="flex justify-end space-x-2 pt-4">
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
onClick={onCancel}
|
|
264
|
+
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"
|
|
265
|
+
>
|
|
266
|
+
Cancel
|
|
267
|
+
</button>
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
onClick={() => setCurrentStep('input')}
|
|
271
|
+
className="rounded-md border border-transparent bg-cyan-600 px-4 py-2 text-sm font-bold text-white hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2"
|
|
272
|
+
>
|
|
273
|
+
Next
|
|
274
|
+
</button>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (currentStep === 'input') {
|
|
281
|
+
return (
|
|
282
|
+
<div className="space-y-6 p-4">
|
|
283
|
+
<div>
|
|
284
|
+
<label
|
|
285
|
+
htmlFor="design-prompt"
|
|
286
|
+
className="block text-lg font-semibold text-gray-800"
|
|
287
|
+
>
|
|
288
|
+
Describe the Design
|
|
289
|
+
</label>
|
|
290
|
+
<p className="mb-2 mt-1 text-sm text-gray-500">
|
|
291
|
+
Example: "dark, minimalist hero", "bright, playful feature box"
|
|
292
|
+
</p>
|
|
293
|
+
<StringInput
|
|
294
|
+
id="design-prompt"
|
|
295
|
+
value={designPrompt}
|
|
296
|
+
onChange={setDesignPrompt}
|
|
297
|
+
placeholder="Enter design prompt..."
|
|
298
|
+
className="block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<div>
|
|
303
|
+
<label className="block text-lg font-semibold text-gray-800">
|
|
304
|
+
Provide Content
|
|
305
|
+
</label>
|
|
306
|
+
<div className="my-2 flex space-x-4">
|
|
307
|
+
<div className="flex items-center space-x-2">
|
|
308
|
+
<input
|
|
309
|
+
type="radio"
|
|
310
|
+
id="copy-prompt-mode"
|
|
311
|
+
name="copyModeOptions"
|
|
312
|
+
value="prompt"
|
|
313
|
+
checked={copyMode === 'prompt'}
|
|
314
|
+
onChange={(e) => setCopyMode(e.target.value as CopyMode)}
|
|
315
|
+
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
316
|
+
/>
|
|
317
|
+
<label
|
|
318
|
+
htmlFor="copy-prompt-mode"
|
|
319
|
+
className="text-sm font-medium text-gray-700"
|
|
320
|
+
>
|
|
321
|
+
Write a prompt
|
|
322
|
+
</label>
|
|
323
|
+
</div>
|
|
324
|
+
<div className="flex items-center space-x-2">
|
|
325
|
+
<input
|
|
326
|
+
type="radio"
|
|
327
|
+
id="copy-raw-mode"
|
|
328
|
+
name="copyModeOptions"
|
|
329
|
+
value="raw"
|
|
330
|
+
checked={copyMode === 'raw'}
|
|
331
|
+
onChange={(e) => setCopyMode(e.target.value as CopyMode)}
|
|
332
|
+
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
333
|
+
/>
|
|
334
|
+
<label
|
|
335
|
+
htmlFor="copy-raw-mode"
|
|
336
|
+
className="text-sm font-medium text-gray-700"
|
|
337
|
+
>
|
|
338
|
+
Provide Copy
|
|
339
|
+
</label>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
{copyMode === 'prompt' ? (
|
|
344
|
+
<>
|
|
345
|
+
<p className="mb-2 text-sm text-gray-500">
|
|
346
|
+
Let the AI write the copy based on your prompt.
|
|
347
|
+
</p>
|
|
348
|
+
<textarea
|
|
349
|
+
id="copy-prompt"
|
|
350
|
+
value={copyPrompt}
|
|
351
|
+
onChange={(e) => setCopyPrompt(e.target.value)}
|
|
352
|
+
placeholder="Enter copy prompt..."
|
|
353
|
+
rows={4}
|
|
354
|
+
className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
|
|
355
|
+
/>
|
|
356
|
+
</>
|
|
357
|
+
) : (
|
|
358
|
+
<>
|
|
359
|
+
<p className="mb-2 text-sm text-gray-500">
|
|
360
|
+
Provide your raw copy text here. The AI will structure and style
|
|
361
|
+
it.
|
|
362
|
+
</p>
|
|
363
|
+
<textarea
|
|
364
|
+
id="raw-copy"
|
|
365
|
+
value={rawCopy}
|
|
366
|
+
onChange={(e) => setRawCopy(e.target.value)}
|
|
367
|
+
placeholder="Paste or type your copy text..."
|
|
368
|
+
rows={6}
|
|
369
|
+
className="block w-full rounded-md border-gray-300 p-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
|
|
370
|
+
/>
|
|
371
|
+
</>
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
376
|
+
|
|
377
|
+
<div className="flex justify-between pt-4">
|
|
378
|
+
<button
|
|
379
|
+
type="button"
|
|
380
|
+
onClick={handleBack}
|
|
381
|
+
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
|
+
>
|
|
383
|
+
Back
|
|
384
|
+
</button>
|
|
385
|
+
<button
|
|
386
|
+
type="button"
|
|
387
|
+
onClick={handleGenerate}
|
|
388
|
+
disabled={
|
|
389
|
+
!designPrompt || (copyMode === 'prompt' ? !copyPrompt : !rawCopy)
|
|
390
|
+
}
|
|
391
|
+
className={`rounded-md border border-transparent px-4 py-2 text-sm font-bold text-white shadow-sm ${
|
|
392
|
+
!designPrompt || (copyMode === 'prompt' ? !copyPrompt : !rawCopy)
|
|
393
|
+
? 'cursor-not-allowed bg-gray-400'
|
|
394
|
+
: 'bg-cyan-600 hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2'
|
|
395
|
+
}`}
|
|
396
|
+
>
|
|
397
|
+
Generate Preview
|
|
398
|
+
</button>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
2
|
+
import type { TemplatePane } from '@/types/compositorTypes';
|
|
3
|
+
import { parseAiPane } from '@/utils/compositor/aiPaneParser';
|
|
4
|
+
import {
|
|
5
|
+
PaneSnapshotGenerator,
|
|
6
|
+
type SnapshotData,
|
|
7
|
+
} from '@/components/compositor/preview/PaneSnapshotGenerator';
|
|
8
|
+
import { ulid } from 'ulid';
|
|
9
|
+
|
|
10
|
+
type LLMShellLayer = {
|
|
11
|
+
mobile?: Record<string, string>;
|
|
12
|
+
tablet?: Record<string, string>;
|
|
13
|
+
desktop?: Record<string, string>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type ShellJson = {
|
|
17
|
+
bgColour: string;
|
|
18
|
+
parentClasses: LLMShellLayer[];
|
|
19
|
+
defaultClasses: Record<string, any>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
interface AiPanePreviewProps {
|
|
23
|
+
shellJson: string;
|
|
24
|
+
copyHtml: string;
|
|
25
|
+
layout: string;
|
|
26
|
+
ownerId: string;
|
|
27
|
+
onComplete: (pane: TemplatePane) => void;
|
|
28
|
+
onBack: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function convertObjectToTailwindString(
|
|
32
|
+
styleObj: Record<string, string> | undefined
|
|
33
|
+
): string {
|
|
34
|
+
if (!styleObj) return '';
|
|
35
|
+
return Object.entries(styleObj)
|
|
36
|
+
.map(([key, value]) => {
|
|
37
|
+
// Basic mapping, might need adjustment based on tailwindClasses structure if prefixes differ
|
|
38
|
+
const prefixMap: Record<string, string> = {
|
|
39
|
+
mx: 'mx',
|
|
40
|
+
my: 'my',
|
|
41
|
+
px: 'px',
|
|
42
|
+
py: 'py',
|
|
43
|
+
textALIGN: 'text',
|
|
44
|
+
textSIZE: 'text',
|
|
45
|
+
textCOLOR: 'text',
|
|
46
|
+
fontWEIGHT: 'font',
|
|
47
|
+
fontFACE: 'font',
|
|
48
|
+
letterSPACING: 'tracking',
|
|
49
|
+
lineHEIGHT: 'leading',
|
|
50
|
+
bgCOLOR: 'bg',
|
|
51
|
+
rounded: 'rounded',
|
|
52
|
+
shadow: 'shadow',
|
|
53
|
+
maxW: 'max-w',
|
|
54
|
+
// Add other mappings as needed based on keys used in compositorTypes vs tailwindClasses
|
|
55
|
+
};
|
|
56
|
+
const prefix = prefixMap[key] || key.toLowerCase();
|
|
57
|
+
if (value === '') return key; // Handle boolean classes like 'relative', 'flex'
|
|
58
|
+
return `${prefix}-${value}`;
|
|
59
|
+
})
|
|
60
|
+
.join(' ');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getPreviewClasses(classes: LLMShellLayer | undefined): string {
|
|
64
|
+
if (!classes) return '';
|
|
65
|
+
|
|
66
|
+
const mobileStyles = convertObjectToTailwindString(classes.mobile);
|
|
67
|
+
const tabletStyles = convertObjectToTailwindString(classes.tablet);
|
|
68
|
+
const desktopStyles = convertObjectToTailwindString(classes.desktop);
|
|
69
|
+
|
|
70
|
+
const combined = `${mobileStyles} ${tabletStyles ? `md:${tabletStyles.split(' ').join(' md:')}` : ''} ${desktopStyles ? `xl:${desktopStyles.split(' ').join(' xl:')}` : ''}`;
|
|
71
|
+
return combined.replace(/\s+/g, ' ').trim();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function AiPanePreview({
|
|
75
|
+
shellJson,
|
|
76
|
+
copyHtml,
|
|
77
|
+
layout,
|
|
78
|
+
onComplete,
|
|
79
|
+
onBack,
|
|
80
|
+
}: AiPanePreviewProps) {
|
|
81
|
+
const [parsedPaneForApply, setParsedPaneForApply] =
|
|
82
|
+
useState<TemplatePane | null>(null);
|
|
83
|
+
const [error, setError] = useState<string | null>(null);
|
|
84
|
+
const [snapshotData, setSnapshotData] = useState<SnapshotData | null>(null);
|
|
85
|
+
const [isGeneratingSnapshot, setIsGeneratingSnapshot] =
|
|
86
|
+
useState<boolean>(false);
|
|
87
|
+
const previewId = useMemo(() => `ai-preview-${ulid()}`, []);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
let isActive = true;
|
|
91
|
+
setError(null);
|
|
92
|
+
setParsedPaneForApply(null);
|
|
93
|
+
setSnapshotData(null);
|
|
94
|
+
setIsGeneratingSnapshot(false);
|
|
95
|
+
try {
|
|
96
|
+
const pane = parseAiPane(shellJson, copyHtml, layout);
|
|
97
|
+
if (isActive) {
|
|
98
|
+
setParsedPaneForApply(pane);
|
|
99
|
+
}
|
|
100
|
+
} catch (err: any) {
|
|
101
|
+
console.error('Error parsing AI Pane for apply:', err);
|
|
102
|
+
if (isActive) {
|
|
103
|
+
setError(
|
|
104
|
+
err.message || 'Failed to parse generated content for application.'
|
|
105
|
+
);
|
|
106
|
+
setParsedPaneForApply(null);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return () => {
|
|
110
|
+
isActive = false;
|
|
111
|
+
};
|
|
112
|
+
}, [shellJson, copyHtml, layout]);
|
|
113
|
+
|
|
114
|
+
const previewHtmlString = useMemo(() => {
|
|
115
|
+
try {
|
|
116
|
+
if (!shellJson || !copyHtml) return '';
|
|
117
|
+
const shell: ShellJson = JSON.parse(shellJson);
|
|
118
|
+
|
|
119
|
+
let currentHtml = copyHtml;
|
|
120
|
+
if (shell.parentClasses && shell.parentClasses.length > 0) {
|
|
121
|
+
[...shell.parentClasses].reverse().forEach((layer) => {
|
|
122
|
+
const layerClasses = getPreviewClasses(layer);
|
|
123
|
+
currentHtml = `<div class="${layerClasses}">${currentHtml}</div>`;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const outerStyle = shell.bgColour
|
|
128
|
+
? `background-color: ${shell.bgColour};`
|
|
129
|
+
: '';
|
|
130
|
+
// Wrap in a div that sets width similar to snapshot generator default for better preview consistency
|
|
131
|
+
return `<div style="${outerStyle} width: 800px; padding: 1px; margin: auto;">${currentHtml}</div>`;
|
|
132
|
+
} catch (err: any) {
|
|
133
|
+
console.error('Error constructing preview HTML string:', err);
|
|
134
|
+
setError(err.message || 'Failed to construct preview HTML.');
|
|
135
|
+
return '';
|
|
136
|
+
}
|
|
137
|
+
}, [shellJson, copyHtml]);
|
|
138
|
+
|
|
139
|
+
const handleSnapshotComplete = useCallback(
|
|
140
|
+
(id: string, data: SnapshotData) => {
|
|
141
|
+
if (id === previewId) {
|
|
142
|
+
setSnapshotData(data);
|
|
143
|
+
setIsGeneratingSnapshot(false);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
[previewId]
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const handleSnapshotError = useCallback(
|
|
150
|
+
(id: string, errorMsg: string) => {
|
|
151
|
+
if (id === previewId) {
|
|
152
|
+
console.error(`Snapshot generation failed for ${id}:`, errorMsg);
|
|
153
|
+
setError(`Snapshot generation failed: ${errorMsg}`);
|
|
154
|
+
setIsGeneratingSnapshot(false);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
[previewId]
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const handleApply = () => {
|
|
161
|
+
if (parsedPaneForApply) {
|
|
162
|
+
onComplete(parsedPaneForApply);
|
|
163
|
+
console.log('FINAL TEMPLATE PANE PAYLOAD:', parsedPaneForApply);
|
|
164
|
+
} else if (!error) {
|
|
165
|
+
// Attempt parsing again if it failed silently initially
|
|
166
|
+
try {
|
|
167
|
+
const pane = parseAiPane(shellJson, copyHtml, layout);
|
|
168
|
+
onComplete(pane);
|
|
169
|
+
} catch (err: any) {
|
|
170
|
+
setError(
|
|
171
|
+
err.message || 'Failed to parse generated content before applying.'
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (previewHtmlString && !snapshotData && !error && !isGeneratingSnapshot) {
|
|
179
|
+
setIsGeneratingSnapshot(true);
|
|
180
|
+
}
|
|
181
|
+
}, [previewHtmlString, snapshotData, error, isGeneratingSnapshot]);
|
|
182
|
+
|
|
183
|
+
const showLoading =
|
|
184
|
+
isGeneratingSnapshot ||
|
|
185
|
+
(!previewHtmlString && !error && !parsedPaneForApply);
|
|
186
|
+
const showPreview = !isGeneratingSnapshot && snapshotData && !error;
|
|
187
|
+
const showError = !!error;
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div className="flex h-full flex-col p-4">
|
|
191
|
+
<div className="relative mb-4 flex min-h-[200px] flex-grow items-center justify-center overflow-auto rounded border bg-gray-50">
|
|
192
|
+
{showError && (
|
|
193
|
+
<div className="p-4 text-center text-red-600">
|
|
194
|
+
<p className="font-semibold">Error:</p>
|
|
195
|
+
<p className="mt-1 text-sm">{error}</p>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
{showLoading && !showError && (
|
|
199
|
+
<div className="p-4 text-center text-gray-500">
|
|
200
|
+
<div className="mx-auto mb-2 h-8 w-8 animate-spin rounded-full border-b-2 border-gray-400"></div>
|
|
201
|
+
<p className="text-sm">
|
|
202
|
+
{isGeneratingSnapshot
|
|
203
|
+
? 'Generating Snapshot...'
|
|
204
|
+
: 'Constructing Preview...'}
|
|
205
|
+
</p>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
{isGeneratingSnapshot && previewHtmlString && (
|
|
209
|
+
<div className="pointer-events-none absolute left-[-9999px] top-[-9999px] w-[800px] opacity-0">
|
|
210
|
+
<PaneSnapshotGenerator
|
|
211
|
+
id={previewId}
|
|
212
|
+
htmlString={previewHtmlString}
|
|
213
|
+
onComplete={handleSnapshotComplete}
|
|
214
|
+
onError={handleSnapshotError}
|
|
215
|
+
/>
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
{showPreview && snapshotData && (
|
|
219
|
+
<img
|
|
220
|
+
src={snapshotData.imageData}
|
|
221
|
+
alt="AI Pane Preview"
|
|
222
|
+
className="block h-auto max-w-full"
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
<div className="flex flex-shrink-0 justify-between">
|
|
227
|
+
<button
|
|
228
|
+
onClick={onBack}
|
|
229
|
+
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"
|
|
230
|
+
type="button"
|
|
231
|
+
>
|
|
232
|
+
Back
|
|
233
|
+
</button>
|
|
234
|
+
<button
|
|
235
|
+
onClick={handleApply}
|
|
236
|
+
disabled={
|
|
237
|
+
!parsedPaneForApply ||
|
|
238
|
+
!!error ||
|
|
239
|
+
!snapshotData ||
|
|
240
|
+
isGeneratingSnapshot
|
|
241
|
+
}
|
|
242
|
+
className={`rounded-md border border-transparent px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors duration-150 ${
|
|
243
|
+
!parsedPaneForApply ||
|
|
244
|
+
!!error ||
|
|
245
|
+
!snapshotData ||
|
|
246
|
+
isGeneratingSnapshot
|
|
247
|
+
? 'cursor-not-allowed bg-gray-400'
|
|
248
|
+
: 'bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2'
|
|
249
|
+
}`}
|
|
250
|
+
type="button"
|
|
251
|
+
>
|
|
252
|
+
Apply Pane
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
@@ -2,14 +2,15 @@ import { useState, useMemo } from 'react';
|
|
|
2
2
|
import { useStore } from '@nanostores/react';
|
|
3
3
|
import { RadioGroup } from '@ark-ui/react/radio-group';
|
|
4
4
|
import CheckCircleIcon from '@heroicons/react/20/solid/CheckCircleIcon';
|
|
5
|
-
import CubeTransparentIcon from '@heroicons/react/24/outline/CubeTransparentIcon';
|
|
5
|
+
//import CubeTransparentIcon from '@heroicons/react/24/outline/CubeTransparentIcon';
|
|
6
6
|
import DocumentIcon from '@heroicons/react/24/outline/DocumentIcon';
|
|
7
|
-
//import NewspaperIcon from '@heroicons/react/24/outline/NewspaperIcon';
|
|
8
7
|
import ExclamationTriangleIcon from '@heroicons/react/24/outline/ExclamationTriangleIcon';
|
|
9
8
|
import AddPanePanel from './AddPanePanel';
|
|
10
9
|
import PageCreationGen from './PageGen';
|
|
11
10
|
import PageCreationSpecial from './PageGenSpecial';
|
|
12
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
/* hasAssemblyAIStore,*/ fullContentMapStore,
|
|
13
|
+
} from '@/stores/storykeep';
|
|
13
14
|
import type { NodesContext } from '@/stores/nodes';
|
|
14
15
|
|
|
15
16
|
interface PageCreationSelectorProps {
|
|
@@ -61,6 +62,7 @@ export const PageCreationSelector = ({
|
|
|
61
62
|
icon: DocumentIcon,
|
|
62
63
|
active: true,
|
|
63
64
|
},
|
|
65
|
+
/*
|
|
64
66
|
...(hasAssemblyAIStore.get()
|
|
65
67
|
? [
|
|
66
68
|
{
|
|
@@ -73,6 +75,7 @@ export const PageCreationSelector = ({
|
|
|
73
75
|
},
|
|
74
76
|
]
|
|
75
77
|
: []),
|
|
78
|
+
*/
|
|
76
79
|
];
|
|
77
80
|
|
|
78
81
|
//const featuredMode = {
|