├── public ├── userflow.png └── architecture.png ├── src ├── app │ ├── favicon.ico │ ├── api │ │ ├── auth │ │ │ └── [...all] │ │ │ │ └── route.ts │ │ └── v1 │ │ │ ├── vercel │ │ │ └── teams │ │ │ │ ├── route.ts │ │ │ │ └── [teamId] │ │ │ │ └── storages │ │ │ │ └── blobs │ │ │ │ └── route.ts │ │ │ ├── vercel-sandboxes │ │ │ └── [vercelSandboxId] │ │ │ │ ├── route.ts │ │ │ │ └── ports │ │ │ │ └── route.ts │ │ │ ├── projects │ │ │ ├── [projectId] │ │ │ │ ├── tasks │ │ │ │ │ ├── [taskId] │ │ │ │ │ │ └── revisions │ │ │ │ │ │ │ ├── [revisionId] │ │ │ │ │ │ │ ├── cmd │ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ │ ├── route.ts │ │ │ │ │ │ │ └── message-stream │ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ └── git-branches │ │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ │ ├── tidbcloud-branches │ │ │ └── [branchId] │ │ │ │ └── route.ts │ │ │ ├── debug │ │ │ └── streams │ │ │ │ └── [sessionId] │ │ │ │ └── route.ts │ │ │ └── sessions │ │ │ └── route.ts │ ├── (customer) │ │ ├── settings │ │ │ ├── openai-account │ │ │ │ └── page.tsx │ │ │ ├── github-account │ │ │ │ └── page.tsx │ │ │ ├── tidbcloud-account │ │ │ │ └── page.tsx │ │ │ └── vercel-account │ │ │ │ └── page.tsx │ │ ├── s │ │ │ └── [slug] │ │ │ │ ├── message-stream-preview.tsx │ │ │ │ ├── preview-index-provider.tsx │ │ │ │ ├── reloader.tsx │ │ │ │ ├── conversation-input.tsx │ │ │ │ ├── query.ts │ │ │ │ └── message-preview.tsx │ │ └── layout.tsx │ ├── (auth) │ │ └── login │ │ │ └── page.tsx │ ├── providers.tsx │ ├── (main) │ │ └── projects │ │ │ ├── page.tsx │ │ │ └── [projectId] │ │ │ ├── tasks │ │ │ └── [taskId] │ │ │ │ └── page.tsx │ │ │ └── tasks-list.tsx │ └── layout.tsx ├── lib │ ├── llm │ │ └── models.ts │ ├── tidbcloud │ │ └── fetch.ts │ ├── utils.ts │ ├── vercel │ │ ├── errors.ts │ │ ├── teams.ts │ │ ├── utils.ts │ │ └── blobs.ts │ ├── tasks.ts │ ├── auth-client.ts │ ├── role.ts │ ├── db │ │ └── db.ts │ ├── user-settings │ │ ├── tidbcloud.ts │ │ ├── vercel.ts │ │ └── github.ts │ ├── auth-common.ts │ ├── system-settings.ts │ ├── errors.ts │ ├── auth.ts │ ├── async-generators.ts │ └── kysely-utils.ts ├── sandboxes │ └── scripts │ │ └── bash.ts ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── textarea.tsx │ │ ├── progress.tsx │ │ ├── collapsible.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── avatar.tsx │ │ ├── hover-card.tsx │ │ ├── badge.tsx │ │ ├── scroll-area.tsx │ │ ├── alert.tsx │ │ ├── tooltip.tsx │ │ ├── card.tsx │ │ ├── button.tsx │ │ ├── button-group.tsx │ │ ├── breadcrumb.tsx │ │ ├── empty.tsx │ │ └── table.tsx │ ├── ai-elements │ │ ├── panel.tsx │ │ ├── toolbar.tsx │ │ ├── image.tsx │ │ ├── canvas.tsx │ │ ├── controls.tsx │ │ ├── connection.tsx │ │ ├── suggestion.tsx │ │ ├── shimmer.tsx │ │ ├── checkpoint.tsx │ │ ├── sources.tsx │ │ ├── node.tsx │ │ ├── loader.tsx │ │ ├── task.tsx │ │ ├── conversation.tsx │ │ ├── edge.tsx │ │ ├── artifact.tsx │ │ └── plan.tsx │ ├── ansi-logs.tsx │ ├── session-context.tsx │ ├── vercel-default-project-team-setup.tsx │ ├── github-repo-branch-select.tsx │ ├── vercel-team-select.tsx │ ├── auto-collapse.tsx │ ├── github-account-settings.tsx │ ├── openai-api-key-setup.tsx │ ├── vercel-token-setup.tsx │ ├── terminal.tsx │ ├── tidbcloud-account-settings.tsx │ ├── vercel-blob-storage-setup.tsx │ ├── app-sidebar.tsx │ └── nav-user.tsx ├── actions │ ├── tasks.ts │ ├── auth.ts │ ├── tidbcloud.ts │ └── vercel.ts ├── hooks │ ├── use-mobile.ts │ └── use-message-session.ts └── proxy.ts ├── postcss.config.mjs ├── migrations ├── 007-better-auth.down.sql ├── 010-remove-password-field.ts ├── 009-create-role.ts ├── 002-user-prompt.ts ├── 006-openai-api-key.ts ├── 011-system-settings.ts ├── 003-multiple-coding-agent.ts ├── 008-create-accounts.ts ├── 004-task-revision-deploy.ts ├── 007-better-auth.ts ├── 005-progressive-ui-session-creation.ts ├── 001-ui-session.ts └── 007-better-auth.up.sql ├── next.config.ts ├── scripts ├── gen-password.js ├── setup-local-user.js └── migrate.ts ├── components.json ├── .gitignore ├── tsconfig.json ├── biome.json ├── package.json └── AGENTS.md /public/userflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingcap/full-stack-app-builder-ai-agent/HEAD/public/userflow.png -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingcap/full-stack-app-builder-ai-agent/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /public/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingcap/full-stack-app-builder-ai-agent/HEAD/public/architecture.png -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /src/lib/llm/models.ts: -------------------------------------------------------------------------------- 1 | import { createOpenAI } from "@ai-sdk/openai"; 2 | 3 | export const openai = createOpenAI({ 4 | apiKey: process.env.OPENAI_API_KEY!, 5 | }); 6 | -------------------------------------------------------------------------------- /src/lib/tidbcloud/fetch.ts: -------------------------------------------------------------------------------- 1 | import { wrapFetchWithDigestFlow } from "@/lib/tidbcloud/digest-auth"; 2 | 3 | export const tidbCloudFetch = wrapFetchWithDigestFlow(fetch); 4 | -------------------------------------------------------------------------------- /src/sandboxes/scripts/bash.ts: -------------------------------------------------------------------------------- 1 | export async function bash(temp: TemplateStringsArray, ...args: any[]) { 2 | return ""; 3 | } 4 | 5 | bash` 6 | set -e ${123} 7 | `; 8 | -------------------------------------------------------------------------------- /src/app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { toNextJsHandler } from "better-auth/next-js"; 2 | import { auth } from "@/lib/auth"; 3 | 4 | export const { POST, GET } = toNextJsHandler(auth); 5 | -------------------------------------------------------------------------------- /src/app/api/v1/vercel/teams/route.ts: -------------------------------------------------------------------------------- 1 | import { listTeams } from "@/lib/vercel/teams"; 2 | 3 | export async function GET() { 4 | const teams = await listTeams(); 5 | 6 | return Response.json(teams); 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/vercel/errors.ts: -------------------------------------------------------------------------------- 1 | export async function handleVercelApiCallError( 2 | response: Response | Promise, 3 | ) { 4 | response = await response; 5 | if (!response.ok) { 6 | throw new Error(`${response.status} ${await response.text()}`); 7 | } 8 | 9 | return response; 10 | } 11 | -------------------------------------------------------------------------------- /migrations/007-better-auth.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `verification`; 2 | DROP TABLE `account`; 3 | DROP TABLE `session`; 4 | 5 | ALTER TABLE `user` 6 | DROP COLUMN `updated_at` ; 7 | 8 | ALTER TABLE `user` 9 | DROP COLUMN `created_at` ; 10 | 11 | ALTER TABLE `user` 12 | DROP COLUMN `email_verified` ; 13 | -------------------------------------------------------------------------------- /src/lib/tasks.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from "node:crypto"; 2 | 3 | export function generateSessionId( 4 | projectId: number, 5 | taskId: number, 6 | revisionId: number, 7 | ) { 8 | return createHash("md5") 9 | .update(`projects/${projectId}/tasks/${taskId}/revisions/${revisionId}`) 10 | .digest("hex"); 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/auth-client.ts: -------------------------------------------------------------------------------- 1 | import { type BetterAuthClientOptions, InferAuth } from "better-auth/client"; 2 | import { createAuthClient } from "better-auth/react"; 3 | import type { auth } from "@/lib/auth"; 4 | 5 | export const authClient = createAuthClient({ 6 | $InferAuth: InferAuth(), 7 | } as const satisfies BetterAuthClientOptions); 8 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | reactCompiler: true, 6 | outputFileTracingIncludes: { 7 | "": ["./src/migrations/*.ts"], 8 | }, 9 | experimental: { 10 | authInterrupts: true, 11 | }, 12 | }; 13 | 14 | export default nextConfig; 15 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ); 11 | } 12 | 13 | export { Skeleton }; 14 | -------------------------------------------------------------------------------- /src/lib/role.ts: -------------------------------------------------------------------------------- 1 | export type Role = 'admin' | 'user'; 2 | 3 | export function parseRole (role: string): Role { 4 | switch (role) { 5 | case 'admin': 6 | return 'admin'; 7 | case 'user': 8 | return 'user'; 9 | default: 10 | return 'user'; 11 | } 12 | } 13 | 14 | export function formatRole (role: Role) { 15 | return role; 16 | } 17 | -------------------------------------------------------------------------------- /scripts/gen-password.js: -------------------------------------------------------------------------------- 1 | import { hash } from "bcrypt"; 2 | import readline from "readline/promises"; 3 | 4 | const rl = new readline.createInterface({ 5 | input: process.stdin, 6 | output: process.stdout, 7 | }); 8 | 9 | const password = await rl.question("new password> "); 10 | 11 | const res = await hash(password, process.env.BCRYPT_SALT); 12 | 13 | console.log(res); 14 | 15 | rl.close(); 16 | -------------------------------------------------------------------------------- /migrations/010-remove-password-field.ts: -------------------------------------------------------------------------------- 1 | import type { Kysely } from "kysely"; 2 | 3 | export async function up(db: Kysely) { 4 | await db.schema.alterTable("user").dropColumn("password").execute(); 5 | } 6 | 7 | export async function down(db: Kysely) { 8 | await db.schema 9 | .alterTable("user") 10 | .addColumn("password", "char(72)", (col) => col.notNull()) 11 | .execute(); 12 | } 13 | -------------------------------------------------------------------------------- /migrations/009-create-role.ts: -------------------------------------------------------------------------------- 1 | import type { Kysely } from "kysely"; 2 | 3 | export async function up(db: Kysely) { 4 | await db.schema 5 | .alterTable("user") 6 | .addColumn("role", "varchar(255)", (col) => col.notNull().defaultTo("user")) 7 | .execute(); 8 | } 9 | 10 | export async function down(db: Kysely) { 11 | await db.schema.alterTable("user").dropColumn("role").execute(); 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/db/db.ts: -------------------------------------------------------------------------------- 1 | import { Kysely, MysqlDialect } from "kysely"; 2 | import { createPool } from "mysql2"; 3 | import type { DB } from "@/lib/db/schema"; 4 | 5 | const db = new Kysely({ 6 | dialect: new MysqlDialect({ 7 | pool: createPool({ 8 | uri: process.env.DATABASE_URL!, 9 | ssl: { 10 | rejectUnauthorized: true, 11 | }, 12 | }), 13 | }), 14 | }); 15 | 16 | export default db; 17 | -------------------------------------------------------------------------------- /src/actions/tasks.ts: -------------------------------------------------------------------------------- 1 | import db from "@/lib/db/db"; 2 | import type { DB } from "@/lib/db/schema"; 3 | import { insert } from "@/lib/kysely-utils"; 4 | import type { Insertable } from "kysely"; 5 | 6 | type CreateTaskParams = Omit, "id">; 7 | 8 | export async function createTask(params: CreateTaskParams) { 9 | const task = await insert(db, "task", { 10 | ...params, 11 | }); 12 | 13 | return task; 14 | } 15 | -------------------------------------------------------------------------------- /migrations/002-user-prompt.ts: -------------------------------------------------------------------------------- 1 | import type { Kysely } from "kysely"; 2 | 3 | export async function up(db: Kysely) { 4 | await db.schema 5 | .alterTable("task_revision") 6 | .addColumn("user_prompt", "text", (col) => col.notNull()) 7 | .execute(); 8 | } 9 | 10 | export async function down(db: Kysely) { 11 | await db.schema 12 | .alterTable("task_revision") 13 | .dropColumn("user_prompt") 14 | .execute(); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/api/v1/vercel/teams/[teamId]/storages/blobs/route.ts: -------------------------------------------------------------------------------- 1 | import { listBlobStorages } from "@/lib/vercel/blobs"; 2 | import { type NextRequest, NextResponse } from "next/server"; 3 | 4 | export async function GET( 5 | request: NextRequest, 6 | { params }: { params: Promise<{ teamId: string }> }, 7 | ) { 8 | const teamId = decodeURIComponent((await params).teamId); 9 | 10 | return NextResponse.json(await listBlobStorages(teamId)); 11 | } 12 | -------------------------------------------------------------------------------- /migrations/006-openai-api-key.ts: -------------------------------------------------------------------------------- 1 | import type { Kysely } from "kysely"; 2 | 3 | export async function up(db: Kysely) { 4 | await db.schema 5 | .alterTable("user_setting") 6 | .addColumn("openai_api_key", "varchar(255)", (col) => col) 7 | .execute(); 8 | } 9 | 10 | export async function down(db: Kysely) { 11 | await db.schema 12 | .alterTable("user_setting") 13 | .dropColumn("openai_api_key") 14 | .execute(); 15 | } 16 | -------------------------------------------------------------------------------- /migrations/011-system-settings.ts: -------------------------------------------------------------------------------- 1 | import type { Kysely } from "kysely"; 2 | 3 | export async function up(db: Kysely) { 4 | await db.schema 5 | .createTable("system_settings") 6 | .addColumn("key", "varchar(255)", (col) => col.primaryKey().notNull()) 7 | .addColumn("value", "json", (col) => col.notNull()) 8 | .execute(); 9 | } 10 | 11 | export async function down(db: Kysely) { 12 | await db.schema.dropTable("system_settings").execute(); 13 | } 14 | -------------------------------------------------------------------------------- /src/app/api/v1/vercel-sandboxes/[vercelSandboxId]/route.ts: -------------------------------------------------------------------------------- 1 | import db from "@/lib/db/db"; 2 | import { get } from "@/lib/kysely-utils"; 3 | import { type NextRequest, NextResponse } from "next/server"; 4 | 5 | export async function GET( 6 | request: NextRequest, 7 | { params }: { params: Promise<{ vercelSandboxId: string }> }, 8 | ) { 9 | const id = decodeURIComponent((await params).vercelSandboxId); 10 | return NextResponse.json(await get(db, "vercel_sandbox", { id })); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/ai-elements/panel.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { Panel as PanelPrimitive } from "@xyflow/react"; 3 | import type { ComponentProps } from "react"; 4 | 5 | type PanelProps = ComponentProps; 6 | 7 | export const Panel = ({ className, ...props }: PanelProps) => ( 8 | 15 | ); 16 | -------------------------------------------------------------------------------- /src/app/api/v1/projects/[projectId]/tasks/[taskId]/revisions/[revisionId]/cmd/route.ts: -------------------------------------------------------------------------------- 1 | import { getTaskRevisionCommandStatus } from "@/actions/task-revisions"; 2 | import { type NextRequest, NextResponse } from "next/server"; 3 | 4 | export async function GET( 5 | request: NextRequest, 6 | { params }: { params: Promise<{ revisionId: string }> }, 7 | ) { 8 | const revisionId = parseInt(decodeURIComponent((await params).revisionId)); 9 | return NextResponse.json(await getTaskRevisionCommandStatus(revisionId)); 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/vercel/teams.ts: -------------------------------------------------------------------------------- 1 | import { getVercelClient } from "@/lib/user-settings/vercel"; 2 | import { 3 | getAllPages, 4 | getValidSessionUserVercelToken, 5 | } from "@/lib/vercel/utils"; 6 | 7 | export async function listTeams() { 8 | const token = await getValidSessionUserVercelToken(); 9 | if (!token) { 10 | throw new Error("No valid session user vercel token found"); 11 | } 12 | 13 | const client = getVercelClient(token); 14 | 15 | return await getAllPages((params) => client.teams.getTeams(params), "teams"); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/ai-elements/toolbar.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { NodeToolbar, Position } from "@xyflow/react"; 3 | import type { ComponentProps } from "react"; 4 | 5 | type ToolbarProps = ComponentProps; 6 | 7 | export const Toolbar = ({ className, ...props }: ToolbarProps) => ( 8 | 16 | ); 17 | -------------------------------------------------------------------------------- /migrations/003-multiple-coding-agent.ts: -------------------------------------------------------------------------------- 1 | import type { Kysely } from "kysely"; 2 | 3 | export async function up(db: Kysely) { 4 | await db.schema 5 | .alterTable("project") 6 | .addColumn("coding_agent_type", "varchar(255)", (col) => col.notNull()) 7 | .execute(); 8 | 9 | await db.updateTable("project").set("coding_agent_type", "codex").execute(); 10 | } 11 | 12 | export async function down(db: Kysely) { 13 | await db.schema 14 | .alterTable("project") 15 | .dropColumn("coding_agent_type") 16 | .execute(); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/api/v1/tidbcloud-branches/[branchId]/route.ts: -------------------------------------------------------------------------------- 1 | import db from "@/lib/db/db"; 2 | import { get, omit } from "@/lib/kysely-utils"; 3 | import { type NextRequest, NextResponse } from "next/server"; 4 | 5 | export async function GET( 6 | request: NextRequest, 7 | { params }: { params: Promise<{ branchId: string }> }, 8 | ) { 9 | const branchId = decodeURIComponent((await params).branchId); 10 | return NextResponse.json( 11 | omit(await get(db, "tidbcloud_branch", { id: branchId }), [ 12 | "connection_url", 13 | ]), 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/ai-elements/image.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import type { Experimental_GeneratedImage } from "ai"; 3 | 4 | export type ImageProps = Experimental_GeneratedImage & { 5 | className?: string; 6 | alt?: string; 7 | }; 8 | 9 | export const Image = ({ 10 | base64, 11 | uint8Array, 12 | mediaType, 13 | ...props 14 | }: ImageProps) => ( 15 | {props.alt} 24 | ); 25 | -------------------------------------------------------------------------------- /src/lib/user-settings/tidbcloud.ts: -------------------------------------------------------------------------------- 1 | import type { SiteSettings } from '@/lib/system-settings'; 2 | 3 | export function isTiDBCloudSettingsValid ( 4 | settings: SiteSettings | undefined | null, 5 | ): settings is SiteSettings & { 6 | tidbcloud_public_key: string; 7 | tidbcloud_private_key: string; 8 | tidbcloud_organization_id: string; 9 | tidbcloud_project_id: string; 10 | } { 11 | return ( 12 | settings?.tidbcloud_public_key != null && 13 | settings.tidbcloud_private_key != null && 14 | settings.tidbcloud_organization_id != null && 15 | settings.tidbcloud_project_id != null 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "iconLibrary": "lucide", 14 | "aliases": { 15 | "components": "@/components", 16 | "utils": "@/lib/utils", 17 | "ui": "@/components/ui", 18 | "lib": "@/lib", 19 | "hooks": "@/hooks" 20 | }, 21 | "registries": { 22 | "@ai-elements": "https://registry.ai-sdk.dev/{name}.json" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/(customer)/settings/openai-account/page.tsx: -------------------------------------------------------------------------------- 1 | import { validateOpenaiApiKey } from "@/actions/user-settings"; 2 | import { OpenaiApiKeySetup } from "@/components/openai-api-key-setup"; 3 | 4 | import { getSiteSettings } from "@/lib/system-settings"; 5 | 6 | export default async function Page() { 7 | const settings = await getSiteSettings(); 8 | const result = await validateOpenaiApiKey(undefined); 9 | 10 | return ( 11 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/(customer)/settings/github-account/page.tsx: -------------------------------------------------------------------------------- 1 | import { validateGitHubToken } from "@/actions/user-settings"; 2 | import { GithubAccountSettings } from "@/components/github-account-settings"; 3 | 4 | import { getSiteSettings } from "@/lib/system-settings"; 5 | 6 | export default async function Page() { 7 | const settings = await getSiteSettings(); 8 | const result = await validateGitHubToken(undefined); 9 | 10 | return ( 11 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/ai-elements/canvas.tsx: -------------------------------------------------------------------------------- 1 | import { Background, ReactFlow, type ReactFlowProps } from "@xyflow/react"; 2 | import type { ReactNode } from "react"; 3 | import "@xyflow/react/dist/style.css"; 4 | 5 | type CanvasProps = ReactFlowProps & { 6 | children?: ReactNode; 7 | }; 8 | 9 | export const Canvas = ({ children, ...props }: CanvasProps) => ( 10 | 19 | 20 | {children} 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /src/app/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from "@/components/login-form"; 2 | 3 | export default async function LoginPage({ 4 | searchParams, 5 | }: { 6 | searchParams?: Promise<{ error?: string | string[]; callbackUrl?: string }>; 7 | }) { 8 | const { error, callbackUrl } = (await searchParams) ?? {}; 9 | return ( 10 |
11 |
12 | 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/ai-elements/controls.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { Controls as ControlsPrimitive } from "@xyflow/react"; 5 | import type { ComponentProps } from "react"; 6 | 7 | export type ControlsProps = ComponentProps; 8 | 9 | export const Controls = ({ className, ...props }: ControlsProps) => ( 10 | button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent! [&>button]:hover:bg-secondary!", 14 | className, 15 | )} 16 | {...props} 17 | /> 18 | ); 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | .env*.local 43 | -------------------------------------------------------------------------------- /src/app/api/v1/projects/[projectId]/tasks/[taskId]/revisions/[revisionId]/route.ts: -------------------------------------------------------------------------------- 1 | import db from "@/lib/db/db"; 2 | import { get } from "@/lib/kysely-utils"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function GET( 6 | request: Request, 7 | { 8 | params, 9 | }: { 10 | params: Promise<{ projectId: string; taskId: string; revisionId: string }>; 11 | }, 12 | ) { 13 | const revisionId = parseInt(decodeURIComponent((await params).revisionId)); 14 | const taskId = parseInt(decodeURIComponent((await params).taskId)); 15 | 16 | return NextResponse.json( 17 | await get(db, "task_revision", { id: revisionId, task_id: taskId }), 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState( 7 | undefined, 8 | ); 9 | 10 | React.useEffect(() => { 11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 12 | const onChange = () => { 13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 14 | }; 15 | mql.addEventListener("change", onChange); 16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 17 | return () => mql.removeEventListener("change", onChange); 18 | }, []); 19 | 20 | return !!isMobile; 21 | } 22 | -------------------------------------------------------------------------------- /migrations/008-create-accounts.ts: -------------------------------------------------------------------------------- 1 | import type { Kysely } from "kysely"; 2 | 3 | export async function up(db: Kysely) { 4 | await db.transaction().execute(async (trx) => { 5 | const users = await trx.selectFrom("user").selectAll().execute(); 6 | 7 | await trx 8 | .insertInto("account") 9 | .values( 10 | users.map((user) => ({ 11 | account_id: String(user.id), 12 | provider_id: "credential", 13 | user_id: user.id, 14 | password: user.password, 15 | created_at: user.created_at, 16 | updated_at: user.updated_at, 17 | })), 18 | ) 19 | .execute(); 20 | }); 21 | } 22 | 23 | export async function down() {} 24 | -------------------------------------------------------------------------------- /src/components/ai-elements/connection.tsx: -------------------------------------------------------------------------------- 1 | import type { ConnectionLineComponent } from "@xyflow/react"; 2 | 3 | const HALF = 0.5; 4 | 5 | export const Connection: ConnectionLineComponent = ({ 6 | fromX, 7 | fromY, 8 | toX, 9 | toY, 10 | }) => ( 11 | 12 | 19 | 27 | 28 | ); 29 | -------------------------------------------------------------------------------- /src/lib/auth-common.ts: -------------------------------------------------------------------------------- 1 | import type { InferUser } from "better-auth"; 2 | import type { auth } from "@/lib/auth"; 3 | import { formatRole, parseRole, type Role } from "@/lib/role"; 4 | 5 | export interface SessionUser 6 | extends Omit, "id" | "role"> { 7 | id: number; 8 | role: Role; 9 | } 10 | 11 | export function toSessionUser(user: InferUser): SessionUser { 12 | return { 13 | ...user, 14 | id: parseInt(user.id, 10), 15 | role: parseRole(user.role), 16 | }; 17 | } 18 | 19 | export function fromSessionUser(user: SessionUser): InferUser { 20 | return { 21 | ...user, 22 | id: user.id.toString(10), 23 | role: formatRole(user.role), 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /src/lib/user-settings/vercel.ts: -------------------------------------------------------------------------------- 1 | import { Vercel } from "@vercel/sdk"; 2 | import { cache } from "react"; 3 | import type { SiteSettings } from "@/lib/system-settings"; 4 | 5 | export function isVercelSettingsValid( 6 | settings: SiteSettings | undefined | null, 7 | ): settings is SiteSettings & { 8 | vercel_token: string; 9 | vercel_blob_team_id: string; 10 | vercel_blob_storage_id: string; 11 | vercel_blob_storage_rw_token: string; 12 | } { 13 | return ( 14 | settings?.vercel_token != null && 15 | settings.vercel_blob_team_id != null && 16 | settings.vercel_blob_storage_id != null && 17 | settings.vercel_blob_storage_rw_token != null 18 | ); 19 | } 20 | 21 | export const getVercelClient = cache((token: string) => { 22 | return new Vercel({ bearerToken: token }); 23 | }); 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "react-jsx", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx", 29 | ".next/types/**/*.ts", 30 | ".next/dev/types/**/*.ts", 31 | "**/*.mts" 32 | ], 33 | "exclude": ["node_modules"] 34 | } 35 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ); 26 | } 27 | 28 | export { Separator }; 29 | -------------------------------------------------------------------------------- /src/lib/user-settings/github.ts: -------------------------------------------------------------------------------- 1 | import type { Selectable } from "kysely"; 2 | import { Octokit } from "octokit"; 3 | import { cache } from "react"; 4 | import type { DB } from "@/lib/db/schema"; 5 | import type { SiteSettings } from "@/lib/system-settings"; 6 | 7 | export const getGitHubClient = cache((token: string) => { 8 | return new Octokit({ auth: token }); 9 | }); 10 | 11 | export function isGitHubSettingsValid( 12 | settings: SiteSettings, 13 | ): settings is SiteSettings & { 14 | github_token: string; 15 | github_login: string; 16 | } { 17 | return settings?.github_token != null; 18 | } 19 | 20 | export function getUserGitHubClient(settings: SiteSettings) { 21 | if (!isGitHubSettingsValid(settings)) { 22 | throw new Error("Invalid GitHub settings"); 23 | } 24 | 25 | return getGitHubClient(settings.github_token); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 | 48 | 49 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/app/api/v1/projects/route.ts: -------------------------------------------------------------------------------- 1 | import { after, type NextRequest, NextResponse } from "next/server"; 2 | import { z } from "zod"; 3 | import { createProjectStreamed } from "@/actions/projects"; 4 | import { consumeAsyncIterator } from "@/lib/async-generators"; 5 | import db from "@/lib/db/db"; 6 | import { getErrorMessage } from "@/lib/errors"; 7 | import { get, omit } from "@/lib/kysely-utils"; 8 | 9 | const requestSchema = z.object({ 10 | name: z 11 | .string() 12 | .regex( 13 | /^[a-z]([a-z0-9-_\s]*)/i, 14 | "Must start with a letter, followed by letters, numbers, dashes, or underscores.", 15 | ), 16 | description: z.string().optional().default(""), 17 | vercel_team_id: z.string(), 18 | coding_agent_type: z.enum(["codex", "claude", "claude-opus"]), 19 | }); 20 | 21 | export async function POST(request: NextRequest) { 22 | const body = await request.json(); 23 | const data = requestSchema.parse(body); 24 | 25 | const iter = createProjectStreamed(data); 26 | 27 | try { 28 | for await (const chunk of iter) { 29 | if (chunk.type === "created-db-project") { 30 | after(consumeAsyncIterator(iter)); 31 | return NextResponse.json( 32 | omit(await get(db, "project", { id: chunk.id }), [ 33 | "tidbcloud_connection_url", 34 | ]), 35 | ); 36 | } 37 | } 38 | return NextResponse.json( 39 | { 40 | message: 41 | "Failed to create project. Please check the logs for more details.", 42 | }, 43 | { status: 400 }, 44 | ); 45 | } catch (e) { 46 | return NextResponse.json( 47 | { 48 | message: getErrorMessage(e), 49 | }, 50 | { status: 400 }, 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function HoverCard({ 9 | ...props 10 | }: React.ComponentProps) { 11 | return ; 12 | } 13 | 14 | function HoverCardTrigger({ 15 | ...props 16 | }: React.ComponentProps) { 17 | return ( 18 | 19 | ); 20 | } 21 | 22 | function HoverCardContent({ 23 | className, 24 | align = "center", 25 | sideOffset = 4, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 30 | 40 | 41 | ); 42 | } 43 | 44 | export { HoverCard, HoverCardTrigger, HoverCardContent }; 45 | -------------------------------------------------------------------------------- /src/components/vercel-default-project-team-setup.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Form from "next/form"; 4 | import { useState } from "react"; 5 | import { 6 | setVercelBlobStorage, 7 | setVercelDefaultProjectTeam, 8 | } from "@/actions/user-settings"; 9 | import { Button } from "@/components/ui/button"; 10 | import { 11 | Field, 12 | FieldDescription, 13 | FieldGroup, 14 | FieldSet, 15 | FieldTitle, 16 | } from "@/components/ui/field"; 17 | import { VercelTeamSelect } from "@/components/vercel-team-select"; 18 | 19 | export function VercelDefaultProjectTeamSetup({ 20 | defaultVercelTeamId, 21 | enabled, 22 | }: { 23 | defaultVercelTeamId?: string; 24 | enabled: boolean; 25 | }) { 26 | const [teamId, setTeamId] = useState(defaultVercelTeamId); 27 | 28 | const canSave = !!teamId; 29 | 30 | return ( 31 |
{ 33 | await setVercelDefaultProjectTeam(formData); 34 | }} 35 | > 36 |
37 | Default Vercel Project Team 38 | 39 | Your projects will be deployed to this team by default. 40 | 41 | 42 | 43 | 49 | 50 | 53 | 54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/app/(customer)/s/[slug]/reloader.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { useEffect } from "react"; 5 | import type { UISessionData } from "@/app/(customer)/s/[slug]/query"; 6 | import type { UISessionMessageChunk } from "@/prompts/ui-session"; 7 | 8 | export function Reloader({ session }: { session: UISessionData }) { 9 | const router = useRouter(); 10 | useEffect(() => { 11 | if (!Array.isArray(session.logs)) { 12 | return; 13 | } 14 | 15 | if ( 16 | session.logs.some((item) => { 17 | return (item as UISessionMessageChunk).type === "error"; 18 | }) 19 | ) { 20 | return; 21 | } 22 | 23 | let shouldReload = false; 24 | let interval = 1000; 25 | 26 | if (session.project?.status !== "ready") { 27 | shouldReload = true; 28 | interval = 3000; 29 | } 30 | 31 | if (session.task_revisions.length === 0) { 32 | shouldReload = true; 33 | interval = 2500; 34 | } 35 | 36 | for (const revision of session.task_revisions) { 37 | if (revision.status === "preparing") { 38 | shouldReload = true; 39 | interval = 5000; 40 | break; 41 | } 42 | if (revision.status === "running") { 43 | shouldReload = true; 44 | interval = 5000; 45 | break; 46 | } 47 | if (revision.status === "deploying") { 48 | shouldReload = true; 49 | interval = 5000; 50 | break; 51 | } 52 | } 53 | 54 | if (shouldReload) { 55 | console.log(`reload after ${interval}ms...`); 56 | 57 | const th = setTimeout(() => { 58 | router.refresh(); 59 | }, interval); 60 | 61 | return () => clearTimeout(th); 62 | } 63 | }, [session, router]); 64 | return null; 65 | } 66 | -------------------------------------------------------------------------------- /src/lib/vercel/blobs.ts: -------------------------------------------------------------------------------- 1 | import { handleVercelApiCallError } from "@/lib/vercel/errors"; 2 | import { getValidSessionUserVercelToken } from "@/lib/vercel/utils"; 3 | 4 | export interface VercelBlobStorageDetails { 5 | id: string; 6 | onwerId: string; 7 | createdAt: number; 8 | updatedAt: number; 9 | type: "blob"; 10 | name: string; 11 | status: string; // available 12 | } 13 | 14 | export async function listBlobStorages( 15 | teamId: string, 16 | ): Promise { 17 | const token = await getValidSessionUserVercelToken(); 18 | if (!token) { 19 | throw new Error("No valid session user vercel token found"); 20 | } 21 | 22 | const response = await fetch( 23 | `https://api.vercel.com/v1/storage/stores?teamId=${encodeURIComponent(teamId)}`, 24 | { 25 | headers: { 26 | Authorization: `Bearer ${token}`, 27 | }, 28 | }, 29 | ).then(handleVercelApiCallError); 30 | 31 | const data = await response.json(); 32 | 33 | return data.stores.filter( 34 | (store: VercelBlobStorageDetails) => store.type === "blob", 35 | ); 36 | } 37 | 38 | export async function getBlobStorageReadWriteToken( 39 | teamId: string, 40 | blobId: string, 41 | ): Promise<{ token: string }> { 42 | const token = await getValidSessionUserVercelToken(); 43 | if (!token) { 44 | throw new Error("No valid session user vercel token found"); 45 | } 46 | 47 | const response = await fetch( 48 | `https://api.vercel.com/v1/storage/stores/${blobId}/secrets?teamId=${encodeURIComponent(teamId)}`, 49 | { 50 | headers: { 51 | Authorization: `Bearer ${token}`, 52 | }, 53 | }, 54 | ).then(handleVercelApiCallError); 55 | 56 | const { rwToken } = await response.json(); 57 | 58 | return { 59 | token: rwToken, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const badgeVariants = cva( 8 | "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", 14 | secondary: 15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", 16 | destructive: 17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 18 | outline: 19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | }, 26 | ); 27 | 28 | function Badge({ 29 | className, 30 | variant, 31 | asChild = false, 32 | ...props 33 | }: React.ComponentProps<"span"> & 34 | VariantProps & { asChild?: boolean }) { 35 | const Comp = asChild ? Slot : "span"; 36 | 37 | return ( 38 | 43 | ); 44 | } 45 | 46 | export { Badge, badgeVariants }; 47 | -------------------------------------------------------------------------------- /src/components/ai-elements/shimmer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { motion } from "motion/react"; 5 | import { 6 | type CSSProperties, 7 | type ElementType, 8 | type JSX, 9 | memo, 10 | useMemo, 11 | } from "react"; 12 | 13 | export type TextShimmerProps = { 14 | children: string; 15 | as?: ElementType; 16 | className?: string; 17 | duration?: number; 18 | spread?: number; 19 | }; 20 | 21 | const ShimmerComponent = ({ 22 | children, 23 | as: Component = "p", 24 | className, 25 | duration = 2, 26 | spread = 2, 27 | }: TextShimmerProps) => { 28 | const MotionComponent = motion.create( 29 | Component as keyof JSX.IntrinsicElements, 30 | ); 31 | 32 | const dynamicSpread = useMemo( 33 | () => (children?.length ?? 0) * spread, 34 | [children, spread], 35 | ); 36 | 37 | return ( 38 | 59 | {children} 60 | 61 | ); 62 | }; 63 | 64 | export const Shimmer = memo(ShimmerComponent); 65 | -------------------------------------------------------------------------------- /src/components/github-repo-branch-select.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@/components/ui/badge"; 2 | import { 3 | Select, 4 | SelectContent, 5 | SelectItem, 6 | SelectTrigger, 7 | SelectValue, 8 | } from "@/components/ui/select"; 9 | import { useQuery } from "@tanstack/react-query"; 10 | 11 | export function GithubRepoBranchSelect({ 12 | id, 13 | name, 14 | className, 15 | enabled, 16 | branch, 17 | onBranchChange, 18 | projectId, 19 | onBlur, 20 | }: { 21 | id?: string; 22 | name?: string; 23 | projectId: number; 24 | className?: string; 25 | enabled?: boolean; 26 | branch?: string | null; 27 | onBranchChange?: (branch: string) => void; 28 | onBlur?: () => void; 29 | }) { 30 | const { 31 | data: branches, 32 | isLoading: isBranchesLoading, 33 | error: branchesError, 34 | } = useQuery<{ name: string; commit: { sha: string } }[]>({ 35 | queryKey: ["projects", projectId, "git-branches"], 36 | enabled, 37 | queryFn: async () => { 38 | const response = await fetch( 39 | `/api/v1/projects/${projectId}/git-branches`, 40 | ); 41 | return response.json(); 42 | }, 43 | }); 44 | 45 | return ( 46 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function ScrollArea({ 9 | className, 10 | children, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 19 | 23 | {children} 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | function ScrollBar({ 32 | className, 33 | orientation = "vertical", 34 | ...props 35 | }: React.ComponentProps) { 36 | return ( 37 | 50 | 54 | 55 | ); 56 | } 57 | 58 | export { ScrollArea, ScrollBar }; 59 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-card text-card-foreground", 12 | destructive: 13 | "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | }, 20 | ); 21 | 22 | function Alert({ 23 | className, 24 | variant, 25 | ...props 26 | }: React.ComponentProps<"div"> & VariantProps) { 27 | return ( 28 |
34 | ); 35 | } 36 | 37 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { 38 | return ( 39 |
47 | ); 48 | } 49 | 50 | function AlertDescription({ 51 | className, 52 | ...props 53 | }: React.ComponentProps<"div">) { 54 | return ( 55 |
63 | ); 64 | } 65 | 66 | export { Alert, AlertTitle, AlertDescription }; 67 | -------------------------------------------------------------------------------- /src/components/ai-elements/checkpoint.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { Separator } from "@/components/ui/separator"; 5 | import { 6 | Tooltip, 7 | TooltipContent, 8 | TooltipTrigger, 9 | } from "@/components/ui/tooltip"; 10 | import { cn } from "@/lib/utils"; 11 | import { BookmarkIcon, type LucideProps } from "lucide-react"; 12 | import type { ComponentProps, HTMLAttributes } from "react"; 13 | 14 | export type CheckpointProps = HTMLAttributes; 15 | 16 | export const Checkpoint = ({ 17 | className, 18 | children, 19 | ...props 20 | }: CheckpointProps) => ( 21 |
28 | {children} 29 | 30 |
31 | ); 32 | 33 | export type CheckpointIconProps = LucideProps; 34 | 35 | export const CheckpointIcon = ({ 36 | className, 37 | children, 38 | ...props 39 | }: CheckpointIconProps) => 40 | children ?? ( 41 | 42 | ); 43 | 44 | export type CheckpointTriggerProps = ComponentProps & { 45 | tooltip?: string; 46 | }; 47 | 48 | export const CheckpointTrigger = ({ 49 | children, 50 | className, 51 | variant = "ghost", 52 | size = "sm", 53 | tooltip, 54 | ...props 55 | }: CheckpointTriggerProps) => 56 | tooltip ? ( 57 | 58 | 59 | 62 | 63 | 64 | {tooltip} 65 | 66 | 67 | ) : ( 68 | 71 | ); 72 | -------------------------------------------------------------------------------- /migrations/001-ui-session.ts: -------------------------------------------------------------------------------- 1 | import type { Kysely } from "kysely"; 2 | 3 | export async function up(db: Kysely) { 4 | await db.schema 5 | .createTable("ui_session") 6 | .addColumn("id", "integer", (col) => 7 | col.primaryKey().autoIncrement().notNull(), 8 | ) 9 | .addColumn("slug", "varchar(255)", (col) => col.notNull()) 10 | .addColumn("user_id", "integer", (col) => col.notNull()) 11 | .addColumn("project_id", "integer", (col) => col.notNull()) 12 | .addColumn("task_id", "integer", (col) => col.notNull()) 13 | .addColumn("created_at", "timestamp", (col) => col.notNull()) 14 | .addColumn("updated_at", "timestamp", (col) => col.notNull()) 15 | .addUniqueConstraint("uq_project_task", ["project_id", "task_id"]) 16 | .addUniqueConstraint("uq_slug", ["slug"]) 17 | .addForeignKeyConstraint("fk_user_id", ["user_id"], "user", ["id"]) 18 | .addForeignKeyConstraint("fk_project_id", ["project_id"], "project", ["id"]) 19 | .addForeignKeyConstraint("fk_task_id", ["task_id"], "task", ["id"]) 20 | .execute(); 21 | 22 | await db.schema 23 | .alterTable("user_setting") 24 | .addColumn("default_vercel_project_team_id", "varchar(255)", (col) => col) 25 | .execute(); 26 | 27 | await db 28 | .updateTable("user_setting") 29 | .set(({ ref }) => ({ 30 | default_vercel_project_team_id: ref("vercel_blob_team_id"), 31 | })) 32 | .execute(); 33 | 34 | await db.schema 35 | .alterTable("task_revision") 36 | .modifyColumn("prompt", "text", (col) => col.notNull()) 37 | .execute(); 38 | 39 | await db.schema 40 | .alterTable("project") 41 | .modifyColumn("description", "text", (col) => col.notNull()) 42 | .execute(); 43 | } 44 | 45 | export async function down(db: Kysely) { 46 | await db.schema 47 | .alterTable("user_setting") 48 | .dropColumn("default_vercel_project_team_id") 49 | .execute(); 50 | await db.schema.dropTable("ui_session").execute(); 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/system-settings.ts: -------------------------------------------------------------------------------- 1 | import { getSessionUser } from '@/lib/auth'; 2 | import db from '@/lib/db/db'; 3 | import { cache } from 'react'; 4 | import { z } from 'zod'; 5 | 6 | export type SiteSettings = z.infer; 7 | 8 | export const siteSettingsSchema = z.object({ 9 | github_login: z.string().optional(), 10 | github_token: z.string().optional(), 11 | tidbcloud_public_key: z.string().optional(), 12 | tidbcloud_private_key: z.string().optional(), 13 | tidbcloud_organization_id: z.string().optional(), 14 | tidbcloud_project_id: z.string().optional(), 15 | vercel_token: z.string().optional(), 16 | vercel_blob_team_id: z.string().optional(), 17 | vercel_blob_storage_id: z.string().optional(), 18 | vercel_blob_storage_rw_token: z.string().optional(), 19 | default_vercel_project_team_id: z.string().optional(), 20 | openai_api_key: z.string().optional(), 21 | }); 22 | 23 | export async function updateSiteSettings (settings: Partial) { 24 | await db.insertInto('system_settings') 25 | .values(Object.entries(settings).map(([key, v]) => ({ 26 | key, 27 | value: JSON.stringify(v), 28 | }))) 29 | .onDuplicateKeyUpdate({ 30 | value: eb => eb.fn('VALUES', ['value']), 31 | }) 32 | .execute(); 33 | } 34 | 35 | async function $readSiteSettings () { 36 | const { settings } = await db 37 | .selectFrom('system_settings') 38 | .select((eb) => 39 | eb 40 | .fn('ifnull', [ 41 | eb.fn('json_objectagg', ['key', 'value']), 42 | eb.fn('json_object', []), 43 | ]) 44 | .$castTo() 45 | .as('settings'), 46 | ) 47 | .executeTakeFirstOrThrow(); 48 | 49 | return siteSettingsSchema.parse(settings); 50 | } 51 | 52 | async function $getSiteSettings () { 53 | const user = await getSessionUser(); 54 | if (user) { 55 | return await $readSiteSettings(); 56 | } 57 | return {} as SiteSettings; 58 | } 59 | 60 | export const getSiteSettings = cache($getSiteSettings); 61 | -------------------------------------------------------------------------------- /src/lib/errors.ts: -------------------------------------------------------------------------------- 1 | import { HTTPClientError } from "@vercel/sdk/models/httpclienterrors"; 2 | import { SDKValidationError } from "@vercel/sdk/models/sdkvalidationerror"; 3 | import { VercelError } from "@vercel/sdk/models/vercelerror"; 4 | 5 | export class ResponseError extends Error { 6 | readonly response: Response; 7 | 8 | constructor(response: Response, message: string) { 9 | super(message); 10 | this.response = response; 11 | } 12 | } 13 | 14 | export async function handleFetchResponseError( 15 | response: Response | Promise, 16 | ) { 17 | response = await response; 18 | 19 | if (response.ok) { 20 | return response; 21 | } 22 | 23 | try { 24 | const jsonResponse = await response.clone().json(); 25 | return Promise.reject( 26 | new ResponseError(response.clone(), getErrorMessage(jsonResponse)), 27 | ); 28 | } catch { 29 | try { 30 | const textResponse = await response.clone().text(); 31 | return Promise.reject( 32 | new ResponseError( 33 | response.clone(), 34 | `${response.status} ${textResponse}`, 35 | ), 36 | ); 37 | } catch { 38 | return Promise.reject( 39 | new ResponseError( 40 | response.clone(), 41 | `${response.status} ${response.statusText}`, 42 | ), 43 | ); 44 | } 45 | } 46 | } 47 | 48 | export function getErrorMessage(error: unknown): string { 49 | if (error == null) { 50 | return "Unknown error"; 51 | } 52 | if (typeof error === "object") { 53 | if (error instanceof VercelError) { 54 | return `${error.name}: ${error.statusCode} ${error.message}`; 55 | } 56 | 57 | if (error instanceof HTTPClientError) { 58 | return `${error.name}: ${error.message}`; 59 | } 60 | 61 | if (error instanceof SDKValidationError) { 62 | return `${error.name}: ${error.pretty()}`; 63 | } 64 | 65 | return String( 66 | getErrorMessage((error as Record).message) ?? 67 | JSON.stringify(error), 68 | ); 69 | } 70 | return String(error); 71 | } 72 | -------------------------------------------------------------------------------- /src/components/vercel-team-select.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarImage } from "@/components/ui/avatar"; 2 | import { 3 | Select, 4 | SelectContent, 5 | SelectItem, 6 | SelectTrigger, 7 | SelectValue, 8 | } from "@/components/ui/select"; 9 | import { useQuery } from "@tanstack/react-query"; 10 | import type { Team } from "@vercel/sdk/models/team"; 11 | import type { TeamLimited } from "@vercel/sdk/models/teamlimited"; 12 | 13 | export function VercelTeamSelect({ 14 | id, 15 | name, 16 | className, 17 | enabled, 18 | teamId, 19 | onTeamIdChange, 20 | onBlur, 21 | }: { 22 | id?: string; 23 | name?: string; 24 | className?: string; 25 | enabled?: boolean; 26 | teamId?: string | null; 27 | onTeamIdChange?: (teamId: string) => void; 28 | onBlur?: () => void; 29 | }) { 30 | const { 31 | data: teams, 32 | isLoading: isTeamsLoading, 33 | error: teamsError, 34 | } = useQuery<(Team | TeamLimited)[]>({ 35 | queryKey: ["teams"], 36 | enabled, 37 | queryFn: async () => { 38 | const response = await fetch("/api/v1/vercel/teams"); 39 | return response.json(); 40 | }, 41 | }); 42 | 43 | return ( 44 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function TooltipProvider({ 9 | delayDuration = 0, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 18 | ); 19 | } 20 | 21 | function Tooltip({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return ( 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | function TooltipTrigger({ 32 | ...props 33 | }: React.ComponentProps) { 34 | return ; 35 | } 36 | 37 | function TooltipContent({ 38 | className, 39 | sideOffset = 0, 40 | children, 41 | ...props 42 | }: React.ComponentProps) { 43 | return ( 44 | 45 | 54 | {children} 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 62 | -------------------------------------------------------------------------------- /src/app/(customer)/s/[slug]/conversation-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { ChatStatus } from "ai"; 4 | import { useRouter } from "next/navigation"; 5 | import { type FormEvent, useEffectEvent, useTransition } from "react"; 6 | import { 7 | PromptInput, 8 | PromptInputBody, 9 | PromptInputFooter, 10 | type PromptInputMessage, 11 | PromptInputProvider, 12 | PromptInputSubmit, 13 | PromptInputTextarea, 14 | PromptInputTools, 15 | } from "@/components/ai-elements/prompt-input"; 16 | 17 | export function SessionConversationInput({ 18 | projectId, 19 | taskId, 20 | status, 21 | }: { 22 | projectId: number | undefined | null; 23 | taskId: number | undefined | null; 24 | status: ChatStatus; 25 | }) { 26 | const [transitioning, startTransition] = useTransition(); 27 | const router = useRouter(); 28 | 29 | const handleSubmit = useEffectEvent( 30 | (message: PromptInputMessage, event: FormEvent) => { 31 | event.preventDefault(); 32 | startTransition(async () => { 33 | await fetch(`/api/v1/projects/${projectId}/tasks/${taskId}/revisions`, { 34 | method: "POST", 35 | headers: { 36 | "Content-Type": "application/json", 37 | }, 38 | body: JSON.stringify({ 39 | prompt: message.text, 40 | }), 41 | }).finally(() => { 42 | startTransition(() => { 43 | router.refresh(); 44 | }); 45 | }); 46 | }); 47 | }, 48 | ); 49 | 50 | return ( 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 67 | 68 | 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/components/ai-elements/sources.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Collapsible, 5 | CollapsibleContent, 6 | CollapsibleTrigger, 7 | } from "@/components/ui/collapsible"; 8 | import { cn } from "@/lib/utils"; 9 | import { BookIcon, ChevronDownIcon } from "lucide-react"; 10 | import type { ComponentProps } from "react"; 11 | 12 | export type SourcesProps = ComponentProps<"div">; 13 | 14 | export const Sources = ({ className, ...props }: SourcesProps) => ( 15 | 19 | ); 20 | 21 | export type SourcesTriggerProps = ComponentProps & { 22 | count: number; 23 | }; 24 | 25 | export const SourcesTrigger = ({ 26 | className, 27 | count, 28 | children, 29 | ...props 30 | }: SourcesTriggerProps) => ( 31 | 35 | {children ?? ( 36 | <> 37 |

Used {count} sources

38 | 39 | 40 | )} 41 |
42 | ); 43 | 44 | export type SourcesContentProps = ComponentProps; 45 | 46 | export const SourcesContent = ({ 47 | className, 48 | ...props 49 | }: SourcesContentProps) => ( 50 | 58 | ); 59 | 60 | export type SourceProps = ComponentProps<"a">; 61 | 62 | export const Source = ({ href, title, children, ...props }: SourceProps) => ( 63 | 70 | {children ?? ( 71 | <> 72 | 73 | {title} 74 | 75 | )} 76 | 77 | ); 78 | -------------------------------------------------------------------------------- /src/components/ai-elements/node.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardAction, 4 | CardContent, 5 | CardDescription, 6 | CardFooter, 7 | CardHeader, 8 | CardTitle, 9 | } from "@/components/ui/card"; 10 | import { cn } from "@/lib/utils"; 11 | import { Handle, Position } from "@xyflow/react"; 12 | import type { ComponentProps } from "react"; 13 | 14 | export type NodeProps = ComponentProps & { 15 | handles: { 16 | target: boolean; 17 | source: boolean; 18 | }; 19 | }; 20 | 21 | export const Node = ({ handles, className, ...props }: NodeProps) => ( 22 | 29 | {handles.target && } 30 | {handles.source && } 31 | {props.children} 32 | 33 | ); 34 | 35 | export type NodeHeaderProps = ComponentProps; 36 | 37 | export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => ( 38 | 42 | ); 43 | 44 | export type NodeTitleProps = ComponentProps; 45 | 46 | export const NodeTitle = (props: NodeTitleProps) => ; 47 | 48 | export type NodeDescriptionProps = ComponentProps; 49 | 50 | export const NodeDescription = (props: NodeDescriptionProps) => ( 51 | 52 | ); 53 | 54 | export type NodeActionProps = ComponentProps; 55 | 56 | export const NodeAction = (props: NodeActionProps) => ; 57 | 58 | export type NodeContentProps = ComponentProps; 59 | 60 | export const NodeContent = ({ className, ...props }: NodeContentProps) => ( 61 | 62 | ); 63 | 64 | export type NodeFooterProps = ComponentProps; 65 | 66 | export const NodeFooter = ({ className, ...props }: NodeFooterProps) => ( 67 | 71 | ); 72 | -------------------------------------------------------------------------------- /src/hooks/use-message-session.ts: -------------------------------------------------------------------------------- 1 | import { handleFetchResponseError } from "@/lib/errors"; 2 | import { 3 | parseJsonEventStream, 4 | readUIMessageStream, 5 | type UIDataTypes, 6 | type UIMessage, 7 | uiMessageChunkSchema, 8 | type UITools, 9 | } from "ai"; 10 | import { useEffect, useState } from "react"; 11 | 12 | export function useMessageSession(sessionId: string) { 13 | const [version, setVersion] = useState(0); 14 | const [error, setError] = useState(undefined); 15 | const [uiMessage, setUIMessage] = useState< 16 | UIMessage | undefined 17 | >(undefined); 18 | 19 | useEffect(() => { 20 | const abortController = new AbortController(); 21 | const request = fetch(`/api/v1/debug/streams/${sessionId}`, { 22 | signal: abortController.signal, 23 | }).then(handleFetchResponseError); 24 | 25 | (async () => { 26 | try { 27 | const response = await request; 28 | 29 | if (!response.body) { 30 | setError("No response body."); 31 | return; 32 | } 33 | 34 | const messageStream = readUIMessageStream< 35 | UIMessage 36 | >({ 37 | stream: parseJsonEventStream({ 38 | stream: response.body, 39 | schema: uiMessageChunkSchema, 40 | }).pipeThrough( 41 | new TransformStream({ 42 | transform(chunk, controller) { 43 | if (chunk.success) { 44 | controller.enqueue(chunk.value); 45 | } else { 46 | console.error( 47 | "Error parsing stream chunk:", 48 | chunk.rawValue, 49 | chunk.error, 50 | ); 51 | } 52 | }, 53 | }), 54 | ), 55 | }); 56 | 57 | for await (const message of messageStream) { 58 | setUIMessage(message); 59 | } 60 | } catch (e) { 61 | setError(e); 62 | } 63 | })(); 64 | 65 | return () => abortController.abort(); 66 | }, [sessionId, version]); 67 | 68 | return { 69 | message: uiMessage, 70 | error, 71 | retry: () => { 72 | setVersion((v) => v + 1); 73 | }, 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ); 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ); 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ); 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ); 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ); 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ); 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ); 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | }; 93 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 15 | outline: 16 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: 20 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 25 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 26 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 27 | icon: "size-9", 28 | "icon-sm": "size-8", 29 | "icon-lg": "size-10", 30 | }, 31 | }, 32 | defaultVariants: { 33 | variant: "default", 34 | size: "default", 35 | }, 36 | }, 37 | ); 38 | 39 | function Button({ 40 | className, 41 | variant, 42 | size, 43 | asChild = false, 44 | ...props 45 | }: React.ComponentProps<"button"> & 46 | VariantProps & { 47 | asChild?: boolean; 48 | }) { 49 | const Comp = asChild ? Slot : "button"; 50 | 51 | return ( 52 | 57 | ); 58 | } 59 | 60 | export { Button, buttonVariants }; 61 | -------------------------------------------------------------------------------- /src/components/auto-collapse.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion } from "framer-motion"; 4 | import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; 5 | import { type ReactNode, useEffect, useRef, useState } from "react"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | export function AutoCollapse({ 9 | children, 10 | collapseThresholdHeight = 72, 11 | className, 12 | enable = true, 13 | }: { 14 | children: ReactNode; 15 | collapseThresholdHeight?: number; 16 | enable?: boolean; 17 | className?: string; 18 | }) { 19 | const [isTooTall, setIsTooTall] = useState(true); 20 | const [collapsed, setCollapsed] = useState(true); 21 | const contentRef = useRef(null); 22 | 23 | useEffect(() => { 24 | const content = contentRef.current; 25 | if (content && enable) { 26 | const ro = new ResizeObserver(([entry]) => { 27 | setIsTooTall(entry!.contentRect.height > collapseThresholdHeight); 28 | }); 29 | 30 | setIsTooTall(content.clientHeight > collapseThresholdHeight); 31 | 32 | ro.observe(content); 33 | 34 | return () => { 35 | ro.disconnect(); 36 | }; 37 | } 38 | }, [enable, collapseThresholdHeight]); 39 | 40 | return ( 41 | 52 |
{children}
53 | {enable && isTooTall && ( 54 | setCollapsed(false)} 64 | > 65 | 66 | 67 | )} 68 | {enable && isTooTall && ( 69 | 76 | )} 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/components/ui/button-group.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | import { Separator } from "@/components/ui/separator"; 6 | 7 | const buttonGroupVariants = cva( 8 | "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2", 9 | { 10 | variants: { 11 | orientation: { 12 | horizontal: 13 | "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none", 14 | vertical: 15 | "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none", 16 | }, 17 | }, 18 | defaultVariants: { 19 | orientation: "horizontal", 20 | }, 21 | }, 22 | ); 23 | 24 | function ButtonGroup({ 25 | className, 26 | orientation, 27 | ...props 28 | }: React.ComponentProps<"div"> & VariantProps) { 29 | return ( 30 |
37 | ); 38 | } 39 | 40 | function ButtonGroupText({ 41 | className, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"div"> & { 45 | asChild?: boolean; 46 | }) { 47 | const Comp = asChild ? Slot : "div"; 48 | 49 | return ( 50 | 57 | ); 58 | } 59 | 60 | function ButtonGroupSeparator({ 61 | className, 62 | orientation = "vertical", 63 | ...props 64 | }: React.ComponentProps) { 65 | return ( 66 | 75 | ); 76 | } 77 | 78 | export { 79 | ButtonGroup, 80 | ButtonGroupSeparator, 81 | ButtonGroupText, 82 | buttonGroupVariants, 83 | }; 84 | -------------------------------------------------------------------------------- /src/app/api/v1/projects/[projectId]/tasks/route.ts: -------------------------------------------------------------------------------- 1 | import { unauthorized } from "next/navigation"; 2 | import { type NextRequest, NextResponse } from "next/server"; 3 | import { z } from "zod"; 4 | import { createTaskRevision } from "@/actions/task-revisions"; 5 | import { createTask } from "@/actions/tasks"; 6 | import { getSessionUser } from "@/lib/auth"; 7 | import db from "@/lib/db/db"; 8 | import { get } from "@/lib/kysely-utils"; 9 | import { getSiteSettings } from "@/lib/system-settings"; 10 | import { 11 | getGitHubClient, 12 | isGitHubSettingsValid, 13 | } from "@/lib/user-settings/github"; 14 | 15 | const requestSchema = z.object({ 16 | name: z.string(), 17 | base_branch: z.string().optional().default("main"), 18 | target_branch: z.string(), 19 | first_prompt: z.string().optional(), 20 | }); 21 | 22 | export async function POST( 23 | request: NextRequest, 24 | { params }: { params: Promise<{ projectId: string }> }, 25 | ) { 26 | const projectId = parseInt(decodeURIComponent((await params).projectId)); 27 | const user = await getSessionUser(); 28 | const settings = await getSiteSettings(); 29 | 30 | if (!user) { 31 | unauthorized(); 32 | } 33 | 34 | if (!settings) { 35 | return NextResponse.json( 36 | { 37 | message: "Invalid user.", 38 | }, 39 | { 40 | status: 400, 41 | }, 42 | ); 43 | } 44 | 45 | if (!isGitHubSettingsValid(settings)) { 46 | return NextResponse.json( 47 | { 48 | message: "GitHub settings are invalid.", 49 | }, 50 | { 51 | status: 400, 52 | }, 53 | ); 54 | } 55 | 56 | const project = await get(db, "project", { 57 | id: projectId, 58 | user_id: user.id, 59 | }); 60 | 61 | const octokit = getGitHubClient(settings.github_token); 62 | 63 | const { name, base_branch, target_branch, first_prompt } = 64 | requestSchema.parse(await request.json()); 65 | 66 | const branch = await octokit.rest.repos.getBranch({ 67 | repo: project.github_repo, 68 | owner: project.github_owner, 69 | branch: base_branch, 70 | }); 71 | 72 | const task = await createTask({ 73 | project_id: projectId, 74 | name, 75 | user_id: project.user_id, 76 | git_revision_ref: branch.data.commit.sha, 77 | git_branch_name: target_branch, 78 | }); 79 | 80 | if (first_prompt) { 81 | await createTaskRevision({ 82 | task_id: task.id, 83 | sandbox_type: project.coding_agent_type as never, 84 | prompt: first_prompt, 85 | user_prompt: first_prompt, 86 | }); 87 | } 88 | 89 | return NextResponse.json(task); 90 | } 91 | -------------------------------------------------------------------------------- /migrations/007-better-auth.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `user` 2 | ADD COLUMN `email_verified` BOOLEAN NOT NULL; 3 | 4 | ALTER TABLE `user` 5 | ADD COLUMN `created_at` TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL; 6 | 7 | ALTER TABLE `user` 8 | ADD COLUMN `updated_at` TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL; 9 | 10 | CREATE TABLE `session` 11 | ( 12 | `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, 13 | `expires_at` TIMESTAMP(3) NOT NULL, 14 | `token` VARCHAR(1024) NOT NULL COLLATE 'ascii_bin', 15 | `created_at` TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL, 16 | `updated_at` TIMESTAMP(3) NOT NULL, 17 | `ip_address` VARCHAR(16), 18 | `user_agent` TEXT, 19 | `user_id` INTEGER NOT NULL, 20 | UNIQUE INDEX uk_session_token (token), 21 | FOREIGN KEY fk_session_user (user_id) REFERENCES `user` (`id`) ON DELETE CASCADE 22 | ); 23 | 24 | CREATE TABLE `account` 25 | ( 26 | `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, 27 | `account_id` VARCHAR(256) NOT NULL, 28 | `provider_id` VARCHAR(256) NOT NULL, 29 | `user_id` INTEGER NOT NULL, 30 | `access_token` TEXT, 31 | `refresh_token` TEXT, 32 | `id_token` TEXT, 33 | `access_token_expires_at` TIMESTAMP(3), 34 | `refresh_token_expires_at` TIMESTAMP(3), 35 | `scope` VARCHAR(256), 36 | `password` VARCHAR(256), 37 | `created_at` TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL, 38 | `updated_at` TIMESTAMP(3) NOT NULL, 39 | FOREIGN KEY fk_account_user (user_id) REFERENCES `user` (`id`) ON DELETE CASCADE 40 | ); 41 | 42 | CREATE TABLE `verification` 43 | ( 44 | `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, 45 | `identifier` VARCHAR(256) NOT NULL, 46 | `value` TEXT NOT NULL, 47 | `expires_at` TIMESTAMP(3) NOT NULL, 48 | `created_at` TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL, 49 | `updated_at` TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL, 50 | INDEX idx_verification_identifier_idx (identifier) 51 | ); 52 | -------------------------------------------------------------------------------- /src/components/ai-elements/loader.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import type { HTMLAttributes } from "react"; 3 | 4 | type LoaderIconProps = { 5 | size?: number; 6 | }; 7 | 8 | const LoaderIcon = ({ size = 16 }: LoaderIconProps) => ( 9 | 16 | Loader 17 | 18 | 19 | 25 | 31 | 37 | 43 | 49 | 55 | 61 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | ); 81 | 82 | export type LoaderProps = HTMLAttributes & { 83 | size?: number; 84 | }; 85 | 86 | export const Loader = ({ className, size = 16, ...props }: LoaderProps) => ( 87 |
94 | 95 |
96 | ); 97 | -------------------------------------------------------------------------------- /src/app/(main)/projects/[projectId]/tasks-list.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CreateTaskDialog } from "@/components/projects/create-task-dialog"; 4 | import { Button } from "@/components/ui/button"; 5 | import { DialogTrigger } from "@/components/ui/dialog"; 6 | import { Empty, EmptyTitle } from "@/components/ui/empty"; 7 | import { 8 | Table, 9 | TableBody, 10 | TableCell, 11 | TableHead, 12 | TableHeader, 13 | TableRow, 14 | } from "@/components/ui/table"; 15 | import type { DB } from "@/lib/db/schema"; 16 | import type { Selectable } from "kysely"; 17 | import Link from "next/link"; 18 | import { useState } from "react"; 19 | 20 | export function TasksList({ 21 | projectId, 22 | tasks, 23 | }: { 24 | projectId: number; 25 | tasks: Array & { revisions_count: number | null }>; 26 | }) { 27 | const [createTaskDialogOpen, setCreateTaskDialogOpen] = useState(false); 28 | 29 | return ( 30 |
31 |
32 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 44 | 45 | Name 46 | Git Revision Ref 47 | Git Branch Name 48 | Revisions 49 | 50 | 51 | 52 | {tasks.map((task) => ( 53 | 54 | 55 | 59 | {task.name} 60 | 61 | 62 | {task.git_revision_ref} 63 | {task.git_branch_name} 64 | {task.revisions_count} 65 | 66 | ))} 67 | {tasks.length === 0 && ( 68 | 69 | 70 | 71 | No task yet 72 | 73 | 74 | 75 | )} 76 | 77 |
78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/components/ai-elements/task.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Collapsible, 5 | CollapsibleContent, 6 | CollapsibleTrigger, 7 | } from "@/components/ui/collapsible"; 8 | import { cn } from "@/lib/utils"; 9 | import { ChevronDownIcon, SearchIcon } from "lucide-react"; 10 | import type { ComponentProps } from "react"; 11 | 12 | export type TaskItemFileProps = ComponentProps<"div">; 13 | 14 | export const TaskItemFile = ({ 15 | children, 16 | className, 17 | ...props 18 | }: TaskItemFileProps) => ( 19 |
26 | {children} 27 |
28 | ); 29 | 30 | export type TaskItemProps = ComponentProps<"div">; 31 | 32 | export const TaskItem = ({ children, className, ...props }: TaskItemProps) => ( 33 |
34 | {children} 35 |
36 | ); 37 | 38 | export type TaskProps = ComponentProps; 39 | 40 | export const Task = ({ 41 | defaultOpen = true, 42 | className, 43 | ...props 44 | }: TaskProps) => ( 45 | 46 | ); 47 | 48 | export type TaskTriggerProps = ComponentProps & { 49 | title: string; 50 | }; 51 | 52 | export const TaskTrigger = ({ 53 | children, 54 | className, 55 | title, 56 | ...props 57 | }: TaskTriggerProps) => ( 58 | 59 | {children ?? ( 60 |
61 | 62 |

{title}

63 | 64 |
65 | )} 66 |
67 | ); 68 | 69 | export type TaskContentProps = ComponentProps; 70 | 71 | export const TaskContent = ({ 72 | children, 73 | className, 74 | ...props 75 | }: TaskContentProps) => ( 76 | 83 |
84 | {children} 85 |
86 |
87 | ); 88 | -------------------------------------------------------------------------------- /src/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { ChevronRight, MoreHorizontal } from "lucide-react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { 8 | return