astro-tractstack 2.0.15 → 2.0.17

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.
Files changed (28) hide show
  1. package/dist/index.js +33 -13
  2. package/package.json +1 -1
  3. package/templates/custom/with-examples/CodeHook.astro +4 -0
  4. package/templates/custom/with-examples/SandboxLauncher.tsx +67 -0
  5. package/templates/env.example +3 -0
  6. package/templates/src/components/codehooks/SandboxAuthWrapper.tsx +75 -0
  7. package/templates/src/components/codehooks/SandboxRegisterForm.tsx +202 -0
  8. package/templates/src/components/compositor/Compositor.tsx +2 -0
  9. package/templates/src/components/compositor/Node.tsx +6 -1
  10. package/templates/src/components/compositor/nodes/Pane_DesignLibrary.tsx +13 -11
  11. package/templates/src/components/compositor/nodes/Pane_layout.tsx +16 -14
  12. package/templates/src/components/edit/Header.tsx +8 -2
  13. package/templates/src/components/edit/PanelSwitch.tsx +4 -4
  14. package/templates/src/components/edit/pane/AddPanePanel.tsx +3 -0
  15. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +61 -46
  16. package/templates/src/components/edit/pane/steps/DirectInjectStep.tsx +96 -0
  17. package/templates/src/components/edit/panels/StyleImagePanel.tsx +10 -8
  18. package/templates/src/components/edit/state/SaveModal.tsx +41 -0
  19. package/templates/src/constants.ts +1 -0
  20. package/templates/src/pages/api/sandbox.ts +86 -0
  21. package/templates/src/pages/sandbox.astro +137 -0
  22. package/templates/src/types/nodeProps.ts +1 -0
  23. package/templates/src/utils/compositor/aiPaneParser.ts +8 -2
  24. package/templates/src/utils/profileStorage.ts +13 -0
  25. package/utils/inject-files.ts +33 -14
  26. package/templates/src/components/edit/pane/AiPaneGenerator.tsx +0 -512
  27. package/templates/src/components/edit/pane/AiPanePreview.tsx +0 -107
  28. package/templates/src/utils/aai/getTitleSlug.ts +0 -72
@@ -10,7 +10,6 @@ import prompts from '@/constants/prompts.json';
10
10
  import type { BrandConfig, DesignLibraryEntry } from '@/types/tractstack';
11
11
  import { PaneAddMode, type TemplatePane } from '@/types/compositorTypes';
12
12
  import { useStore } from '@nanostores/react';
13
-
14
13
  import { CopyInputStep } from './steps/CopyInputStep';
15
14
  import { DesignLibraryStep } from './steps/DesignLibraryStep';
16
15
  import { AiDesignStep, type AiDesignConfig } from './steps/AiDesignStep';
@@ -20,20 +19,20 @@ import {
20
19
  mergeCopyIntoTemplate,
21
20
  convertTemplateToAIShell,
22
21
  } from '@/utils/compositor/designLibraryHelper';
22
+ import { DirectInjectStep } from './steps/DirectInjectStep';
23
23
 
24
- // --- Types for Workflow State ---
25
24
  type Step =
26
25
  | 'initial'
27
26
  | 'copyInput'
28
27
  | 'designLibrary'
29
28
  | 'aiDesign'
30
29
  | 'loading'
31
- | 'error';
30
+ | 'error'
31
+ | 'directInject';
32
32
 
33
33
  type InitialChoice = 'library' | 'ai' | 'blank';
34
34
  type CopyMode = 'prompt' | 'raw';
35
35
 
36
- // --- API Call Helper ---
37
36
  interface GenerationResponse {
38
37
  success: boolean;
39
38
  data?: { response: string | object };
@@ -43,7 +42,8 @@ interface GenerationResponse {
43
42
  const callAskLemurAPI = async (
44
43
  prompt: string,
45
44
  context: string,
46
- expectJson: boolean
45
+ expectJson: boolean,
46
+ isSandboxMode: boolean
47
47
  ): Promise<string> => {
48
48
  const goBackend =
49
49
  import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
@@ -56,12 +56,22 @@ const callAskLemurAPI = async (
56
56
  max_tokens: 2000,
57
57
  };
58
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
- });
59
+ let response: Response;
60
+ if (isSandboxMode) {
61
+ response = await fetch(`/api/sandbox`, {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
64
+ credentials: 'include',
65
+ body: JSON.stringify({ action: 'askLemur', payload: requestBody }),
66
+ });
67
+ } else {
68
+ response = await fetch(`${goBackend}/api/v1/aai/askLemur`, {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
71
+ credentials: 'include',
72
+ body: JSON.stringify(requestBody),
73
+ });
74
+ }
65
75
 
66
76
  if (!response.ok) {
67
77
  const errorText = await response.text();
@@ -98,7 +108,6 @@ const callAskLemurAPI = async (
98
108
  throw new Error('Unexpected response format received from API.');
99
109
  };
100
110
 
101
- // --- Main Component ---
102
111
  interface AddPaneNewPanelProps {
103
112
  nodeId: string;
104
113
  first: boolean;
@@ -107,6 +116,7 @@ interface AddPaneNewPanelProps {
107
116
  isStoryFragment?: boolean;
108
117
  isContextPane?: boolean;
109
118
  config?: BrandConfig;
119
+ isSandboxMode?: boolean;
110
120
  }
111
121
 
112
122
  const AddPaneNewPanel = ({
@@ -117,23 +127,18 @@ const AddPaneNewPanel = ({
117
127
  isStoryFragment = false,
118
128
  isContextPane = false,
119
129
  config,
130
+ isSandboxMode = false,
120
131
  }: AddPaneNewPanelProps) => {
121
132
  const ctx = providedCtx || getCtx();
122
133
  const hasAssemblyAI = useStore(hasAssemblyAIStore);
123
-
124
- // --- State Machine and Data Stores ---
125
134
  const [step, setStep] = useState<Step>('initial');
126
135
  const [initialChoice, setInitialChoice] = useState<InitialChoice | null>(
127
136
  null
128
137
  );
129
138
  const [error, setError] = useState<string | null>(null);
130
-
131
- // State for CopyInputStep
132
- const [copyMode, setCopyMode] = useState<CopyMode>('raw');
139
+ const [copyMode, setCopyMode] = useState<CopyMode>('prompt');
133
140
  const [promptValue, setPromptValue] = useState('');
134
141
  const [copyValue, setCopyValue] = useState('');
135
-
136
- // State for AiDesignStep
137
142
  const [aiDesignConfig, setAiDesignConfig] = useState<AiDesignConfig>({
138
143
  harmony: 'Analogous',
139
144
  baseColor: '',
@@ -142,8 +147,6 @@ const AddPaneNewPanel = ({
142
147
  additionalNotes: '',
143
148
  });
144
149
 
145
- // --- Handlers & Logic ---
146
-
147
150
  const handleInitialChoice = (choice: InitialChoice) => {
148
151
  setInitialChoice(choice);
149
152
  setError(null);
@@ -159,6 +162,8 @@ const AddPaneNewPanel = ({
159
162
  setError(null);
160
163
  if (step === 'copyInput') {
161
164
  setStep('initial');
165
+ } else if (step === 'directInject') {
166
+ setStep('aiDesign');
162
167
  } else if (step === 'designLibrary' || step === 'aiDesign' || 'error') {
163
168
  setStep('copyInput');
164
169
  }
@@ -174,18 +179,18 @@ const AddPaneNewPanel = ({
174
179
 
175
180
  const handleBlankSlate = () => {
176
181
  const blankTemplate: TemplatePane = {
177
- id: '', // ctx will assign
182
+ id: '',
178
183
  nodeType: 'Pane',
179
- parentId: '', // ctx will assign
184
+ parentId: '',
180
185
  title: 'New Pane',
181
186
  slug: '',
182
187
  isDecorative: false,
183
188
  markdown: {
184
- id: '', // ctx will assign
189
+ id: '',
185
190
  nodeType: 'Markdown',
186
- parentId: '', // ctx will assign
191
+ parentId: '',
187
192
  type: 'markdown',
188
- markdownId: '', // ctx will assign
193
+ markdownId: '',
189
194
  defaultClasses: {},
190
195
  parentClasses: [],
191
196
  nodes: [],
@@ -195,10 +200,9 @@ const AddPaneNewPanel = ({
195
200
  };
196
201
 
197
202
  const handleDesignLibrarySelect = async (entry: DesignLibraryEntry) => {
198
- // This flow is for "Design Library + Provide Copy"
199
203
  if (copyMode === 'raw') {
200
204
  const liveTemplate = convertStorageToLiveTemplate(
201
- mergeCopyIntoTemplate(entry.template, []) // Start with blank copy
205
+ mergeCopyIntoTemplate(entry.template, [])
202
206
  );
203
207
  if (liveTemplate.markdown) {
204
208
  liveTemplate.markdown.markdownBody = copyValue;
@@ -207,12 +211,10 @@ const AddPaneNewPanel = ({
207
211
  return;
208
212
  }
209
213
 
210
- // This flow is for "Design Library + Write a Prompt" (Hybrid AI)
211
214
  if (copyMode === 'prompt') {
212
215
  setError(null);
213
216
  setStep('loading');
214
217
  try {
215
- // 1. Get the full, rich template from the library
216
218
  const liveTemplate = convertStorageToLiveTemplate(entry.template);
217
219
  if (!liveTemplate.markdown) {
218
220
  throw new Error(
@@ -220,7 +222,6 @@ const AddPaneNewPanel = ({
220
222
  );
221
223
  }
222
224
 
223
- // 2. Create the simplified shell for the AI
224
225
  const shellJson = convertTemplateToAIShell(liveTemplate);
225
226
  if (!shellJson || shellJson === '{}') {
226
227
  throw new Error(
@@ -228,7 +229,6 @@ const AddPaneNewPanel = ({
228
229
  );
229
230
  }
230
231
 
231
- // 3. Get the AI to write copy based on the shell and prompt
232
232
  const copyPromptDetails = prompts.aiPaneCopyPrompt;
233
233
  const layout = 'Text Only';
234
234
  const formattedCopyPrompt = copyPromptDetails.user_template
@@ -243,19 +243,16 @@ const AddPaneNewPanel = ({
243
243
  const copyResult = await callAskLemurAPI(
244
244
  formattedCopyPrompt,
245
245
  copyPromptDetails.system || '',
246
- false
246
+ false,
247
+ isSandboxMode
247
248
  );
248
249
 
249
- // 4. Parse ONLY the AI-generated HTML into content nodes
250
250
  const newNodes = parseAiCopyHtml(copyResult, liveTemplate.markdown.id);
251
251
 
252
- // 5. Create the final pane by cloning the original rich template
253
252
  const finalPane = cloneDeep(liveTemplate);
254
253
 
255
- // 6. Inject the new AI content, preserving the original rich design
256
254
  finalPane.markdown!.nodes = newNodes;
257
255
 
258
- // 7. Apply the complete, correctly merged pane
259
256
  handleApplyTemplate(finalPane);
260
257
  } catch (err: any) {
261
258
  setError(err.message || 'Failed to generate AI copy for this design.');
@@ -279,7 +276,7 @@ const AddPaneNewPanel = ({
279
276
  try {
280
277
  const shellPromptDetails = prompts.aiPaneShellPrompt;
281
278
  const copyPromptDetails = prompts.aiPaneCopyPrompt;
282
- const layout = 'Text Only'; // Hardcoded for this simplified AI path
279
+ const layout = 'Text Only';
283
280
 
284
281
  const formattedShellPrompt = shellPromptDetails.user_template
285
282
  .replace('{{DESIGN_INPUT}}', designInput)
@@ -288,7 +285,8 @@ const AddPaneNewPanel = ({
288
285
  const shellResult = await callAskLemurAPI(
289
286
  formattedShellPrompt,
290
287
  shellPromptDetails.system || '',
291
- true
288
+ true,
289
+ isSandboxMode
292
290
  );
293
291
 
294
292
  const copyInputContent = copyMode === 'prompt' ? promptValue : copyValue;
@@ -301,7 +299,8 @@ const AddPaneNewPanel = ({
301
299
  const copyResult = await callAskLemurAPI(
302
300
  formattedCopyPrompt,
303
301
  copyPromptDetails.system || '',
304
- false
302
+ false,
303
+ isSandboxMode
305
304
  );
306
305
 
307
306
  const finalPane = parseAiPane(shellResult, copyResult, layout);
@@ -310,9 +309,10 @@ const AddPaneNewPanel = ({
310
309
  setError(err.message || 'Failed to generate AI pane.');
311
310
  setStep('error');
312
311
  }
313
- }, [aiDesignConfig, copyMode, promptValue, copyValue]);
312
+ }, [aiDesignConfig, copyMode, promptValue, copyValue, isSandboxMode]);
314
313
 
315
314
  const handleApplyTemplate = async (template: TemplatePane) => {
315
+ console.log(template);
316
316
  if (!ctx) return;
317
317
  try {
318
318
  const insertTemplate = cloneDeep(template);
@@ -352,8 +352,6 @@ const AddPaneNewPanel = ({
352
352
  }
353
353
  };
354
354
 
355
- // --- Render Logic ---
356
-
357
355
  const renderInitialStep = () => (
358
356
  <div className="p-4">
359
357
  <h3 className="font-action mb-4 text-center text-xl font-bold text-gray-800">
@@ -365,7 +363,7 @@ const AddPaneNewPanel = ({
365
363
  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
364
  >
367
365
  <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>
366
+ <h4 className="font-bold text-gray-800">Use Design Library</h4>
369
367
  <p className="text-sm text-gray-600">
370
368
  Start with a pre-made design and add your own content.
371
369
  </p>
@@ -376,7 +374,7 @@ const AddPaneNewPanel = ({
376
374
  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
375
  >
378
376
  <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>
377
+ <h4 className="font-bold text-gray-800">Design with AI</h4>
380
378
  <p className="text-sm text-gray-600">
381
379
  Let AI generate a complete design and copy from your prompt.
382
380
  </p>
@@ -387,7 +385,7 @@ const AddPaneNewPanel = ({
387
385
  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
386
  >
389
387
  <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>
388
+ <h4 className="font-bold text-gray-800">Blank Slate</h4>
391
389
  <p className="text-sm text-gray-600">
392
390
  Add a simple, empty pane to build from scratch.
393
391
  </p>
@@ -428,6 +426,17 @@ const AddPaneNewPanel = ({
428
426
  Continue →
429
427
  </button>
430
428
  </div>
429
+ {initialChoice === `ai` && !isSandboxMode && (
430
+ <div className="mt-6 text-center text-sm text-gray-600">
431
+ ADVANCED:{' '}
432
+ <button
433
+ onClick={() => setStep('directInject')}
434
+ className="font-bold text-cyan-700 underline hover:text-cyan-900 focus:outline-none"
435
+ >
436
+ Direct Inject
437
+ </button>
438
+ </div>
439
+ )}
431
440
  </div>
432
441
  );
433
442
 
@@ -472,6 +481,10 @@ const AddPaneNewPanel = ({
472
481
  </div>
473
482
  );
474
483
 
484
+ const renderDirectInjectStep = () => (
485
+ <DirectInjectStep onBack={handleBack} onCreatePane={handleApplyTemplate} />
486
+ );
487
+
475
488
  const renderLoading = () => (
476
489
  <div className="flex min-h-[300px] flex-col items-center justify-center space-y-4 p-6">
477
490
  <div className="h-8 w-8 animate-spin rounded-full border-b-2 border-cyan-600"></div>
@@ -505,6 +518,8 @@ const AddPaneNewPanel = ({
505
518
  return renderAiDesignStep();
506
519
  case 'loading':
507
520
  return renderLoading();
521
+ case 'directInject':
522
+ return renderDirectInjectStep();
508
523
  case 'error':
509
524
  return renderError();
510
525
  default:
@@ -0,0 +1,96 @@
1
+ import { useState } from 'react';
2
+ import { parseAiPane } from '@/utils/compositor/aiPaneParser';
3
+ import type { TemplatePane } from '@/types/compositorTypes';
4
+
5
+ interface DirectInjectStepProps {
6
+ onBack: () => void;
7
+ onCreatePane: (template: TemplatePane) => void;
8
+ }
9
+
10
+ export const DirectInjectStep = ({
11
+ onBack,
12
+ onCreatePane,
13
+ }: DirectInjectStepProps) => {
14
+ const [shellJson, setShellJson] = useState('');
15
+ const [copyHtml, setCopyHtml] = useState('');
16
+ const [error, setError] = useState<string | null>(null);
17
+
18
+ const handleCreate = () => {
19
+ setError(null);
20
+ if (!shellJson.trim() || !copyHtml.trim()) {
21
+ setError('Both Shell JSON and Inner HTML must be provided.');
22
+ return;
23
+ }
24
+
25
+ try {
26
+ const finalPane = parseAiPane(shellJson, copyHtml, 'DirectInject');
27
+ onCreatePane(finalPane);
28
+ } catch (err: any) {
29
+ console.error('Direct Inject Error:', err);
30
+ setError(
31
+ `Failed to parse inputs: ${err.message || 'Unknown error'}. Check console.`
32
+ );
33
+ }
34
+ };
35
+
36
+ return (
37
+ <div className="space-y-6 p-4">
38
+ <div className="space-y-4">
39
+ <div>
40
+ <label
41
+ htmlFor="shellJson"
42
+ className="block text-sm font-bold text-gray-700"
43
+ >
44
+ Shell JSON
45
+ </label>
46
+ <textarea
47
+ id="shellJson"
48
+ rows={10}
49
+ value={shellJson}
50
+ onChange={(e) => setShellJson(e.target.value)}
51
+ className="mt-1 block w-full rounded-md border-gray-300 p-2 font-mono text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
52
+ placeholder={`{ "bgColour": "#ffffff", "parentClasses": [...], "defaultClasses": {...} }`}
53
+ />
54
+ </div>
55
+ <div>
56
+ <label
57
+ htmlFor="copyHtml"
58
+ className="block text-sm font-bold text-gray-700"
59
+ >
60
+ Inner HTML
61
+ </label>
62
+ <textarea
63
+ id="copyHtml"
64
+ rows={10}
65
+ value={copyHtml}
66
+ onChange={(e) => setCopyHtml(e.target.value)}
67
+ className="mt-1 block w-full rounded-md border-gray-300 p-2 font-mono text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
68
+ placeholder={`<h2 class="...">...</h2>\n<p class="...">...</p>`}
69
+ />
70
+ </div>
71
+ </div>
72
+
73
+ {error && (
74
+ <div className="rounded-md bg-red-50 p-4">
75
+ <p className="text-sm font-bold text-red-800">{error}</p>
76
+ </div>
77
+ )}
78
+
79
+ <div className="flex justify-between">
80
+ <button
81
+ onClick={onBack}
82
+ 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"
83
+ >
84
+ ← Back
85
+ </button>
86
+ <button
87
+ onClick={handleCreate}
88
+ disabled={!shellJson.trim() || !copyHtml.trim()}
89
+ 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"
90
+ >
91
+ Create Pane
92
+ </button>
93
+ </div>
94
+ </div>
95
+ );
96
+ };
@@ -25,14 +25,7 @@ const StyleImagePanel = ({
25
25
  parentNode,
26
26
  }: StyleImagePanelProps) => {
27
27
  const [altDescription, setAltDescription] = useState(node.alt || '');
28
- if (
29
- !node?.tagName ||
30
- !containerNode?.tagName ||
31
- !outerContainerNode?.tagName ||
32
- !isMarkdownPaneFragmentNode(parentNode)
33
- ) {
34
- return null;
35
- }
28
+
36
29
  const imgDefaultClasses = parentNode.defaultClasses?.[node.tagName];
37
30
  const imgOverrideClasses = node.overrideClasses;
38
31
  const containerDefaultClasses =
@@ -303,6 +296,15 @@ const StyleImagePanel = ({
303
296
  ctx.modifyNodes([{ ...imgNode, isChanged: true }]);
304
297
  };
305
298
 
299
+ if (
300
+ !node?.tagName ||
301
+ !containerNode?.tagName ||
302
+ !outerContainerNode?.tagName ||
303
+ !isMarkdownPaneFragmentNode(parentNode)
304
+ ) {
305
+ return null;
306
+ }
307
+
306
308
  return (
307
309
  <div className="space-y-8">
308
310
  <div className="space-y-4">
@@ -45,6 +45,7 @@ interface SaveModalProps {
45
45
  slug: string;
46
46
  isContext: boolean;
47
47
  onClose: () => void;
48
+ isSandboxMode?: boolean;
48
49
  }
49
50
 
50
51
  const PROGRESS_PHASES = {
@@ -61,11 +62,47 @@ const INDETERMINATE_STAGES: SaveStage[] = [
61
62
  'UPDATING_HOME_PAGE',
62
63
  ];
63
64
 
65
+ const SandboxUpgradeNotice = ({ onClose }: { onClose: () => void }) => (
66
+ <Dialog.Root open={true} onOpenChange={() => onClose()} modal={true}>
67
+ <Portal>
68
+ <Dialog.Backdrop className="fixed inset-0 z-[9005] bg-black bg-opacity-75" />
69
+ <Dialog.Positioner className="fixed inset-0 z-[9005] flex items-center justify-center p-4">
70
+ <Dialog.Content className="w-full max-w-md overflow-hidden rounded-lg bg-white shadow-xl">
71
+ <div className="p-6 text-center">
72
+ <Dialog.Title className="text-xl font-bold text-gray-900">
73
+ Save Your Work
74
+ </Dialog.Title>
75
+ <Dialog.Description className="mt-2 text-gray-600">
76
+ To save your changes and get a shareable link, please sign up for
77
+ a full account.
78
+ </Dialog.Description>
79
+ <div className="mt-6 flex justify-center gap-3">
80
+ <a
81
+ href="/sandbox/register"
82
+ className="bg-myblue hover:bg-myorange rounded-md px-4 py-2 font-bold text-white"
83
+ >
84
+ Sign Up Now
85
+ </a>
86
+ <button
87
+ onClick={onClose}
88
+ className="rounded-md bg-gray-200 px-4 py-2 text-gray-800 hover:bg-gray-300"
89
+ >
90
+ Keep Editing
91
+ </button>
92
+ </div>
93
+ </div>
94
+ </Dialog.Content>
95
+ </Dialog.Positioner>
96
+ </Portal>
97
+ </Dialog.Root>
98
+ );
99
+
64
100
  export default function SaveModal({
65
101
  show,
66
102
  slug,
67
103
  isContext,
68
104
  onClose,
105
+ isSandboxMode = false,
69
106
  }: SaveModalProps) {
70
107
  const [stage, setStage] = useState<SaveStage>('PREPARING');
71
108
  const [progress, setProgress] = useState(0);
@@ -871,6 +908,10 @@ export default function SaveModal({
871
908
  }
872
909
  })();
873
910
 
911
+ if (isSandboxMode) {
912
+ return show ? <SandboxUpgradeNotice onClose={onClose} /> : null;
913
+ }
914
+
874
915
  return (
875
916
  <Dialog.Root
876
917
  open={show}
@@ -18,6 +18,7 @@ export const reservedSlugs = [
18
18
  `404`,
19
19
  `transcribe`,
20
20
  `sitemap`,
21
+ `sandbox`,
21
22
  `robots`,
22
23
  `llm`,
23
24
  ];
@@ -0,0 +1,86 @@
1
+ import type { APIRoute } from '@/types/astro';
2
+
3
+ export const POST: APIRoute = async ({ request }) => {
4
+ console.log(1);
5
+ const goBackend =
6
+ import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
7
+ const sharedSecret = import.meta.env.PRIVATE_SANDBOX_SECRET;
8
+ const tenantId =
9
+ request.headers.get('X-Tenant-ID') ||
10
+ import.meta.env.PUBLIC_TENANTID ||
11
+ 'default';
12
+ console.log(goBackend);
13
+ console.log(sharedSecret);
14
+ console.log(tenantId);
15
+
16
+ if (!sharedSecret || sharedSecret === 'false' || sharedSecret === 'true') {
17
+ return new Response(
18
+ JSON.stringify({
19
+ success: false,
20
+ error: 'Sandbox feature is not configured on the server.',
21
+ }),
22
+ { status: 501, headers: { 'Content-Type': 'application/json' } }
23
+ );
24
+ }
25
+
26
+ const profileCookie = request.headers
27
+ .get('cookie')
28
+ ?.includes('tractstack_profile');
29
+ if (!profileCookie) {
30
+ return new Response(
31
+ JSON.stringify({
32
+ success: false,
33
+ error: 'Forbidden: Missing sandbox profile.',
34
+ }),
35
+ { status: 403, headers: { 'Content-Type': 'application/json' } }
36
+ );
37
+ }
38
+ console.log(profileCookie);
39
+
40
+ try {
41
+ const body = await request.json();
42
+ const { action, payload } = body;
43
+ console.log(action, payload);
44
+
45
+ if (action !== 'askLemur') {
46
+ return new Response(
47
+ JSON.stringify({ success: false, error: 'Invalid action.' }),
48
+ { status: 400, headers: { 'Content-Type': 'application/json' } }
49
+ );
50
+ }
51
+
52
+ const backendResponse = await fetch(`${goBackend}/api/v1/aai/askLemur`, {
53
+ method: 'POST',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ 'X-Tenant-ID': tenantId,
57
+ Authorization: `Bearer ${sharedSecret}`,
58
+ },
59
+ body: JSON.stringify(payload),
60
+ });
61
+
62
+ if (!backendResponse.ok) {
63
+ const errorText = await backendResponse.text();
64
+ return new Response(errorText, {
65
+ status: backendResponse.status,
66
+ headers: { 'Content-Type': 'application/json' },
67
+ });
68
+ }
69
+
70
+ const data = await backendResponse.json();
71
+ return new Response(JSON.stringify(data), {
72
+ status: 200,
73
+ headers: { 'Content-Type': 'application/json' },
74
+ });
75
+ } catch (error) {
76
+ const errorMessage =
77
+ error instanceof Error ? error.message : 'An unknown error occurred.';
78
+ return new Response(
79
+ JSON.stringify({ success: false, error: errorMessage }),
80
+ {
81
+ status: 500,
82
+ headers: { 'Content-Type': 'application/json' },
83
+ }
84
+ );
85
+ }
86
+ };