astro-tractstack 2.0.43 → 2.0.45
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 +6 -0
- package/package.json +1 -1
- package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +24 -3
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +156 -343
- package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +230 -0
- package/templates/src/components/edit/pane/RestylePaneModal.tsx +7 -5
- package/templates/src/components/edit/pane/steps/AiDesignStep.tsx +28 -14
- package/templates/src/components/edit/pane/steps/CopyInputStep.tsx +238 -96
- package/templates/src/components/edit/panels/StyleParentPanel.tsx +29 -1
- package/templates/src/stores/nodes.ts +65 -0
- package/templates/src/stores/selection.ts +2 -0
- package/utils/inject-files.ts +6 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { useStore } from '@nanostores/react';
|
|
4
|
+
import { Dialog } from '@ark-ui/react';
|
|
5
|
+
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
|
|
6
|
+
import SparklesIcon from '@heroicons/react/24/outline/SparklesIcon';
|
|
7
|
+
import { getCtx } from '@/stores/nodes';
|
|
8
|
+
import { selectionStore } from '@/stores/selection';
|
|
9
|
+
import { AiDesignStep, type AiDesignConfig } from './steps/AiDesignStep';
|
|
10
|
+
import prompts from '@/constants/prompts.json';
|
|
11
|
+
import { TractStackAPI } from '@/utils/api';
|
|
12
|
+
import { parseAiPane } from '@/utils/compositor/aiPaneParser';
|
|
13
|
+
|
|
14
|
+
const callAskLemurAPI = async (
|
|
15
|
+
prompt: string,
|
|
16
|
+
context: string,
|
|
17
|
+
expectJson: boolean,
|
|
18
|
+
isSandboxMode: boolean
|
|
19
|
+
): Promise<string> => {
|
|
20
|
+
const tenantId =
|
|
21
|
+
(window as any).TRACTSTACK_CONFIG?.tenantId ||
|
|
22
|
+
import.meta.env.PUBLIC_TENANTID ||
|
|
23
|
+
'default';
|
|
24
|
+
const api = new TractStackAPI(tenantId);
|
|
25
|
+
|
|
26
|
+
const requestBody = {
|
|
27
|
+
prompt,
|
|
28
|
+
input_text: context,
|
|
29
|
+
final_model: '',
|
|
30
|
+
temperature: 0.5,
|
|
31
|
+
max_tokens: 2000,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let resultData: any;
|
|
35
|
+
|
|
36
|
+
if (isSandboxMode) {
|
|
37
|
+
const response = await fetch(`/api/sandbox`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
'X-Tenant-ID': tenantId,
|
|
42
|
+
},
|
|
43
|
+
credentials: 'include',
|
|
44
|
+
body: JSON.stringify({ action: 'askLemur', payload: requestBody }),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const errorText = await response.text();
|
|
49
|
+
throw new Error(`Sandbox API failed: ${response.status} ${errorText}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const json = await response.json();
|
|
53
|
+
if (!json.success) {
|
|
54
|
+
throw new Error(json.error || 'Sandbox generation failed');
|
|
55
|
+
}
|
|
56
|
+
resultData = json.data;
|
|
57
|
+
} else {
|
|
58
|
+
const response = await api.post('/api/v1/aai/askLemur', requestBody);
|
|
59
|
+
if (!response.success || !response.data?.response) {
|
|
60
|
+
throw new Error(response.error || 'AI generation failed');
|
|
61
|
+
}
|
|
62
|
+
resultData = response.data;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let raw = resultData.response;
|
|
66
|
+
if (typeof raw === 'string') {
|
|
67
|
+
if (raw.startsWith('```json')) raw = raw.slice(7, -3).trim();
|
|
68
|
+
}
|
|
69
|
+
if (expectJson && typeof raw === 'object') return JSON.stringify(raw);
|
|
70
|
+
return raw;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
interface AiRestylePaneModalProps {
|
|
74
|
+
isSandboxMode?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const AiRestylePaneModal = ({
|
|
78
|
+
isSandboxMode = false,
|
|
79
|
+
}: AiRestylePaneModalProps) => {
|
|
80
|
+
const ctx = getCtx();
|
|
81
|
+
const { isAiRestyleModalOpen, paneToRestyleId } = useStore(selectionStore);
|
|
82
|
+
|
|
83
|
+
const [loading, setLoading] = useState(false);
|
|
84
|
+
const [error, setError] = useState<string | null>(null);
|
|
85
|
+
const [aiDesignConfig, setAiDesignConfig] = useState<AiDesignConfig>({
|
|
86
|
+
harmony: 'Analogous',
|
|
87
|
+
baseColor: '',
|
|
88
|
+
accentColor: '',
|
|
89
|
+
theme: 'Light',
|
|
90
|
+
additionalNotes: '',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const handleClose = () => {
|
|
94
|
+
if (loading) return;
|
|
95
|
+
selectionStore.setKey('isAiRestyleModalOpen', false);
|
|
96
|
+
selectionStore.setKey('paneToRestyleId', null);
|
|
97
|
+
setError(null);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleGenerate = async () => {
|
|
101
|
+
if (!paneToRestyleId) return;
|
|
102
|
+
setLoading(true);
|
|
103
|
+
setError(null);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const childIds = ctx.getChildNodeIDs(paneToRestyleId);
|
|
107
|
+
const nodesMap = ctx.allNodes.get();
|
|
108
|
+
const gridNode = childIds.find(
|
|
109
|
+
(id) => nodesMap.get(id)?.nodeType === 'GridLayoutNode'
|
|
110
|
+
);
|
|
111
|
+
const isGrid = !!gridNode;
|
|
112
|
+
|
|
113
|
+
const promptConfig = isGrid
|
|
114
|
+
? prompts.aiPromptsIndex.find((p) => p.layout === 'grid')
|
|
115
|
+
: prompts.aiPromptsIndex.find((p) => p.layout === 'standard');
|
|
116
|
+
|
|
117
|
+
if (!promptConfig) throw new Error('No suitable prompt found');
|
|
118
|
+
|
|
119
|
+
let designInput = `Generate a design using a **${aiDesignConfig.harmony.toLowerCase()}** color scheme with a **${aiDesignConfig.theme.toLowerCase()}** theme.`;
|
|
120
|
+
if (aiDesignConfig.baseColor)
|
|
121
|
+
designInput += ` Base around **${aiDesignConfig.baseColor}**.`;
|
|
122
|
+
if (aiDesignConfig.accentColor)
|
|
123
|
+
designInput += ` Accent with **${aiDesignConfig.accentColor}**.`;
|
|
124
|
+
if (aiDesignConfig.additionalNotes)
|
|
125
|
+
designInput += ` Notes: "${aiDesignConfig.additionalNotes}"`;
|
|
126
|
+
|
|
127
|
+
const shellPromptKey = promptConfig.prompts.shell as keyof typeof prompts;
|
|
128
|
+
const shellPromptDetails = prompts[shellPromptKey] as any;
|
|
129
|
+
|
|
130
|
+
const formattedPrompt = shellPromptDetails.user_template
|
|
131
|
+
.replace('{{DESIGN_INPUT}}', designInput)
|
|
132
|
+
.replace('{{COPY_INPUT}}', 'A generic content section')
|
|
133
|
+
.replace('{{LAYOUT_TYPE}}', 'Text Only');
|
|
134
|
+
|
|
135
|
+
const resultStr = await callAskLemurAPI(
|
|
136
|
+
formattedPrompt,
|
|
137
|
+
shellPromptDetails.system || '',
|
|
138
|
+
true,
|
|
139
|
+
isSandboxMode
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
let dummyCopy: string | string[] = '';
|
|
143
|
+
if (isGrid) {
|
|
144
|
+
dummyCopy = ['', ''];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const hydratedTemplate = parseAiPane(resultStr, dummyCopy, 'Text Only');
|
|
148
|
+
|
|
149
|
+
ctx.applyShellToPane(paneToRestyleId, hydratedTemplate);
|
|
150
|
+
handleClose();
|
|
151
|
+
} catch (err: any) {
|
|
152
|
+
console.error(err);
|
|
153
|
+
setError(err.message || 'Failed to restyle pane');
|
|
154
|
+
} finally {
|
|
155
|
+
setLoading(false);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (!isAiRestyleModalOpen) return null;
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<Dialog.Root
|
|
163
|
+
open={isAiRestyleModalOpen}
|
|
164
|
+
onOpenChange={(e) => !e.open && handleClose()}
|
|
165
|
+
>
|
|
166
|
+
<Dialog.Backdrop className="z-103 fixed inset-0 bg-black bg-opacity-75" />
|
|
167
|
+
<Dialog.Positioner className="z-104 fixed inset-0 flex items-center justify-center p-4">
|
|
168
|
+
<Dialog.Content className="flex max-w-2xl flex-col rounded-lg bg-white shadow-2xl">
|
|
169
|
+
<div className="flex items-center justify-between border-b p-4">
|
|
170
|
+
<h3 className="flex items-center gap-2 text-lg font-bold">
|
|
171
|
+
<SparklesIcon className="h-5 w-5 text-purple-600" />
|
|
172
|
+
Re-Color Pane
|
|
173
|
+
</h3>
|
|
174
|
+
<button
|
|
175
|
+
onClick={handleClose}
|
|
176
|
+
disabled={loading}
|
|
177
|
+
className="rounded-full p-1 text-gray-600 hover:bg-gray-100 disabled:opacity-50"
|
|
178
|
+
>
|
|
179
|
+
<XMarkIcon className="h-6 w-6" />
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<form className="p-6" onSubmit={(e) => e.preventDefault()}>
|
|
184
|
+
<div className={loading ? 'pointer-events-none opacity-50' : ''}>
|
|
185
|
+
<AiDesignStep
|
|
186
|
+
designConfig={aiDesignConfig}
|
|
187
|
+
onDesignConfigChange={setAiDesignConfig}
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{error && (
|
|
192
|
+
<div className="mt-4 rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700">
|
|
193
|
+
{error}
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
<div className="mt-8 flex justify-end gap-3">
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
onClick={handleClose}
|
|
201
|
+
disabled={loading}
|
|
202
|
+
className="rounded-lg px-4 py-2 font-bold text-gray-600 hover:bg-gray-100 disabled:opacity-50"
|
|
203
|
+
>
|
|
204
|
+
Cancel
|
|
205
|
+
</button>
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
onClick={handleGenerate}
|
|
209
|
+
disabled={loading}
|
|
210
|
+
className="flex items-center gap-2 rounded-lg bg-gradient-to-r from-purple-600 to-indigo-600 px-6 py-2 font-bold text-white shadow transition-all hover:from-purple-500 hover:to-indigo-500 hover:shadow-lg disabled:cursor-not-allowed disabled:opacity-75"
|
|
211
|
+
>
|
|
212
|
+
{loading ? (
|
|
213
|
+
<>
|
|
214
|
+
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
|
215
|
+
Generating...
|
|
216
|
+
</>
|
|
217
|
+
) : (
|
|
218
|
+
<>
|
|
219
|
+
<SparklesIcon className="h-5 w-5" />
|
|
220
|
+
Apply Re-Color
|
|
221
|
+
</>
|
|
222
|
+
)}
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
</form>
|
|
226
|
+
</Dialog.Content>
|
|
227
|
+
</Dialog.Positioner>
|
|
228
|
+
</Dialog.Root>
|
|
229
|
+
);
|
|
230
|
+
};
|
|
@@ -211,7 +211,8 @@ export const RestylePaneModal = () => {
|
|
|
211
211
|
(entry: DesignLibraryEntry) =>
|
|
212
212
|
(selectedCategory === 'all' || entry.category === selectedCategory) &&
|
|
213
213
|
entry.title.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
|
214
|
-
entry.markdownCount === targetMarkdownCount
|
|
214
|
+
entry.markdownCount === targetMarkdownCount &&
|
|
215
|
+
!entry.locked
|
|
215
216
|
);
|
|
216
217
|
}, [designLibrary, selectedCategory, searchTerm, targetMarkdownCount]);
|
|
217
218
|
|
|
@@ -350,14 +351,15 @@ export const RestylePaneModal = () => {
|
|
|
350
351
|
<Dialog.Root
|
|
351
352
|
open={isRestyleModalOpen}
|
|
352
353
|
onOpenChange={handleDialogStateChange}
|
|
353
|
-
modal={
|
|
354
|
+
modal={true}
|
|
355
|
+
preventScroll={true}
|
|
354
356
|
>
|
|
355
|
-
<Dialog.Backdrop className="z-103 fixed inset-0 bg-black
|
|
357
|
+
<Dialog.Backdrop className="z-103 fixed inset-0 bg-black bg-opacity-75" />
|
|
356
358
|
<Dialog.Positioner className="z-104 fixed inset-0 flex items-center justify-center">
|
|
357
359
|
<Dialog.Content
|
|
358
360
|
ref={contentRef}
|
|
359
|
-
className="flex flex-col rounded-lg bg-white shadow-2xl"
|
|
360
|
-
style={{ maxHeight: '
|
|
361
|
+
className="flex max-w-5xl flex-col rounded-lg bg-white shadow-2xl xl:max-w-7xl"
|
|
362
|
+
style={{ maxHeight: '90vh', width: '90vw' }}
|
|
361
363
|
>
|
|
362
364
|
<header className="flex items-center justify-between border-b p-4">
|
|
363
365
|
<Dialog.Title className="text-xl font-bold">
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ColorPickerCombo from '@/components/fields/ColorPickerCombo';
|
|
2
|
+
import { findClosestTailwindColor } from '@/utils/compositor/tailwindColors';
|
|
2
3
|
|
|
3
4
|
export interface AiDesignConfig {
|
|
4
5
|
harmony: string;
|
|
@@ -11,6 +12,7 @@ export interface AiDesignConfig {
|
|
|
11
12
|
interface AiDesignStepProps {
|
|
12
13
|
designConfig: AiDesignConfig;
|
|
13
14
|
onDesignConfigChange: (newConfig: AiDesignConfig) => void;
|
|
15
|
+
idPrefix?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
const harmonyOptions = [
|
|
@@ -24,6 +26,7 @@ const themeOptions = ['Light', 'Dark', 'Bright', 'Muted', 'Pastel', 'Earthy'];
|
|
|
24
26
|
export const AiDesignStep = ({
|
|
25
27
|
designConfig,
|
|
26
28
|
onDesignConfigChange,
|
|
29
|
+
idPrefix = '',
|
|
27
30
|
}: AiDesignStepProps) => {
|
|
28
31
|
const updateField = <K extends keyof AiDesignConfig>(
|
|
29
32
|
field: K,
|
|
@@ -32,11 +35,22 @@ export const AiDesignStep = ({
|
|
|
32
35
|
onDesignConfigChange({ ...designConfig, [field]: value });
|
|
33
36
|
};
|
|
34
37
|
|
|
38
|
+
const handleColorChange = (
|
|
39
|
+
field: 'baseColor' | 'accentColor',
|
|
40
|
+
color: string
|
|
41
|
+
) => {
|
|
42
|
+
if (!color) {
|
|
43
|
+
updateField(field, '');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const closest = findClosestTailwindColor(color);
|
|
47
|
+
if (closest) {
|
|
48
|
+
updateField(field, `${closest.name}-${closest.shade}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
35
52
|
return (
|
|
36
|
-
<div className="space-y-6
|
|
37
|
-
<label className="block text-lg font-bold text-gray-800">
|
|
38
|
-
2. Configure AI Design
|
|
39
|
-
</label>
|
|
53
|
+
<div className="space-y-6">
|
|
40
54
|
<div>
|
|
41
55
|
<label className="block text-base font-bold text-gray-800">
|
|
42
56
|
Color Harmony
|
|
@@ -46,15 +60,15 @@ export const AiDesignStep = ({
|
|
|
46
60
|
<div key={option} className="flex items-center space-x-2">
|
|
47
61
|
<input
|
|
48
62
|
type="radio"
|
|
49
|
-
id={
|
|
50
|
-
name=
|
|
63
|
+
id={`${idPrefix}harmony-${option}`}
|
|
64
|
+
name={`${idPrefix}harmonyOptions`}
|
|
51
65
|
value={option}
|
|
52
66
|
checked={designConfig.harmony === option}
|
|
53
67
|
onChange={(e) => updateField('harmony', e.target.value)}
|
|
54
68
|
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
55
69
|
/>
|
|
56
70
|
<label
|
|
57
|
-
htmlFor={
|
|
71
|
+
htmlFor={`${idPrefix}harmony-${option}`}
|
|
58
72
|
className="text-sm font-bold text-gray-700"
|
|
59
73
|
>
|
|
60
74
|
{option}
|
|
@@ -69,7 +83,7 @@ export const AiDesignStep = ({
|
|
|
69
83
|
<ColorPickerCombo
|
|
70
84
|
title="Base Color (Optional)"
|
|
71
85
|
defaultColor={designConfig.baseColor}
|
|
72
|
-
onColorChange={(color) =>
|
|
86
|
+
onColorChange={(color) => handleColorChange('baseColor', color)}
|
|
73
87
|
allowNull={true}
|
|
74
88
|
/>
|
|
75
89
|
</div>
|
|
@@ -77,7 +91,7 @@ export const AiDesignStep = ({
|
|
|
77
91
|
<ColorPickerCombo
|
|
78
92
|
title="Accent Color (Optional)"
|
|
79
93
|
defaultColor={designConfig.accentColor}
|
|
80
|
-
onColorChange={(color) =>
|
|
94
|
+
onColorChange={(color) => handleColorChange('accentColor', color)}
|
|
81
95
|
allowNull={true}
|
|
82
96
|
/>
|
|
83
97
|
</div>
|
|
@@ -92,15 +106,15 @@ export const AiDesignStep = ({
|
|
|
92
106
|
<div key={option} className="flex items-center space-x-2">
|
|
93
107
|
<input
|
|
94
108
|
type="radio"
|
|
95
|
-
id={
|
|
96
|
-
name=
|
|
109
|
+
id={`${idPrefix}theme-${option}`}
|
|
110
|
+
name={`${idPrefix}themeOptions`}
|
|
97
111
|
value={option}
|
|
98
112
|
checked={designConfig.theme === option}
|
|
99
113
|
onChange={(e) => updateField('theme', e.target.value)}
|
|
100
114
|
className="h-4 w-4 border-gray-300 text-cyan-600 focus:ring-cyan-500"
|
|
101
115
|
/>
|
|
102
116
|
<label
|
|
103
|
-
htmlFor={
|
|
117
|
+
htmlFor={`${idPrefix}theme-${option}`}
|
|
104
118
|
className="text-sm font-bold text-gray-700"
|
|
105
119
|
>
|
|
106
120
|
{option}
|
|
@@ -112,7 +126,7 @@ export const AiDesignStep = ({
|
|
|
112
126
|
|
|
113
127
|
<div>
|
|
114
128
|
<label
|
|
115
|
-
htmlFor=
|
|
129
|
+
htmlFor={`${idPrefix}additional-notes`}
|
|
116
130
|
className="block text-base font-bold text-gray-800"
|
|
117
131
|
>
|
|
118
132
|
Additional Design Notes (Optional)
|
|
@@ -122,7 +136,7 @@ export const AiDesignStep = ({
|
|
|
122
136
|
texture".
|
|
123
137
|
</p>
|
|
124
138
|
<textarea
|
|
125
|
-
id=
|
|
139
|
+
id={`${idPrefix}additional-notes`}
|
|
126
140
|
value={designConfig.additionalNotes}
|
|
127
141
|
onChange={(e) => updateField('additionalNotes', e.target.value)}
|
|
128
142
|
placeholder="Enter additional notes..."
|