astro-tractstack 2.0.13 → 2.0.15
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 +40 -0
- package/package.json +1 -1
- package/templates/src/client/view.js +5 -0
- package/templates/src/components/compositor/Compositor.tsx +3 -2
- package/templates/src/components/compositor/Node.tsx +25 -8
- package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +105 -0
- package/templates/src/components/edit/ToolMode.tsx +7 -0
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +459 -561
- package/templates/src/components/edit/pane/AiPaneGenerator.tsx +19 -82
- package/templates/src/components/edit/pane/RestylePaneModal.tsx +573 -0
- 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/state/SaveToLibraryModal.tsx +205 -0
- package/templates/src/constants/prompts.json +3 -1
- package/templates/src/stores/selection.ts +4 -0
- package/templates/src/types/compositorTypes.ts +51 -1
- package/templates/src/types/tractstack.ts +36 -31
- package/templates/src/utils/aai/getTitleSlug.ts +1 -1
- package/templates/src/utils/api/brandConfig.ts +8 -2
- package/templates/src/utils/api/brandHelpers.ts +4 -0
- package/templates/src/utils/compositor/aiPaneParser.ts +32 -84
- package/templates/src/utils/compositor/designLibraryHelper.ts +416 -0
- package/templates/src/utils/compositor/processMarkdown.ts +1 -1
- package/utils/inject-files.ts +40 -0
- 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
|
@@ -1,35 +1,104 @@
|
|
|
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';
|
|
32
13
|
|
|
14
|
+
import { CopyInputStep } from './steps/CopyInputStep';
|
|
15
|
+
import { DesignLibraryStep } from './steps/DesignLibraryStep';
|
|
16
|
+
import { AiDesignStep, type AiDesignConfig } from './steps/AiDesignStep';
|
|
17
|
+
import { parseAiPane, parseAiCopyHtml } from '@/utils/compositor/aiPaneParser';
|
|
18
|
+
import {
|
|
19
|
+
convertStorageToLiveTemplate,
|
|
20
|
+
mergeCopyIntoTemplate,
|
|
21
|
+
convertTemplateToAIShell,
|
|
22
|
+
} from '@/utils/compositor/designLibraryHelper';
|
|
23
|
+
|
|
24
|
+
// --- Types for Workflow State ---
|
|
25
|
+
type Step =
|
|
26
|
+
| 'initial'
|
|
27
|
+
| 'copyInput'
|
|
28
|
+
| 'designLibrary'
|
|
29
|
+
| 'aiDesign'
|
|
30
|
+
| 'loading'
|
|
31
|
+
| 'error';
|
|
32
|
+
|
|
33
|
+
type InitialChoice = 'library' | 'ai' | 'blank';
|
|
34
|
+
type CopyMode = 'prompt' | 'raw';
|
|
35
|
+
|
|
36
|
+
// --- API Call Helper ---
|
|
37
|
+
interface GenerationResponse {
|
|
38
|
+
success: boolean;
|
|
39
|
+
data?: { response: string | object };
|
|
40
|
+
error?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const callAskLemurAPI = async (
|
|
44
|
+
prompt: string,
|
|
45
|
+
context: string,
|
|
46
|
+
expectJson: boolean
|
|
47
|
+
): Promise<string> => {
|
|
48
|
+
const goBackend =
|
|
49
|
+
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
50
|
+
const tenantId = import.meta.env.PUBLIC_TENANTID || 'default';
|
|
51
|
+
const requestBody = {
|
|
52
|
+
prompt,
|
|
53
|
+
input_text: context,
|
|
54
|
+
final_model: '',
|
|
55
|
+
temperature: 0.5,
|
|
56
|
+
max_tokens: 2000,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const response = await fetch(`${goBackend}/api/v1/aai/askLemur`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
|
|
62
|
+
credentials: 'include',
|
|
63
|
+
body: JSON.stringify(requestBody),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const errorText = await response.text();
|
|
68
|
+
let backendError = `API call failed: ${response.status} ${response.statusText}`;
|
|
69
|
+
try {
|
|
70
|
+
const errorJson = JSON.parse(errorText);
|
|
71
|
+
if (errorJson?.error) backendError = errorJson.error;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
/* ignore */
|
|
74
|
+
}
|
|
75
|
+
throw new Error(backendError);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = (await response.json()) as GenerationResponse;
|
|
79
|
+
if (!result.success || !result.data?.response) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
result.error || 'Generation failed to return valid response.'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let rawResponseData = result.data.response;
|
|
86
|
+
if (expectJson && typeof rawResponseData === 'object') {
|
|
87
|
+
return JSON.stringify(rawResponseData);
|
|
88
|
+
}
|
|
89
|
+
if (typeof rawResponseData === 'string') {
|
|
90
|
+
let responseString = rawResponseData;
|
|
91
|
+
if (responseString.startsWith('```json')) {
|
|
92
|
+
responseString = responseString.slice(7, -3).trim();
|
|
93
|
+
} else if (responseString.startsWith('```html')) {
|
|
94
|
+
responseString = responseString.slice(7, -3).trim();
|
|
95
|
+
}
|
|
96
|
+
return responseString;
|
|
97
|
+
}
|
|
98
|
+
throw new Error('Unexpected response format received from API.');
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// --- Main Component ---
|
|
33
102
|
interface AddPaneNewPanelProps {
|
|
34
103
|
nodeId: string;
|
|
35
104
|
first: boolean;
|
|
@@ -40,242 +109,231 @@ interface AddPaneNewPanelProps {
|
|
|
40
109
|
config?: BrandConfig;
|
|
41
110
|
}
|
|
42
111
|
|
|
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
112
|
const AddPaneNewPanel = ({
|
|
63
113
|
nodeId,
|
|
64
114
|
first,
|
|
65
|
-
setMode: setParentMode,
|
|
66
|
-
ctx,
|
|
115
|
+
setMode: setParentMode,
|
|
116
|
+
ctx: providedCtx,
|
|
67
117
|
isStoryFragment = false,
|
|
68
118
|
isContextPane = false,
|
|
69
119
|
config,
|
|
70
120
|
}: AddPaneNewPanelProps) => {
|
|
71
|
-
const
|
|
121
|
+
const ctx = providedCtx || getCtx();
|
|
72
122
|
const hasAssemblyAI = useStore(hasAssemblyAIStore);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const [
|
|
76
|
-
const [
|
|
77
|
-
|
|
78
|
-
const [selectedTheme, setSelectedTheme] = useState<Theme>(
|
|
79
|
-
useStore(preferredThemeStore)
|
|
80
|
-
);
|
|
81
|
-
const [useOddVariant, setUseOddVariant] = useState(false);
|
|
82
|
-
const [selectedCategory, setSelectedCategory] = useState<TemplateCategory>(
|
|
83
|
-
templateCategories[isContextPane ? 1 : first ? 4 : 0]
|
|
123
|
+
|
|
124
|
+
// --- State Machine and Data Stores ---
|
|
125
|
+
const [step, setStep] = useState<Step>('initial');
|
|
126
|
+
const [initialChoice, setInitialChoice] = useState<InitialChoice | null>(
|
|
127
|
+
null
|
|
84
128
|
);
|
|
85
|
-
const [
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
>(
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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]);
|
|
129
|
+
const [error, setError] = useState<string | null>(null);
|
|
130
|
+
|
|
131
|
+
// State for CopyInputStep
|
|
132
|
+
const [copyMode, setCopyMode] = useState<CopyMode>('raw');
|
|
133
|
+
const [promptValue, setPromptValue] = useState('');
|
|
134
|
+
const [copyValue, setCopyValue] = useState('');
|
|
135
|
+
|
|
136
|
+
// State for AiDesignStep
|
|
137
|
+
const [aiDesignConfig, setAiDesignConfig] = useState<AiDesignConfig>({
|
|
138
|
+
harmony: 'Analogous',
|
|
139
|
+
baseColor: '',
|
|
140
|
+
accentColor: '',
|
|
141
|
+
theme: 'Light',
|
|
142
|
+
additionalNotes: '',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// --- Handlers & Logic ---
|
|
146
|
+
|
|
147
|
+
const handleInitialChoice = (choice: InitialChoice) => {
|
|
148
|
+
setInitialChoice(choice);
|
|
149
|
+
setError(null);
|
|
150
|
+
|
|
151
|
+
if (choice === 'blank') {
|
|
152
|
+
handleBlankSlate();
|
|
119
153
|
} else {
|
|
120
|
-
|
|
154
|
+
setStep('copyInput');
|
|
121
155
|
}
|
|
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
|
-
);
|
|
156
|
+
};
|
|
158
157
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}));
|
|
166
|
-
setFragmentsToGenerate(newRequests);
|
|
167
|
-
} else {
|
|
168
|
-
setFragmentsToGenerate([]);
|
|
158
|
+
const handleBack = () => {
|
|
159
|
+
setError(null);
|
|
160
|
+
if (step === 'copyInput') {
|
|
161
|
+
setStep('initial');
|
|
162
|
+
} else if (step === 'designLibrary' || step === 'aiDesign' || 'error') {
|
|
163
|
+
setStep('copyInput');
|
|
169
164
|
}
|
|
170
|
-
}
|
|
165
|
+
};
|
|
171
166
|
|
|
172
|
-
const
|
|
173
|
-
if (
|
|
174
|
-
|
|
167
|
+
const handleCopyContinue = () => {
|
|
168
|
+
if (initialChoice === 'library') {
|
|
169
|
+
setStep('designLibrary');
|
|
170
|
+
} else if (initialChoice === 'ai') {
|
|
171
|
+
setStep('aiDesign');
|
|
175
172
|
}
|
|
176
173
|
};
|
|
177
174
|
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
175
|
+
const handleBlankSlate = () => {
|
|
176
|
+
const blankTemplate: TemplatePane = {
|
|
177
|
+
id: '', // ctx will assign
|
|
178
|
+
nodeType: 'Pane',
|
|
179
|
+
parentId: '', // ctx will assign
|
|
180
|
+
title: 'New Pane',
|
|
181
|
+
slug: '',
|
|
182
|
+
isDecorative: false,
|
|
183
|
+
markdown: {
|
|
184
|
+
id: '', // ctx will assign
|
|
185
|
+
nodeType: 'Markdown',
|
|
186
|
+
parentId: '', // ctx will assign
|
|
187
|
+
type: 'markdown',
|
|
188
|
+
markdownId: '', // ctx will assign
|
|
189
|
+
defaultClasses: {},
|
|
190
|
+
parentClasses: [],
|
|
191
|
+
nodes: [],
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
handleApplyTemplate(blankTemplate);
|
|
195
195
|
};
|
|
196
196
|
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
snapshot,
|
|
206
|
-
};
|
|
197
|
+
const handleDesignLibrarySelect = async (entry: DesignLibraryEntry) => {
|
|
198
|
+
// This flow is for "Design Library + Provide Copy"
|
|
199
|
+
if (copyMode === 'raw') {
|
|
200
|
+
const liveTemplate = convertStorageToLiveTemplate(
|
|
201
|
+
mergeCopyIntoTemplate(entry.template, []) // Start with blank copy
|
|
202
|
+
);
|
|
203
|
+
if (liveTemplate.markdown) {
|
|
204
|
+
liveTemplate.markdown.markdownBody = copyValue;
|
|
207
205
|
}
|
|
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 = '';
|
|
206
|
+
handleApplyTemplate(liveTemplate);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
230
209
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
210
|
+
// This flow is for "Design Library + Write a Prompt" (Hybrid AI)
|
|
211
|
+
if (copyMode === 'prompt') {
|
|
212
|
+
setError(null);
|
|
213
|
+
setStep('loading');
|
|
214
|
+
try {
|
|
215
|
+
// 1. Get the full, rich template from the library
|
|
216
|
+
const liveTemplate = convertStorageToLiveTemplate(entry.template);
|
|
217
|
+
if (!liveTemplate.markdown) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
'The selected design library item is not compatible with this workflow as it has no markdown section.'
|
|
220
|
+
);
|
|
221
|
+
}
|
|
235
222
|
|
|
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'
|
|
223
|
+
// 2. Create the simplified shell for the AI
|
|
224
|
+
const shellJson = convertTemplateToAIShell(liveTemplate);
|
|
225
|
+
if (!shellJson || shellJson === '{}') {
|
|
226
|
+
throw new Error(
|
|
227
|
+
'Could not generate a valid AI shell from this design.'
|
|
248
228
|
);
|
|
249
|
-
}
|
|
250
|
-
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 3. Get the AI to write copy based on the shell and prompt
|
|
232
|
+
const copyPromptDetails = prompts.aiPaneCopyPrompt;
|
|
233
|
+
const layout = 'Text Only';
|
|
234
|
+
const formattedCopyPrompt = copyPromptDetails.user_template
|
|
235
|
+
.replace('{{COPY_INPUT}}', promptValue)
|
|
236
|
+
.replace(
|
|
237
|
+
'{{DESIGN_INPUT}}',
|
|
238
|
+
"N/A - Use the provided Shell JSON's design."
|
|
239
|
+
)
|
|
240
|
+
.replace('{{LAYOUT_TYPE}}', layout)
|
|
241
|
+
.replace('{{SHELL_JSON}}', shellJson);
|
|
242
|
+
|
|
243
|
+
const copyResult = await callAskLemurAPI(
|
|
244
|
+
formattedCopyPrompt,
|
|
245
|
+
copyPromptDetails.system || '',
|
|
246
|
+
false
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// 4. Parse ONLY the AI-generated HTML into content nodes
|
|
250
|
+
const newNodes = parseAiCopyHtml(copyResult, liveTemplate.markdown.id);
|
|
251
|
+
|
|
252
|
+
// 5. Create the final pane by cloning the original rich template
|
|
253
|
+
const finalPane = cloneDeep(liveTemplate);
|
|
254
|
+
|
|
255
|
+
// 6. Inject the new AI content, preserving the original rich design
|
|
256
|
+
finalPane.markdown!.nodes = newNodes;
|
|
257
|
+
|
|
258
|
+
// 7. Apply the complete, correctly merged pane
|
|
259
|
+
handleApplyTemplate(finalPane);
|
|
260
|
+
} catch (err: any) {
|
|
261
|
+
setError(err.message || 'Failed to generate AI copy for this design.');
|
|
262
|
+
setStep('error');
|
|
251
263
|
}
|
|
252
|
-
setParentMode(PaneAddMode.DEFAULT, false);
|
|
253
|
-
} catch (error) {
|
|
254
|
-
console.error('Error inserting template:', error);
|
|
255
|
-
} finally {
|
|
256
|
-
setIsInserting(false);
|
|
257
264
|
}
|
|
258
265
|
};
|
|
259
266
|
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
267
|
+
const handleAiDesignGenerate = useCallback(async () => {
|
|
268
|
+
setError(null);
|
|
269
|
+
setStep('loading');
|
|
270
|
+
|
|
271
|
+
let designInput = `Generate a design using a **${aiDesignConfig.harmony.toLowerCase()}** color scheme with a **${aiDesignConfig.theme.toLowerCase()}** theme.`;
|
|
272
|
+
if (aiDesignConfig.baseColor)
|
|
273
|
+
designInput += ` Base the colors around **${aiDesignConfig.baseColor}**.`;
|
|
274
|
+
if (aiDesignConfig.accentColor)
|
|
275
|
+
designInput += ` Use **${aiDesignConfig.accentColor}** as an accent color.`;
|
|
276
|
+
if (aiDesignConfig.additionalNotes)
|
|
277
|
+
designInput += ` Refine with these notes: "${aiDesignConfig.additionalNotes}"`;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const shellPromptDetails = prompts.aiPaneShellPrompt;
|
|
281
|
+
const copyPromptDetails = prompts.aiPaneCopyPrompt;
|
|
282
|
+
const layout = 'Text Only'; // Hardcoded for this simplified AI path
|
|
283
|
+
|
|
284
|
+
const formattedShellPrompt = shellPromptDetails.user_template
|
|
285
|
+
.replace('{{DESIGN_INPUT}}', designInput)
|
|
286
|
+
.replace('{{LAYOUT_TYPE}}', layout);
|
|
287
|
+
|
|
288
|
+
const shellResult = await callAskLemurAPI(
|
|
289
|
+
formattedShellPrompt,
|
|
290
|
+
shellPromptDetails.system || '',
|
|
291
|
+
true
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const copyInputContent = copyMode === 'prompt' ? promptValue : copyValue;
|
|
295
|
+
const formattedCopyPrompt = copyPromptDetails.user_template
|
|
296
|
+
.replace('{{COPY_INPUT}}', copyInputContent)
|
|
297
|
+
.replace('{{DESIGN_INPUT}}', designInput)
|
|
298
|
+
.replace('{{LAYOUT_TYPE}}', layout)
|
|
299
|
+
.replace('{{SHELL_JSON}}', shellResult);
|
|
300
|
+
|
|
301
|
+
const copyResult = await callAskLemurAPI(
|
|
302
|
+
formattedCopyPrompt,
|
|
303
|
+
copyPromptDetails.system || '',
|
|
304
|
+
false
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const finalPane = parseAiPane(shellResult, copyResult, layout);
|
|
308
|
+
handleApplyTemplate(finalPane);
|
|
309
|
+
} catch (err: any) {
|
|
310
|
+
setError(err.message || 'Failed to generate AI pane.');
|
|
311
|
+
setStep('error');
|
|
312
|
+
}
|
|
313
|
+
}, [aiDesignConfig, copyMode, promptValue, copyValue]);
|
|
314
|
+
|
|
315
|
+
const handleApplyTemplate = async (template: TemplatePane) => {
|
|
316
|
+
if (!ctx) return;
|
|
263
317
|
try {
|
|
318
|
+
const insertTemplate = cloneDeep(template);
|
|
319
|
+
insertTemplate.title = insertTemplate.title || 'New Pane';
|
|
320
|
+
insertTemplate.slug = insertTemplate.slug || '';
|
|
321
|
+
|
|
264
322
|
const ownerId =
|
|
265
323
|
isStoryFragment || isContextPane
|
|
266
324
|
? nodeId
|
|
267
325
|
: ctx.getClosestNodeTypeFromId(nodeId, 'StoryFragment');
|
|
268
326
|
|
|
269
327
|
if (isContextPane) {
|
|
270
|
-
|
|
328
|
+
insertTemplate.isContextPane = true;
|
|
271
329
|
await ctx.applyAtomicUpdate(async (tmpCtx) => {
|
|
272
|
-
tmpCtx.addContextTemplatePane(ownerId,
|
|
330
|
+
tmpCtx.addContextTemplatePane(ownerId, insertTemplate);
|
|
273
331
|
});
|
|
274
332
|
} else {
|
|
275
333
|
await ctx.applyAtomicUpdate(async (tmpCtx) => {
|
|
276
334
|
tmpCtx.addTemplatePane(
|
|
277
335
|
ownerId,
|
|
278
|
-
|
|
336
|
+
insertTemplate,
|
|
279
337
|
nodeId,
|
|
280
338
|
first ? 'before' : 'after'
|
|
281
339
|
);
|
|
@@ -283,363 +341,203 @@ const AddPaneNewPanel = ({
|
|
|
283
341
|
ctx.notifyNode(`root`);
|
|
284
342
|
}
|
|
285
343
|
setParentMode(PaneAddMode.DEFAULT, false);
|
|
286
|
-
} catch (
|
|
287
|
-
console.error('Error
|
|
288
|
-
|
|
289
|
-
|
|
344
|
+
} catch (err) {
|
|
345
|
+
console.error('Error inserting template:', err);
|
|
346
|
+
setError(
|
|
347
|
+
err instanceof Error
|
|
348
|
+
? err.message
|
|
349
|
+
: 'An unexpected error occurred while adding the pane.'
|
|
350
|
+
);
|
|
351
|
+
setStep('error');
|
|
290
352
|
}
|
|
291
353
|
};
|
|
292
354
|
|
|
293
|
-
|
|
294
|
-
const newTheme = details.value[0] as Theme;
|
|
295
|
-
if (newTheme) {
|
|
296
|
-
setSelectedTheme(newTheme);
|
|
297
|
-
}
|
|
298
|
-
};
|
|
355
|
+
// --- Render Logic ---
|
|
299
356
|
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
357
|
+
const renderInitialStep = () => (
|
|
358
|
+
<div className="p-4">
|
|
359
|
+
<h3 className="font-action mb-4 text-center text-xl font-bold text-gray-800">
|
|
360
|
+
How would you like to design your new pane?
|
|
361
|
+
</h3>
|
|
362
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
363
|
+
<button
|
|
364
|
+
onClick={() => handleInitialChoice('library')}
|
|
365
|
+
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"
|
|
366
|
+
>
|
|
367
|
+
<SwatchIcon className="h-10 w-10 text-gray-500 transition-colors group-hover:text-cyan-600" />
|
|
368
|
+
<h4 className="font-semibold text-gray-800">Use Design Library</h4>
|
|
369
|
+
<p className="text-sm text-gray-600">
|
|
370
|
+
Start with a pre-made design and add your own content.
|
|
371
|
+
</p>
|
|
372
|
+
</button>
|
|
373
|
+
{hasAssemblyAI && (
|
|
374
|
+
<button
|
|
375
|
+
onClick={() => handleInitialChoice('ai')}
|
|
376
|
+
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"
|
|
377
|
+
>
|
|
378
|
+
<SparklesIcon className="h-10 w-10 text-gray-500 transition-colors group-hover:text-cyan-600" />
|
|
379
|
+
<h4 className="font-semibold text-gray-800">Design with AI</h4>
|
|
380
|
+
<p className="text-sm text-gray-600">
|
|
381
|
+
Let AI generate a complete design and copy from your prompt.
|
|
382
|
+
</p>
|
|
383
|
+
</button>
|
|
384
|
+
)}
|
|
385
|
+
<button
|
|
386
|
+
onClick={() => handleInitialChoice('blank')}
|
|
387
|
+
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"
|
|
388
|
+
>
|
|
389
|
+
<DocumentPlusIcon className="h-10 w-10 text-gray-500 transition-colors group-hover:text-cyan-600" />
|
|
390
|
+
<h4 className="font-semibold text-gray-800">Blank Slate</h4>
|
|
391
|
+
<p className="text-sm text-gray-600">
|
|
392
|
+
Add a simple, empty pane to build from scratch.
|
|
393
|
+
</p>
|
|
394
|
+
</button>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
);
|
|
320
398
|
|
|
321
|
-
|
|
322
|
-
<div className="
|
|
323
|
-
<
|
|
324
|
-
|
|
399
|
+
const renderContentStep = () => (
|
|
400
|
+
<div className="space-y-4 p-4">
|
|
401
|
+
<CopyInputStep
|
|
402
|
+
copyMode={copyMode}
|
|
403
|
+
onCopyModeChange={setCopyMode}
|
|
404
|
+
promptValue={promptValue}
|
|
405
|
+
onPromptValueChange={setPromptValue}
|
|
406
|
+
copyValue={copyValue}
|
|
407
|
+
onCopyValueChange={setCopyValue}
|
|
408
|
+
defaultPrompt={
|
|
409
|
+
first
|
|
410
|
+
? prompts.aiPaneCopyPrompt.heroDefault
|
|
411
|
+
: prompts.aiPaneCopyPrompt.contentDefault
|
|
412
|
+
}
|
|
413
|
+
/>
|
|
414
|
+
<div className="flex justify-between">
|
|
325
415
|
<button
|
|
326
|
-
onClick={
|
|
327
|
-
className="
|
|
328
|
-
type="button"
|
|
416
|
+
onClick={handleBack}
|
|
417
|
+
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
418
|
>
|
|
330
|
-
←
|
|
419
|
+
← Back
|
|
331
420
|
</button>
|
|
421
|
+
<button
|
|
422
|
+
onClick={handleCopyContinue}
|
|
423
|
+
disabled={
|
|
424
|
+
copyMode === 'prompt' ? !promptValue.trim() : !copyValue.trim()
|
|
425
|
+
}
|
|
426
|
+
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"
|
|
427
|
+
>
|
|
428
|
+
Continue →
|
|
429
|
+
</button>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
332
433
|
|
|
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>
|
|
434
|
+
const renderDesignLibraryStep = () => (
|
|
435
|
+
<div className="space-y-4 p-4">
|
|
436
|
+
<div className="flex justify-start">
|
|
437
|
+
<button
|
|
438
|
+
onClick={handleBack}
|
|
439
|
+
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"
|
|
440
|
+
>
|
|
441
|
+
← Back to Content
|
|
442
|
+
</button>
|
|
443
|
+
</div>
|
|
444
|
+
<DesignLibraryStep
|
|
445
|
+
config={config!}
|
|
446
|
+
onSelect={handleDesignLibrarySelect}
|
|
447
|
+
/>
|
|
448
|
+
</div>
|
|
449
|
+
);
|
|
374
450
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
451
|
+
const renderAiDesignStep = () => (
|
|
452
|
+
<div className="space-y-4 p-4">
|
|
453
|
+
<AiDesignStep
|
|
454
|
+
config={config!}
|
|
455
|
+
designConfig={aiDesignConfig}
|
|
456
|
+
onDesignConfigChange={setAiDesignConfig}
|
|
457
|
+
/>
|
|
458
|
+
<div className="flex justify-between">
|
|
459
|
+
<button
|
|
460
|
+
onClick={handleBack}
|
|
461
|
+
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"
|
|
462
|
+
>
|
|
463
|
+
← Back
|
|
464
|
+
</button>
|
|
465
|
+
<button
|
|
466
|
+
onClick={handleAiDesignGenerate}
|
|
467
|
+
className="rounded-md bg-cyan-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-700"
|
|
468
|
+
>
|
|
469
|
+
✨ Generate with AI
|
|
470
|
+
</button>
|
|
394
471
|
</div>
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
395
474
|
|
|
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>
|
|
475
|
+
const renderLoading = () => (
|
|
476
|
+
<div className="flex min-h-[300px] flex-col items-center justify-center space-y-4 p-6">
|
|
477
|
+
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-cyan-600"></div>
|
|
478
|
+
<p className="text-sm text-gray-600">Generating AI Pane...</p>
|
|
479
|
+
<p className="text-xs text-gray-500">This may take a moment.</p>
|
|
480
|
+
</div>
|
|
481
|
+
);
|
|
447
482
|
|
|
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>
|
|
483
|
+
const renderError = () => (
|
|
484
|
+
<div className="space-y-4 rounded-lg bg-red-50 p-6 text-center">
|
|
485
|
+
<h4 className="text-lg font-bold text-red-800">Generation Failed</h4>
|
|
486
|
+
<p className="text-sm text-red-700">{error}</p>
|
|
487
|
+
<button
|
|
488
|
+
onClick={handleBack}
|
|
489
|
+
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"
|
|
490
|
+
>
|
|
491
|
+
← Try Again
|
|
492
|
+
</button>
|
|
493
|
+
</div>
|
|
494
|
+
);
|
|
524
495
|
|
|
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>
|
|
496
|
+
const renderStep = () => {
|
|
497
|
+
switch (step) {
|
|
498
|
+
case 'initial':
|
|
499
|
+
return renderInitialStep();
|
|
500
|
+
case 'copyInput':
|
|
501
|
+
return renderContentStep();
|
|
502
|
+
case 'designLibrary':
|
|
503
|
+
return renderDesignLibraryStep();
|
|
504
|
+
case 'aiDesign':
|
|
505
|
+
return renderAiDesignStep();
|
|
506
|
+
case 'loading':
|
|
507
|
+
return renderLoading();
|
|
508
|
+
case 'error':
|
|
509
|
+
return renderError();
|
|
510
|
+
default:
|
|
511
|
+
return renderInitialStep();
|
|
512
|
+
}
|
|
513
|
+
};
|
|
606
514
|
|
|
607
|
-
|
|
515
|
+
return (
|
|
516
|
+
<div className="bg-white p-2 shadow-inner">
|
|
517
|
+
<div className="group mb-2 flex w-full items-center gap-1 rounded-md bg-white p-1.5">
|
|
518
|
+
{first ? (
|
|
519
|
+
<div className="w-full text-center">
|
|
520
|
+
<h2 className="font-action py-1.5 text-lg font-bold text-gray-800">
|
|
521
|
+
Welcome to Tract Stack
|
|
522
|
+
</h2>
|
|
523
|
+
</div>
|
|
524
|
+
) : (
|
|
525
|
+
<>
|
|
608
526
|
<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"
|
|
527
|
+
onClick={() => setParentMode(PaneAddMode.DEFAULT, first)}
|
|
528
|
+
className="w-fit rounded bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-gray-200"
|
|
613
529
|
>
|
|
614
|
-
|
|
530
|
+
← Go Back
|
|
615
531
|
</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
|
-
))}
|
|
532
|
+
<div className="font-action ml-4 flex-none rounded px-2 py-2.5 text-sm font-bold text-cyan-700 shadow-sm">
|
|
533
|
+
+ Design New Pane
|
|
631
534
|
</div>
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
Next
|
|
639
|
-
</button>
|
|
640
|
-
</div>
|
|
641
|
-
</>
|
|
642
|
-
)}
|
|
535
|
+
</>
|
|
536
|
+
)}
|
|
537
|
+
</div>
|
|
538
|
+
<div className="min-h-[400px] rounded-md border bg-gray-50">
|
|
539
|
+
{renderStep()}
|
|
540
|
+
</div>
|
|
643
541
|
</div>
|
|
644
542
|
);
|
|
645
543
|
};
|