astro-tractstack 2.0.11 → 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.
@@ -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 { hasAssemblyAIStore, fullContentMapStore } from '@/stores/storykeep';
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 = {