-
Notifications
You must be signed in to change notification settings - Fork 516
Custom Dashboards #1192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom Dashboards #1192
Changes from 109 commits
9e41309
3879001
937db90
3f2a8ef
dcda402
9e954b9
f0cc311
1d7a0de
8c2d1c3
409d89b
0d2b3b9
ef8f74e
8cdd107
871fe12
5d6bde2
c4ed09a
b102db3
2eeb537
c61a2b9
ac261d2
0c1a02e
79e330f
939b1a9
2974c83
0799a0b
1f67742
bd788b4
4b7dd53
044377e
744b871
74c634b
ce5a1bb
2c5440b
f726f61
665c084
85bb893
856aaf3
8f3ad45
fe393e9
b9c0ef4
b5781a1
b841f7b
2e0d8de
e5cbc1a
ba1df26
093eaf0
5192875
686a1e6
8910138
62171dc
1038d1a
3370e63
d04e944
61f2b79
bbac70e
8e92205
3b9c22e
c0a3f7a
d34a2c7
c91998e
64d9d93
7fd7886
719d1c2
43c1f15
04970c2
4abd410
8247481
362e1fb
98d451d
6b89370
ff370b6
8be639d
caa3dca
445e889
63df87a
9c340f4
45e7d2d
925a865
b83063c
b673ab5
fcf493e
8534caa
2f7acb4
cf5558e
4ab4be8
1421461
90eb9ae
afa76be
a509be3
02b6ef6
62d054e
558d0f6
0dc2e94
3db832e
3cde6fa
a2033bc
b01d17b
0fcc37c
7c6173e
416051f
8cff361
e107078
5accb8b
c52bdd1
ad6eb03
579c723
59f6998
4799b34
bb94e48
3dac1cd
805a78f
e89989e
183ce10
593cb26
7c96c52
64cc8ab
4309859
00c01b3
f764f50
aaeab15
44f4303
fc45436
90701e8
5acb916
91c6bcb
56c4951
66ba625
0b895f9
261feaf
c031124
3fa91d5
bcb2534
fad9ef4
b92a8e9
b412b68
93f7984
33d9e44
a7aa24c
6d39d36
d866df4
e418165
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,3 +34,6 @@ yarn-error.log* | |
| # typescript | ||
| *.tsbuildinfo | ||
| next-env.d.ts | ||
|
|
||
| # Generated files | ||
| src/generated | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,11 +4,14 @@ | |
| "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", | ||
| "predev": "pnpm run bundle-type-definitions", | ||
| "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 +20,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", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| 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 | ||
|
|
||
| export type TypeDefinitionFile = { | ||
| path: string, | ||
| content: string, | ||
| }; | ||
|
|
||
| export const BUNDLED_TYPE_DEFINITIONS: TypeDefinitionFile[] = ${JSON.stringify(bundledFiles, null, 2)}; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| `; | ||
|
|
||
| await fs.mkdir(path.dirname(outputPath), { recursive: true }); | ||
| writeFileSyncIfChanged(outputPath, output); | ||
|
aadesh18 marked this conversation as resolved.
|
||
|
|
||
| console.log(`[Bundle Type Definitions] Generated ${outputPath}`); | ||
| console.log(`[Bundle Type Definitions] Total size: ${(output.length / 1024).toFixed(2)} KB, ${bundledFiles.length} files bundled`); | ||
|
aadesh18 marked this conversation as resolved.
|
||
| } | ||
|
|
||
| main().catch((...args) => { | ||
| console.error('[Bundle Type Definitions] ERROR! Failed to bundle type definitions:', ...args); | ||
| process.exit(1); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
aadesh18 marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const user = await stackServerApp.getUser({ or: "redirect" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const payload = await requestSchema.validate(await req.json()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
aadesh18 marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const projects = await user.listOwnedProjects(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const project = projects.find((p: { id: string }) => p.id === payload.projectId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!project) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response.json({ error: "You do not own this project" }, { status: 403 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const runtimeCodegen = await generateDashboardRuntimeCodegen(payload.prompt); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prompt: payload.prompt, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| projectId: payload.projectId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runtimeCodegen, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const runtimeCodegen = await generateDashboardRuntimeCodegen(payload.prompt); | |
| return Response.json({ | |
| prompt: payload.prompt, | |
| projectId: payload.projectId, | |
| runtimeCodegen, | |
| }); | |
| try { | |
| const runtimeCodegen = await generateDashboardRuntimeCodegen(payload.prompt); | |
| return Response.json({ | |
| prompt: payload.prompt, | |
| projectId: payload.projectId, | |
| runtimeCodegen, | |
| }); | |
| } catch (error: unknown) { | |
| // Log the error for server-side diagnostics | |
| console.error("Failed to generate dashboard runtime codegen:", error); | |
| const message = | |
| error && typeof error === "object" && "message" in error | |
| ? String((error as { message?: unknown }).message ?? "Failed to generate dashboard runtime codegen.") | |
| : "Failed to generate dashboard runtime codegen."; | |
| return Response.json( | |
| { error: message }, | |
| { status: 502 }, | |
| ); | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| "use client"; | ||
|
|
||
| import { useProjectId } from "@/app/(main)/(protected)/projects/[projectId]/use-admin-app"; | ||
| import { Button } from "@/components/ui"; | ||
| import { useDebouncedAction } from "@/hooks/use-debounced-action"; | ||
| import { | ||
| CreateDashboardResponse, | ||
| CreateDashboardResponseSchema, | ||
| } from "@/lib/ai-dashboard/contracts"; | ||
| import { cn } from "@/lib/utils"; | ||
| import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; | ||
| import { memo, useCallback, useState } from "react"; | ||
| import { CmdKPreviewProps } from "../../cmdk-commands"; | ||
| import { DashboardSandboxHost } from "./dashboard-sandbox-host"; | ||
|
|
||
| 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]); | ||
|
aadesh18 marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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" | ||
|
vercel[bot] marked this conversation as resolved.
Outdated
|
||
| disabled={state === "generating"} | ||
| onClick={() => runAsynchronouslyWithAlert(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> | ||
| )} | ||
|
aadesh18 marked this conversation as resolved.
|
||
| </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> | ||
| ); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of predev, let's use
concurrentlywith a watch mode (see the backend package.json for some examples)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i removed the predev hook and switched to the same concurrently + watch pattern used in the backend