astro-tractstack 2.0.15 → 2.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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';
@@ -21,7 +20,6 @@ import {
21
20
  convertTemplateToAIShell,
22
21
  } from '@/utils/compositor/designLibraryHelper';
23
22
 
24
- // --- Types for Workflow State ---
25
23
  type Step =
26
24
  | 'initial'
27
25
  | 'copyInput'
@@ -33,7 +31,6 @@ type Step =
33
31
  type InitialChoice = 'library' | 'ai' | 'blank';
34
32
  type CopyMode = 'prompt' | 'raw';
35
33
 
36
- // --- API Call Helper ---
37
34
  interface GenerationResponse {
38
35
  success: boolean;
39
36
  data?: { response: string | object };
@@ -43,7 +40,8 @@ interface GenerationResponse {
43
40
  const callAskLemurAPI = async (
44
41
  prompt: string,
45
42
  context: string,
46
- expectJson: boolean
43
+ expectJson: boolean,
44
+ isSandboxMode: boolean
47
45
  ): Promise<string> => {
48
46
  const goBackend =
49
47
  import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
@@ -56,12 +54,22 @@ const callAskLemurAPI = async (
56
54
  max_tokens: 2000,
57
55
  };
58
56
 
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
- });
57
+ let response: Response;
58
+ if (isSandboxMode) {
59
+ response = await fetch(`/api/sandbox`, {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
62
+ credentials: 'include',
63
+ body: JSON.stringify({ action: 'askLemur', payload: requestBody }),
64
+ });
65
+ } else {
66
+ response = await fetch(`${goBackend}/api/v1/aai/askLemur`, {
67
+ method: 'POST',
68
+ headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
69
+ credentials: 'include',
70
+ body: JSON.stringify(requestBody),
71
+ });
72
+ }
65
73
 
66
74
  if (!response.ok) {
67
75
  const errorText = await response.text();
@@ -98,7 +106,6 @@ const callAskLemurAPI = async (
98
106
  throw new Error('Unexpected response format received from API.');
99
107
  };
100
108
 
101
- // --- Main Component ---
102
109
  interface AddPaneNewPanelProps {
103
110
  nodeId: string;
104
111
  first: boolean;
@@ -107,6 +114,7 @@ interface AddPaneNewPanelProps {
107
114
  isStoryFragment?: boolean;
108
115
  isContextPane?: boolean;
109
116
  config?: BrandConfig;
117
+ isSandboxMode?: boolean;
110
118
  }
111
119
 
112
120
  const AddPaneNewPanel = ({
@@ -117,23 +125,18 @@ const AddPaneNewPanel = ({
117
125
  isStoryFragment = false,
118
126
  isContextPane = false,
119
127
  config,
128
+ isSandboxMode = false,
120
129
  }: AddPaneNewPanelProps) => {
121
130
  const ctx = providedCtx || getCtx();
122
131
  const hasAssemblyAI = useStore(hasAssemblyAIStore);
123
-
124
- // --- State Machine and Data Stores ---
125
132
  const [step, setStep] = useState<Step>('initial');
126
133
  const [initialChoice, setInitialChoice] = useState<InitialChoice | null>(
127
134
  null
128
135
  );
129
136
  const [error, setError] = useState<string | null>(null);
130
-
131
- // State for CopyInputStep
132
- const [copyMode, setCopyMode] = useState<CopyMode>('raw');
137
+ const [copyMode, setCopyMode] = useState<CopyMode>('prompt');
133
138
  const [promptValue, setPromptValue] = useState('');
134
139
  const [copyValue, setCopyValue] = useState('');
135
-
136
- // State for AiDesignStep
137
140
  const [aiDesignConfig, setAiDesignConfig] = useState<AiDesignConfig>({
138
141
  harmony: 'Analogous',
139
142
  baseColor: '',
@@ -142,8 +145,6 @@ const AddPaneNewPanel = ({
142
145
  additionalNotes: '',
143
146
  });
144
147
 
145
- // --- Handlers & Logic ---
146
-
147
148
  const handleInitialChoice = (choice: InitialChoice) => {
148
149
  setInitialChoice(choice);
149
150
  setError(null);
@@ -243,7 +244,8 @@ const AddPaneNewPanel = ({
243
244
  const copyResult = await callAskLemurAPI(
244
245
  formattedCopyPrompt,
245
246
  copyPromptDetails.system || '',
246
- false
247
+ false,
248
+ isSandboxMode
247
249
  );
248
250
 
249
251
  // 4. Parse ONLY the AI-generated HTML into content nodes
@@ -288,7 +290,8 @@ const AddPaneNewPanel = ({
288
290
  const shellResult = await callAskLemurAPI(
289
291
  formattedShellPrompt,
290
292
  shellPromptDetails.system || '',
291
- true
293
+ true,
294
+ isSandboxMode
292
295
  );
293
296
 
294
297
  const copyInputContent = copyMode === 'prompt' ? promptValue : copyValue;
@@ -301,7 +304,8 @@ const AddPaneNewPanel = ({
301
304
  const copyResult = await callAskLemurAPI(
302
305
  formattedCopyPrompt,
303
306
  copyPromptDetails.system || '',
304
- false
307
+ false,
308
+ isSandboxMode
305
309
  );
306
310
 
307
311
  const finalPane = parseAiPane(shellResult, copyResult, layout);
@@ -310,7 +314,7 @@ const AddPaneNewPanel = ({
310
314
  setError(err.message || 'Failed to generate AI pane.');
311
315
  setStep('error');
312
316
  }
313
- }, [aiDesignConfig, copyMode, promptValue, copyValue]);
317
+ }, [aiDesignConfig, copyMode, promptValue, copyValue, isSandboxMode]);
314
318
 
315
319
  const handleApplyTemplate = async (template: TemplatePane) => {
316
320
  if (!ctx) return;
@@ -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}
@@ -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
+ };
@@ -0,0 +1,137 @@
1
+ ---
2
+ import { ulid } from 'ulid';
3
+ import Layout from '@/layouts/Layout.astro';
4
+ import Header from '@/components/Header.astro';
5
+ import { getFullContentMap } from '@/stores/analytics';
6
+ import { getBrandConfig } from '@/utils/api/brandConfig';
7
+ import { components as codeHookComponents } from '@/custom/CodeHook.astro';
8
+ import StoryKeepHeader from '@/components/edit/Header';
9
+ import StoryKeepToolBar from '@/components/edit/ToolBar';
10
+ import StoryKeepToolMode from '@/components/edit/ToolMode';
11
+ import SettingsPanel from '@/components/edit/SettingsPanel';
12
+ import { Compositor } from '@/components/compositor/Compositor';
13
+ import SandboxAuthWrapper from '@/components/codehooks/SandboxAuthWrapper.tsx';
14
+ import { preHealthCheck } from '@/utils/backend';
15
+
16
+ if (!import.meta.env.PRIVATE_SANDBOX_SECRET) {
17
+ return Astro.redirect('/');
18
+ }
19
+
20
+ const tenantId =
21
+ Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
22
+
23
+ const healthCheckRedirect = await preHealthCheck(tenantId);
24
+ if (healthCheckRedirect !== undefined) {
25
+ return healthCheckRedirect;
26
+ }
27
+
28
+ const brandConfig = await getBrandConfig(tenantId);
29
+
30
+ const emptyStoryFragment = {
31
+ id: ulid(),
32
+ nodeType: 'StoryFragment' as const,
33
+ parentId: null,
34
+ title: 'Sandbox Page',
35
+ slug: 'sandbox-page',
36
+ paneIds: [],
37
+ isChanged: false,
38
+ created: new Date(),
39
+ changed: new Date(),
40
+ };
41
+ const loadData = {
42
+ storyfragmentNodes: [emptyStoryFragment],
43
+ };
44
+ const title = 'Sandbox - TractStack Editor';
45
+ const storyFragmentID = emptyStoryFragment.id;
46
+
47
+ const fullContentMap = await getFullContentMap(tenantId);
48
+ const urlParams: Record<string, string | boolean> = {};
49
+ for (const [key, value] of Astro.url.searchParams) {
50
+ urlParams[key] = value === '' ? true : value;
51
+ }
52
+ ---
53
+
54
+ <Layout
55
+ title={title}
56
+ slug="sandbox"
57
+ brandConfig={brandConfig}
58
+ storyfragmentId={storyFragmentID}
59
+ isStoryKeep={true}
60
+ isEditor={true}
61
+ >
62
+ <SandboxAuthWrapper client:load />
63
+ <Header
64
+ title={title}
65
+ slug="sandbox"
66
+ brandConfig={brandConfig}
67
+ isContext={false}
68
+ isStoryKeep={true}
69
+ isEditable={false}
70
+ menu={null}
71
+ />
72
+
73
+ <section
74
+ id="storykeepHeader"
75
+ role="banner"
76
+ class="z-101 bg-mywhite left-0 right-0 drop-shadow transition-all duration-200"
77
+ >
78
+ <StoryKeepHeader
79
+ slug="sandbox"
80
+ isContext={false}
81
+ isSandboxMode={true}
82
+ client:only="react"
83
+ />
84
+ </section>
85
+
86
+ <div class="flex min-h-screen">
87
+ <StoryKeepToolMode isContext={false} client:only="react" />
88
+
89
+ <main id="mainContent" class="relative flex-1 overflow-x-auto">
90
+ <div class="bg-myblue/20 bg-mylightgrey h-full p-1.5">
91
+ <div
92
+ class="h-fit min-h-screen pb-96"
93
+ style={{
94
+ backgroundImage:
95
+ 'repeating-linear-gradient(135deg, transparent, transparent 10px, rgba(0,0,0,0.05) 10px, rgba(0,0,0,0.05) 20px)',
96
+ }}
97
+ >
98
+ <Compositor
99
+ id={storyFragmentID}
100
+ nodes={loadData}
101
+ config={brandConfig}
102
+ fullContentMap={fullContentMap}
103
+ fullCanonicalURL="/sandbox"
104
+ urlParams={urlParams}
105
+ availableCodeHooks={Object.keys(codeHookComponents)}
106
+ isSandboxMode={true}
107
+ client:only="react"
108
+ />
109
+ </div>
110
+ </div>
111
+ </main>
112
+ </div>
113
+
114
+ <aside
115
+ id="settingsControls"
116
+ class="z-101 pointer-events-none fixed bottom-16 right-2 flex flex-col items-end gap-2 md:bottom-2"
117
+ >
118
+ <div class="pointer-events-none flex-grow"></div>
119
+
120
+ <div class="pointer-events-auto flex-shrink-0">
121
+ <StoryKeepToolBar client:only="react" />
122
+ </div>
123
+
124
+ <div class="pointer-events-auto max-h-full">
125
+ <SettingsPanel
126
+ config={brandConfig}
127
+ availableCodeHooks={Object.keys(codeHookComponents)}
128
+ client:only="react"
129
+ />
130
+ </div>
131
+ </aside>
132
+ </Layout>
133
+
134
+ <script>
135
+ import { setupLayoutObservers } from '@/utils/layout';
136
+ document.addEventListener('astro:page-load', setupLayoutObservers);
137
+ </script>
@@ -27,6 +27,7 @@ export type NodeProps = {
27
27
  ctx?: NodesContext;
28
28
  first?: boolean;
29
29
  onDragStart?: (origin: SelectionOrigin, e: MouseEvent<HTMLElement>) => void;
30
+ isSandboxMode?: boolean;
30
31
  isSelectableText?: boolean;
31
32
  };
32
33
 
@@ -148,6 +148,13 @@ export class ProfileStorage {
148
148
  StorageManager.set(this.STORAGE_KEYS.profileToken, token);
149
149
  StorageManager.set(this.STORAGE_KEYS.hasProfile, '1');
150
150
  StorageManager.set(this.STORAGE_KEYS.unlockedProfile, '1');
151
+
152
+ try {
153
+ const maxAge = 60 * 60 * 24;
154
+ document.cookie = `tractstack_profile=true; path=/; SameSite=Lax; max-age=${maxAge}`;
155
+ } catch {
156
+ // Silently fail if cookies are blocked
157
+ }
151
158
  }
152
159
 
153
160
  /**
@@ -157,6 +164,12 @@ export class ProfileStorage {
157
164
  StorageManager.remove(this.STORAGE_KEYS.profileToken);
158
165
  StorageManager.remove(this.STORAGE_KEYS.hasProfile);
159
166
  StorageManager.remove(this.STORAGE_KEYS.unlockedProfile);
167
+ try {
168
+ document.cookie =
169
+ 'tractstack_profile=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
170
+ } catch {
171
+ // Silently fail
172
+ }
160
173
  }
161
174
 
162
175
  /**
@@ -139,6 +139,18 @@ export async function injectTemplateFiles(
139
139
  ),
140
140
  dest: 'src/components/compositor/nodes/Pane_layout.tsx',
141
141
  },
142
+ {
143
+ src: resolve(
144
+ '../templates/src/components/codehooks/SandboxAuthWrapper.tsx'
145
+ ),
146
+ dest: 'src/components/codehooks/SandboxAuthWrapper.tsx',
147
+ },
148
+ {
149
+ src: resolve(
150
+ '../templates/src/components/codehooks/SandboxRegisterForm.tsx'
151
+ ),
152
+ dest: 'src/components/codehooks/SandboxRegisterForm.tsx',
153
+ },
142
154
  {
143
155
  src: resolve('../templates/src/components/compositor/nodes/Markdown.tsx'),
144
156
  dest: 'src/components/compositor/nodes/Markdown.tsx',
@@ -441,14 +453,6 @@ export async function injectTemplateFiles(
441
453
  ),
442
454
  dest: 'src/components/edit/pane/RestylePaneModal.tsx',
443
455
  },
444
- {
445
- src: resolve('../templates/src/components/edit/pane/AiPaneGenerator.tsx'),
446
- dest: 'src/components/edit/pane/AiPaneGenerator.tsx',
447
- },
448
- {
449
- src: resolve('../templates/src/components/edit/pane/AiPanePreview.tsx'),
450
- dest: 'src/components/edit/pane/AiPanePreview.tsx',
451
- },
452
456
  {
453
457
  src: resolve(
454
458
  '../templates/src/components/edit/pane/steps/CopyInputStep.tsx'
@@ -603,12 +607,6 @@ export async function injectTemplateFiles(
603
607
  dest: 'src/stores/selection.ts',
604
608
  },
605
609
 
606
- // AAI utils
607
- {
608
- src: resolve('../templates/src/utils/aai/getTitleSlug.ts'),
609
- dest: 'src/utils/aai/getTitleSlug.ts',
610
- },
611
-
612
610
  // Compositor utils - etl
613
611
  {
614
612
  src: resolve('../templates/src/utils/etl/index.ts'),
@@ -844,6 +842,10 @@ export async function injectTemplateFiles(
844
842
  ),
845
843
  dest: 'src/pages/context/[...contextSlug]/edit.astro',
846
844
  },
845
+ {
846
+ src: resolve('../templates/src/pages/sandbox.astro'),
847
+ dest: 'src/pages/sandbox.astro',
848
+ },
847
849
  {
848
850
  src: resolve('../templates/src/pages/storykeep.astro'),
849
851
  dest: 'src/pages/storykeep.astro',
@@ -888,6 +890,10 @@ export async function injectTemplateFiles(
888
890
  src: resolve('../templates/src/pages/api/tailwind.ts'),
889
891
  dest: 'src/pages/api/tailwind.ts',
890
892
  },
893
+ {
894
+ src: resolve('../templates/src/pages/api/sandbox.ts'),
895
+ dest: 'src/pages/api/sandbox.ts',
896
+ },
891
897
 
892
898
  // Authentication Pages
893
899
  {
@@ -2156,6 +2162,13 @@ export async function injectTemplateFiles(
2156
2162
  // Example Components (Conditional)
2157
2163
  ...(config?.includeExamples
2158
2164
  ? [
2165
+ {
2166
+ src: resolve(
2167
+ '../templates/custom/with-examples/SandboxLauncher.tsx'
2168
+ ),
2169
+ dest: 'src/custom/SandboxLauncher.tsx',
2170
+ protected: true,
2171
+ },
2159
2172
  {
2160
2173
  src: resolve('../templates/custom/with-examples/CustomHero.astro'),
2161
2174
  dest: 'src/custom/CustomHero.astro',