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.
@@ -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={false}
354
+ modal={true}
355
+ preventScroll={true}
354
356
  >
355
- <Dialog.Backdrop className="z-103 fixed inset-0 bg-black/70" />
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: '90vw', width: '90vw' }}
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 rounded-lg bg-gray-50 p-4 shadow-inner">
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={`harmony-${option}`}
50
- name="harmonyOptions"
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={`harmony-${option}`}
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) => updateField('baseColor', 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) => updateField('accentColor', 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={`theme-${option}`}
96
- name="themeOptions"
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={`theme-${option}`}
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="additional-notes"
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="additional-notes"
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..."