mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
added ai custom dashboards
This commit is contained in:
parent
3cde6faaa8
commit
a2033bcaec
3
apps/dashboard/.gitignore
vendored
3
apps/dashboard/.gitignore
vendored
@ -34,3 +34,6 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Generated files
|
||||
src/generated
|
||||
|
||||
@ -4,11 +4,13 @@
|
||||
"repository": "https://github.com/stack-auth/stack-auth",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "rimraf .next && rimraf node_modules",
|
||||
"clean": "rimraf .next && rimraf node_modules && rimraf src/generated",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"with-env": "dotenv -c development --",
|
||||
"with-env:prod": "dotenv -c --",
|
||||
"dev": "next dev --turbopack --port ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01",
|
||||
"bundle-type-definitions": "tsx scripts/bundle-type-definitions.ts",
|
||||
"prebuild": "pnpm run bundle-type-definitions",
|
||||
"build": "next build",
|
||||
"docker-build": "next build --experimental-build-mode compile",
|
||||
"analyze-bundle": "next experimental-analyze",
|
||||
@ -17,6 +19,7 @@
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^3.0.41",
|
||||
"@ai-sdk/openai": "^3.0.25",
|
||||
"@ai-sdk/react": "^3.0.72",
|
||||
"@assistant-ui/react": "^0.10.24",
|
||||
|
||||
65
apps/dashboard/scripts/bundle-type-definitions.ts
Normal file
65
apps/dashboard/scripts/bundle-type-definitions.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { writeFileSyncIfChanged } from '@stackframe/stack-shared/dist/utils/fs';
|
||||
import { glob } from 'glob';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
|
||||
type TypeDefinitionFile = {
|
||||
path: string,
|
||||
content: string,
|
||||
};
|
||||
|
||||
async function main() {
|
||||
console.log('[Bundle Type Definitions] Finding Stack SDK type definition files...');
|
||||
|
||||
const rootPath = path.resolve(process.cwd(), '../..');
|
||||
const stackAppPath = path.join(rootPath, 'packages/template/src/lib/stack-app');
|
||||
const outputPath = path.join(rootPath, 'apps/dashboard/src/generated/bundled-type-definitions.ts');
|
||||
|
||||
const files = await glob(`${stackAppPath}/**/*.ts`, {
|
||||
ignore: [
|
||||
`${stackAppPath}/**/implementations/**`,
|
||||
`${stackAppPath}/**/utils/**`,
|
||||
`${stackAppPath}/**/*.d.ts`,
|
||||
`${stackAppPath}/**/global.css`,
|
||||
],
|
||||
});
|
||||
|
||||
console.log(`[Bundle Type Definitions] Found ${files.length} type definition files`);
|
||||
|
||||
const bundledFiles: TypeDefinitionFile[] = [];
|
||||
|
||||
for (const filePath of files) {
|
||||
const relativePath = path.relative(stackAppPath, filePath);
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
|
||||
bundledFiles.push({
|
||||
path: relativePath,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[Bundle Type Definitions] Generating bundled-type-definitions.ts...');
|
||||
|
||||
const output = `// This file is auto-generated by scripts/bundle-type-definitions.ts
|
||||
// Do not edit manually - changes will be overwritten
|
||||
// Last generated: ${new Date().toISOString()}
|
||||
|
||||
export type TypeDefinitionFile = {
|
||||
path: string,
|
||||
content: string,
|
||||
};
|
||||
|
||||
export const BUNDLED_TYPE_DEFINITIONS: TypeDefinitionFile[] = ${JSON.stringify(bundledFiles, null, 2)};
|
||||
`;
|
||||
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
writeFileSyncIfChanged(outputPath, output);
|
||||
|
||||
console.log(`[Bundle Type Definitions] Generated ${outputPath}`);
|
||||
console.log(`[Bundle Type Definitions] Total size: ${(output.length / 1024).toFixed(2)} KB, ${bundledFiles.length} files bundled`);
|
||||
}
|
||||
|
||||
main().catch((...args) => {
|
||||
console.error('[Bundle Type Definitions] ERROR! Failed to bundle type definitions:', ...args);
|
||||
process.exit(1);
|
||||
});
|
||||
28
apps/dashboard/src/app/api/create-dashboard/route.ts
Normal file
28
apps/dashboard/src/app/api/create-dashboard/route.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { generateDashboardRuntimeCodegen } from "@/lib/ai-dashboard/model";
|
||||
import { stackServerApp } from "@/stack";
|
||||
import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
|
||||
const requestSchema = yupObject({
|
||||
projectId: yupString().defined().nonEmpty(),
|
||||
prompt: yupString().defined().nonEmpty(),
|
||||
}).defined();
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const user = await stackServerApp.getUser({ or: "redirect" });
|
||||
const payload = await requestSchema.validate(await req.json());
|
||||
|
||||
const projects = await user.listOwnedProjects();
|
||||
const project = projects.find((p: { id: string }) => p.id === payload.projectId);
|
||||
if (!project) {
|
||||
throwErr("You do not own this project");
|
||||
}
|
||||
|
||||
const runtimeCodegen = await generateDashboardRuntimeCodegen(payload.prompt);
|
||||
|
||||
return Response.json({
|
||||
prompt: payload.prompt,
|
||||
projectId: payload.projectId,
|
||||
runtimeCodegen,
|
||||
});
|
||||
}
|
||||
@ -12,6 +12,7 @@ import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/
|
||||
import Image from "next/image";
|
||||
import React, { memo, useEffect, useMemo } from "react";
|
||||
import { AIChatPreview } from "./commands/ask-ai";
|
||||
import { CreateDashboardPreview } from "./commands/create-dashboard/create-dashboard-preview";
|
||||
import { RunQueryPreview } from "./commands/run-query";
|
||||
|
||||
export type CmdKPreviewProps = {
|
||||
@ -31,38 +32,6 @@ export type CmdKPreviewProps = {
|
||||
pathname: string,
|
||||
};
|
||||
|
||||
// Create Dashboard Preview Component - shows a TODO message for now
|
||||
const CreateDashboardPreview = memo(function CreateDashboardPreview({
|
||||
query,
|
||||
}: CmdKPreviewProps) {
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full items-center justify-center p-6">
|
||||
<div className="flex flex-col items-center gap-4 max-w-md text-center">
|
||||
<div className="w-16 h-16 rounded-2xl bg-cyan-500/10 flex items-center justify-center">
|
||||
<LayoutIcon className="h-8 w-8 text-cyan-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-foreground mb-2">Create Dashboard</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Generate custom dashboards for your users.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full p-4 rounded-xl bg-cyan-500/5 border border-cyan-500/20">
|
||||
<p className="text-xs text-cyan-600 dark:text-cyan-400 font-medium mb-2">Your query:</p>
|
||||
<p className="text-sm text-foreground italic">“{query}”</p>
|
||||
</div>
|
||||
<div className="mt-4 p-4 rounded-xl bg-muted/50 border border-border">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
🚧 <span className="font-medium">Coming Soon</span> — This feature is under development.
|
||||
Soon you'll be able to create custom dashboards like “analytics overview”,
|
||||
“user management panel”, or “team activity feed”.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// Available App Preview Component - shows app store page in preview panel
|
||||
const AvailableAppPreview = memo(function AvailableAppPreview({
|
||||
appId,
|
||||
|
||||
@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui";
|
||||
import { useDebouncedAction } from "@/hooks/use-debounced-action";
|
||||
import {
|
||||
CreateDashboardResponseSchema,
|
||||
CreateDashboardResponse,
|
||||
} from "@/lib/ai-dashboard/contracts";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { memo, useCallback, useMemo, useState } from "react";
|
||||
import { CmdKPreviewProps } from "../../cmdk-commands";
|
||||
import { DashboardSandboxHost } from "./dashboard-sandbox-host";
|
||||
import { useProjectId } from "@/app/(main)/(protected)/projects/[projectId]/use-admin-app";
|
||||
|
||||
type GenerationState = "idle" | "generating" | "ready" | "error";
|
||||
|
||||
export function CreateDashboardPreview({ query, ...rest }: CmdKPreviewProps) {
|
||||
return <CreateDashboardPreviewInner key={query} query={query} {...rest} />;
|
||||
}
|
||||
|
||||
const CreateDashboardPreviewInner = memo(function CreateDashboardPreviewInner({
|
||||
query,
|
||||
}: CmdKPreviewProps) {
|
||||
const projectId = useProjectId();
|
||||
const prompt = query.trim();
|
||||
|
||||
const [state, setState] = useState<GenerationState>("idle");
|
||||
const [errorText, setErrorText] = useState<string | null>(null);
|
||||
const [artifact, setArtifact] = useState<CreateDashboardResponse | null>(null);
|
||||
|
||||
const generateDashboard = useCallback(async () => {
|
||||
if (!projectId || !prompt) {
|
||||
return;
|
||||
}
|
||||
setState("generating");
|
||||
setErrorText(null);
|
||||
setArtifact(null);
|
||||
|
||||
const response = await fetch("/api/create-dashboard", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
projectId,
|
||||
prompt,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
setState("error");
|
||||
setErrorText(responseText || `Request failed with status ${response.status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
const parsed = CreateDashboardResponseSchema.safeParse(json);
|
||||
if (!parsed.success) {
|
||||
setState("error");
|
||||
setErrorText(`Failed to parse generation response: ${parsed.error.issues[0]?.message ?? "Unknown error"}`);
|
||||
return;
|
||||
}
|
||||
setArtifact(parsed.data);
|
||||
setState("ready");
|
||||
}, [projectId, prompt]);
|
||||
|
||||
useDebouncedAction({
|
||||
action: generateDashboard,
|
||||
delayMs: 500,
|
||||
skip: !projectId || !prompt,
|
||||
});
|
||||
|
||||
if (!prompt) {
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full items-center justify-center p-6 text-center">
|
||||
<h3 className="text-base font-semibold text-foreground">Create Dashboard</h3>
|
||||
<p className="text-xs text-muted-foreground mt-1">Describe the dashboard you want and we will generate it in a sandbox.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="px-3 py-2 border-b border-foreground/[0.08] space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[12px] font-medium text-foreground">Create Dashboard</div>
|
||||
<div className="text-[10px] text-muted-foreground truncate">{prompt}</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={state === "generating"}
|
||||
onClick={async () => {
|
||||
await generateDashboard();
|
||||
}}
|
||||
>
|
||||
{state === "generating" ? "Generating..." : "Regenerate"}
|
||||
</Button>
|
||||
</div>
|
||||
{state === "error" && errorText && (
|
||||
<div className={cn("rounded-md border px-2 py-1.5 text-[10px]", "border-red-500/30 bg-red-500/10 text-red-200")}>
|
||||
{errorText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 p-2">
|
||||
{state === "generating" && (
|
||||
<div className="flex h-full items-center justify-center text-xs text-muted-foreground">Generating dashboard...</div>
|
||||
)}
|
||||
{state !== "generating" && artifact && (
|
||||
<DashboardSandboxHost artifact={artifact} />
|
||||
)}
|
||||
{state !== "generating" && !artifact && state !== "error" && (
|
||||
<div className="flex h-full items-center justify-center text-xs text-muted-foreground">Waiting for generation...</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@ -0,0 +1,497 @@
|
||||
"use client";
|
||||
|
||||
import { DashboardRuntimeCodegen } from "@/lib/ai-dashboard/contracts";
|
||||
import { getPublicEnvVar } from "@/lib/env";
|
||||
import { useUser } from "@stackframe/stack";
|
||||
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
type DashboardArtifact = {
|
||||
prompt: string,
|
||||
projectId: string,
|
||||
runtimeCodegen: DashboardRuntimeCodegen,
|
||||
};
|
||||
|
||||
function getSandboxDocument(artifact: DashboardArtifact, baseUrl: string): string {
|
||||
const sourceCode = artifact.runtimeCodegen.uiRuntimeSourceCode;
|
||||
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' https://unpkg.com https://cdn.jsdelivr.net https://cdn.tailwindcss.com https://esm.sh https://js.stripe.com; style-src 'unsafe-inline' https://cdn.jsdelivr.net; img-src data:; connect-src ${baseUrl} https://unpkg.com https://cdn.jsdelivr.net https://esm.sh https://api.stripe.com https://m.stripe.com https://m.stripe.network; font-src 'none'; frame-src https://js.stripe.com https://hooks.stripe.com https://m.stripe.network; worker-src 'none';" />
|
||||
|
||||
<!-- Tailwind CSS Play CDN (for on-the-fly processing) -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(240 3.7% 15.9%)',
|
||||
input: 'hsl(240 3.7% 15.9%)',
|
||||
ring: 'hsl(240 4.9% 83.9%)',
|
||||
background: 'hsl(240 10% 3.9%)',
|
||||
foreground: 'hsl(0 0% 98%)',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(0 0% 98%)',
|
||||
foreground: 'hsl(240 5.9% 10%)',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(240 3.7% 15.9%)',
|
||||
foreground: 'hsl(0 0% 98%)',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(0 62.8% 30.6%)',
|
||||
foreground: 'hsl(0 0% 98%)',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(240 3.7% 15.9%)',
|
||||
foreground: 'hsl(240 5% 64.9%)',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(240 3.7% 15.9%)',
|
||||
foreground: 'hsl(0 0% 98%)',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(240 10% 3.9%)',
|
||||
foreground: 'hsl(0 0% 98%)',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow-x: hidden; font-family: Inter, system-ui, -apple-system, Segoe UI, sans-serif; background: #0b0b0f; color: #f3f4f6; }
|
||||
#root { width: 100%; height: 100%; overflow-x: hidden; }
|
||||
* { box-sizing: border-box; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<!-- React, ReactDOM, and Babel -->
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/prop-types@15.8.1/prop-types.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-is@18/umd/react-is.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
|
||||
<!-- Recharts (via CDN) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/recharts@2.15.4/umd/Recharts.js"></script>
|
||||
|
||||
<!-- Stack SDK (via esm.sh CDN) -->
|
||||
<script type="module">
|
||||
import * as StackSDK from 'https://esm.sh/@stackframe/stack@2.8.67';
|
||||
|
||||
// Expose Stack SDK globally for the Babel-transpiled code
|
||||
window.StackAdminApp = StackSDK.StackAdminApp;
|
||||
window.StackServerApp = StackSDK.StackServerApp;
|
||||
window.StackSDK = StackSDK;
|
||||
|
||||
// Signal that SDK is loaded
|
||||
window.__stackSdkReady = true;
|
||||
window.dispatchEvent(new Event('stack-sdk-ready'));
|
||||
</script>
|
||||
|
||||
<!-- UUID utility (via esm.sh CDN) -->
|
||||
<script type="module">
|
||||
import { generateUuid } from 'https://esm.sh/@stackframe/stack-shared@2.8.67/dist/utils/uuids';
|
||||
window.generateUuid = generateUuid;
|
||||
</script>
|
||||
|
||||
<script type="text/babel">
|
||||
// Stack Server App config (no embedded token - fetched via postMessage)
|
||||
const STACK_CONFIG = {
|
||||
baseUrl: ${JSON.stringify(baseUrl)},
|
||||
projectId: ${JSON.stringify(artifact.projectId)},
|
||||
};
|
||||
|
||||
const Recharts = window.Recharts;
|
||||
if (!Recharts) {
|
||||
throw new Error("Recharts failed to load in sandbox. Check CDN dependencies.");
|
||||
}
|
||||
|
||||
async function requestAccessToken() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestId = window.generateUuid();
|
||||
const timeout = setTimeout(() => {
|
||||
window.removeEventListener('message', handler);
|
||||
reject(new Error('Token request timeout'));
|
||||
}, 5000);
|
||||
|
||||
const handler = (event) => {
|
||||
if (event.data?.type === 'stack-access-token-response' && event.data?.requestId === requestId) {
|
||||
clearTimeout(timeout);
|
||||
window.removeEventListener('message', handler);
|
||||
|
||||
if (event.data.accessToken) {
|
||||
resolve(event.data.accessToken);
|
||||
} else {
|
||||
reject(new Error('No access token received from parent'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handler);
|
||||
window.parent.postMessage({
|
||||
type: 'stack-access-token-request',
|
||||
requestId
|
||||
}, '*');
|
||||
});
|
||||
}
|
||||
|
||||
async function initializeStackApp() {
|
||||
if (!window.__stackSdkReady) {
|
||||
await new Promise(resolve => {
|
||||
window.addEventListener('stack-sdk-ready', resolve, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (!window.StackAdminApp) {
|
||||
throw new Error("Stack SDK failed to load. The SDK should expose window.StackAdminApp.");
|
||||
}
|
||||
|
||||
const stackServerApp = new window.StackAdminApp({
|
||||
projectId: STACK_CONFIG.projectId,
|
||||
baseUrl: STACK_CONFIG.baseUrl,
|
||||
projectOwnerSession: async () => {
|
||||
return await requestAccessToken();
|
||||
},
|
||||
});
|
||||
|
||||
// Make it globally available for AI-generated code
|
||||
// Note: Variable name remains stackServerApp for compatibility, but it's a StackAdminApp instance
|
||||
window.stackServerApp = stackServerApp;
|
||||
|
||||
return stackServerApp;
|
||||
}
|
||||
|
||||
// Shadcn-style components
|
||||
const Card = ({ children, className = "", ...props }) => (
|
||||
<div className={\`rounded-lg border bg-card text-card-foreground shadow-sm \${className}\`} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const CardHeader = ({ children, className = "", ...props }) => (
|
||||
<div className={\`flex flex-col space-y-1.5 p-6 \${className}\`} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const CardTitle = ({ children, className = "", ...props }) => (
|
||||
<h3 className={\`text-2xl font-semibold leading-none tracking-tight \${className}\`} {...props}>
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
|
||||
const CardDescription = ({ children, className = "", ...props }) => (
|
||||
<p className={\`text-sm text-muted-foreground \${className}\`} {...props}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
|
||||
const CardContent = ({ children, className = "", ...props }) => (
|
||||
<div className={\`p-6 pt-0 \${className}\`} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const CardFooter = ({ children, className = "", ...props }) => (
|
||||
<div className={\`flex items-center p-6 pt-0 \${className}\`} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Button = ({ children, className = "", variant = "default", size = "default", ...props }) => {
|
||||
const variants = {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
};
|
||||
const sizes = {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
};
|
||||
return (
|
||||
<button
|
||||
className={\`inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 \${variants[variant]} \${sizes[size]} \${className}\`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const Table = ({ children, className = "", ...props }) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table className={\`w-full caption-bottom text-sm \${className}\`} {...props}>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
const TableHeader = ({ children, className = "", ...props }) => (
|
||||
<thead className={\`[&_tr]:border-b \${className}\`} {...props}>
|
||||
{children}
|
||||
</thead>
|
||||
);
|
||||
|
||||
const TableBody = ({ children, className = "", ...props }) => (
|
||||
<tbody className={\`[&_tr:last-child]:border-0 \${className}\`} {...props}>
|
||||
{children}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
const TableRow = ({ children, className = "", ...props }) => (
|
||||
<tr className={\`border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted \${className}\`} {...props}>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
|
||||
const TableHead = ({ children, className = "", ...props }) => (
|
||||
<th className={\`h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 \${className}\`} {...props}>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
|
||||
const TableCell = ({ children, className = "", ...props }) => (
|
||||
<td className={\`p-4 align-middle [&:has([role=checkbox])]:pr-0 \${className}\`} {...props}>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
|
||||
const Badge = ({ children, className = "", variant = "default", ...props }) => {
|
||||
const variants = {
|
||||
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
};
|
||||
return (
|
||||
<div className={\`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 \${variants[variant]} \${className}\`} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Skeleton = ({ className = "", ...props }) => (
|
||||
<div className={\`animate-pulse rounded-md bg-muted \${className}\`} {...props} />
|
||||
);
|
||||
|
||||
const Separator = ({ className = "", orientation = "horizontal", ...props }) => (
|
||||
<div
|
||||
className={\`shrink-0 bg-border \${orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]"} \${className}\`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
// Error Boundary Component
|
||||
class ErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
console.error('[ErrorBoundary] Caught error:', error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="p-6 text-red-500">
|
||||
<h2 className="text-xl font-bold mb-2">Dashboard Error</h2>
|
||||
<pre className="text-sm bg-red-950/20 p-4 rounded overflow-auto">
|
||||
{this.state.error?.message || 'Unknown error'}
|
||||
</pre>
|
||||
{this.state.error?.stack && (
|
||||
<pre className="text-xs bg-red-950/10 p-4 rounded overflow-auto mt-2">
|
||||
{this.state.error.stack}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
// AI-generated code will be inserted here
|
||||
${sourceCode}
|
||||
|
||||
// Boot the dashboard
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
throw new Error('Root element not found');
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
|
||||
// Initialize Stack SDK and boot the dashboard
|
||||
initializeStackApp().then(() => {
|
||||
try {
|
||||
// Dashboard should be defined by the AI-generated code
|
||||
if (typeof Dashboard !== 'function') {
|
||||
throw new Error('Dashboard component not found in generated code');
|
||||
}
|
||||
|
||||
root.render(
|
||||
<ErrorBoundary>
|
||||
<Dashboard />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
// Notify parent that sandbox is ready
|
||||
parent.postMessage({ type: "stack-ai-dashboard-ready" }, "*");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown sandbox error";
|
||||
parent.postMessage({
|
||||
type: "stack-ai-dashboard-error",
|
||||
message: message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}, "*");
|
||||
|
||||
// Render error in UI
|
||||
root.render(
|
||||
<div className="p-6 text-red-500">
|
||||
<h2 className="text-xl font-bold mb-2">Failed to load dashboard</h2>
|
||||
<pre className="text-sm bg-red-950/20 p-4 rounded">
|
||||
{message}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}).catch(error => {
|
||||
const message = error instanceof Error ? error.message : "Failed to initialize Stack SDK";
|
||||
parent.postMessage({
|
||||
type: "stack-ai-dashboard-error",
|
||||
message: message,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}, "*");
|
||||
|
||||
root.render(
|
||||
<div className="p-6 text-red-500">
|
||||
<h2 className="text-xl font-bold mb-2">Failed to initialize SDK</h2>
|
||||
<pre className="text-sm bg-red-950/20 p-4 rounded">
|
||||
{message}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
export const DashboardSandboxHost = memo(function DashboardSandboxHost({
|
||||
artifact,
|
||||
}: {
|
||||
artifact: DashboardArtifact,
|
||||
}) {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const [sandboxMessage, setSandboxMessage] = useState<string>("Waiting for sandbox...");
|
||||
const user = useUser({ or: "redirect" });
|
||||
|
||||
// Get base URL from environment (same as used by stackServerApp)
|
||||
const baseUrl = useMemo(() => {
|
||||
return getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL") ?? 'http://localhost:8102';
|
||||
}, []);
|
||||
|
||||
const srcDoc = useMemo(() => getSandboxDocument(artifact, baseUrl), [artifact, baseUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
setSandboxMessage("Waiting for sandbox...");
|
||||
const timeoutId = setTimeout(() => {
|
||||
setSandboxMessage("Sandbox loading...");
|
||||
}, 10000);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [artifact.prompt]);
|
||||
|
||||
useEffect(() => {
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
if (typeof event.data !== "object" || event.data === null) {
|
||||
return;
|
||||
}
|
||||
if (iframeRef.current?.contentWindow && event.source !== iframeRef.current.contentWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = event.data.type;
|
||||
|
||||
if (type === "stack-access-token-request") {
|
||||
const requestId = event.data.requestId;
|
||||
runAsynchronously(async () => {
|
||||
try {
|
||||
const accessToken = await user.getAccessToken();
|
||||
if (!accessToken) {
|
||||
event.source?.postMessage({
|
||||
type: 'stack-access-token-response',
|
||||
requestId,
|
||||
accessToken: null,
|
||||
}, { targetOrigin: '*' } as any);
|
||||
return;
|
||||
}
|
||||
|
||||
event.source?.postMessage({
|
||||
type: 'stack-access-token-response',
|
||||
requestId,
|
||||
accessToken,
|
||||
}, { targetOrigin: '*' } as any);
|
||||
} catch (error) {
|
||||
event.source?.postMessage({
|
||||
type: 'stack-access-token-response',
|
||||
requestId,
|
||||
accessToken: null,
|
||||
}, { targetOrigin: '*' } as any);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle sandbox ready/error messages
|
||||
if (type === "stack-ai-dashboard-ready") {
|
||||
setSandboxMessage("Sandbox ready");
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "stack-ai-dashboard-error") {
|
||||
const message = typeof event.data.message === "string" ? event.data.message : "Unknown sandbox error";
|
||||
setSandboxMessage(`Sandbox error: ${message}`);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", onMessage);
|
||||
return () => {
|
||||
window.removeEventListener("message", onMessage);
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full gap-2">
|
||||
<div className="text-[10px] text-muted-foreground/70">{sandboxMessage}</div>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title="AI Dashboard Preview"
|
||||
sandbox="allow-scripts"
|
||||
srcDoc={srcDoc}
|
||||
className="h-full w-full rounded-lg border border-foreground/[0.08] bg-[#0b0b0f]"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
29
apps/dashboard/src/lib/ai-dashboard/contracts.ts
Normal file
29
apps/dashboard/src/lib/ai-dashboard/contracts.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
// Schema for AI-generated dashboard code
|
||||
export const DashboardRuntimeCodegenSchema = z.object({
|
||||
title: z.string().min(1).max(120),
|
||||
description: z.string().min(1).max(800),
|
||||
uiRuntimeSourceCode: z.string().min(1),
|
||||
});
|
||||
|
||||
// Envelope for AI model output
|
||||
export const DashboardRuntimeCodegenEnvelopeSchema = z.object({
|
||||
runtimeCodegen: DashboardRuntimeCodegenSchema,
|
||||
});
|
||||
|
||||
// Schema for file selection step
|
||||
export const FileSelectionResponseSchema = z.object({
|
||||
selectedFiles: z.array(z.string()),
|
||||
});
|
||||
|
||||
// Backend API response schema
|
||||
export const CreateDashboardResponseSchema = z.object({
|
||||
prompt: z.string().min(1),
|
||||
projectId: z.string().min(1),
|
||||
runtimeCodegen: DashboardRuntimeCodegenSchema,
|
||||
});
|
||||
|
||||
// Type exports
|
||||
export type DashboardRuntimeCodegen = z.infer<typeof DashboardRuntimeCodegenSchema>;
|
||||
export type CreateDashboardResponse = z.infer<typeof CreateDashboardResponseSchema>;
|
||||
311
apps/dashboard/src/lib/ai-dashboard/model.ts
Normal file
311
apps/dashboard/src/lib/ai-dashboard/model.ts
Normal file
@ -0,0 +1,311 @@
|
||||
import { BUNDLED_TYPE_DEFINITIONS } from "@/generated/bundled-type-definitions";
|
||||
import { createAnthropic } from "@ai-sdk/anthropic";
|
||||
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
|
||||
import { generateText, Output } from "ai";
|
||||
import { DashboardRuntimeCodegen, DashboardRuntimeCodegenEnvelopeSchema, FileSelectionResponseSchema } from "./contracts";
|
||||
|
||||
|
||||
const anthropic = createAnthropic({
|
||||
apiKey: getEnvVariable("STACK_ANTHROPIC_API_KEY", "MISSING_ANTHROPIC_API_KEY"),
|
||||
});
|
||||
|
||||
|
||||
const RUNTIME_CODEGEN_SYSTEM_PROMPT = `
|
||||
[IDENTITY]
|
||||
You are an analytics dashboard generator. You answer the user’s question with a focused, minimal dashboard of metrics + charts.
|
||||
|
||||
Your output is used to render a real UI. Therefore: prioritize clarity, relevance, and visual explanation over text.
|
||||
────────────────────────────────────────
|
||||
CRITICAL: API ACCESS METHOD (HARD RULE)
|
||||
────────────────────────────────────────
|
||||
You MUST use the global stackServerApp instance (already initialized).
|
||||
Authentication is handled automatically - the SDK fetches access tokens from the parent window as needed.
|
||||
|
||||
You MUST NOT create a new StackServerApp or StackAdminApp instance.
|
||||
You MUST NOT use fetch() directly.
|
||||
|
||||
IMPORTANT: All Stack API calls are async and may fail. ALWAYS:
|
||||
1. Wrap API calls in try-catch blocks
|
||||
2. Set error state when calls fail
|
||||
3. Show user-friendly error messages (not technical details)
|
||||
4. Log errors to console for debugging: console.error('[Dashboard]', error)
|
||||
|
||||
Example:
|
||||
try {
|
||||
const users = await stackServerApp.listUsers({ includeAnonymous: true });
|
||||
setData(users);
|
||||
} catch (error) {
|
||||
console.error('[Dashboard] Failed to load users:', error);
|
||||
setError('Failed to load user data');
|
||||
}
|
||||
|
||||
await stackServerApp.getProject() // Admin API
|
||||
await stackServerApp.listInternalApiKeys() // Admin API
|
||||
|
||||
Violating this is a failure condition.
|
||||
|
||||
────────────────────────────────────────
|
||||
PRIMARY OBJECTIVE
|
||||
────────────────────────────────────────
|
||||
Build a dashboard that directly answers THE USER’S SPECIFIC QUESTION.
|
||||
|
||||
A “generic analytics dashboard” is wrong.
|
||||
Every card, chart, and table must exist only because it helps answer the query.
|
||||
|
||||
────────────────────────────────────────
|
||||
RESPONSE FORMAT (HARD RULE)
|
||||
────────────────────────────────────────
|
||||
Return JSON ONLY, matching the required schema.
|
||||
Do not include markdown. Do not include prose outside the JSON.
|
||||
|
||||
────────────────────────────────────────
|
||||
DASHBOARD REQUIREMENTS (HARD RULES)
|
||||
────────────────────────────────────────
|
||||
1) Read the user’s query carefully. Build ONLY what answers it.
|
||||
2) The dashboard MUST include at least one Recharts chart that visualizes the answer.
|
||||
- Text-only dashboards are not allowed.
|
||||
3) Keep it concise:
|
||||
- 2–4 metric cards
|
||||
- 1–2 charts
|
||||
- Optional: a small table ONLY if it adds decision-useful detail
|
||||
4) Never show technical details in the UI:
|
||||
- No API names, method names, SDK details, types, or implementation notes.
|
||||
5) Use professional, clean design:
|
||||
- Clear hierarchy, good spacing, good contrast, readable labels.
|
||||
|
||||
────────────────────────────────────────
|
||||
DEFAULT-TO-ACTION BEHAVIOR
|
||||
────────────────────────────────────────
|
||||
By default, implement the dashboard (data fetch + transformation + UI) rather than suggesting ideas.
|
||||
If the user’s intent is slightly ambiguous, infer the most useful dashboard and proceed.
|
||||
|
||||
────────────────────────────────────────
|
||||
RUNTIME CONTRACT (HARD RULES)
|
||||
────────────────────────────────────────
|
||||
- Define a React functional component named "Dashboard" (no props)
|
||||
- Use hooks via the React global object: React.useState, React.useEffect, React.useCallback
|
||||
- All shadcn components are globally available (no imports)
|
||||
- Recharts is available via the global Recharts object
|
||||
- Use stackServerApp for all Stack API calls
|
||||
|
||||
No import/export/require statements. No external networking calls.
|
||||
|
||||
────────────────────────────────────────
|
||||
CORE DATA FETCHING RULES (STACK)
|
||||
────────────────────────────────────────
|
||||
Users:
|
||||
- stackServerApp.listUsers(options?)
|
||||
- ALWAYS set includeAnonymous: true
|
||||
- Prefer limit: 500 (or higher only if clearly necessary)
|
||||
- Avoid pagination/cursor unless the UI explicitly needs it
|
||||
- Result is an array that may contain .nextCursor; treat it as an array for normal usage
|
||||
|
||||
Teams:
|
||||
- stackServerApp.listTeams(options?) → Promise<ServerTeam[]>
|
||||
|
||||
Project:
|
||||
- stackServerApp.getProject() → Promise<Project>
|
||||
|
||||
Important:
|
||||
- Use camelCase options (includeAnonymous)
|
||||
- The SDK handles auth/retries/errors; still show graceful UI states
|
||||
|
||||
────────────────────────────────────────
|
||||
CHART RULES (RECHARTS REQUIRED)
|
||||
────────────────────────────────────────
|
||||
- Every dashboard MUST include at least one chart.
|
||||
- Choose chart types that match the question:
|
||||
- Trends over time → LineChart / AreaChart
|
||||
- Comparisons/top-N → BarChart
|
||||
- Distributions → PieChart (or BarChart if many categories)
|
||||
- Always wrap charts in ResponsiveContainer.
|
||||
- Use XAxis/YAxis + Tooltip; include CartesianGrid when useful.
|
||||
- If the query is time-series, ALWAYS show a time-series chart.
|
||||
|
||||
Do not overwhelm: 1–2 charts maximum.
|
||||
|
||||
────────────────────────────────────────
|
||||
LAYOUT & DESIGN RULES (PRACTICAL)
|
||||
────────────────────────────────────────
|
||||
Use this container baseline:
|
||||
<div className="p-6 space-y-6 max-w-7xl mx-auto">
|
||||
|
||||
Header:
|
||||
- Clear title that matches the question
|
||||
- Optional Refresh button (disabled while loading)
|
||||
|
||||
Metrics:
|
||||
- 2–4 cards, grid layout:
|
||||
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
|
||||
- Big numbers:
|
||||
"text-4xl font-bold" (or "text-3xl font-bold")
|
||||
- Keep titles short. Minimize CardDescription.
|
||||
|
||||
Tables (optional):
|
||||
- Only include if it helps answer the question (e.g., top teams list, recent users)
|
||||
- Keep it small and readable. Add row hover: "hover:bg-muted/50"
|
||||
|
||||
Loading & Errors:
|
||||
- Always show Skeleton while loading
|
||||
- Disable interactions during loading
|
||||
- If an error happens, show a small, user-friendly message in the UI (non-technical)
|
||||
- Still avoid technical details (no stack traces, no method names)
|
||||
|
||||
────────────────────────────────────────
|
||||
EXAMPLES (MENTAL MODEL, NOT UI TEXT)
|
||||
────────────────────────────────────────
|
||||
Query: "how many users do I have?"
|
||||
→ Total users card, verified card, anonymous card, signup trend chart
|
||||
|
||||
Query: "what users came from oauth providers?"
|
||||
→ OAuth vs email cards, provider distribution chart (Google/GitHub/etc.)
|
||||
|
||||
Query: "show me user growth over time"
|
||||
→ Total users card, net-new in period card, growth rate card, line chart
|
||||
|
||||
Query: "which teams have the most users?"
|
||||
→ Total teams card, avg users per team card, bar chart of top teams
|
||||
|
||||
────────────────────────────────────────
|
||||
AVAILABLE SHADCN COMPONENTS (GLOBAL)
|
||||
────────────────────────────────────────
|
||||
Card:
|
||||
- <Card>, <CardHeader>, <CardTitle>, <CardDescription>, <CardContent>, <CardFooter>
|
||||
|
||||
Button:
|
||||
- <Button variant="default|destructive|outline|secondary|ghost|link" size="default|sm|lg|icon">
|
||||
|
||||
Table:
|
||||
- <Table>, <TableHeader>, <TableBody>, <TableRow>, <TableHead>, <TableCell>
|
||||
|
||||
Other:
|
||||
- <Badge variant="default|secondary|destructive|outline">
|
||||
- <Skeleton className="..." />
|
||||
- <Separator orientation="horizontal|vertical" />
|
||||
|
||||
────────────────────────────────────────
|
||||
RECHARTS (GLOBAL)
|
||||
────────────────────────────────────────
|
||||
Use via Recharts.*:
|
||||
- Recharts.LineChart, Recharts.BarChart, Recharts.AreaChart, Recharts.PieChart
|
||||
- Recharts.XAxis, Recharts.YAxis, Recharts.CartesianGrid, Recharts.Tooltip, Recharts.Legend
|
||||
- Recharts.Line, Recharts.Bar, Recharts.Area, Recharts.ResponsiveContainer
|
||||
|
||||
────────────────────────────────────────
|
||||
IMPORTANT IMPLEMENTATION NOTES (HARD RULES)
|
||||
────────────────────────────────────────
|
||||
- Always define: function Dashboard() { ... }
|
||||
- Use React.useState / React.useEffect (no imports)
|
||||
- No exports, no imports
|
||||
- No new StackServerApp(...)
|
||||
- No fetch()
|
||||
- No technical implementation text in the UI
|
||||
- Keep it minimal: short titles, big numbers, clear visuals
|
||||
|
||||
TYPE DEFINITIONS
|
||||
The typeDefinitions field contains TypeScript source defining Stack API shapes.
|
||||
Use it to determine available fields.
|
||||
Key: ServerUser.oauthProviders is readonly { id: string }[] with provider IDs like "google", "github".
|
||||
|
||||
CLICKHOUSE (queryAnalytics only)
|
||||
Available tables:
|
||||
|
||||
events:
|
||||
- event_type: LowCardinality(String) ($token-refresh only)
|
||||
- event_at: DateTime64(3, 'UTC')
|
||||
- data: JSON
|
||||
- user_id: Nullable(String)
|
||||
- team_id: Nullable(String)
|
||||
- created_at: DateTime64(3, 'UTC')
|
||||
|
||||
users (limited fields):
|
||||
- id: UUID
|
||||
- display_name: Nullable(String)
|
||||
- primary_email: Nullable(String)
|
||||
- primary_email_verified: UInt8 (0/1)
|
||||
- signed_up_at: DateTime64(3, 'UTC')
|
||||
- client_metadata: JSON
|
||||
- client_read_only_metadata: JSON
|
||||
- server_metadata: JSON
|
||||
- is_anonymous: UInt8 (0/1)
|
||||
|
||||
GENERAL
|
||||
- Keep code practical and deterministic
|
||||
- Prefer robust null checks and safe date handling
|
||||
- Write clean JSX with proper indentation
|
||||
- Use semantic HTML and ARIA where appropriate
|
||||
`;
|
||||
|
||||
async function selectRelevantFiles(prompt: string, availableFiles: string[]): Promise<string[]> {
|
||||
const result = await generateText({
|
||||
model: anthropic("claude-haiku-4-5"),
|
||||
system: `You are a code assistant helping to generate dashboard code for Stack Auth.
|
||||
|
||||
Your task is to select which Stack SDK type definition files you'll need to generate the requested dashboard.
|
||||
|
||||
IMPORTANT GUIDELINES:
|
||||
- DO NOT be conservative in file selection - when in doubt, INCLUDE the file
|
||||
- If a file might be relevant to the dashboard, SELECT IT
|
||||
- For user/team dashboards: select users and/or teams files
|
||||
- For project info: select projects files
|
||||
- Always select server-app.ts as it contains the main SDK interface
|
||||
- It's better to include extra files than to miss necessary types
|
||||
|
||||
Available files:
|
||||
${availableFiles.map(f => `- ${f}`).join('\n')}`,
|
||||
prompt: `Dashboard request: "${prompt}"
|
||||
|
||||
Which type definition files do you need? When uncertain, err on the side of INCLUDING more files rather than fewer.`,
|
||||
output: Output.object({
|
||||
schema: FileSelectionResponseSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
return result.output.selectedFiles;
|
||||
}
|
||||
|
||||
|
||||
function loadSelectedTypeDefinitions(selectedFiles: string[]): string {
|
||||
const fileContents = selectedFiles.map((relativePath: string) => {
|
||||
const file = BUNDLED_TYPE_DEFINITIONS.find((f: {path: string}) => f.path === relativePath);
|
||||
if (!file) {
|
||||
throw new Error(`Type definition file not found in bundle: ${relativePath}`);
|
||||
}
|
||||
return `
|
||||
=== ${relativePath} ===
|
||||
${file.content}
|
||||
`;
|
||||
});
|
||||
|
||||
return `
|
||||
Complete Stack Auth SDK Type Definitions (Selected Files):
|
||||
These files show the available methods, types, and interfaces for the Stack SDK.
|
||||
${fileContents.join('\n')}
|
||||
`.trim();
|
||||
}
|
||||
|
||||
export async function generateDashboardRuntimeCodegen(prompt: string): Promise<DashboardRuntimeCodegen> {
|
||||
// Step 1: Get available file paths from bundled definitions
|
||||
const availableFiles = BUNDLED_TYPE_DEFINITIONS.map((f: {path: string}) => f.path);
|
||||
|
||||
// Step 2: Ask Claude which files it needs
|
||||
const selectedFiles = await selectRelevantFiles(prompt, availableFiles);
|
||||
|
||||
// Step 3: Load only the selected files from bundle
|
||||
const typeDefinitions = loadSelectedTypeDefinitions(selectedFiles);
|
||||
|
||||
// Step 4: Generate the dashboard with selected type definitions
|
||||
const result = await generateText({
|
||||
model: anthropic("claude-haiku-4-5"),
|
||||
system: RUNTIME_CODEGEN_SYSTEM_PROMPT,
|
||||
prompt: JSON.stringify({
|
||||
prompt: prompt,
|
||||
typeDefinitions,
|
||||
}),
|
||||
output: Output.object({
|
||||
schema: DashboardRuntimeCodegenEnvelopeSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
return result.output.runtimeCodegen;
|
||||
}
|
||||
@ -340,6 +340,9 @@ importers:
|
||||
|
||||
apps/dashboard:
|
||||
dependencies:
|
||||
'@ai-sdk/anthropic':
|
||||
specifier: ^3.0.41
|
||||
version: 3.0.44(zod@4.1.12)
|
||||
'@ai-sdk/openai':
|
||||
specifier: ^3.0.25
|
||||
version: 3.0.27(zod@4.1.12)
|
||||
@ -2225,6 +2228,12 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@ai-sdk/anthropic@3.0.44':
|
||||
resolution: {integrity: sha512-ke1NldgohWJ7sWLqm9Um9TVIOrtg8Y8AecWeB6PgaLt+paTPisAsyNfe8FNOVusuv58ugLBqY/78AkhUmbjXHA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/gateway@3.0.41':
|
||||
resolution: {integrity: sha512-dYNhtvEomccNNGSxfSP8f4g6yPcoDHyQ6Rb7dALFE0FvvVP9UqfFWi3D2dLIz0VVKaSkiNLQAJ7lsdTVlBdRrw==}
|
||||
engines: {node: '>=18'}
|
||||
@ -2255,6 +2264,12 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider-utils@4.0.15':
|
||||
resolution: {integrity: sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider@1.1.3':
|
||||
resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -16066,6 +16081,12 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@ai-sdk/anthropic@3.0.44(zod@4.1.12)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 3.0.8
|
||||
'@ai-sdk/provider-utils': 4.0.15(zod@4.1.12)
|
||||
zod: 4.1.12
|
||||
|
||||
'@ai-sdk/gateway@3.0.41(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 3.0.8
|
||||
@ -16113,6 +16134,13 @@ snapshots:
|
||||
eventsource-parser: 3.0.6
|
||||
zod: 4.1.12
|
||||
|
||||
'@ai-sdk/provider-utils@4.0.15(zod@4.1.12)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 3.0.8
|
||||
'@standard-schema/spec': 1.1.0
|
||||
eventsource-parser: 3.0.6
|
||||
zod: 4.1.12
|
||||
|
||||
'@ai-sdk/provider@1.1.3':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
@ -26627,7 +26655,7 @@ snapshots:
|
||||
|
||||
effect@3.18.4:
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.0.0
|
||||
'@standard-schema/spec': 1.1.0
|
||||
fast-check: 3.23.2
|
||||
|
||||
electron-to-chromium@1.4.803: {}
|
||||
|
||||
@ -9,6 +9,7 @@ packages:
|
||||
minimumReleaseAge: 2880
|
||||
minimumReleaseAgeExclude:
|
||||
- ai
|
||||
- '@ai-sdk/anthropic'
|
||||
- '@ai-sdk/openai'
|
||||
- '@ai-sdk/react'
|
||||
- '@ai-sdk/provider'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user