@sampleapp.ai/sdk 1.0.31 → 1.0.33

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.
@@ -6,44 +6,116 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
6
6
  import SimplifiedEditor from "./simplified-editor";
7
7
  import PillFileSelector from "./pill-file-selector";
8
8
  import { useGuardianContext } from "../context/guardian-context";
9
- import { useEffect, useMemo, useState } from "react";
9
+ import { useVmContext } from "../context/vm-context";
10
+ import { useEffect, useMemo, useRef, useState } from "react";
10
11
  import { zipPathToFileTree } from "../zip-to-filetree";
11
12
  import { codeZipFileToCodebase } from "../zip-to-codebase";
12
13
  import { DownloadIcon, Globe } from "lucide-react";
13
14
  import { useFrameParams } from "../hooks/use-frame-params";
14
15
  import { cn } from "../../../../lib/utils";
15
- import { extractContainerIdFromUrl } from "../../../../lib/api-client";
16
- export default function RightView({ reloadCounter, overlayStage, browserUrl, useVm, codeZipFile, themeColor, completeCodeZipFile, hasPreview, isPreviewMinimized = false, isGuardian = false, isBrowserMaximized = false, }) {
16
+ import { handleSandboxDownload } from "../utils";
17
+ export default function RightView({ reloadCounter, overlayStage, browserUrl, useVm, codeZipFile, themeColor, completeCodeZipFile, hasPreview, isPreviewMinimized = false, isGuardian = false, isBrowserMaximized = false, isBrowserUrlReady = false, sandboxId, apiKey, sandboxError, }) {
17
18
  const { setFileTree, setGeneratedCode } = useGuardianContext();
19
+ const { setVmResolution } = useVmContext();
18
20
  const frameParams = useFrameParams();
19
21
  const [activeTab, setActiveTab] = useState("code");
20
22
  const [networkRequests, setNetworkRequests] = useState([]);
21
23
  const [showLoader, setShowLoader] = useState(!browserUrl);
22
- // Delay hiding the loader for 5 seconds after browserUrl becomes available
24
+ // Ref to measure the preview container for VNC resolution
25
+ const previewContainerRef = useRef(null);
26
+ // Hide loader based on whether URL is ready or if there's an error
27
+ // - If sandboxError exists, hide loader immediately to show error UI
28
+ // - If isBrowserUrlReady (published_url), hide immediately when available
29
+ // - Otherwise, wait 5 seconds for sandbox to boot
23
30
  useEffect(() => {
24
- if (browserUrl) {
31
+ if (sandboxError) {
32
+ // Show error UI immediately instead of loader
33
+ setShowLoader(false);
34
+ }
35
+ else if (browserUrl) {
36
+ const delay = isBrowserUrlReady ? 0 : 5000;
25
37
  const timer = setTimeout(() => {
26
38
  setShowLoader(false);
27
- }, 5000);
39
+ }, delay);
28
40
  return () => clearTimeout(timer);
29
41
  }
30
42
  else {
31
43
  setShowLoader(true);
32
44
  }
33
- }, [browserUrl]);
45
+ }, [browserUrl, isBrowserUrlReady, sandboxError]);
34
46
  // Use frame param override for theme if in frame mode
35
47
  const effectiveThemeColor = frameParams.isFrame && frameParams.theme ? frameParams.theme : themeColor;
36
- // Extract container ID from browserUrl and build download URL
37
- const downloadCodeUrl = useMemo(() => {
38
- if (!browserUrl)
39
- return null;
40
- const containerId = extractContainerIdFromUrl(browserUrl);
41
- if (!containerId)
42
- return null;
43
- // Use the API base URL for the download endpoint
44
- const baseUrl = process.env.BASE_API_URL || "https://api.sampleapp.ai";
45
- return `${baseUrl}/api/v1/sdk/download-code?container_id=${encodeURIComponent(containerId)}`;
46
- }, [browserUrl]);
48
+ // Measure the preview container for VNC resolution when useVm is true
49
+ // IMPORTANT: Re-run when isBrowserMaximized changes because the ref points to different elements
50
+ useEffect(() => {
51
+ if (!useVm)
52
+ return;
53
+ let mounted = true;
54
+ let resizeObserver = null;
55
+ let lastReportedResolution = null;
56
+ const updateResolution = () => {
57
+ if (!mounted)
58
+ return;
59
+ const container = previewContainerRef.current;
60
+ if (!container) {
61
+ return;
62
+ }
63
+ // Get the exact inner dimensions - clientWidth/Height excludes borders and scrollbars
64
+ let widthPx = container.clientWidth;
65
+ let heightPx = container.clientHeight;
66
+ // Fallback to offsetWidth/Height if client dimensions are 0
67
+ if (widthPx === 0 || heightPx === 0) {
68
+ widthPx = container.offsetWidth;
69
+ heightPx = container.offsetHeight;
70
+ }
71
+ // Final fallback to getBoundingClientRect
72
+ if (widthPx === 0 || heightPx === 0) {
73
+ const rect = container.getBoundingClientRect();
74
+ widthPx = Math.floor(rect.width);
75
+ heightPx = Math.floor(rect.height);
76
+ }
77
+ widthPx = Math.floor(widthPx);
78
+ heightPx = Math.floor(heightPx);
79
+ if (widthPx > 0 && heightPx > 0) {
80
+ if (!lastReportedResolution ||
81
+ lastReportedResolution[0] !== widthPx ||
82
+ lastReportedResolution[1] !== heightPx) {
83
+ lastReportedResolution = [widthPx, heightPx];
84
+ setVmResolution([widthPx, heightPx]);
85
+ }
86
+ }
87
+ };
88
+ const setupObserver = () => {
89
+ if (previewContainerRef.current) {
90
+ resizeObserver = new ResizeObserver(() => {
91
+ updateResolution();
92
+ });
93
+ resizeObserver.observe(previewContainerRef.current);
94
+ // Initial measurement
95
+ setTimeout(updateResolution, 10);
96
+ }
97
+ else {
98
+ setTimeout(setupObserver, 50);
99
+ }
100
+ };
101
+ setupObserver();
102
+ // Additional delayed measurements as fallback
103
+ const t1 = setTimeout(updateResolution, 100);
104
+ const t2 = setTimeout(updateResolution, 300);
105
+ const t3 = setTimeout(updateResolution, 500);
106
+ return () => {
107
+ mounted = false;
108
+ clearTimeout(t1);
109
+ clearTimeout(t2);
110
+ clearTimeout(t3);
111
+ if (resizeObserver) {
112
+ resizeObserver.disconnect();
113
+ }
114
+ };
115
+ }, [useVm, setVmResolution, isBrowserMaximized]);
116
+ const handleDownloadAsZip = async () => {
117
+ await handleSandboxDownload(sandboxId, apiKey);
118
+ };
47
119
  // Extract only the nested children (actual API calls to external services)
48
120
  const childrenRequests = useMemo(() => {
49
121
  const children = [];
@@ -56,15 +128,18 @@ export default function RightView({ reloadCounter, overlayStage, browserUrl, use
56
128
  }, [networkRequests]);
57
129
  useEffect(() => {
58
130
  let isMounted = true;
59
- zipPathToFileTree(completeCodeZipFile)
60
- .then((fileTree) => {
61
- if (isMounted) {
62
- setFileTree(fileTree);
63
- }
64
- })
65
- .catch((error) => {
66
- console.error("Failed to load zip file:", error);
67
- });
131
+ // Only load zip file if completeCodeZipFile is provided
132
+ if (completeCodeZipFile) {
133
+ zipPathToFileTree(completeCodeZipFile)
134
+ .then((fileTree) => {
135
+ if (isMounted) {
136
+ setFileTree(fileTree);
137
+ }
138
+ })
139
+ .catch((error) => {
140
+ console.error("Failed to load zip file:", error);
141
+ });
142
+ }
68
143
  codeZipFileToCodebase(codeZipFile)
69
144
  .then((codebase) => {
70
145
  if (isMounted) {
@@ -100,8 +175,8 @@ export default function RightView({ reloadCounter, overlayStage, browserUrl, use
100
175
  }, []);
101
176
  // When browser is maximized, render only the browser at full size
102
177
  if (isBrowserMaximized) {
103
- return (React.createElement("div", { className: "h-full w-full" }, showLoader ? (React.createElement("div", { className: "h-full w-full relative overflow-hidden" },
104
- React.createElement(AiLoader, { text: "Booting up...", fullScreen: false, themeColor: effectiveThemeColor }))) : (React.createElement(Browser, { previewUrl: browserUrl, setPreviewUrl: () => { }, containerEndpoint: browserUrl, outerContainerClassName: "h-full w-full border-none", reloadSignal: reloadCounter, useVm: useVm, isGuardian: isGuardian }, overlayStage !== "hidden" && (React.createElement("div", { className: "absolute inset-0 z-20 flex items-center justify-center" },
178
+ return (React.createElement("div", { ref: previewContainerRef, className: "h-full w-full relative" }, showLoader ? (React.createElement("div", { className: "h-full w-full relative overflow-hidden" },
179
+ React.createElement(AiLoader, { text: "Booting up...", fullScreen: false, themeColor: effectiveThemeColor }))) : (React.createElement(Browser, { previewUrl: browserUrl || "", setPreviewUrl: () => { }, containerEndpoint: browserUrl, outerContainerClassName: "h-full w-full border-none", reloadSignal: reloadCounter, useVm: useVm, isGuardian: isGuardian, sandboxError: sandboxError }, overlayStage !== "hidden" && (React.createElement("div", { className: "absolute inset-0 z-20 flex items-center justify-center" },
105
180
  overlayStage === "error" && (React.createElement("div", { className: "w-full h-full bg-red-950 text-red-200 flex items-center justify-center" },
106
181
  React.createElement("div", { className: "text-center" },
107
182
  React.createElement("div", { className: "text-2xl font-semibold" }, "Service Unavailable"),
@@ -118,9 +193,9 @@ export default function RightView({ reloadCounter, overlayStage, browserUrl, use
118
193
  return (React.createElement("div", { className: "h-full w-full flex flex-col" },
119
194
  React.createElement(PanelGroup, { direction: "vertical", className: "relative h-full" },
120
195
  hasPreview && !isPreviewMinimized && (React.createElement(React.Fragment, null,
121
- React.createElement(Panel, { defaultSize: 58, minSize: 20, maxSize: 80, order: 1 },
122
- React.createElement("div", { className: "h-full w-full" }, showLoader ? (React.createElement("div", { className: "h-full w-full relative overflow-hidden" },
123
- React.createElement(AiLoader, { text: "Booting up...", fullScreen: false, themeColor: effectiveThemeColor }))) : (React.createElement(Browser, { previewUrl: browserUrl, setPreviewUrl: () => { }, containerEndpoint: browserUrl, outerContainerClassName: "h-full w-full border-none", reloadSignal: reloadCounter, useVm: useVm, isGuardian: isGuardian }, overlayStage !== "hidden" && (React.createElement("div", { className: "absolute inset-0 z-20 flex items-center justify-center" },
196
+ React.createElement(Panel, { defaultSize: 68, minSize: 20, maxSize: 80, order: 1 },
197
+ React.createElement("div", { ref: previewContainerRef, className: "h-full w-full relative" }, showLoader ? (React.createElement("div", { className: "h-full w-full relative overflow-hidden" },
198
+ React.createElement(AiLoader, { text: "Booting up...", fullScreen: false, themeColor: effectiveThemeColor }))) : (React.createElement(Browser, { previewUrl: browserUrl || "", setPreviewUrl: () => { }, containerEndpoint: browserUrl, outerContainerClassName: "h-full w-full border-none", reloadSignal: reloadCounter, useVm: useVm, isGuardian: isGuardian, sandboxError: sandboxError }, overlayStage !== "hidden" && (React.createElement("div", { className: "absolute inset-0 z-20 flex items-center justify-center" },
124
199
  overlayStage === "error" && (React.createElement("div", { className: "w-full h-full bg-red-950 text-red-200 flex items-center justify-center" },
125
200
  React.createElement("div", { className: "text-center" },
126
201
  React.createElement("div", { className: "text-2xl font-semibold" }, "Service Unavailable"),
@@ -138,18 +213,16 @@ export default function RightView({ reloadCounter, overlayStage, browserUrl, use
138
213
  React.createElement("div", { className: "absolute inset-x-0 top-0 h-1/2 group-hover:bg-blue-400/20 transition-colors" }),
139
214
  React.createElement("div", { className: "absolute inset-x-0 bottom-0 h-1/2 group-hover:bg-blue-400/20 transition-colors" }),
140
215
  React.createElement("div", { className: "h-full w-px bg-border group-hover:bg-blue-400 transition-colors relative z-10" }))))),
141
- React.createElement(Panel, { defaultSize: 42, minSize: 20, maxSize: 80, order: 2 },
216
+ React.createElement(Panel, { defaultSize: 32, minSize: 20, maxSize: 80, order: 2 },
142
217
  React.createElement("div", { className: "h-full w-full flex flex-col" },
143
218
  React.createElement("div", { className: "flex-1 min-h-0 flex flex-col" },
144
219
  React.createElement(React.Fragment, null,
145
220
  React.createElement("div", { className: "flex items-center justify-between border-b border-zinc-800 dark:border-zinc-700 bg-zinc-900 dark:bg-zinc-950" },
146
221
  React.createElement(PillFileSelector, { themeColor: effectiveThemeColor }),
147
- downloadCodeUrl && (React.createElement("div", { className: "px-2 flex-shrink-0" },
148
- React.createElement("button", { type: "button", className: "inline-flex items-center gap-1 text-[10px] font-medium transition-colors hover:opacity-80", style: { color: effectiveThemeColor }, onClick: () => {
149
- window.open(downloadCodeUrl, "_blank");
150
- } },
222
+ React.createElement("div", { className: "px-2 flex-shrink-0" },
223
+ React.createElement("button", { type: "button", className: "inline-flex items-center gap-1 text-[10px] font-medium transition-colors hover:opacity-80", style: { color: effectiveThemeColor }, onClick: handleDownloadAsZip },
151
224
  React.createElement(DownloadIcon, { className: "w-4 h-4" }),
152
- "Download Code")))),
225
+ "Download Code"))),
153
226
  React.createElement("div", { className: "flex-1 min-h-0" },
154
227
  React.createElement(SimplifiedEditor, { themeColor: effectiveThemeColor })))))))));
155
228
  }
@@ -3,7 +3,7 @@ import React, { useState, useEffect } from "react";
3
3
  import RightPanelView from "./right-panel-view";
4
4
  import RightTopDownView from "./right-top-down-view";
5
5
  import PreviewControlBar from "./preview-control-bar";
6
- export default function RightView({ reloadCounter, overlayStage, browserUrl, useVm, codeZipFile, completeCodeZipFile, variant = "top-down", themeColor, hasPreview, isGuardian = false, isBrowserMaximized = false, }) {
6
+ export default function RightView({ reloadCounter, overlayStage, browserUrl, useVm, codeZipFile, completeCodeZipFile, variant = "top-down", themeColor, hasPreview, isGuardian = false, isBrowserMaximized = false, isBrowserUrlReady = false, sandboxId, apiKey, sandboxError, }) {
7
7
  const [isPreviewMinimized, setIsPreviewMinimized] = useState(false);
8
8
  const [reloadCounterState, setReloadCounterState] = useState(reloadCounter);
9
9
  // Sync with external reloadCounter changes
@@ -14,13 +14,13 @@ export default function RightView({ reloadCounter, overlayStage, browserUrl, use
14
14
  setReloadCounterState((c) => c + 1);
15
15
  };
16
16
  if (variant === "panel") {
17
- return (React.createElement(RightPanelView, { reloadCounter: reloadCounterState, overlayStage: overlayStage, browserUrl: browserUrl, useVm: useVm, codeZipFile: codeZipFile }));
17
+ return (React.createElement(RightPanelView, { reloadCounter: reloadCounterState, overlayStage: overlayStage, browserUrl: browserUrl, useVm: useVm, codeZipFile: codeZipFile, sandboxError: sandboxError }));
18
18
  }
19
19
  else if (variant === "top-down") {
20
20
  return (React.createElement("div", { className: "h-full w-full flex flex-col" },
21
21
  hasPreview && (React.createElement(PreviewControlBar, { isMinimized: isPreviewMinimized, onToggle: () => setIsPreviewMinimized(!isPreviewMinimized), onRefresh: handleRefresh, themeColor: themeColor, externalUrl: browserUrl })),
22
22
  React.createElement("div", { className: "flex-1 min-h-0" },
23
- React.createElement(RightTopDownView, { reloadCounter: reloadCounterState, overlayStage: overlayStage, browserUrl: browserUrl, useVm: useVm, codeZipFile: codeZipFile, themeColor: themeColor, completeCodeZipFile: completeCodeZipFile, hasPreview: hasPreview, isPreviewMinimized: isPreviewMinimized, isGuardian: isGuardian, isBrowserMaximized: isBrowserMaximized }))));
23
+ React.createElement(RightTopDownView, { reloadCounter: reloadCounterState, overlayStage: overlayStage, browserUrl: browserUrl, useVm: useVm, codeZipFile: codeZipFile, themeColor: themeColor, completeCodeZipFile: completeCodeZipFile, hasPreview: hasPreview, isPreviewMinimized: isPreviewMinimized, isGuardian: isGuardian, isBrowserMaximized: isBrowserMaximized, isBrowserUrlReady: isBrowserUrlReady, sandboxId: sandboxId, apiKey: apiKey, sandboxError: sandboxError }))));
24
24
  }
25
25
  else {
26
26
  return null;
@@ -3,60 +3,30 @@ import React from "react";
3
3
  import { DownloadIcon, Check, Copy } from "lucide-react";
4
4
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "./dialog";
5
5
  import vscodeLogo from "../../../../assets/vscode.png";
6
- import { extractContainerIdFromUrl } from "../../../../lib/api-client";
6
+ import { handleSandboxDownload } from "../utils";
7
7
  // VSCode logo component using imported asset
8
8
  function VSCodeLogo({ className }) {
9
9
  return (React.createElement("img", { src: vscodeLogo, alt: "VSCode", width: 20, height: 20, className: className }));
10
10
  }
11
- export const DownloadAndOpenButtons = ({ downloadUrl, themeColor, gitUrl, browserUrl, }) => {
11
+ export const DownloadAndOpenButtons = ({ downloadUrl, themeColor, gitUrl, browserUrl, sandboxId, apiKey, }) => {
12
12
  const [isModalOpen, setIsModalOpen] = React.useState(false);
13
13
  const [hasCopied, setHasCopied] = React.useState(false);
14
- // Extract container ID from browserUrl and build download URL (same as Download Code button)
15
- const containerDownloadUrl = React.useMemo(() => {
16
- if (!browserUrl)
17
- return null;
18
- const containerId = extractContainerIdFromUrl(browserUrl);
19
- if (!containerId)
20
- return null;
21
- const baseUrl = process.env.BASE_API_URL || "https://api.sampleapp.ai";
22
- return `${baseUrl}/api/v1/sdk/download-code?container_id=${encodeURIComponent(containerId)}`;
23
- }, [browserUrl]);
24
- // Normalize the download path to work with public directory (fallback for static files)
25
- const normalizedDownloadUrl = React.useMemo(() => {
26
- let normalizedPath = downloadUrl;
27
- if (normalizedPath.startsWith("/public/")) {
28
- normalizedPath = normalizedPath.slice("/public".length);
29
- }
30
- if (!normalizedPath.startsWith("/")) {
31
- normalizedPath = "/" + normalizedPath;
32
- }
33
- return normalizedPath;
34
- }, [downloadUrl]);
35
- // Use container-based download URL if available, otherwise fall back to static file
36
- const finalDownloadUrl = containerDownloadUrl || normalizedDownloadUrl;
37
- // Build the npx command using the static API route
14
+ // Build the npx command using the SDK download endpoint
38
15
  // Use default MAIN_APP_URL for SDK (can be overridden via env)
39
16
  const MAIN_APP_URL = typeof window !== "undefined" && window.SAMPLEAPP_MAIN_URL
40
17
  ? window.SAMPLEAPP_MAIN_URL
41
18
  : "https://sampleapp.ai";
42
19
  const npxCommand = React.useMemo(() => {
43
- // If using container-based download, we can't generate a static npx command
44
- // Use the container download URL instead
45
- if (containerDownloadUrl) {
46
- return `npx sampleappai add "${containerDownloadUrl}"`;
47
- }
48
- // Remove .zip extension and leading slash for the API path
49
- let apiPath = normalizedDownloadUrl;
50
- if (apiPath.startsWith("/")) {
51
- apiPath = apiPath.slice(1);
52
- }
53
- if (apiPath.endsWith(".zip")) {
54
- apiPath = apiPath.slice(0, -4);
20
+ if (!sandboxId) {
21
+ return "";
55
22
  }
56
- return `npx sampleappai add "${MAIN_APP_URL}/api/code/static/${apiPath}"`;
57
- }, [containerDownloadUrl, normalizedDownloadUrl, MAIN_APP_URL]);
58
- const handleDownload = () => {
59
- window.open(finalDownloadUrl, "_blank");
23
+ const apiBaseUrl = typeof window !== "undefined" && window.SAMPLEAPP_API_URL
24
+ ? window.SAMPLEAPP_API_URL
25
+ : MAIN_APP_URL.replace("sampleapp.ai", "api.sampleapp.ai");
26
+ return `npx sampleappai add "${apiBaseUrl}/api/v1/sdk/download-code?sandbox_id=${sandboxId}"`;
27
+ }, [sandboxId, MAIN_APP_URL]);
28
+ const handleDownload = async () => {
29
+ await handleSandboxDownload(sandboxId, apiKey, () => setIsModalOpen(true));
60
30
  };
61
31
  const handleOpenInVSCode = () => {
62
32
  if (gitUrl) {
@@ -105,7 +75,7 @@ export const DownloadAndOpenButtons = ({ downloadUrl, themeColor, gitUrl, browse
105
75
  React.createElement("div", { className: "flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-bold text-white", style: { backgroundColor: themeColor } }, "1"),
106
76
  React.createElement("p", { className: "text-sm font-medium" }, "Copy this command")),
107
77
  React.createElement("div", { className: "flex items-center gap-2 overflow-hidden" },
108
- React.createElement("code", { className: "flex-1 bg-zinc-100 dark:bg-zinc-800 px-3 py-2.5 rounded-md text-xs font-mono truncate block max-w-[calc(100%-3rem)]" }, npxCommand.replace("http://localhost:3000", "https://sampleapp.ai")),
78
+ React.createElement("code", { className: "flex-1 bg-zinc-100 dark:bg-zinc-800 px-3 py-2.5 rounded-md text-xs font-mono truncate block max-w-[calc(100%-3rem)]" }, npxCommand || "npx sampleappai add <download-url>"),
109
79
  React.createElement("button", { type: "button", onClick: handleCopyCommand, className: "flex-shrink-0 p-2 rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors border border-zinc-200 dark:border-zinc-700" }, hasCopied ? (React.createElement(Check, { className: "w-4 h-4 text-green-500" })) : (React.createElement(Copy, { className: "w-4 h-4 text-muted-foreground" }))))),
110
80
  React.createElement("div", { className: "flex flex-col gap-2" },
111
81
  React.createElement("div", { className: "flex items-center gap-2" },
@@ -51,7 +51,7 @@ const createId = (text) => {
51
51
  };
52
52
  export const MemoizedReactMarkdown = memo(ReactMarkdown, (prevProps, nextProps) => prevProps.children === nextProps.children &&
53
53
  prevProps.className === nextProps.className);
54
- export const Markdown = ({ children, className, isRestricted, showToc, themeColor, browserUrl, gitUrl }) => {
54
+ export const Markdown = ({ children, className, isRestricted, showToc, themeColor, browserUrl, gitUrl, sandboxId, apiKey }) => {
55
55
  const headingRenderer = (level) => {
56
56
  return (_a) => {
57
57
  var _b, _c;
@@ -149,11 +149,13 @@ export const Markdown = ({ children, className, isRestricted, showToc, themeColo
149
149
  // Use gitUrl from Markdown component props (from guardian-playground) as priority,
150
150
  // fall back to HTML attribute if not provided
151
151
  const effectiveGitUrl = gitUrl || giturl;
152
+ // Use sandboxId from Markdown component props
153
+ const effectiveSandboxId = sandboxId;
152
154
  if (!downloadurl || !effectiveThemeColor) {
153
155
  return React.createElement("div", { className: className }, children);
154
156
  }
155
157
  return (React.createElement("div", { className: cn("my-6", className) },
156
- React.createElement(DownloadAndOpenButtons, { downloadUrl: downloadurl, themeColor: effectiveThemeColor, gitUrl: effectiveGitUrl, browserUrl: browserUrl }),
158
+ React.createElement(DownloadAndOpenButtons, { downloadUrl: downloadurl, themeColor: effectiveThemeColor, gitUrl: effectiveGitUrl, browserUrl: browserUrl, sandboxId: effectiveSandboxId, apiKey: apiKey }),
157
159
  children));
158
160
  };
159
161
  const sharedComponents = {
@@ -0,0 +1,6 @@
1
+ import { default as React } from 'react';
2
+ export declare const ThemeColorProvider: React.FC<{
3
+ themeColor?: string;
4
+ children: React.ReactNode;
5
+ }>;
6
+ export declare const useThemeColor: () => string;
@@ -1,4 +1,5 @@
1
- import { FrameworkLabel, } from "./types/ide-types";
1
+ import { FrameworkLabel } from "./types/ide-types";
2
+ import { createApiClient } from "../../../lib/api-client";
2
3
  /**
3
4
  * Creates sandbox URL configs for preloading from a framework's urlsToPreload.
4
5
  * Only includes URLs that need VM preloading (useVm: true).
@@ -87,3 +88,31 @@ export function buildGuardianConfig(useCases, options) {
87
88
  }
88
89
  return result;
89
90
  }
91
+ /**
92
+ * Shared download handler for sandbox code
93
+ * Uses the backend API endpoint to download code as zip file
94
+ * @param sandboxId - The sandbox content UID
95
+ * @param apiKey - API key for authentication (required)
96
+ * @param onError - Optional callback when download fails (e.g., to show modal)
97
+ * @returns Promise that resolves when download is initiated
98
+ */
99
+ export async function handleSandboxDownload(sandboxId, apiKey, onError) {
100
+ // Always use the backend endpoint when sandboxId is available
101
+ if (!sandboxId) {
102
+ onError === null || onError === void 0 ? void 0 : onError();
103
+ return;
104
+ }
105
+ if (!apiKey) {
106
+ console.error("API key is required for downloading code");
107
+ onError === null || onError === void 0 ? void 0 : onError();
108
+ return;
109
+ }
110
+ try {
111
+ const apiClient = createApiClient({ apiKey });
112
+ await apiClient.sdk.downloadCode(sandboxId);
113
+ }
114
+ catch (error) {
115
+ console.error("Failed to download code:", error);
116
+ onError === null || onError === void 0 ? void 0 : onError();
117
+ }
118
+ }