From a3d7da965b5e42cd1536ffc01fdec1bd64c87910 Mon Sep 17 00:00:00 2001 From: Benjamin Shafii Date: Wed, 11 Dec 2024 09:27:16 +0100 Subject: [PATCH] working --- web/app/actions.ts | 3 +- web/app/api/cron/redeploy/route.ts | 25 +- web/app/api/deployment/status/route.ts | 58 ++++ web/app/api/deployment/update-key/route.ts | 79 +++++ web/app/api/redeploy/route.ts | 29 +- web/app/dashboard/deployment/actions.ts | 132 ++++++++ web/app/dashboard/deployment/page.tsx | 334 +++++++++++++++++++++ web/components/ui/alert.tsx | 59 ++++ web/components/ui/badge.tsx | 37 +++ web/components/ui/separator.tsx | 30 ++ web/drizzle/0011_lame_green_goblin.sql | 6 + web/drizzle/0012_same_zodiak.sql | 2 + web/drizzle/0013_deep_blazing_skull.sql | 1 + web/drizzle/0014_demonic_flatman.sql | 1 + web/drizzle/meta/0011_snapshot.json | 194 ++++++++++++ web/drizzle/meta/0012_snapshot.json | 206 +++++++++++++ web/drizzle/meta/0013_snapshot.json | 213 +++++++++++++ web/drizzle/meta/0014_snapshot.json | 213 +++++++++++++ web/drizzle/meta/_journal.json | 28 ++ web/drizzle/schema.ts | 5 + web/lib/models.ts | 3 + web/package.json | 1 + web/pnpm-lock.yaml | 23 ++ 23 files changed, 1665 insertions(+), 17 deletions(-) create mode 100644 web/app/api/deployment/status/route.ts create mode 100644 web/app/api/deployment/update-key/route.ts create mode 100644 web/app/dashboard/deployment/actions.ts create mode 100644 web/app/dashboard/deployment/page.tsx create mode 100644 web/components/ui/alert.tsx create mode 100644 web/components/ui/badge.tsx create mode 100644 web/components/ui/separator.tsx create mode 100644 web/drizzle/0011_lame_green_goblin.sql create mode 100644 web/drizzle/0012_same_zodiak.sql create mode 100644 web/drizzle/0013_deep_blazing_skull.sql create mode 100644 web/drizzle/0014_demonic_flatman.sql create mode 100644 web/drizzle/meta/0011_snapshot.json create mode 100644 web/drizzle/meta/0012_snapshot.json create mode 100644 web/drizzle/meta/0013_snapshot.json create mode 100644 web/drizzle/meta/0014_snapshot.json diff --git a/web/app/actions.ts b/web/app/actions.ts index 9bf4b578..cbbdf066 100644 --- a/web/app/actions.ts +++ b/web/app/actions.ts @@ -1,5 +1,5 @@ "use server"; -import { auth } from "@clerk/nextjs/server"; +import { auth, } from "@clerk/nextjs/server"; import { Unkey } from "@unkey/api"; import { db, UserUsageTable as UserUsageTableImport } from "@/drizzle/schema"; import { eq } from "drizzle-orm"; @@ -33,6 +33,7 @@ export async function createLicenseKeyFromUserId(userId: string) { const name = "my api key"; const unkey = new Unkey({ token }); + console.log("Creating Unkey license key"); const key = await unkey.keys.create({ name: name, diff --git a/web/app/api/cron/redeploy/route.ts b/web/app/api/cron/redeploy/route.ts index 4c4ced37..59409c9b 100644 --- a/web/app/api/cron/redeploy/route.ts +++ b/web/app/api/cron/redeploy/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server"; import { db, vercelTokens } from "@/drizzle/schema"; import { Vercel } from "@vercel/sdk"; import { headers } from "next/headers"; +import { eq } from "drizzle-orm"; const CRON_SECRET = process.env.CRON_SECRET; @@ -23,6 +24,10 @@ export async function GET(request: Request) { console.log(`Found ${tokens.length} tokens to process`); + const repo = "file-organizer-2000"; + const org = "different-ai"; + const ref = "master"; + const results = await Promise.allSettled( tokens.map(async (tokenRecord) => { try { @@ -34,11 +39,8 @@ export async function GET(request: Request) { console.log(`No project ID for user ${tokenRecord.userId}`); return; } - const repo = "file-organizer-2000"; - const org = "different-ai"; - const ref = "master"; - // Create new deployment with correct properties + // Create new deployment const deployment = await vercel.deployments.createDeployment({ requestBody: { name: `file-organizer-redeploy-${Date.now()}`, @@ -46,9 +48,9 @@ export async function GET(request: Request) { project: tokenRecord.projectId, gitSource: { type: "github", - repo: repo, - ref: ref, - org: org, + repo, + ref, + org, }, projectSettings: { framework: "nextjs", @@ -60,6 +62,15 @@ export async function GET(request: Request) { }, }); + // Update last deployment timestamp + await db + .update(vercelTokens) + .set({ + lastDeployment: new Date(), + updatedAt: new Date(), + }) + .where(eq(vercelTokens.userId, tokenRecord.userId)); + console.log(`Redeployed project ${tokenRecord.projectId} for user ${tokenRecord.userId}`); return deployment; } catch (error) { diff --git a/web/app/api/deployment/status/route.ts b/web/app/api/deployment/status/route.ts new file mode 100644 index 00000000..05cec432 --- /dev/null +++ b/web/app/api/deployment/status/route.ts @@ -0,0 +1,58 @@ +import { NextResponse } from "next/server"; +import { db, vercelTokens } from "@/drizzle/schema"; +import { auth } from "@clerk/nextjs/server"; +import { eq } from "drizzle-orm"; +import { Vercel } from "@vercel/sdk"; + +export async function GET() { + try { + const { userId } = auth(); + if (!userId) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const [deployment] = await db + .select() + .from(vercelTokens) + .where(eq(vercelTokens.userId, userId)) + .limit(1); + + if (!deployment) { + return new NextResponse("No deployment found", { status: 404 }); + } + + // Check for API keys in Vercel environment + const vercel = new Vercel({ + bearerToken: deployment.token, + }); + + const envVars = await vercel.projects.filterProjectEnvs({ + idOrName: deployment.projectId, + }); + console.log('env vars', envVars); + + const openaiKeyPresent = envVars.envs.some( + (env) => env.key === "OPENAI_API_KEY" + ); + const anthropicKeyPresent = envVars.envs.some( + (env) => env.key === "ANTHROPIC_API_KEY" + ); + + return NextResponse.json({ + projectUrl: deployment.projectUrl, + deploymentUrl: deployment.deploymentUrl, + lastDeployment: deployment.lastDeployment, + modelName: deployment.modelName, + visionModelName: deployment.visionModelName, + lastApiKeyUpdate: deployment.lastApiKeyUpdate, + openaiKeyPresent, + anthropicKeyPresent, + }); + } catch (error) { + console.error("Error fetching deployment status:", error); + return NextResponse.json( + { error: "Failed to fetch deployment status" }, + { status: 500 } + ); + } +} diff --git a/web/app/api/deployment/update-key/route.ts b/web/app/api/deployment/update-key/route.ts new file mode 100644 index 00000000..6890a6c4 --- /dev/null +++ b/web/app/api/deployment/update-key/route.ts @@ -0,0 +1,79 @@ +import { NextResponse } from "next/server"; +import { db, vercelTokens } from "@/drizzle/schema"; +import { auth } from "@clerk/nextjs/server"; +import { eq } from "drizzle-orm"; +import { Vercel } from "@vercel/sdk"; + +export async function POST(request: Request) { + try { + const { userId } = auth(); + if (!userId) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const { provider, modelApiKey, modelName, soloApiKey } = await request.json(); + + const [deployment] = await db + .select() + .from(vercelTokens) + .where(eq(vercelTokens.userId, userId)) + .limit(1); + + if (!deployment) { + return new NextResponse("No deployment found", { status: 404 }); + } + + const vercel = new Vercel({ + bearerToken: deployment.token, + }); + + // Prepare environment variables + const envVars = [ + { + key: provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY', + value: modelApiKey, + type: "encrypted", + target: ["production"], + }, + ]; + + // Add SOLO_API_KEY if provided + if (soloApiKey) { + envVars.push({ + key: 'SOLO_API_KEY', + value: soloApiKey, + type: "encrypted", + target: ["production"], + }); + } + + // Update environment variables using the correct method + await vercel.projects.createProjectEnv({ + idOrName: deployment.projectId, + upsert: 'true', + // @ts-ignore + requestBody: envVars, + }) + + + + // Update database record + await db + .update(vercelTokens) + .set({ + modelProvider: provider, + modelName, + lastApiKeyUpdate: new Date(), + updatedAt: new Date(), + }) + .where(eq(vercelTokens.userId, userId)); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error updating API key:", error); + return NextResponse.json( + { error: "Failed to update API key" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/web/app/api/redeploy/route.ts b/web/app/api/redeploy/route.ts index 005dadf0..0b65ff43 100644 --- a/web/app/api/redeploy/route.ts +++ b/web/app/api/redeploy/route.ts @@ -11,33 +11,35 @@ export async function POST() { return new NextResponse("Unauthorized", { status: 401 }); } - const tokenRecord = await db + const [deployment] = await db .select() .from(vercelTokens) .where(eq(vercelTokens.userId, userId)) .limit(1); - if (!tokenRecord[0] || !tokenRecord[0].projectId) { + if (!deployment) { return new NextResponse("No deployment found", { status: 404 }); } const vercel = new Vercel({ - bearerToken: tokenRecord[0].token, + bearerToken: deployment.token, }); + const repo = "file-organizer-2000"; const org = "different-ai"; const ref = "master"; - const deployment = await vercel.deployments.createDeployment({ + // Create new deployment + const result = await vercel.deployments.createDeployment({ requestBody: { name: `file-organizer-redeploy-${Date.now()}`, target: "production", - project: tokenRecord[0].projectId, + project: deployment.projectId, gitSource: { type: "github", - repo: repo, - ref: ref, - org: org, + repo, + ref, + org, }, projectSettings: { framework: "nextjs", @@ -49,9 +51,18 @@ export async function POST() { }, }); + // Update last deployment timestamp + await db + .update(vercelTokens) + .set({ + lastDeployment: new Date(), + updatedAt: new Date(), + }) + .where(eq(vercelTokens.userId, userId)); + return NextResponse.json({ success: true, - deploymentUrl: deployment.url, + deploymentUrl: result.url, }); } catch (error) { console.error("Error in redeploy:", error); diff --git a/web/app/dashboard/deployment/actions.ts b/web/app/dashboard/deployment/actions.ts new file mode 100644 index 00000000..2cf9b841 --- /dev/null +++ b/web/app/dashboard/deployment/actions.ts @@ -0,0 +1,132 @@ +"use server"; + +import { db, vercelTokens } from "@/drizzle/schema"; +import { auth } from "@clerk/nextjs/server"; +import { eq } from "drizzle-orm"; +import { Vercel } from "@vercel/sdk"; +import { createLicenseKeyFromUserId } from "@/app/actions"; + +type UpdateKeysResult = { + success: boolean; + error?: string; + newLicenseKey?: string; +}; + +export async function updateKeys({ + modelName, + visionModelName, + openaiKey, + anthropicKey, + generateNewLicenseKey, +}: { + modelName: string; + visionModelName?: string; + openaiKey?: string; + anthropicKey?: string; + generateNewLicenseKey?: boolean; +}): Promise { + try { + const { userId } = auth(); + if (!userId) { + return { success: false, error: "Unauthorized" }; + } + + const [deployment] = await db + .select() + .from(vercelTokens) + .where(eq(vercelTokens.userId, userId)) + .limit(1); + + if (!deployment) { + return { success: false, error: "No deployment found" }; + } + + const vercel = new Vercel({ + bearerToken: deployment.token, + }); + + // Generate new license key if requested + let newLicenseKey: string | undefined; + if (generateNewLicenseKey) { + const apiKey = await createLicenseKeyFromUserId(userId); + newLicenseKey = apiKey.key.key; + } + + // Prepare environment variables + const envVars = [ + { + key: 'MODEL_NAME', + value: modelName, + type: "plain", + target: ["production"], + }, + ]; + + if (visionModelName) { + envVars.push({ + key: 'VISION_MODEL_NAME', + value: visionModelName, + type: "plain", + target: ["production"], + }); + } + + // Only add API keys if provided + if (openaiKey?.trim()) { + envVars.push({ + key: 'OPENAI_API_KEY', + value: openaiKey, + type: "encrypted", + target: ["production"], + }); + } + + if (anthropicKey?.trim()) { + envVars.push({ + key: 'ANTHROPIC_API_KEY', + value: anthropicKey, + type: "encrypted", + target: ["production"], + }); + } + + if (newLicenseKey) { + envVars.push({ + key: 'SOLO_API_KEY', + value: newLicenseKey, + type: "encrypted", + target: ["production"], + }); + } + + // Update environment variables + await vercel.projects.createProjectEnv({ + idOrName: deployment.projectId, + upsert: 'true', + // @ts-ignore + requestBody: envVars, + }); + + // Update database record + await db + .update(vercelTokens) + .set({ + modelName, + visionModelName: visionModelName || modelName, + lastApiKeyUpdate: new Date(), + updatedAt: new Date(), + }) + .where(eq(vercelTokens.userId, userId)); + + return { + success: true, + newLicenseKey, + }; + } catch (error) { + console.error("Error updating keys:", error); + return { + success: false, + error: "Failed to update keys", + }; + } +} \ No newline at end of file diff --git a/web/app/dashboard/deployment/page.tsx b/web/app/dashboard/deployment/page.tsx new file mode 100644 index 00000000..e104ff55 --- /dev/null +++ b/web/app/dashboard/deployment/page.tsx @@ -0,0 +1,334 @@ +"use client"; +import { useState, useEffect } from "react"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { toast } from "react-hot-toast"; +import { Loader2, RefreshCw, Settings2, Key, Wand2, AlertCircle, ExternalLink } from "lucide-react"; +import { updateKeys } from "./actions"; +import { Switch } from "@/components/ui/switch"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { getAvailableModels } from "@/lib/models"; + +export default function DeploymentDashboard() { + const [deployment, setDeployment] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isRedeploying, setIsRedeploying] = useState(false); + const [modelName, setModelName] = useState(''); + const [visionModelName, setVisionModelName] = useState(''); + const [openaiKey, setOpenaiKey] = useState(''); + const [anthropicKey, setAnthropicKey] = useState(''); + const [isGeneratingNewLicense, setIsGeneratingNewLicense] = useState(false); + const [availableModels] = useState(() => getAvailableModels()); + + useEffect(() => { + fetchDeploymentStatus(); + }, []); + + const fetchDeploymentStatus = async () => { + try { + const response = await fetch('/api/deployment/status'); + const data = await response.json(); + setDeployment(data); + } catch (error) { + toast.error('Failed to fetch deployment status'); + } finally { + setIsLoading(false); + } + }; + + const handleRedeploy = async () => { + setIsRedeploying(true); + try { + const response = await fetch('/api/redeploy', { + method: 'POST', + }); + + if (!response.ok) throw new Error('Failed to redeploy'); + + toast.success('Redeployment triggered successfully'); + await fetchDeploymentStatus(); + } catch (error) { + toast.error('Failed to trigger redeployment'); + } finally { + setIsRedeploying(false); + } + }; + + const handleUpdateKeys = async () => { + try { + const result = await updateKeys({ + modelName, + visionModelName, + openaiKey, + anthropicKey, + generateNewLicenseKey: isGeneratingNewLicense, + }); + + if (!result.success) { + throw new Error(result.error); + } + + if (result.newLicenseKey) { + toast.success('New license key generated successfully'); + } + + toast.success('Configuration updated successfully'); + setOpenaiKey(''); + setAnthropicKey(''); + await fetchDeploymentStatus(); + } catch (error) { + toast.error(error.message || 'Failed to update configuration'); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+

Deployment Dashboard

+

+ Manage your self-hosted instance configuration and deployment +

+
+ +
+ {/* Status Overview */} + + +
+
+ + Deployment Status + + {deployment?.lastDeployment ? "Active" : "Not Deployed"} + + + + Your instance details and deployment controls + +
+
+
+ +
+
+ +
+ + {deployment?.projectUrl || 'Not deployed'} + + {deployment?.projectUrl && ( + + )} +
+
+
+ +

+ {deployment?.lastDeployment + ? new Date(deployment.lastDeployment).toLocaleString() + : 'Never'} +

+
+
+ + + + Deployment Required + + After updating your configuration, you'll need to redeploy your instance for changes to take effect. + + + + +
+
+ + {/* Configuration */} + + +
+ + + Model Configuration + + + Configure your AI models and API keys + +
+
+ + {/* Models */} +
+

Models

+
+
+ + +

+ Current model: + + {deployment?.modelName || 'gpt-4o (default)'} + +

+
+ +
+ + +

+ Current model: + + {deployment?.visionModelName || 'gpt-4o (default)'} + +

+
+
+
+ + + + {/* API Keys */} +
+

API Keys

+
+
+
+ + + {deployment?.openaiKeyPresent ? "Configured" : "Not Set"} + +
+ setOpenaiKey(e.target.value)} + placeholder="Enter OpenAI API key (optional)" + /> +
+ +
+
+ + + {deployment?.anthropicKeyPresent ? "Configured" : "Not Set"} + +
+ setAnthropicKey(e.target.value)} + placeholder="Enter Anthropic API key (optional)" + /> +
+
+
+ + + + {/* License Key Generation */} +
+
+
+ +

+ Create a new license key for your instance +

+
+ +
+ + {isGeneratingNewLicense && ( + + + New License Key Generation + + This will create a new license key and update your deployment. + Make sure to update your plugin settings afterward. + + + )} +
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/web/components/ui/alert.tsx b/web/components/ui/alert.tsx new file mode 100644 index 00000000..69b6c7d4 --- /dev/null +++ b/web/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + warning: "border-yellow-500/50 text-yellow-600 dark:text-yellow-500 [&>svg]:text-yellow-600", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } \ No newline at end of file diff --git a/web/components/ui/badge.tsx b/web/components/ui/badge.tsx new file mode 100644 index 00000000..a3b53f31 --- /dev/null +++ b/web/components/ui/badge.tsx @@ -0,0 +1,37 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + success: + "border-transparent bg-green-500 text-white hover:bg-green-600", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } \ No newline at end of file diff --git a/web/components/ui/separator.tsx b/web/components/ui/separator.tsx new file mode 100644 index 00000000..335d0445 --- /dev/null +++ b/web/components/ui/separator.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" +import { cn } from "@/lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } \ No newline at end of file diff --git a/web/drizzle/0011_lame_green_goblin.sql b/web/drizzle/0011_lame_green_goblin.sql new file mode 100644 index 00000000..b1daea2b --- /dev/null +++ b/web/drizzle/0011_lame_green_goblin.sql @@ -0,0 +1,6 @@ +ALTER TABLE "vercel_tokens" ADD COLUMN "last_deployment" timestamp;--> statement-breakpoint +ALTER TABLE "vercel_tokens" ADD COLUMN "model_provider" text DEFAULT 'openai';--> statement-breakpoint +ALTER TABLE "vercel_tokens" ADD COLUMN "model_name" text DEFAULT 'gpt-4o';--> statement-breakpoint +ALTER TABLE "vercel_tokens" ADD COLUMN "last_api_key_update" timestamp;--> statement-breakpoint +ALTER TABLE "user_usage" DROP COLUMN IF EXISTS "currentProduct";--> statement-breakpoint +ALTER TABLE "user_usage" DROP COLUMN IF EXISTS "currentPlan"; \ No newline at end of file diff --git a/web/drizzle/0012_same_zodiak.sql b/web/drizzle/0012_same_zodiak.sql new file mode 100644 index 00000000..f492c8f8 --- /dev/null +++ b/web/drizzle/0012_same_zodiak.sql @@ -0,0 +1,2 @@ +ALTER TABLE "user_usage" ADD COLUMN "currentProduct" text;--> statement-breakpoint +ALTER TABLE "user_usage" ADD COLUMN "currentPlan" text; \ No newline at end of file diff --git a/web/drizzle/0013_deep_blazing_skull.sql b/web/drizzle/0013_deep_blazing_skull.sql new file mode 100644 index 00000000..cdd407c4 --- /dev/null +++ b/web/drizzle/0013_deep_blazing_skull.sql @@ -0,0 +1 @@ +ALTER TABLE "vercel_tokens" ADD COLUMN "vision_model_name" text DEFAULT 'gpt-4-vision'; \ No newline at end of file diff --git a/web/drizzle/0014_demonic_flatman.sql b/web/drizzle/0014_demonic_flatman.sql new file mode 100644 index 00000000..58bc5d93 --- /dev/null +++ b/web/drizzle/0014_demonic_flatman.sql @@ -0,0 +1 @@ +ALTER TABLE "vercel_tokens" ALTER COLUMN "vision_model_name" SET DEFAULT 'gpt-4o'; \ No newline at end of file diff --git a/web/drizzle/meta/0011_snapshot.json b/web/drizzle/meta/0011_snapshot.json new file mode 100644 index 00000000..9b351310 --- /dev/null +++ b/web/drizzle/meta/0011_snapshot.json @@ -0,0 +1,194 @@ +{ + "id": "7ee7870b-84ba-4c23-9617-df8fa9d3ed83", + "prevId": "0f046a77-dcd3-4849-91bd-893616881b72", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.user_usage": { + "name": "user_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billingCycle": { + "name": "billingCycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tokenUsage": { + "name": "tokenUsage", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "maxTokenUsage": { + "name": "maxTokenUsage", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "subscriptionStatus": { + "name": "subscriptionStatus", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inactive'" + }, + "paymentStatus": { + "name": "paymentStatus", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unpaid'" + }, + "lastPayment": { + "name": "lastPayment", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unique_user_idx": { + "name": "unique_user_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_usage_userId_unique": { + "name": "user_usage_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "userId" + ] + } + } + }, + "public.vercel_tokens": { + "name": "vercel_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_url": { + "name": "deployment_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_url": { + "name": "project_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_deployment": { + "name": "last_deployment", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "model_provider": { + "name": "model_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openai'" + }, + "model_name": { + "name": "model_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'gpt-4o'" + }, + "last_api_key_update": { + "name": "last_api_key_update", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/web/drizzle/meta/0012_snapshot.json b/web/drizzle/meta/0012_snapshot.json new file mode 100644 index 00000000..393c795c --- /dev/null +++ b/web/drizzle/meta/0012_snapshot.json @@ -0,0 +1,206 @@ +{ + "id": "0379405f-a95f-42db-8460-21bcbb06cda0", + "prevId": "7ee7870b-84ba-4c23-9617-df8fa9d3ed83", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.user_usage": { + "name": "user_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billingCycle": { + "name": "billingCycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tokenUsage": { + "name": "tokenUsage", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "maxTokenUsage": { + "name": "maxTokenUsage", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "subscriptionStatus": { + "name": "subscriptionStatus", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inactive'" + }, + "paymentStatus": { + "name": "paymentStatus", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unpaid'" + }, + "lastPayment": { + "name": "lastPayment", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "currentProduct": { + "name": "currentProduct", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currentPlan": { + "name": "currentPlan", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unique_user_idx": { + "name": "unique_user_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_usage_userId_unique": { + "name": "user_usage_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "userId" + ] + } + } + }, + "public.vercel_tokens": { + "name": "vercel_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_url": { + "name": "deployment_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_url": { + "name": "project_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_deployment": { + "name": "last_deployment", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "model_provider": { + "name": "model_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openai'" + }, + "model_name": { + "name": "model_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'gpt-4o'" + }, + "last_api_key_update": { + "name": "last_api_key_update", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/web/drizzle/meta/0013_snapshot.json b/web/drizzle/meta/0013_snapshot.json new file mode 100644 index 00000000..6c7b9a1f --- /dev/null +++ b/web/drizzle/meta/0013_snapshot.json @@ -0,0 +1,213 @@ +{ + "id": "787189fb-c9f2-44fe-8452-51e682d78d7d", + "prevId": "0379405f-a95f-42db-8460-21bcbb06cda0", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.user_usage": { + "name": "user_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billingCycle": { + "name": "billingCycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tokenUsage": { + "name": "tokenUsage", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "maxTokenUsage": { + "name": "maxTokenUsage", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "subscriptionStatus": { + "name": "subscriptionStatus", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inactive'" + }, + "paymentStatus": { + "name": "paymentStatus", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unpaid'" + }, + "lastPayment": { + "name": "lastPayment", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "currentProduct": { + "name": "currentProduct", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currentPlan": { + "name": "currentPlan", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unique_user_idx": { + "name": "unique_user_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_usage_userId_unique": { + "name": "user_usage_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "userId" + ] + } + } + }, + "public.vercel_tokens": { + "name": "vercel_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_url": { + "name": "deployment_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_url": { + "name": "project_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_deployment": { + "name": "last_deployment", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "model_provider": { + "name": "model_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openai'" + }, + "model_name": { + "name": "model_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'gpt-4o'" + }, + "vision_model_name": { + "name": "vision_model_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'gpt-4-vision'" + }, + "last_api_key_update": { + "name": "last_api_key_update", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/web/drizzle/meta/0014_snapshot.json b/web/drizzle/meta/0014_snapshot.json new file mode 100644 index 00000000..2034d1e7 --- /dev/null +++ b/web/drizzle/meta/0014_snapshot.json @@ -0,0 +1,213 @@ +{ + "id": "b87b6b2b-adda-4b61-804a-049e755e0118", + "prevId": "787189fb-c9f2-44fe-8452-51e682d78d7d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.user_usage": { + "name": "user_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billingCycle": { + "name": "billingCycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tokenUsage": { + "name": "tokenUsage", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "maxTokenUsage": { + "name": "maxTokenUsage", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "subscriptionStatus": { + "name": "subscriptionStatus", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inactive'" + }, + "paymentStatus": { + "name": "paymentStatus", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unpaid'" + }, + "lastPayment": { + "name": "lastPayment", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "currentProduct": { + "name": "currentProduct", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currentPlan": { + "name": "currentPlan", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unique_user_idx": { + "name": "unique_user_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_usage_userId_unique": { + "name": "user_usage_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "userId" + ] + } + } + }, + "public.vercel_tokens": { + "name": "vercel_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_url": { + "name": "deployment_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_url": { + "name": "project_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_deployment": { + "name": "last_deployment", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "model_provider": { + "name": "model_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openai'" + }, + "model_name": { + "name": "model_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'gpt-4o'" + }, + "vision_model_name": { + "name": "vision_model_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'gpt-4o'" + }, + "last_api_key_update": { + "name": "last_api_key_update", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/web/drizzle/meta/_journal.json b/web/drizzle/meta/_journal.json index e8787e8e..c252591a 100644 --- a/web/drizzle/meta/_journal.json +++ b/web/drizzle/meta/_journal.json @@ -78,6 +78,34 @@ "when": 1733842232823, "tag": "0010_certain_groot", "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1733850672730, + "tag": "0011_lame_green_goblin", + "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1733851737569, + "tag": "0012_same_zodiak", + "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1733860577320, + "tag": "0013_deep_blazing_skull", + "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1733904157417, + "tag": "0014_demonic_flatman", + "breakpoints": true } ] } \ No newline at end of file diff --git a/web/drizzle/schema.ts b/web/drizzle/schema.ts index b1122e60..b12e186f 100644 --- a/web/drizzle/schema.ts +++ b/web/drizzle/schema.ts @@ -47,6 +47,11 @@ export const vercelTokens = pgTable('vercel_tokens', { projectUrl: text("project_url"), createdAt: timestamp("created_at").defaultNow(), updatedAt: timestamp("updated_at").defaultNow(), + lastDeployment: timestamp("last_deployment"), + modelProvider: text("model_provider").default('openai'), + modelName: text("model_name").default('gpt-4o'), + visionModelName: text("vision_model_name").default('gpt-4o'), + lastApiKeyUpdate: timestamp("last_api_key_update"), }); export type VercelToken = typeof vercelTokens.$inferSelect; diff --git a/web/lib/models.ts b/web/lib/models.ts index 132858b9..775eeecc 100644 --- a/web/lib/models.ts +++ b/web/lib/models.ts @@ -21,3 +21,6 @@ export const getModel = (name: string) => { return models[name]; }; +export const getAvailableModels = () => { + return Object.keys(models); +}; diff --git a/web/package.json b/web/package.json index 1a356b13..5ec24c24 100644 --- a/web/package.json +++ b/web/package.json @@ -24,6 +24,7 @@ "@hookform/resolvers": "^3.9.1", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 92cba0b3..5d44a75c 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -26,6 +26,9 @@ dependencies: '@radix-ui/react-select': specifier: ^2.1.2 version: 2.1.2(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-separator': + specifier: ^1.1.0 + version: 1.1.0(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': specifier: ^1.1.0 version: 1.1.0(@types/react@18.2.28)(react@18.2.0) @@ -2509,6 +2512,26 @@ packages: react-remove-scroll: 2.6.0(@types/react@18.2.28)(react@18.2.0) dev: false + /@radix-ui/react-separator@1.1.0(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.28 + '@types/react-dom': 18.2.13 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-slot@1.1.0(@types/react@18.2.28)(react@18.2.0): resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} peerDependencies: