astro-tractstack 2.0.14 → 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 +41 -9
- 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 +27 -9
- 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 +463 -561
- package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +140 -0
- package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +105 -0
- package/templates/src/components/edit/pane/steps/DesignLibraryStep.tsx +395 -0
- package/templates/src/components/edit/panels/StyleImagePanel.tsx +10 -8
- package/templates/src/components/edit/state/SaveModal.tsx +41 -0
- package/templates/src/constants/prompts.json +3 -1
- 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/compositor/aiPaneParser.ts +32 -84
- package/templates/src/utils/compositor/designLibraryHelper.ts +87 -2
- package/templates/src/utils/profileStorage.ts +13 -0
- package/utils/inject-files.ts +41 -10
- package/templates/src/components/edit/pane/AiPaneGenerator.tsx +0 -575
- package/templates/src/components/edit/pane/AiPanePreview.tsx +0 -107
- package/templates/src/components/edit/pane/PageGen.tsx +0 -485
- package/templates/src/components/edit/pane/PageGenSelector.tsx +0 -245
- package/templates/src/components/edit/pane/PageGenSpecial.tsx +0 -339
- package/templates/src/utils/aai/getTitleSlug.ts +0 -72
|
@@ -1,34 +1,110 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import ChevronUpDownIcon from '@heroicons/react/20/solid/ChevronUpDownIcon';
|
|
8
|
-
import CheckIcon from '@heroicons/react/20/solid/CheckIcon';
|
|
9
|
-
import { NodesContext } from '@/stores/nodes';
|
|
10
|
-
import {
|
|
11
|
-
PanesPreviewGenerator,
|
|
12
|
-
type PanePreviewRequest,
|
|
13
|
-
type PaneFragmentResult,
|
|
14
|
-
} from '@/components/compositor/preview/PanesPreviewGenerator';
|
|
15
|
-
import {
|
|
16
|
-
PaneSnapshotGenerator,
|
|
17
|
-
type SnapshotData,
|
|
18
|
-
} from '@/components/compositor/preview/PaneSnapshotGenerator';
|
|
19
|
-
import { createEmptyStorykeep } from '@/utils/compositor/nodesHelper';
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
import DocumentPlusIcon from '@heroicons/react/24/outline/DocumentPlusIcon';
|
|
4
|
+
import SparklesIcon from '@heroicons/react/24/outline/SparklesIcon';
|
|
5
|
+
import SwatchIcon from '@heroicons/react/24/outline/SwatchIcon';
|
|
6
|
+
import { NodesContext, getCtx } from '@/stores/nodes';
|
|
20
7
|
import { cloneDeep } from '@/utils/helpers';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
hasAssemblyAIStore,
|
|
25
|
-
} from '@/stores/storykeep';
|
|
26
|
-
import { templateCategories } from '@/utils/compositor/templateMarkdownStyles';
|
|
27
|
-
import { AiPaneGenerator } from './AiPaneGenerator';
|
|
28
|
-
import { AddPaneNewCustomCopy } from './AddPanePanel_newCustomCopy';
|
|
29
|
-
import { themes, type Theme, type BrandConfig } from '@/types/tractstack';
|
|
8
|
+
import { hasAssemblyAIStore } from '@/stores/storykeep';
|
|
9
|
+
import prompts from '@/constants/prompts.json';
|
|
10
|
+
import type { BrandConfig, DesignLibraryEntry } from '@/types/tractstack';
|
|
30
11
|
import { PaneAddMode, type TemplatePane } from '@/types/compositorTypes';
|
|
31
12
|
import { useStore } from '@nanostores/react';
|
|
13
|
+
import { CopyInputStep } from './steps/CopyInputStep';
|
|
14
|
+
import { DesignLibraryStep } from './steps/DesignLibraryStep';
|
|
15
|
+
import { AiDesignStep, type AiDesignConfig } from './steps/AiDesignStep';
|
|
16
|
+
import { parseAiPane, parseAiCopyHtml } from '@/utils/compositor/aiPaneParser';
|
|
17
|
+
import {
|
|
18
|
+
convertStorageToLiveTemplate,
|
|
19
|
+
mergeCopyIntoTemplate,
|
|
20
|
+
convertTemplateToAIShell,
|
|
21
|
+
} from '@/utils/compositor/designLibraryHelper';
|
|
22
|
+
|
|
23
|
+
type Step =
|
|
24
|
+
| 'initial'
|
|
25
|
+
| 'copyInput'
|
|
26
|
+
| 'designLibrary'
|
|
27
|
+
| 'aiDesign'
|
|
28
|
+
| 'loading'
|
|
29
|
+
| 'error';
|
|
30
|
+
|
|
31
|
+
type InitialChoice = 'library' | 'ai' | 'blank';
|
|
32
|
+
type CopyMode = 'prompt' | 'raw';
|
|
33
|
+
|
|
34
|
+
interface GenerationResponse {
|
|
35
|
+
success: boolean;
|
|
36
|
+
data?: { response: string | object };
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const callAskLemurAPI = async (
|
|
41
|
+
prompt: string,
|
|
42
|
+
context: string,
|
|
43
|
+
expectJson: boolean,
|
|
44
|
+
isSandboxMode: boolean
|
|
45
|
+
): Promise<string> => {
|
|
46
|
+
const goBackend =
|
|
47
|
+
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
48
|
+
const tenantId = import.meta.env.PUBLIC_TENANTID || 'default';
|
|
49
|
+
const requestBody = {
|
|
50
|
+
prompt,
|
|
51
|
+
input_text: context,
|
|
52
|
+
final_model: '',
|
|
53
|
+
temperature: 0.5,
|
|
54
|
+
max_tokens: 2000,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
let response: Response;
|
|
58
|
+
if (isSandboxMode) {
|
|
59
|
+
response = await fetch(`/api/sandbox`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
|
|
62
|
+
credentials: 'include',
|
|
63
|
+
body: JSON.stringify({ action: 'askLemur', payload: requestBody }),
|
|
64
|
+
});
|
|
65
|
+
} else {
|
|
66
|
+
response = await fetch(`${goBackend}/api/v1/aai/askLemur`, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
|
|
69
|
+
credentials: 'include',
|
|
70
|
+
body: JSON.stringify(requestBody),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const errorText = await response.text();
|
|
76
|
+
let backendError = `API call failed: ${response.status} ${response.statusText}`;
|
|
77
|
+
try {
|
|
78
|
+
const errorJson = JSON.parse(errorText);
|
|
79
|
+
if (errorJson?.error) backendError = errorJson.error;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
/* ignore */
|
|
82
|
+
}
|
|
83
|
+
throw new Error(backendError);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = (await response.json()) as GenerationResponse;
|
|
87
|
+
if (!result.success || !result.data?.response) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
result.error || 'Generation failed to return valid response.'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let rawResponseData = result.data.response;
|
|
94
|
+
if (expectJson && typeof rawResponseData === 'object') {
|
|
95
|
+
return JSON.stringify(rawResponseData);
|
|
96
|
+
}
|
|
97
|
+
if (typeof rawResponseData === 'string') {
|
|
98
|
+
let responseString = rawResponseData;
|
|
99
|
+
if (responseString.startsWith('```json')) {
|
|
100
|
+
responseString = responseString.slice(7, -3).trim();
|
|
101
|
+
} else if (responseString.startsWith('```html')) {
|
|
102
|
+
responseString = responseString.slice(7, -3).trim();
|
|
103
|
+
}
|
|
104
|
+
return responseString;
|
|
105
|
+
}
|
|
106
|
+
throw new Error('Unexpected response format received from API.');
|
|
107
|
+
};
|
|
32
108
|
|
|
33
109
|
interface AddPaneNewPanelProps {
|
|
34
110
|
nodeId: string;
|
|
@@ -38,244 +114,230 @@ interface AddPaneNewPanelProps {
|
|
|
38
114
|
isStoryFragment?: boolean;
|
|
39
115
|
isContextPane?: boolean;
|
|
40
116
|
config?: BrandConfig;
|
|
117
|
+
isSandboxMode?: boolean;
|
|
41
118
|
}
|
|
42
119
|
|
|
43
|
-
interface PreviewPane {
|
|
44
|
-
ctx: NodesContext;
|
|
45
|
-
snapshot?: SnapshotData;
|
|
46
|
-
template: any;
|
|
47
|
-
index: number;
|
|
48
|
-
htmlFragment?: string;
|
|
49
|
-
fragmentError?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface TemplateCategory {
|
|
53
|
-
id: string;
|
|
54
|
-
title: string;
|
|
55
|
-
getTemplates: (theme: Theme, brand: string, useOdd: boolean) => any[];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const ITEMS_PER_PAGE = 8;
|
|
59
|
-
|
|
60
|
-
type Mode = 'template' | 'custom' | 'ai';
|
|
61
|
-
|
|
62
120
|
const AddPaneNewPanel = ({
|
|
63
121
|
nodeId,
|
|
64
122
|
first,
|
|
65
|
-
setMode: setParentMode,
|
|
66
|
-
ctx,
|
|
123
|
+
setMode: setParentMode,
|
|
124
|
+
ctx: providedCtx,
|
|
67
125
|
isStoryFragment = false,
|
|
68
126
|
isContextPane = false,
|
|
69
127
|
config,
|
|
128
|
+
isSandboxMode = false,
|
|
70
129
|
}: AddPaneNewPanelProps) => {
|
|
71
|
-
const
|
|
130
|
+
const ctx = providedCtx || getCtx();
|
|
72
131
|
const hasAssemblyAI = useStore(hasAssemblyAIStore);
|
|
73
|
-
const [
|
|
74
|
-
const [
|
|
75
|
-
|
|
76
|
-
const [currentPage, setCurrentPage] = useState(0);
|
|
77
|
-
const [renderedPages, setRenderedPages] = useState<Set<number>>(new Set());
|
|
78
|
-
const [selectedTheme, setSelectedTheme] = useState<Theme>(
|
|
79
|
-
useStore(preferredThemeStore)
|
|
132
|
+
const [step, setStep] = useState<Step>('initial');
|
|
133
|
+
const [initialChoice, setInitialChoice] = useState<InitialChoice | null>(
|
|
134
|
+
null
|
|
80
135
|
);
|
|
81
|
-
const [
|
|
82
|
-
const [
|
|
83
|
-
|
|
84
|
-
);
|
|
85
|
-
const [
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
100
|
-
}, [isContextPane]);
|
|
101
|
-
|
|
102
|
-
const themesCollection = useMemo(() => {
|
|
103
|
-
return createListCollection({
|
|
104
|
-
items: themes,
|
|
105
|
-
itemToValue: (item) => item,
|
|
106
|
-
itemToString: (item) => item.replace(/-/g, ' '),
|
|
107
|
-
});
|
|
108
|
-
}, []);
|
|
109
|
-
|
|
110
|
-
const filteredTemplates = useMemo(() => {
|
|
111
|
-
return selectedCategory.getTemplates(selectedTheme, brand, useOddVariant);
|
|
112
|
-
}, [selectedTheme, useOddVariant, selectedCategory, brand]);
|
|
113
|
-
|
|
114
|
-
useEffect(() => {
|
|
115
|
-
if (isContextPane) {
|
|
116
|
-
setSelectedCategory(templateCategories[1]);
|
|
117
|
-
} else if (first) {
|
|
118
|
-
setSelectedCategory(templateCategories[4]);
|
|
136
|
+
const [error, setError] = useState<string | null>(null);
|
|
137
|
+
const [copyMode, setCopyMode] = useState<CopyMode>('prompt');
|
|
138
|
+
const [promptValue, setPromptValue] = useState('');
|
|
139
|
+
const [copyValue, setCopyValue] = useState('');
|
|
140
|
+
const [aiDesignConfig, setAiDesignConfig] = useState<AiDesignConfig>({
|
|
141
|
+
harmony: 'Analogous',
|
|
142
|
+
baseColor: '',
|
|
143
|
+
accentColor: '',
|
|
144
|
+
theme: 'Light',
|
|
145
|
+
additionalNotes: '',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const handleInitialChoice = (choice: InitialChoice) => {
|
|
149
|
+
setInitialChoice(choice);
|
|
150
|
+
setError(null);
|
|
151
|
+
|
|
152
|
+
if (choice === 'blank') {
|
|
153
|
+
handleBlankSlate();
|
|
119
154
|
} else {
|
|
120
|
-
|
|
155
|
+
setStep('copyInput');
|
|
121
156
|
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
const newPreviews = filteredTemplates.map((template, index: number) => {
|
|
126
|
-
const previewCtx = new NodesContext();
|
|
127
|
-
previewCtx.addNode(createEmptyStorykeep('tmp'));
|
|
128
|
-
const thisTemplate =
|
|
129
|
-
mode === 'custom'
|
|
130
|
-
? {
|
|
131
|
-
...template,
|
|
132
|
-
markdown: template.markdown && {
|
|
133
|
-
...template.markdown,
|
|
134
|
-
markdownBody: customMarkdown,
|
|
135
|
-
},
|
|
136
|
-
}
|
|
137
|
-
: template;
|
|
138
|
-
previewCtx.addTemplatePane('tmp', thisTemplate);
|
|
139
|
-
return { ctx: previewCtx, template: thisTemplate, index };
|
|
140
|
-
});
|
|
141
|
-
setPreviews(newPreviews);
|
|
142
|
-
setCurrentPage(0);
|
|
143
|
-
setRenderedPages(new Set());
|
|
144
|
-
}, [filteredTemplates, customMarkdown, mode]);
|
|
145
|
-
|
|
146
|
-
const totalPages = Math.ceil(previews.length / ITEMS_PER_PAGE);
|
|
147
|
-
|
|
148
|
-
const visiblePreviews = useMemo(() => {
|
|
149
|
-
const startIndex = currentPage * ITEMS_PER_PAGE;
|
|
150
|
-
return previews.slice(startIndex, startIndex + ITEMS_PER_PAGE);
|
|
151
|
-
}, [previews, currentPage]);
|
|
152
|
-
|
|
153
|
-
useEffect(() => {
|
|
154
|
-
const pageHasBeenRendered = renderedPages.has(currentPage);
|
|
155
|
-
const previewsOnThisPageNeedFetching = visiblePreviews.some(
|
|
156
|
-
(p) => !p.htmlFragment && !p.fragmentError
|
|
157
|
-
);
|
|
157
|
+
};
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}));
|
|
166
|
-
setFragmentsToGenerate(newRequests);
|
|
167
|
-
} else {
|
|
168
|
-
setFragmentsToGenerate([]);
|
|
159
|
+
const handleBack = () => {
|
|
160
|
+
setError(null);
|
|
161
|
+
if (step === 'copyInput') {
|
|
162
|
+
setStep('initial');
|
|
163
|
+
} else if (step === 'designLibrary' || step === 'aiDesign' || 'error') {
|
|
164
|
+
setStep('copyInput');
|
|
169
165
|
}
|
|
170
|
-
}
|
|
166
|
+
};
|
|
171
167
|
|
|
172
|
-
const
|
|
173
|
-
if (
|
|
174
|
-
|
|
168
|
+
const handleCopyContinue = () => {
|
|
169
|
+
if (initialChoice === 'library') {
|
|
170
|
+
setStep('designLibrary');
|
|
171
|
+
} else if (initialChoice === 'ai') {
|
|
172
|
+
setStep('aiDesign');
|
|
175
173
|
}
|
|
176
174
|
};
|
|
177
175
|
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
176
|
+
const handleBlankSlate = () => {
|
|
177
|
+
const blankTemplate: TemplatePane = {
|
|
178
|
+
id: '', // ctx will assign
|
|
179
|
+
nodeType: 'Pane',
|
|
180
|
+
parentId: '', // ctx will assign
|
|
181
|
+
title: 'New Pane',
|
|
182
|
+
slug: '',
|
|
183
|
+
isDecorative: false,
|
|
184
|
+
markdown: {
|
|
185
|
+
id: '', // ctx will assign
|
|
186
|
+
nodeType: 'Markdown',
|
|
187
|
+
parentId: '', // ctx will assign
|
|
188
|
+
type: 'markdown',
|
|
189
|
+
markdownId: '', // ctx will assign
|
|
190
|
+
defaultClasses: {},
|
|
191
|
+
parentClasses: [],
|
|
192
|
+
nodes: [],
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
handleApplyTemplate(blankTemplate);
|
|
195
196
|
};
|
|
196
197
|
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
snapshot,
|
|
206
|
-
};
|
|
198
|
+
const handleDesignLibrarySelect = async (entry: DesignLibraryEntry) => {
|
|
199
|
+
// This flow is for "Design Library + Provide Copy"
|
|
200
|
+
if (copyMode === 'raw') {
|
|
201
|
+
const liveTemplate = convertStorageToLiveTemplate(
|
|
202
|
+
mergeCopyIntoTemplate(entry.template, []) // Start with blank copy
|
|
203
|
+
);
|
|
204
|
+
if (liveTemplate.markdown) {
|
|
205
|
+
liveTemplate.markdown.markdownBody = copyValue;
|
|
207
206
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const handleApplyTemplate = async (template: any) => {
|
|
213
|
-
if (isInserting || !ctx) return;
|
|
214
|
-
setIsInserting(true);
|
|
215
|
-
|
|
216
|
-
try {
|
|
217
|
-
const insertTemplate =
|
|
218
|
-
mode === 'custom'
|
|
219
|
-
? {
|
|
220
|
-
...cloneDeep(template),
|
|
221
|
-
markdown: template.markdown && {
|
|
222
|
-
...template.markdown,
|
|
223
|
-
markdownBody: customMarkdown,
|
|
224
|
-
},
|
|
225
|
-
}
|
|
226
|
-
: cloneDeep(template);
|
|
227
|
-
|
|
228
|
-
insertTemplate.title = '';
|
|
229
|
-
insertTemplate.slug = '';
|
|
207
|
+
handleApplyTemplate(liveTemplate);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
230
210
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
211
|
+
// This flow is for "Design Library + Write a Prompt" (Hybrid AI)
|
|
212
|
+
if (copyMode === 'prompt') {
|
|
213
|
+
setError(null);
|
|
214
|
+
setStep('loading');
|
|
215
|
+
try {
|
|
216
|
+
// 1. Get the full, rich template from the library
|
|
217
|
+
const liveTemplate = convertStorageToLiveTemplate(entry.template);
|
|
218
|
+
if (!liveTemplate.markdown) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
'The selected design library item is not compatible with this workflow as it has no markdown section.'
|
|
221
|
+
);
|
|
222
|
+
}
|
|
235
223
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
} else {
|
|
242
|
-
await ctx.applyAtomicUpdate(async (tmpCtx) => {
|
|
243
|
-
tmpCtx.addTemplatePane(
|
|
244
|
-
ownerId,
|
|
245
|
-
insertTemplate,
|
|
246
|
-
nodeId,
|
|
247
|
-
first ? 'before' : 'after'
|
|
224
|
+
// 2. Create the simplified shell for the AI
|
|
225
|
+
const shellJson = convertTemplateToAIShell(liveTemplate);
|
|
226
|
+
if (!shellJson || shellJson === '{}') {
|
|
227
|
+
throw new Error(
|
|
228
|
+
'Could not generate a valid AI shell from this design.'
|
|
248
229
|
);
|
|
249
|
-
}
|
|
250
|
-
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 3. Get the AI to write copy based on the shell and prompt
|
|
233
|
+
const copyPromptDetails = prompts.aiPaneCopyPrompt;
|
|
234
|
+
const layout = 'Text Only';
|
|
235
|
+
const formattedCopyPrompt = copyPromptDetails.user_template
|
|
236
|
+
.replace('{{COPY_INPUT}}', promptValue)
|
|
237
|
+
.replace(
|
|
238
|
+
'{{DESIGN_INPUT}}',
|
|
239
|
+
"N/A - Use the provided Shell JSON's design."
|
|
240
|
+
)
|
|
241
|
+
.replace('{{LAYOUT_TYPE}}', layout)
|
|
242
|
+
.replace('{{SHELL_JSON}}', shellJson);
|
|
243
|
+
|
|
244
|
+
const copyResult = await callAskLemurAPI(
|
|
245
|
+
formattedCopyPrompt,
|
|
246
|
+
copyPromptDetails.system || '',
|
|
247
|
+
false,
|
|
248
|
+
isSandboxMode
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// 4. Parse ONLY the AI-generated HTML into content nodes
|
|
252
|
+
const newNodes = parseAiCopyHtml(copyResult, liveTemplate.markdown.id);
|
|
253
|
+
|
|
254
|
+
// 5. Create the final pane by cloning the original rich template
|
|
255
|
+
const finalPane = cloneDeep(liveTemplate);
|
|
256
|
+
|
|
257
|
+
// 6. Inject the new AI content, preserving the original rich design
|
|
258
|
+
finalPane.markdown!.nodes = newNodes;
|
|
259
|
+
|
|
260
|
+
// 7. Apply the complete, correctly merged pane
|
|
261
|
+
handleApplyTemplate(finalPane);
|
|
262
|
+
} catch (err: any) {
|
|
263
|
+
setError(err.message || 'Failed to generate AI copy for this design.');
|
|
264
|
+
setStep('error');
|
|
251
265
|
}
|
|
252
|
-
setParentMode(PaneAddMode.DEFAULT, false);
|
|
253
|
-
} catch (error) {
|
|
254
|
-
console.error('Error inserting template:', error);
|
|
255
|
-
} finally {
|
|
256
|
-
setIsInserting(false);
|
|
257
266
|
}
|
|
258
267
|
};
|
|
259
268
|
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
269
|
+
const handleAiDesignGenerate = useCallback(async () => {
|
|
270
|
+
setError(null);
|
|
271
|
+
setStep('loading');
|
|
272
|
+
|
|
273
|
+
let designInput = `Generate a design using a **${aiDesignConfig.harmony.toLowerCase()}** color scheme with a **${aiDesignConfig.theme.toLowerCase()}** theme.`;
|
|
274
|
+
if (aiDesignConfig.baseColor)
|
|
275
|
+
designInput += ` Base the colors around **${aiDesignConfig.baseColor}**.`;
|
|
276
|
+
if (aiDesignConfig.accentColor)
|
|
277
|
+
designInput += ` Use **${aiDesignConfig.accentColor}** as an accent color.`;
|
|
278
|
+
if (aiDesignConfig.additionalNotes)
|
|
279
|
+
designInput += ` Refine with these notes: "${aiDesignConfig.additionalNotes}"`;
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const shellPromptDetails = prompts.aiPaneShellPrompt;
|
|
283
|
+
const copyPromptDetails = prompts.aiPaneCopyPrompt;
|
|
284
|
+
const layout = 'Text Only'; // Hardcoded for this simplified AI path
|
|
285
|
+
|
|
286
|
+
const formattedShellPrompt = shellPromptDetails.user_template
|
|
287
|
+
.replace('{{DESIGN_INPUT}}', designInput)
|
|
288
|
+
.replace('{{LAYOUT_TYPE}}', layout);
|
|
289
|
+
|
|
290
|
+
const shellResult = await callAskLemurAPI(
|
|
291
|
+
formattedShellPrompt,
|
|
292
|
+
shellPromptDetails.system || '',
|
|
293
|
+
true,
|
|
294
|
+
isSandboxMode
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const copyInputContent = copyMode === 'prompt' ? promptValue : copyValue;
|
|
298
|
+
const formattedCopyPrompt = copyPromptDetails.user_template
|
|
299
|
+
.replace('{{COPY_INPUT}}', copyInputContent)
|
|
300
|
+
.replace('{{DESIGN_INPUT}}', designInput)
|
|
301
|
+
.replace('{{LAYOUT_TYPE}}', layout)
|
|
302
|
+
.replace('{{SHELL_JSON}}', shellResult);
|
|
303
|
+
|
|
304
|
+
const copyResult = await callAskLemurAPI(
|
|
305
|
+
formattedCopyPrompt,
|
|
306
|
+
copyPromptDetails.system || '',
|
|
307
|
+
false,
|
|
308
|
+
isSandboxMode
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const finalPane = parseAiPane(shellResult, copyResult, layout);
|
|
312
|
+
handleApplyTemplate(finalPane);
|
|
313
|
+
} catch (err: any) {
|
|
314
|
+
setError(err.message || 'Failed to generate AI pane.');
|
|
315
|
+
setStep('error');
|
|
316
|
+
}
|
|
317
|
+
}, [aiDesignConfig, copyMode, promptValue, copyValue, isSandboxMode]);
|
|
318
|
+
|
|
319
|
+
const handleApplyTemplate = async (template: TemplatePane) => {
|
|
320
|
+
if (!ctx) return;
|
|
263
321
|
try {
|
|
322
|
+
const insertTemplate = cloneDeep(template);
|
|
323
|
+
insertTemplate.title = insertTemplate.title || 'New Pane';
|
|
324
|
+
insertTemplate.slug = insertTemplate.slug || '';
|
|
325
|
+
|
|
264
326
|
const ownerId =
|
|
265
327
|
isStoryFragment || isContextPane
|
|
266
328
|
? nodeId
|
|
267
329
|
: ctx.getClosestNodeTypeFromId(nodeId, 'StoryFragment');
|
|
268
330
|
|
|
269
331
|
if (isContextPane) {
|
|
270
|
-
|
|
332
|
+
insertTemplate.isContextPane = true;
|
|
271
333
|
await ctx.applyAtomicUpdate(async (tmpCtx) => {
|
|
272
|
-
tmpCtx.addContextTemplatePane(ownerId,
|
|
334
|
+
tmpCtx.addContextTemplatePane(ownerId, insertTemplate);
|
|
273
335
|
});
|
|
274
336
|
} else {
|
|
275
337
|
await ctx.applyAtomicUpdate(async (tmpCtx) => {
|
|
276
338
|
tmpCtx.addTemplatePane(
|
|
277
339
|
ownerId,
|
|
278
|
-
|
|
340
|
+
insertTemplate,
|
|
279
341
|
nodeId,
|
|
280
342
|
first ? 'before' : 'after'
|
|
281
343
|
);
|
|
@@ -283,363 +345,203 @@ const AddPaneNewPanel = ({
|
|
|
283
345
|
ctx.notifyNode(`root`);
|
|
284
346
|
}
|
|
285
347
|
setParentMode(PaneAddMode.DEFAULT, false);
|
|
286
|
-
} catch (
|
|
287
|
-
console.error('Error
|
|
288
|
-
|
|
289
|
-
|
|
348
|
+
} catch (err) {
|
|
349
|
+
console.error('Error inserting template:', err);
|
|
350
|
+
setError(
|
|
351
|
+
err instanceof Error
|
|
352
|
+
? err.message
|
|
353
|
+
: 'An unexpected error occurred while adding the pane.'
|
|
354
|
+
);
|
|
355
|
+
setStep('error');
|
|
290
356
|
}
|
|
291
357
|
};
|
|
292
358
|
|
|
293
|
-
|
|
294
|
-
const newTheme = details.value[0] as Theme;
|
|
295
|
-
if (newTheme) {
|
|
296
|
-
setSelectedTheme(newTheme);
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
const handleCategoryChange = (details: { value: string[] }) => {
|
|
301
|
-
const id = details.value[0];
|
|
302
|
-
if (id) {
|
|
303
|
-
const category = templateCategories.find((cat) => cat.id === id);
|
|
304
|
-
if (category) setSelectedCategory(category);
|
|
305
|
-
}
|
|
306
|
-
};
|
|
359
|
+
// --- Render Logic ---
|
|
307
360
|
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
361
|
+
const renderInitialStep = () => (
|
|
362
|
+
<div className="p-4">
|
|
363
|
+
<h3 className="font-action mb-4 text-center text-xl font-bold text-gray-800">
|
|
364
|
+
How would you like to design your new pane?
|
|
365
|
+
</h3>
|
|
366
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
367
|
+
<button
|
|
368
|
+
onClick={() => handleInitialChoice('library')}
|
|
369
|
+
className="group flex flex-col items-center space-y-3 rounded-lg border bg-white p-6 text-center shadow-sm transition-all hover:border-cyan-600 hover:shadow-lg"
|
|
370
|
+
>
|
|
371
|
+
<SwatchIcon className="h-10 w-10 text-gray-500 transition-colors group-hover:text-cyan-600" />
|
|
372
|
+
<h4 className="font-semibold text-gray-800">Use Design Library</h4>
|
|
373
|
+
<p className="text-sm text-gray-600">
|
|
374
|
+
Start with a pre-made design and add your own content.
|
|
375
|
+
</p>
|
|
376
|
+
</button>
|
|
377
|
+
{hasAssemblyAI && (
|
|
378
|
+
<button
|
|
379
|
+
onClick={() => handleInitialChoice('ai')}
|
|
380
|
+
className="group flex flex-col items-center space-y-3 rounded-lg border bg-white p-6 text-center shadow-sm transition-all hover:border-cyan-600 hover:shadow-lg"
|
|
381
|
+
>
|
|
382
|
+
<SparklesIcon className="h-10 w-10 text-gray-500 transition-colors group-hover:text-cyan-600" />
|
|
383
|
+
<h4 className="font-semibold text-gray-800">Design with AI</h4>
|
|
384
|
+
<p className="text-sm text-gray-600">
|
|
385
|
+
Let AI generate a complete design and copy from your prompt.
|
|
386
|
+
</p>
|
|
387
|
+
</button>
|
|
388
|
+
)}
|
|
389
|
+
<button
|
|
390
|
+
onClick={() => handleInitialChoice('blank')}
|
|
391
|
+
className="group flex flex-col items-center space-y-3 rounded-lg border bg-white p-6 text-center shadow-sm transition-all hover:border-cyan-600 hover:shadow-lg"
|
|
392
|
+
>
|
|
393
|
+
<DocumentPlusIcon className="h-10 w-10 text-gray-500 transition-colors group-hover:text-cyan-600" />
|
|
394
|
+
<h4 className="font-semibold text-gray-800">Blank Slate</h4>
|
|
395
|
+
<p className="text-sm text-gray-600">
|
|
396
|
+
Add a simple, empty pane to build from scratch.
|
|
397
|
+
</p>
|
|
398
|
+
</button>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
);
|
|
320
402
|
|
|
321
|
-
|
|
322
|
-
<div className="
|
|
323
|
-
<
|
|
324
|
-
|
|
403
|
+
const renderContentStep = () => (
|
|
404
|
+
<div className="space-y-4 p-4">
|
|
405
|
+
<CopyInputStep
|
|
406
|
+
copyMode={copyMode}
|
|
407
|
+
onCopyModeChange={setCopyMode}
|
|
408
|
+
promptValue={promptValue}
|
|
409
|
+
onPromptValueChange={setPromptValue}
|
|
410
|
+
copyValue={copyValue}
|
|
411
|
+
onCopyValueChange={setCopyValue}
|
|
412
|
+
defaultPrompt={
|
|
413
|
+
first
|
|
414
|
+
? prompts.aiPaneCopyPrompt.heroDefault
|
|
415
|
+
: prompts.aiPaneCopyPrompt.contentDefault
|
|
416
|
+
}
|
|
417
|
+
/>
|
|
418
|
+
<div className="flex justify-between">
|
|
325
419
|
<button
|
|
326
|
-
onClick={
|
|
327
|
-
className="
|
|
328
|
-
type="button"
|
|
420
|
+
onClick={handleBack}
|
|
421
|
+
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 shadow-sm hover:bg-gray-50"
|
|
329
422
|
>
|
|
330
|
-
←
|
|
423
|
+
← Back
|
|
331
424
|
</button>
|
|
425
|
+
<button
|
|
426
|
+
onClick={handleCopyContinue}
|
|
427
|
+
disabled={
|
|
428
|
+
copyMode === 'prompt' ? !promptValue.trim() : !copyValue.trim()
|
|
429
|
+
}
|
|
430
|
+
className="rounded-md bg-cyan-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-700 disabled:cursor-not-allowed disabled:bg-gray-400"
|
|
431
|
+
>
|
|
432
|
+
Continue →
|
|
433
|
+
</button>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
);
|
|
332
437
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
<button
|
|
350
|
-
onClick={() => setMode('custom')}
|
|
351
|
-
className={`rounded-md px-3 py-1 text-sm font-medium transition-colors ${
|
|
352
|
-
mode === 'custom'
|
|
353
|
-
? 'bg-white text-cyan-700 shadow'
|
|
354
|
-
: 'text-gray-600 hover:text-gray-800'
|
|
355
|
-
}`}
|
|
356
|
-
type="button"
|
|
357
|
-
>
|
|
358
|
-
Paste Markdown
|
|
359
|
-
</button>
|
|
360
|
-
{hasAssemblyAI && (
|
|
361
|
-
<button
|
|
362
|
-
onClick={() => setMode('ai')}
|
|
363
|
-
className={`rounded-md px-3 py-1 text-sm font-medium transition-colors ${
|
|
364
|
-
mode === 'ai'
|
|
365
|
-
? 'rounded-md border border-transparent bg-cyan-600 px-3 py-1.5 text-sm font-bold text-white shadow-sm hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2'
|
|
366
|
-
: 'text-gray-600 hover:text-gray-800'
|
|
367
|
-
}`}
|
|
368
|
-
type="button"
|
|
369
|
-
>
|
|
370
|
-
✨ Generate with AI
|
|
371
|
-
</button>
|
|
372
|
-
)}
|
|
373
|
-
</div>
|
|
438
|
+
const renderDesignLibraryStep = () => (
|
|
439
|
+
<div className="space-y-4 p-4">
|
|
440
|
+
<div className="flex justify-start">
|
|
441
|
+
<button
|
|
442
|
+
onClick={handleBack}
|
|
443
|
+
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 shadow-sm hover:bg-gray-50"
|
|
444
|
+
>
|
|
445
|
+
← Back to Content
|
|
446
|
+
</button>
|
|
447
|
+
</div>
|
|
448
|
+
<DesignLibraryStep
|
|
449
|
+
config={config!}
|
|
450
|
+
onSelect={handleDesignLibrarySelect}
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
);
|
|
374
454
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
455
|
+
const renderAiDesignStep = () => (
|
|
456
|
+
<div className="space-y-4 p-4">
|
|
457
|
+
<AiDesignStep
|
|
458
|
+
config={config!}
|
|
459
|
+
designConfig={aiDesignConfig}
|
|
460
|
+
onDesignConfigChange={setAiDesignConfig}
|
|
461
|
+
/>
|
|
462
|
+
<div className="flex justify-between">
|
|
463
|
+
<button
|
|
464
|
+
onClick={handleBack}
|
|
465
|
+
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 shadow-sm hover:bg-gray-50"
|
|
466
|
+
>
|
|
467
|
+
← Back
|
|
468
|
+
</button>
|
|
469
|
+
<button
|
|
470
|
+
onClick={handleAiDesignGenerate}
|
|
471
|
+
className="rounded-md bg-cyan-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-700"
|
|
472
|
+
>
|
|
473
|
+
✨ Generate with AI
|
|
474
|
+
</button>
|
|
394
475
|
</div>
|
|
476
|
+
</div>
|
|
477
|
+
);
|
|
395
478
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
<div className="w-full">
|
|
404
|
-
<Select.Root
|
|
405
|
-
collection={themesCollection}
|
|
406
|
-
value={[selectedTheme]}
|
|
407
|
-
onValueChange={handleThemeChange}
|
|
408
|
-
>
|
|
409
|
-
<Select.Label className="block text-sm font-bold text-gray-700">
|
|
410
|
-
Theme
|
|
411
|
-
</Select.Label>
|
|
412
|
-
<Select.Control className="relative mt-1">
|
|
413
|
-
<Select.Trigger className="relative w-full cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-cyan-600 focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-cyan-600">
|
|
414
|
-
<Select.ValueText className="block truncate capitalize">
|
|
415
|
-
{selectedTheme.replace(/-/g, ' ')}
|
|
416
|
-
</Select.ValueText>
|
|
417
|
-
<Select.Indicator className="absolute inset-y-0 right-0 flex items-center pr-2">
|
|
418
|
-
<ChevronUpDownIcon
|
|
419
|
-
className="h-5 w-5 text-gray-400"
|
|
420
|
-
aria-hidden="true"
|
|
421
|
-
/>
|
|
422
|
-
</Select.Indicator>
|
|
423
|
-
</Select.Trigger>
|
|
424
|
-
</Select.Control>
|
|
425
|
-
<Portal>
|
|
426
|
-
<Select.Positioner>
|
|
427
|
-
<Select.Content className="z-50 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
|
|
428
|
-
{themesCollection.items.map((theme) => (
|
|
429
|
-
<Select.Item
|
|
430
|
-
key={theme}
|
|
431
|
-
item={theme}
|
|
432
|
-
className="theme-item relative cursor-default select-none py-2 pl-10 pr-4 text-gray-900"
|
|
433
|
-
>
|
|
434
|
-
<Select.ItemText className="block truncate capitalize">
|
|
435
|
-
{theme.replace(/-/g, ' ')}
|
|
436
|
-
</Select.ItemText>
|
|
437
|
-
<Select.ItemIndicator className="theme-indicator absolute inset-y-0 left-0 flex items-center pl-3 text-cyan-600">
|
|
438
|
-
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
|
439
|
-
</Select.ItemIndicator>
|
|
440
|
-
</Select.Item>
|
|
441
|
-
))}
|
|
442
|
-
</Select.Content>
|
|
443
|
-
</Select.Positioner>
|
|
444
|
-
</Portal>
|
|
445
|
-
</Select.Root>
|
|
446
|
-
</div>
|
|
479
|
+
const renderLoading = () => (
|
|
480
|
+
<div className="flex min-h-[300px] flex-col items-center justify-center space-y-4 p-6">
|
|
481
|
+
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-cyan-600"></div>
|
|
482
|
+
<p className="text-sm text-gray-600">Generating AI Pane...</p>
|
|
483
|
+
<p className="text-xs text-gray-500">This may take a moment.</p>
|
|
484
|
+
</div>
|
|
485
|
+
);
|
|
447
486
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
<Select.ValueText className="block truncate">
|
|
461
|
-
{selectedCategory.title}
|
|
462
|
-
</Select.ValueText>
|
|
463
|
-
<Select.Indicator className="absolute inset-y-0 right-0 flex items-center pr-2">
|
|
464
|
-
<ChevronUpDownIcon
|
|
465
|
-
className="h-5 w-5 text-gray-400"
|
|
466
|
-
aria-hidden="true"
|
|
467
|
-
/>
|
|
468
|
-
</Select.Indicator>
|
|
469
|
-
</Select.Trigger>
|
|
470
|
-
</Select.Control>
|
|
471
|
-
<Portal>
|
|
472
|
-
<Select.Positioner>
|
|
473
|
-
<Select.Content className="z-50 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
|
|
474
|
-
{categoryCollection.items.map((category) => (
|
|
475
|
-
<Select.Item
|
|
476
|
-
key={category.id}
|
|
477
|
-
item={category}
|
|
478
|
-
className="category-item relative cursor-default select-none py-2 pl-10 pr-4 text-gray-900"
|
|
479
|
-
>
|
|
480
|
-
<Select.ItemText className="block truncate">
|
|
481
|
-
{category.title}
|
|
482
|
-
</Select.ItemText>
|
|
483
|
-
<Select.ItemIndicator className="category-indicator absolute inset-y-0 left-0 flex items-center pl-3 text-cyan-600">
|
|
484
|
-
<CheckIcon
|
|
485
|
-
className="h-5 w-5"
|
|
486
|
-
aria-hidden="true"
|
|
487
|
-
/>
|
|
488
|
-
</Select.ItemIndicator>
|
|
489
|
-
</Select.Item>
|
|
490
|
-
))}
|
|
491
|
-
</Select.Content>
|
|
492
|
-
</Select.Positioner>
|
|
493
|
-
</Portal>
|
|
494
|
-
</Select.Root>
|
|
495
|
-
</div>
|
|
496
|
-
)}
|
|
497
|
-
|
|
498
|
-
<div className="flex items-center gap-2">
|
|
499
|
-
<Switch.Root
|
|
500
|
-
checked={useOddVariant}
|
|
501
|
-
onCheckedChange={(details) => setUseOddVariant(details.checked)}
|
|
502
|
-
className="inline-flex items-center"
|
|
503
|
-
>
|
|
504
|
-
<Switch.Control
|
|
505
|
-
className={`${
|
|
506
|
-
useOddVariant ? 'bg-cyan-600' : 'bg-gray-200'
|
|
507
|
-
} relative my-2 inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2`}
|
|
508
|
-
>
|
|
509
|
-
<Switch.Thumb
|
|
510
|
-
className={`${
|
|
511
|
-
useOddVariant ? 'translate-x-6' : 'translate-x-1'
|
|
512
|
-
} inline-block h-4 w-4 rounded-full bg-white shadow-lg transition-transform duration-200`}
|
|
513
|
-
/>
|
|
514
|
-
</Switch.Control>
|
|
515
|
-
<Switch.HiddenInput />
|
|
516
|
-
<div className="flex h-6 items-center">
|
|
517
|
-
<Switch.Label className="px-4 text-sm text-gray-700">
|
|
518
|
-
Toggle subtle variation (great for stacking content)
|
|
519
|
-
</Switch.Label>
|
|
520
|
-
</div>
|
|
521
|
-
</Switch.Root>
|
|
522
|
-
</div>
|
|
523
|
-
</div>
|
|
487
|
+
const renderError = () => (
|
|
488
|
+
<div className="space-y-4 rounded-lg bg-red-50 p-6 text-center">
|
|
489
|
+
<h4 className="text-lg font-bold text-red-800">Generation Failed</h4>
|
|
490
|
+
<p className="text-sm text-red-700">{error}</p>
|
|
491
|
+
<button
|
|
492
|
+
onClick={handleBack}
|
|
493
|
+
className="mt-2 rounded-md border border-red-300 bg-white px-4 py-2 text-sm font-bold text-red-800 shadow-sm hover:bg-red-50"
|
|
494
|
+
>
|
|
495
|
+
← Try Again
|
|
496
|
+
</button>
|
|
497
|
+
</div>
|
|
498
|
+
);
|
|
524
499
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
onClick={
|
|
544
|
-
isInserting
|
|
545
|
-
? undefined
|
|
546
|
-
: () => handleApplyTemplate(preview.template)
|
|
547
|
-
}
|
|
548
|
-
className={`bg-mywhite group relative w-full rounded-sm shadow-inner ${
|
|
549
|
-
isInserting
|
|
550
|
-
? 'cursor-not-allowed opacity-50'
|
|
551
|
-
: 'cursor-pointer'
|
|
552
|
-
} transition-all duration-200 ${
|
|
553
|
-
preview.snapshot
|
|
554
|
-
? 'hover:outline-solid hover:outline hover:outline-4'
|
|
555
|
-
: ''
|
|
556
|
-
}`}
|
|
557
|
-
style={{
|
|
558
|
-
...(!preview.snapshot ? { minHeight: '150px' } : {}),
|
|
559
|
-
}}
|
|
560
|
-
role="button"
|
|
561
|
-
tabIndex={0}
|
|
562
|
-
aria-label={preview.template.title}
|
|
563
|
-
>
|
|
564
|
-
{preview.htmlFragment &&
|
|
565
|
-
!preview.snapshot &&
|
|
566
|
-
!preview.fragmentError && (
|
|
567
|
-
<PaneSnapshotGenerator
|
|
568
|
-
id={`template-${preview.index}`}
|
|
569
|
-
htmlString={preview.htmlFragment}
|
|
570
|
-
onComplete={handleSnapshotComplete}
|
|
571
|
-
onError={(id, error) =>
|
|
572
|
-
console.error(`Snapshot error for ${id}:`, error)
|
|
573
|
-
}
|
|
574
|
-
outputWidth={800}
|
|
575
|
-
/>
|
|
576
|
-
)}
|
|
577
|
-
|
|
578
|
-
{!preview.htmlFragment && !preview.fragmentError && (
|
|
579
|
-
<div className="flex h-48 items-center justify-center">
|
|
580
|
-
<div className="text-gray-500">Loading preview...</div>
|
|
581
|
-
</div>
|
|
582
|
-
)}
|
|
583
|
-
|
|
584
|
-
{preview.fragmentError && (
|
|
585
|
-
<div className="flex h-48 items-center justify-center">
|
|
586
|
-
<div className="text-red-500">Preview error</div>
|
|
587
|
-
</div>
|
|
588
|
-
)}
|
|
589
|
-
|
|
590
|
-
{preview.snapshot && (
|
|
591
|
-
<div className="p-0.5">
|
|
592
|
-
<img
|
|
593
|
-
src={preview.snapshot.imageData}
|
|
594
|
-
alt={`Template: ${preview.template.title}`}
|
|
595
|
-
className="w-full"
|
|
596
|
-
/>
|
|
597
|
-
</div>
|
|
598
|
-
)}
|
|
599
|
-
</div>
|
|
600
|
-
<p className="bg-mydarkgrey mt-2 w-full break-words p-2 text-center text-sm text-white">
|
|
601
|
-
{preview.template.title}
|
|
602
|
-
</p>
|
|
603
|
-
</div>
|
|
604
|
-
))}
|
|
605
|
-
</div>
|
|
500
|
+
const renderStep = () => {
|
|
501
|
+
switch (step) {
|
|
502
|
+
case 'initial':
|
|
503
|
+
return renderInitialStep();
|
|
504
|
+
case 'copyInput':
|
|
505
|
+
return renderContentStep();
|
|
506
|
+
case 'designLibrary':
|
|
507
|
+
return renderDesignLibraryStep();
|
|
508
|
+
case 'aiDesign':
|
|
509
|
+
return renderAiDesignStep();
|
|
510
|
+
case 'loading':
|
|
511
|
+
return renderLoading();
|
|
512
|
+
case 'error':
|
|
513
|
+
return renderError();
|
|
514
|
+
default:
|
|
515
|
+
return renderInitialStep();
|
|
516
|
+
}
|
|
517
|
+
};
|
|
606
518
|
|
|
607
|
-
|
|
519
|
+
return (
|
|
520
|
+
<div className="bg-white p-2 shadow-inner">
|
|
521
|
+
<div className="group mb-2 flex w-full items-center gap-1 rounded-md bg-white p-1.5">
|
|
522
|
+
{first ? (
|
|
523
|
+
<div className="w-full text-center">
|
|
524
|
+
<h2 className="font-action py-1.5 text-lg font-bold text-gray-800">
|
|
525
|
+
Welcome to Tract Stack
|
|
526
|
+
</h2>
|
|
527
|
+
</div>
|
|
528
|
+
) : (
|
|
529
|
+
<>
|
|
608
530
|
<button
|
|
609
|
-
onClick={() =>
|
|
610
|
-
|
|
611
|
-
className="rounded bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-gray-200 focus:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50"
|
|
612
|
-
type="button"
|
|
531
|
+
onClick={() => setParentMode(PaneAddMode.DEFAULT, first)}
|
|
532
|
+
className="w-fit rounded bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-gray-200"
|
|
613
533
|
>
|
|
614
|
-
|
|
534
|
+
← Go Back
|
|
615
535
|
</button>
|
|
616
|
-
<div className="flex
|
|
617
|
-
|
|
618
|
-
<button
|
|
619
|
-
key={index}
|
|
620
|
-
onClick={() => handlePageChange(index)}
|
|
621
|
-
className={`rounded px-3 py-1 text-sm transition-colors ${
|
|
622
|
-
currentPage === index
|
|
623
|
-
? 'bg-cyan-700 text-white'
|
|
624
|
-
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
|
625
|
-
}`}
|
|
626
|
-
type="button"
|
|
627
|
-
>
|
|
628
|
-
{index + 1}
|
|
629
|
-
</button>
|
|
630
|
-
))}
|
|
536
|
+
<div className="font-action ml-4 flex-none rounded px-2 py-2.5 text-sm font-bold text-cyan-700 shadow-sm">
|
|
537
|
+
+ Design New Pane
|
|
631
538
|
</div>
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
Next
|
|
639
|
-
</button>
|
|
640
|
-
</div>
|
|
641
|
-
</>
|
|
642
|
-
)}
|
|
539
|
+
</>
|
|
540
|
+
)}
|
|
541
|
+
</div>
|
|
542
|
+
<div className="min-h-[400px] rounded-md border bg-gray-50">
|
|
543
|
+
{renderStep()}
|
|
544
|
+
</div>
|
|
643
545
|
</div>
|
|
644
546
|
);
|
|
645
547
|
};
|