From 3e21c2334cd053f5d0e497e686d3ce34be9a6e00 Mon Sep 17 00:00:00 2001 From: "dmitry.galkin" Date: Mon, 25 May 2026 14:54:04 +0400 Subject: [PATCH] Initial commit: EGBE chatbot template Stripped from vercel/chatbot (Apache 2.0): - Dropped @vercel/* packages and AI Gateway - Removed artifacts feature (code/text/sheet/image side panel) - Switched AI provider to @ai-sdk/openai-compatible -> EGBE LiteLLM - Replaced Vercel Blob upload with data URLs - Dropped Redis resumable streams and rate limiter (in-memory now) - Added Dockerfile (Next.js standalone) + entrypoint that runs migrations - Wired DATABASE_URL, EGBE_AI_API_URL/KEY, NEXT_PUBLIC_BASE_URL for app-deploy.sh --- .dockerignore | 15 + .env.example | 12 + .gitignore | 27 + .vscode/extensions.json | 3 + .vscode/settings.json | 53 + Dockerfile | 46 + LICENSE | 13 + README.md | 57 + app/(auth)/actions.ts | 84 + app/(auth)/api/auth/[...nextauth]/route.ts | 1 + app/(auth)/api/auth/guest/route.ts | 26 + app/(auth)/auth.config.ts | 14 + app/(auth)/auth.ts | 99 + app/(auth)/layout.tsx | 43 + app/(auth)/login/page.tsx | 66 + app/(auth)/register/page.tsx | 66 + app/(chat)/actions.ts | 78 + app/(chat)/api/chat/[id]/stream/route.ts | 3 + app/(chat)/api/chat/route.ts | 272 + app/(chat)/api/chat/schema.ts | 37 + app/(chat)/api/files/upload/route.ts | 64 + app/(chat)/api/history/route.ts | 49 + app/(chat)/api/messages/route.ts | 43 + app/(chat)/api/models/route.ts | 9 + app/(chat)/api/vote/route.ts | 84 + app/(chat)/chat/[id]/page.tsx | 3 + app/(chat)/layout.tsx | 46 + app/(chat)/opengraph-image.png | Bin 0 -> 14418 bytes app/(chat)/page.tsx | 3 + app/(chat)/twitter-image.png | Bin 0 -> 52049 bytes app/favicon.ico | Bin 0 -> 15406 bytes app/globals.css | 500 ++ app/layout.tsx | 88 + biome.jsonc | 56 + components.json | 20 + components/ai-elements/code-block.tsx | 555 ++ components/ai-elements/conversation.tsx | 167 + components/ai-elements/message.tsx | 357 + components/ai-elements/model-selector.tsx | 208 + components/ai-elements/prompt-input.tsx | 1341 ++++ components/ai-elements/reasoning.tsx | 241 + components/ai-elements/shimmer.tsx | 78 + components/ai-elements/suggestion.tsx | 58 + components/ai-elements/tool.tsx | 172 + components/chat/app-sidebar.tsx | 165 + components/chat/auth-form.tsx | 53 + components/chat/chat-header.tsx | 76 + components/chat/data-stream-handler.tsx | 29 + components/chat/data-stream-provider.tsx | 41 + components/chat/greeting.tsx | 24 + components/chat/icons.tsx | 1213 ++++ components/chat/message-actions.tsx | 207 + components/chat/message-editor.tsx | 33 + components/chat/message-reasoning.tsx | 37 + components/chat/message.tsx | 301 + components/chat/messages.tsx | 126 + components/chat/multimodal-input.tsx | 816 +++ components/chat/preview-attachment.tsx | 54 + components/chat/preview.tsx | 59 + components/chat/shell.tsx | 128 + components/chat/sidebar-history-item.tsx | 124 + components/chat/sidebar-history.tsx | 373 + components/chat/sidebar-toggle.tsx | 35 + components/chat/sidebar-user-nav.tsx | 121 + components/chat/sign-out-form.tsx | 25 + components/chat/slash-commands.tsx | 137 + components/chat/submit-button.tsx | 38 + components/chat/suggested-actions.tsx | 78 + components/chat/toast.tsx | 75 + components/chat/visibility-selector.tsx | 111 + components/chat/weather.tsx | 434 ++ components/theme-provider.tsx | 8 + components/ui/alert-dialog.tsx | 199 + components/ui/badge.tsx | 49 + components/ui/button-group.tsx | 83 + components/ui/button.tsx | 65 + components/ui/collapsible.tsx | 33 + components/ui/command.tsx | 194 + components/ui/dialog.tsx | 165 + components/ui/dropdown-menu.tsx | 269 + components/ui/hover-card.tsx | 44 + components/ui/input-group.tsx | 155 + components/ui/input.tsx | 19 + components/ui/label.tsx | 24 + components/ui/popover.tsx | 40 + components/ui/scroll-area.tsx | 55 + components/ui/select.tsx | 195 + components/ui/separator.tsx | 28 + components/ui/sheet.tsx | 144 + components/ui/sidebar.tsx | 713 ++ components/ui/skeleton.tsx | 13 + components/ui/spinner.tsx | 10 + components/ui/textarea.tsx | 18 + components/ui/tooltip.tsx | 57 + docker-entrypoint.sh | 22 + drizzle.config.ts | 15 + hooks/use-active-chat.tsx | 284 + hooks/use-chat-visibility.ts | 55 + hooks/use-messages.tsx | 39 + hooks/use-mobile.ts | 19 + hooks/use-scroll-to-bottom.tsx | 124 + lib/ai/entitlements.ts | 14 + lib/ai/models.mock.ts | 123 + lib/ai/models.test.ts | 67 + lib/ai/models.ts | 88 + lib/ai/prompts.ts | 37 + lib/ai/providers.ts | 36 + lib/ai/tools/get-weather.ts | 78 + lib/constants.ts | 20 + lib/db/migrate.ts | 33 + lib/db/migrations/0000_bouncy_payback.sql | 59 + lib/db/migrations/meta/0000_snapshot.json | 265 + lib/db/migrations/meta/_journal.json | 13 + lib/db/queries.ts | 426 ++ lib/db/schema.ts | 70 + lib/db/utils.ts | 16 + lib/errors.ts | 119 + lib/ratelimit.ts | 27 + lib/types.ts | 32 + lib/utils.ts | 77 + next.config.ts | 29 + package.json | 77 + pnpm-lock.yaml | 6905 +++++++++++++++++++ postcss.config.mjs | 8 + proxy.ts | 51 + public/images/demo-thumbnail.png | Bin 0 -> 23198 bytes public/images/mouth of the seine, monet.jpg | Bin 0 -> 33497 bytes public/preview.png | Bin 0 -> 39834 bytes tsconfig.json | 35 + 129 files changed, 21913 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/(auth)/actions.ts create mode 100644 app/(auth)/api/auth/[...nextauth]/route.ts create mode 100644 app/(auth)/api/auth/guest/route.ts create mode 100644 app/(auth)/auth.config.ts create mode 100644 app/(auth)/auth.ts create mode 100644 app/(auth)/layout.tsx create mode 100644 app/(auth)/login/page.tsx create mode 100644 app/(auth)/register/page.tsx create mode 100644 app/(chat)/actions.ts create mode 100644 app/(chat)/api/chat/[id]/stream/route.ts create mode 100644 app/(chat)/api/chat/route.ts create mode 100644 app/(chat)/api/chat/schema.ts create mode 100644 app/(chat)/api/files/upload/route.ts create mode 100644 app/(chat)/api/history/route.ts create mode 100644 app/(chat)/api/messages/route.ts create mode 100644 app/(chat)/api/models/route.ts create mode 100644 app/(chat)/api/vote/route.ts create mode 100644 app/(chat)/chat/[id]/page.tsx create mode 100644 app/(chat)/layout.tsx create mode 100644 app/(chat)/opengraph-image.png create mode 100644 app/(chat)/page.tsx create mode 100644 app/(chat)/twitter-image.png create mode 100644 app/favicon.ico create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 biome.jsonc create mode 100644 components.json create mode 100644 components/ai-elements/code-block.tsx create mode 100644 components/ai-elements/conversation.tsx create mode 100644 components/ai-elements/message.tsx create mode 100644 components/ai-elements/model-selector.tsx create mode 100644 components/ai-elements/prompt-input.tsx create mode 100644 components/ai-elements/reasoning.tsx create mode 100644 components/ai-elements/shimmer.tsx create mode 100644 components/ai-elements/suggestion.tsx create mode 100644 components/ai-elements/tool.tsx create mode 100644 components/chat/app-sidebar.tsx create mode 100644 components/chat/auth-form.tsx create mode 100644 components/chat/chat-header.tsx create mode 100644 components/chat/data-stream-handler.tsx create mode 100644 components/chat/data-stream-provider.tsx create mode 100644 components/chat/greeting.tsx create mode 100644 components/chat/icons.tsx create mode 100644 components/chat/message-actions.tsx create mode 100644 components/chat/message-editor.tsx create mode 100644 components/chat/message-reasoning.tsx create mode 100644 components/chat/message.tsx create mode 100644 components/chat/messages.tsx create mode 100644 components/chat/multimodal-input.tsx create mode 100644 components/chat/preview-attachment.tsx create mode 100644 components/chat/preview.tsx create mode 100644 components/chat/shell.tsx create mode 100644 components/chat/sidebar-history-item.tsx create mode 100644 components/chat/sidebar-history.tsx create mode 100644 components/chat/sidebar-toggle.tsx create mode 100644 components/chat/sidebar-user-nav.tsx create mode 100644 components/chat/sign-out-form.tsx create mode 100644 components/chat/slash-commands.tsx create mode 100644 components/chat/submit-button.tsx create mode 100644 components/chat/suggested-actions.tsx create mode 100644 components/chat/toast.tsx create mode 100644 components/chat/visibility-selector.tsx create mode 100644 components/chat/weather.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button-group.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/hover-card.tsx create mode 100644 components/ui/input-group.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/spinner.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 docker-entrypoint.sh create mode 100644 drizzle.config.ts create mode 100644 hooks/use-active-chat.tsx create mode 100644 hooks/use-chat-visibility.ts create mode 100644 hooks/use-messages.tsx create mode 100644 hooks/use-mobile.ts create mode 100644 hooks/use-scroll-to-bottom.tsx create mode 100644 lib/ai/entitlements.ts create mode 100644 lib/ai/models.mock.ts create mode 100644 lib/ai/models.test.ts create mode 100644 lib/ai/models.ts create mode 100644 lib/ai/prompts.ts create mode 100644 lib/ai/providers.ts create mode 100644 lib/ai/tools/get-weather.ts create mode 100644 lib/constants.ts create mode 100644 lib/db/migrate.ts create mode 100644 lib/db/migrations/0000_bouncy_payback.sql create mode 100644 lib/db/migrations/meta/0000_snapshot.json create mode 100644 lib/db/migrations/meta/_journal.json create mode 100644 lib/db/queries.ts create mode 100644 lib/db/schema.ts create mode 100644 lib/db/utils.ts create mode 100644 lib/errors.ts create mode 100644 lib/ratelimit.ts create mode 100644 lib/types.ts create mode 100644 lib/utils.ts create mode 100644 next.config.ts create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.mjs create mode 100644 proxy.ts create mode 100644 public/images/demo-thumbnail.png create mode 100644 public/images/mouth of the seine, monet.jpg create mode 100644 public/preview.png create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3cabaa5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +Dockerfile +.dockerignore +.git +.gitignore +.next +node_modules +npm-debug.log +README.md +.env* +!.env.example +tests +playwright-report +test-results +.vscode +.idea diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..73adfe5 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# NextAuth secret — generate with: openssl rand -base64 32 +AUTH_SECRET=replace-me + +# Postgres connection string (provisioned automatically by app-deploy.sh) +DATABASE_URL=postgres://user:pass@host:5432/db + +# EGBE LiteLLM proxy — auto-injected by app-deploy.sh from the deploying instance +EGBE_AI_API_URL=https://proxy.gcp.egbe.dev/v1 +EGBE_AI_API_KEY= + +# Public origin of this app — auto-injected by app-deploy.sh +NEXT_PUBLIC_BASE_URL=http://localhost:3000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..344b787 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +node_modules +.pnp +.pnp.js +coverage +.next/ +out/ +build +.DS_Store +*.pem +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +.env.local +.env.development.local +.env.test.local +.env.production.local +.turbo +.env +.vercel +.env*.local +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/* +next-env.d.ts +tsconfig.tsbuildinfo diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..699ed73 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["biomejs.biome"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b52f23 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,53 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[css]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[graphql]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "typescript.tsdk": "node_modules/typescript/lib", + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "emmet.showExpandedAbbreviation": "never", + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "[html]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[vue]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[svelte]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[yaml]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[markdown]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[mdx]": { + "editor.defaultFormatter": "biomejs.biome" + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1633b84 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +FROM node:20-alpine AS base + +# --- deps --- +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app +RUN corepack enable +COPY package.json pnpm-lock.yaml* ./ +RUN pnpm install --frozen-lockfile + +# --- builder --- +FROM base AS builder +WORKDIR /app +RUN corepack enable +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +RUN pnpm run build + +# --- runner --- +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV HOSTNAME=0.0.0.0 +ENV PORT=3000 + +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/lib/db/migrations ./lib/db/migrations +COPY --from=builder --chown=nextjs:nodejs /app/lib/db/migrate.ts ./lib/db/migrate.ts +COPY --from=builder --chown=nextjs:nodejs /app/node_modules/drizzle-orm ./node_modules/drizzle-orm +COPY --from=builder --chown=nextjs:nodejs /app/node_modules/postgres ./node_modules/postgres +COPY --from=builder --chown=nextjs:nodejs /app/node_modules/dotenv ./node_modules/dotenv +COPY --chown=nextjs:nodejs docker-entrypoint.sh ./docker-entrypoint.sh +RUN chmod +x ./docker-entrypoint.sh + +USER nextjs +EXPOSE 3000 +ENTRYPOINT ["./docker-entrypoint.sh"] +CMD ["node", "server.js"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..695ee2d --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2024 Vercel, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f164e63 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# EGBE Chatbot Template + +Next.js chatbot wired to the EGBE LiteLLM proxy. Based on [vercel/chatbot](https://github.com/vercel/chatbot) (Apache 2.0), stripped of Vercel-specific bits and rewired for our infra. + +## Stack + +- **Next.js 16** (App Router, standalone output) +- **AI SDK** + `@ai-sdk/openai-compatible` → routes to `EGBE_AI_API_URL` (our LiteLLM) +- **NextAuth** (credentials + guest) +- **Drizzle ORM** + Postgres (DB gateway) +- **shadcn/ui** + Tailwind + +## Available models (via LiteLLM) + +`claude-sonnet`, `claude-haiku`, `gpt-5.4`, `gpt-5.4-mini`, `kimi-k2.6`, `minimax-m2.7`, `qwen3-coder`. + +Edit `lib/ai/models.ts` to add or remove. + +## Deploy (from inside an OpenClaw instance) + +```bash +${SKILLS_DIR}/scripts/app-deploy.sh chatbot-template +``` + +`app-deploy.sh` auto-injects: +- `DATABASE_URL` (per-app Postgres) +- `EGBE_AI_API_URL`, `EGBE_AI_API_KEY` (instance LiteLLM key) +- `NEXT_PUBLIC_BASE_URL` (the deployed `*.p.egbe.app` host) + +You also need `AUTH_SECRET` — set it via `APP_ENV_JSON`: + +```bash +APP_ENV_JSON='{"AUTH_SECRET":"'$(openssl rand -base64 32)'"}' \ + ${SKILLS_DIR}/scripts/app-deploy.sh mychat chatbot-template +``` + +## Local dev + +```bash +cp .env.example .env.local +# fill in DATABASE_URL, EGBE_AI_API_KEY, AUTH_SECRET +pnpm install +pnpm db:migrate +pnpm dev +``` + +## Tracking upstream + +Pull new vercel/chatbot features manually — `origin/upstream` is not configured by default here. Add it back with: + +```bash +git remote add upstream https://github.com/vercel/chatbot.git +``` + +## License + +Apache 2.0 — see `LICENSE`. Derivative work of Vercel, Inc.'s [chatbot](https://github.com/vercel/chatbot). diff --git a/app/(auth)/actions.ts b/app/(auth)/actions.ts new file mode 100644 index 0000000..024ff51 --- /dev/null +++ b/app/(auth)/actions.ts @@ -0,0 +1,84 @@ +"use server"; + +import { z } from "zod"; + +import { createUser, getUser } from "@/lib/db/queries"; + +import { signIn } from "./auth"; + +const authFormSchema = z.object({ + email: z.string().email(), + password: z.string().min(6), +}); + +export type LoginActionState = { + status: "idle" | "in_progress" | "success" | "failed" | "invalid_data"; +}; + +export const login = async ( + _: LoginActionState, + formData: FormData +): Promise => { + try { + const validatedData = authFormSchema.parse({ + email: formData.get("email"), + password: formData.get("password"), + }); + + await signIn("credentials", { + email: validatedData.email, + password: validatedData.password, + redirect: false, + }); + + return { status: "success" }; + } catch (error) { + if (error instanceof z.ZodError) { + return { status: "invalid_data" }; + } + + return { status: "failed" }; + } +}; + +export type RegisterActionState = { + status: + | "idle" + | "in_progress" + | "success" + | "failed" + | "user_exists" + | "invalid_data"; +}; + +export const register = async ( + _: RegisterActionState, + formData: FormData +): Promise => { + try { + const validatedData = authFormSchema.parse({ + email: formData.get("email"), + password: formData.get("password"), + }); + + const [user] = await getUser(validatedData.email); + + if (user) { + return { status: "user_exists" } as RegisterActionState; + } + await createUser(validatedData.email, validatedData.password); + await signIn("credentials", { + email: validatedData.email, + password: validatedData.password, + redirect: false, + }); + + return { status: "success" }; + } catch (error) { + if (error instanceof z.ZodError) { + return { status: "invalid_data" }; + } + + return { status: "failed" }; + } +}; diff --git a/app/(auth)/api/auth/[...nextauth]/route.ts b/app/(auth)/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..d104b65 --- /dev/null +++ b/app/(auth)/api/auth/[...nextauth]/route.ts @@ -0,0 +1 @@ +export { GET, POST } from "@/app/(auth)/auth"; diff --git a/app/(auth)/api/auth/guest/route.ts b/app/(auth)/api/auth/guest/route.ts new file mode 100644 index 0000000..97ce3d2 --- /dev/null +++ b/app/(auth)/api/auth/guest/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; +import { getToken } from "next-auth/jwt"; +import { signIn } from "@/app/(auth)/auth"; +import { isDevelopmentEnvironment } from "@/lib/constants"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const rawRedirect = searchParams.get("redirectUrl") || "/"; + const redirectUrl = + rawRedirect.startsWith("/") && !rawRedirect.startsWith("//") + ? rawRedirect + : "/"; + + const token = await getToken({ + req: request, + secret: process.env.AUTH_SECRET, + secureCookie: !isDevelopmentEnvironment, + }); + + if (token) { + const base = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; + return NextResponse.redirect(new URL(`${base}/`, request.url)); + } + + return signIn("guest", { redirect: true, redirectTo: redirectUrl }); +} diff --git a/app/(auth)/auth.config.ts b/app/(auth)/auth.config.ts new file mode 100644 index 0000000..5056101 --- /dev/null +++ b/app/(auth)/auth.config.ts @@ -0,0 +1,14 @@ +import type { NextAuthConfig } from "next-auth"; + +const base = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; + +export const authConfig = { + basePath: "/api/auth", + trustHost: true, + pages: { + signIn: `${base}/login`, + newUser: `${base}/`, + }, + providers: [], + callbacks: {}, +} satisfies NextAuthConfig; diff --git a/app/(auth)/auth.ts b/app/(auth)/auth.ts new file mode 100644 index 0000000..b75369f --- /dev/null +++ b/app/(auth)/auth.ts @@ -0,0 +1,99 @@ +import { compare } from "bcrypt-ts"; +import NextAuth, { type DefaultSession } from "next-auth"; +import type { DefaultJWT } from "next-auth/jwt"; +import Credentials from "next-auth/providers/credentials"; +import { DUMMY_PASSWORD } from "@/lib/constants"; +import { createGuestUser, getUser } from "@/lib/db/queries"; +import { authConfig } from "./auth.config"; + +export type UserType = "guest" | "regular"; + +declare module "next-auth" { + interface Session extends DefaultSession { + user: { + id: string; + type: UserType; + } & DefaultSession["user"]; + } + + interface User { + id?: string; + email?: string | null; + type: UserType; + } +} + +declare module "next-auth/jwt" { + interface JWT extends DefaultJWT { + id: string; + type: UserType; + } +} + +export const { + handlers: { GET, POST }, + auth, + signIn, + signOut, +} = NextAuth({ + ...authConfig, + providers: [ + Credentials({ + credentials: { + email: { label: "Email", type: "email" }, + password: { label: "Password", type: "password" }, + }, + async authorize(credentials) { + const email = String(credentials.email ?? ""); + const password = String(credentials.password ?? ""); + const users = await getUser(email); + + if (users.length === 0) { + await compare(password, DUMMY_PASSWORD); + return null; + } + + const [user] = users; + + if (!user.password) { + await compare(password, DUMMY_PASSWORD); + return null; + } + + const passwordsMatch = await compare(password, user.password); + + if (!passwordsMatch) { + return null; + } + + return { ...user, type: "regular" }; + }, + }), + Credentials({ + id: "guest", + credentials: {}, + async authorize() { + const [guestUser] = await createGuestUser(); + return { ...guestUser, type: "guest" }; + }, + }), + ], + callbacks: { + jwt({ token, user }) { + if (user) { + token.id = user.id as string; + token.type = user.type; + } + + return token; + }, + session({ session, token }) { + if (session.user) { + session.user.id = token.id; + session.user.type = token.type; + } + + return session; + }, + }, +}); diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx new file mode 100644 index 0000000..d14e92c --- /dev/null +++ b/app/(auth)/layout.tsx @@ -0,0 +1,43 @@ +import { ArrowLeftIcon } from "lucide-react"; +import Link from "next/link"; +import { SparklesIcon, VercelIcon } from "@/components/chat/icons"; +import { Preview } from "@/components/chat/preview"; + +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
+ + + Back + +
+
+
+ +
+ {children} +
+
+
+ +
+
+ Powered by + + AI Gateway +
+
+ +
+
+
+ ); +} diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx new file mode 100644 index 0000000..ea4c602 --- /dev/null +++ b/app/(auth)/login/page.tsx @@ -0,0 +1,66 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useSession } from "next-auth/react"; +import { useActionState, useEffect, useState } from "react"; + +import { AuthForm } from "@/components/chat/auth-form"; +import { SubmitButton } from "@/components/chat/submit-button"; +import { toast } from "@/components/chat/toast"; +import { type LoginActionState, login } from "../actions"; + +export default function Page() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [isSuccessful, setIsSuccessful] = useState(false); + + const [state, formAction] = useActionState( + login, + { status: "idle" } + ); + + const { update: updateSession } = useSession(); + + // biome-ignore lint/correctness/useExhaustiveDependencies: router and updateSession are stable refs + useEffect(() => { + if (state.status === "failed") { + toast({ type: "error", description: "Invalid credentials!" }); + } else if (state.status === "invalid_data") { + toast({ + type: "error", + description: "Failed validating your submission!", + }); + } else if (state.status === "success") { + setIsSuccessful(true); + updateSession(); + router.refresh(); + } + }, [state.status]); + + const handleSubmit = (formData: FormData) => { + setEmail(formData.get("email") as string); + formAction(formData); + }; + + return ( + <> +

Welcome back

+

+ Sign in to your account to continue +

+ + Sign in +

+ {"No account? "} + + Sign up + +

+
+ + ); +} diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx new file mode 100644 index 0000000..f2abbc8 --- /dev/null +++ b/app/(auth)/register/page.tsx @@ -0,0 +1,66 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useSession } from "next-auth/react"; +import { useActionState, useEffect, useState } from "react"; +import { AuthForm } from "@/components/chat/auth-form"; +import { SubmitButton } from "@/components/chat/submit-button"; +import { toast } from "@/components/chat/toast"; +import { type RegisterActionState, register } from "../actions"; + +export default function Page() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [isSuccessful, setIsSuccessful] = useState(false); + + const [state, formAction] = useActionState( + register, + { status: "idle" } + ); + + const { update: updateSession } = useSession(); + + // biome-ignore lint/correctness/useExhaustiveDependencies: router and updateSession are stable refs + useEffect(() => { + if (state.status === "user_exists") { + toast({ type: "error", description: "Account already exists!" }); + } else if (state.status === "failed") { + toast({ type: "error", description: "Failed to create account!" }); + } else if (state.status === "invalid_data") { + toast({ + type: "error", + description: "Failed validating your submission!", + }); + } else if (state.status === "success") { + toast({ type: "success", description: "Account created!" }); + setIsSuccessful(true); + updateSession(); + router.refresh(); + } + }, [state.status]); + + const handleSubmit = (formData: FormData) => { + setEmail(formData.get("email") as string); + formAction(formData); + }; + + return ( + <> +

Create account

+

Get started for free

+ + Sign up +

+ {"Have an account? "} + + Sign in + +

+
+ + ); +} diff --git a/app/(chat)/actions.ts b/app/(chat)/actions.ts new file mode 100644 index 0000000..83e287c --- /dev/null +++ b/app/(chat)/actions.ts @@ -0,0 +1,78 @@ +"use server"; + +import { generateText, type UIMessage } from "ai"; +import { cookies } from "next/headers"; +import { auth } from "@/app/(auth)/auth"; +import type { VisibilityType } from "@/components/chat/visibility-selector"; +import { titlePrompt } from "@/lib/ai/prompts"; +import { getTitleModel } from "@/lib/ai/providers"; +import { + deleteMessagesByChatIdAfterTimestamp, + getChatById, + getMessageById, + updateChatVisibilityById, +} from "@/lib/db/queries"; +import { getTextFromMessage } from "@/lib/utils"; + +export async function saveChatModelAsCookie(model: string) { + const cookieStore = await cookies(); + cookieStore.set("chat-model", model); +} + +export async function generateTitleFromUserMessage({ + message, +}: { + message: UIMessage; +}) { + const { text } = await generateText({ + model: getTitleModel(), + system: titlePrompt, + prompt: getTextFromMessage(message), + }); + return text + .replace(/^[#*"\s]+/, "") + .replace(/["]+$/, "") + .trim(); +} + +export async function deleteTrailingMessages({ id }: { id: string }) { + const session = await auth(); + if (!session?.user?.id) { + throw new Error("Unauthorized"); + } + + const [message] = await getMessageById({ id }); + if (!message) { + throw new Error("Message not found"); + } + + const chat = await getChatById({ id: message.chatId }); + if (!chat || chat.userId !== session.user.id) { + throw new Error("Unauthorized"); + } + + await deleteMessagesByChatIdAfterTimestamp({ + chatId: message.chatId, + timestamp: message.createdAt, + }); +} + +export async function updateChatVisibility({ + chatId, + visibility, +}: { + chatId: string; + visibility: VisibilityType; +}) { + const session = await auth(); + if (!session?.user?.id) { + throw new Error("Unauthorized"); + } + + const chat = await getChatById({ id: chatId }); + if (!chat || chat.userId !== session.user.id) { + throw new Error("Unauthorized"); + } + + await updateChatVisibilityById({ chatId, visibility }); +} diff --git a/app/(chat)/api/chat/[id]/stream/route.ts b/app/(chat)/api/chat/[id]/stream/route.ts new file mode 100644 index 0000000..3713ec5 --- /dev/null +++ b/app/(chat)/api/chat/[id]/stream/route.ts @@ -0,0 +1,3 @@ +export function GET() { + return new Response(null, { status: 204 }); +} diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts new file mode 100644 index 0000000..04230f3 --- /dev/null +++ b/app/(chat)/api/chat/route.ts @@ -0,0 +1,272 @@ +import { + convertToModelMessages, + createUIMessageStream, + createUIMessageStreamResponse, + stepCountIs, + streamText, +} from "ai"; +import { auth, type UserType } from "@/app/(auth)/auth"; +import { entitlementsByUserType } from "@/lib/ai/entitlements"; +import { + allowedModelIds, + DEFAULT_CHAT_MODEL, + modelCapabilities, +} from "@/lib/ai/models"; +import { type RequestHints, systemPrompt } from "@/lib/ai/prompts"; +import { getLanguageModel } from "@/lib/ai/providers"; +import { getWeather } from "@/lib/ai/tools/get-weather"; +import { + deleteChatById, + getChatById, + getMessageCountByUserId, + getMessagesByChatId, + saveChat, + saveMessages, + updateChatTitleById, + updateMessage, +} from "@/lib/db/queries"; +import type { DBMessage } from "@/lib/db/schema"; +import { ChatbotError } from "@/lib/errors"; +import { checkIpRateLimit } from "@/lib/ratelimit"; +import type { ChatMessage } from "@/lib/types"; +import { convertToUIMessages, generateUUID } from "@/lib/utils"; +import { generateTitleFromUserMessage } from "../../actions"; +import { type PostRequestBody, postRequestBodySchema } from "./schema"; + +export const maxDuration = 60; + +function getClientIp(request: Request): string | undefined { + const fwd = request.headers.get("x-forwarded-for"); + if (fwd) { + return fwd.split(",")[0]?.trim(); + } + return request.headers.get("x-real-ip") ?? undefined; +} + +export async function POST(request: Request) { + let requestBody: PostRequestBody; + + try { + const json = await request.json(); + requestBody = postRequestBodySchema.parse(json); + } catch (_) { + return new ChatbotError("bad_request:api").toResponse(); + } + + try { + const { id, message, messages, selectedChatModel, selectedVisibilityType } = + requestBody; + + const session = await auth(); + + if (!session?.user) { + return new ChatbotError("unauthorized:chat").toResponse(); + } + + const chatModel = allowedModelIds.has(selectedChatModel) + ? selectedChatModel + : DEFAULT_CHAT_MODEL; + + await checkIpRateLimit(getClientIp(request)); + + const userType: UserType = session.user.type; + + const messageCount = await getMessageCountByUserId({ + id: session.user.id, + differenceInHours: 1, + }); + + if (messageCount > entitlementsByUserType[userType].maxMessagesPerHour) { + return new ChatbotError("rate_limit:chat").toResponse(); + } + + const isToolApprovalFlow = Boolean(messages); + + const chat = await getChatById({ id }); + let messagesFromDb: DBMessage[] = []; + let titlePromise: Promise | null = null; + + if (chat) { + if (chat.userId !== session.user.id) { + return new ChatbotError("forbidden:chat").toResponse(); + } + messagesFromDb = await getMessagesByChatId({ id }); + } else if (message?.role === "user") { + await saveChat({ + id, + userId: session.user.id, + title: "New chat", + visibility: selectedVisibilityType, + }); + titlePromise = generateTitleFromUserMessage({ message }); + } + + let uiMessages: ChatMessage[]; + + if (isToolApprovalFlow && messages) { + const dbMessages = convertToUIMessages(messagesFromDb); + const approvalStates = new Map( + messages.flatMap( + (m) => + m.parts + ?.filter( + (p: Record) => + p.state === "approval-responded" || + p.state === "output-denied" + ) + .map((p: Record) => [ + String(p.toolCallId ?? ""), + p, + ]) ?? [] + ) + ); + uiMessages = dbMessages.map((msg) => ({ + ...msg, + parts: msg.parts.map((part) => { + if ( + "toolCallId" in part && + approvalStates.has(String(part.toolCallId)) + ) { + return { ...part, ...approvalStates.get(String(part.toolCallId)) }; + } + return part; + }), + })) as ChatMessage[]; + } else { + uiMessages = [ + ...convertToUIMessages(messagesFromDb), + message as ChatMessage, + ]; + } + + const requestHints: RequestHints = { + city: request.headers.get("x-vercel-ip-city") ?? undefined, + country: request.headers.get("x-vercel-ip-country") ?? undefined, + }; + + if (message?.role === "user") { + await saveMessages({ + messages: [ + { + chatId: id, + id: message.id, + role: "user", + parts: message.parts, + attachments: [], + createdAt: new Date(), + }, + ], + }); + } + + const capabilities = modelCapabilities[chatModel]; + const isReasoningModel = capabilities?.reasoning === true; + const supportsTools = capabilities?.tools === true; + + const modelMessages = await convertToModelMessages(uiMessages); + + const stream = createUIMessageStream({ + originalMessages: isToolApprovalFlow ? uiMessages : undefined, + execute: async ({ writer: dataStream }) => { + const result = streamText({ + model: getLanguageModel(chatModel), + system: systemPrompt({ requestHints, supportsTools }), + messages: modelMessages, + stopWhen: stepCountIs(5), + experimental_activeTools: + isReasoningModel && !supportsTools ? [] : ["getWeather"], + tools: { + getWeather, + }, + }); + + dataStream.merge( + result.toUIMessageStream({ sendReasoning: isReasoningModel }) + ); + + if (titlePromise) { + try { + const title = await titlePromise; + dataStream.write({ type: "data-chat-title", data: title }); + updateChatTitleById({ chatId: id, title }); + } catch (_) { + /* non-fatal */ + } + } + }, + generateId: generateUUID, + onFinish: async ({ messages: finishedMessages }) => { + if (isToolApprovalFlow) { + for (const finishedMsg of finishedMessages) { + const existingMsg = uiMessages.find((m) => m.id === finishedMsg.id); + if (existingMsg) { + await updateMessage({ + id: finishedMsg.id, + parts: finishedMsg.parts, + }); + } else { + await saveMessages({ + messages: [ + { + id: finishedMsg.id, + role: finishedMsg.role, + parts: finishedMsg.parts, + createdAt: new Date(), + attachments: [], + chatId: id, + }, + ], + }); + } + } + } else if (finishedMessages.length > 0) { + await saveMessages({ + messages: finishedMessages.map((currentMessage) => ({ + id: currentMessage.id, + role: currentMessage.role, + parts: currentMessage.parts, + createdAt: new Date(), + attachments: [], + chatId: id, + })), + }); + } + }, + onError: () => "Oops, an error occurred!", + }); + + return createUIMessageStreamResponse({ stream }); + } catch (error) { + if (error instanceof ChatbotError) { + return error.toResponse(); + } + + console.error("Unhandled error in chat API:", error); + return new ChatbotError("offline:chat").toResponse(); + } +} + +export async function DELETE(request: Request) { + const { searchParams } = new URL(request.url); + const id = searchParams.get("id"); + + if (!id) { + return new ChatbotError("bad_request:api").toResponse(); + } + + const session = await auth(); + + if (!session?.user) { + return new ChatbotError("unauthorized:chat").toResponse(); + } + + const chat = await getChatById({ id }); + + if (chat?.userId !== session.user.id) { + return new ChatbotError("forbidden:chat").toResponse(); + } + + const deletedChat = await deleteChatById({ id }); + + return Response.json(deletedChat, { status: 200 }); +} diff --git a/app/(chat)/api/chat/schema.ts b/app/(chat)/api/chat/schema.ts new file mode 100644 index 0000000..d340721 --- /dev/null +++ b/app/(chat)/api/chat/schema.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; + +const textPartSchema = z.object({ + type: z.enum(["text"]), + text: z.string().min(1).max(2000), +}); + +const filePartSchema = z.object({ + type: z.enum(["file"]), + mediaType: z.enum(["image/jpeg", "image/png", "image/webp", "image/gif"]), + name: z.string().min(1).max(100), + url: z.string(), +}); + +const partSchema = z.union([textPartSchema, filePartSchema]); + +const userMessageSchema = z.object({ + id: z.string().uuid(), + role: z.enum(["user"]), + parts: z.array(partSchema), +}); + +const toolApprovalMessageSchema = z.object({ + id: z.string(), + role: z.enum(["user", "assistant"]), + parts: z.array(z.record(z.unknown())), +}); + +export const postRequestBodySchema = z.object({ + id: z.string().uuid(), + message: userMessageSchema.optional(), + messages: z.array(toolApprovalMessageSchema).optional(), + selectedChatModel: z.string(), + selectedVisibilityType: z.enum(["public", "private"]), +}); + +export type PostRequestBody = z.infer; diff --git a/app/(chat)/api/files/upload/route.ts b/app/(chat)/api/files/upload/route.ts new file mode 100644 index 0000000..9dbd3b4 --- /dev/null +++ b/app/(chat)/api/files/upload/route.ts @@ -0,0 +1,64 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; + +import { auth } from "@/app/(auth)/auth"; + +const MAX_FILE_SIZE = 5 * 1024 * 1024; +const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"]; + +const FileSchema = z.object({ + file: z + .instanceof(Blob) + .refine((file) => file.size <= MAX_FILE_SIZE, { + message: "File size should be less than 5MB", + }) + .refine((file) => ALLOWED_TYPES.includes(file.type), { + message: "File type should be JPEG, PNG, WebP, or GIF", + }), +}); + +export async function POST(request: Request) { + const session = await auth(); + + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + if (request.body === null) { + return new Response("Request body is empty", { status: 400 }); + } + + try { + const formData = await request.formData(); + const file = formData.get("file") as Blob; + + if (!file) { + return NextResponse.json({ error: "No file uploaded" }, { status: 400 }); + } + + const validatedFile = FileSchema.safeParse({ file }); + + if (!validatedFile.success) { + const errorMessage = validatedFile.error.errors + .map((error) => error.message) + .join(", "); + return NextResponse.json({ error: errorMessage }, { status: 400 }); + } + + const filename = (formData.get("file") as File).name; + const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_"); + const buffer = Buffer.from(await file.arrayBuffer()); + const dataUrl = `data:${file.type};base64,${buffer.toString("base64")}`; + + return NextResponse.json({ + url: dataUrl, + pathname: safeName, + contentType: file.type, + }); + } catch (_error) { + return NextResponse.json( + { error: "Failed to process request" }, + { status: 500 } + ); + } +} diff --git a/app/(chat)/api/history/route.ts b/app/(chat)/api/history/route.ts new file mode 100644 index 0000000..064a385 --- /dev/null +++ b/app/(chat)/api/history/route.ts @@ -0,0 +1,49 @@ +import type { NextRequest } from "next/server"; +import { auth } from "@/app/(auth)/auth"; +import { deleteAllChatsByUserId, getChatsByUserId } from "@/lib/db/queries"; +import { ChatbotError } from "@/lib/errors"; + +export async function GET(request: NextRequest) { + const { searchParams } = request.nextUrl; + + const limit = Math.min( + Math.max(Number.parseInt(searchParams.get("limit") || "10", 10), 1), + 50 + ); + const startingAfter = searchParams.get("starting_after"); + const endingBefore = searchParams.get("ending_before"); + + if (startingAfter && endingBefore) { + return new ChatbotError( + "bad_request:api", + "Only one of starting_after or ending_before can be provided." + ).toResponse(); + } + + const session = await auth(); + + if (!session?.user) { + return new ChatbotError("unauthorized:chat").toResponse(); + } + + const chats = await getChatsByUserId({ + id: session.user.id, + limit, + startingAfter, + endingBefore, + }); + + return Response.json(chats); +} + +export async function DELETE() { + const session = await auth(); + + if (!session?.user) { + return new ChatbotError("unauthorized:chat").toResponse(); + } + + const result = await deleteAllChatsByUserId({ userId: session.user.id }); + + return Response.json(result, { status: 200 }); +} diff --git a/app/(chat)/api/messages/route.ts b/app/(chat)/api/messages/route.ts new file mode 100644 index 0000000..cda98de --- /dev/null +++ b/app/(chat)/api/messages/route.ts @@ -0,0 +1,43 @@ +import { auth } from "@/app/(auth)/auth"; +import { getChatById, getMessagesByChatId } from "@/lib/db/queries"; +import { convertToUIMessages } from "@/lib/utils"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const chatId = searchParams.get("chatId"); + + if (!chatId) { + return Response.json({ error: "chatId required" }, { status: 400 }); + } + + const [session, chat, messages] = await Promise.all([ + auth(), + getChatById({ id: chatId }), + getMessagesByChatId({ id: chatId }), + ]); + + if (!chat) { + return Response.json({ + messages: [], + visibility: "private", + userId: null, + isReadonly: false, + }); + } + + if ( + chat.visibility === "private" && + (!session?.user || session.user.id !== chat.userId) + ) { + return Response.json({ error: "forbidden" }, { status: 403 }); + } + + const isReadonly = !session?.user || session.user.id !== chat.userId; + + return Response.json({ + messages: convertToUIMessages(messages), + visibility: chat.visibility, + userId: chat.userId, + isReadonly, + }); +} diff --git a/app/(chat)/api/models/route.ts b/app/(chat)/api/models/route.ts new file mode 100644 index 0000000..f15eae6 --- /dev/null +++ b/app/(chat)/api/models/route.ts @@ -0,0 +1,9 @@ +import { modelCapabilities } from "@/lib/ai/models"; + +export function GET() { + return Response.json(modelCapabilities, { + headers: { + "Cache-Control": "public, max-age=86400, s-maxage=86400", + }, + }); +} diff --git a/app/(chat)/api/vote/route.ts b/app/(chat)/api/vote/route.ts new file mode 100644 index 0000000..726ba56 --- /dev/null +++ b/app/(chat)/api/vote/route.ts @@ -0,0 +1,84 @@ +import { z } from "zod"; +import { auth } from "@/app/(auth)/auth"; +import { getChatById, getVotesByChatId, voteMessage } from "@/lib/db/queries"; +import { ChatbotError } from "@/lib/errors"; + +const voteSchema = z.object({ + chatId: z.string(), + messageId: z.string(), + type: z.enum(["up", "down"]), +}); + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const chatId = searchParams.get("chatId"); + + if (!chatId) { + return new ChatbotError( + "bad_request:api", + "Parameter chatId is required." + ).toResponse(); + } + + const session = await auth(); + + if (!session?.user) { + return new ChatbotError("unauthorized:vote").toResponse(); + } + + const chat = await getChatById({ id: chatId }); + + if (!chat) { + return new ChatbotError("not_found:chat").toResponse(); + } + + if (chat.userId !== session.user.id) { + return new ChatbotError("forbidden:vote").toResponse(); + } + + const votes = await getVotesByChatId({ id: chatId }); + + return Response.json(votes, { status: 200 }); +} + +export async function PATCH(request: Request) { + let chatId: string; + let messageId: string; + let type: "up" | "down"; + + try { + const parsed = voteSchema.parse(await request.json()); + chatId = parsed.chatId; + messageId = parsed.messageId; + type = parsed.type; + } catch { + return new ChatbotError( + "bad_request:api", + "Parameters chatId, messageId, and type are required." + ).toResponse(); + } + + const session = await auth(); + + if (!session?.user) { + return new ChatbotError("unauthorized:vote").toResponse(); + } + + const chat = await getChatById({ id: chatId }); + + if (!chat) { + return new ChatbotError("not_found:vote").toResponse(); + } + + if (chat.userId !== session.user.id) { + return new ChatbotError("forbidden:vote").toResponse(); + } + + await voteMessage({ + chatId, + messageId, + type, + }); + + return new Response("Message voted", { status: 200 }); +} diff --git a/app/(chat)/chat/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx new file mode 100644 index 0000000..67e0859 --- /dev/null +++ b/app/(chat)/chat/[id]/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return null; +} diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx new file mode 100644 index 0000000..7ece3a7 --- /dev/null +++ b/app/(chat)/layout.tsx @@ -0,0 +1,46 @@ +import { cookies } from "next/headers"; +import { Suspense } from "react"; +import { Toaster } from "sonner"; +import { AppSidebar } from "@/components/chat/app-sidebar"; +import { DataStreamProvider } from "@/components/chat/data-stream-provider"; +import { ChatShell } from "@/components/chat/shell"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; +import { ActiveChatProvider } from "@/hooks/use-active-chat"; +import { auth } from "../(auth)/auth"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + }> + {children} + + + ); +} + +async function SidebarShell({ children }: { children: React.ReactNode }) { + const [session, cookieStore] = await Promise.all([auth(), cookies()]); + const isCollapsed = cookieStore.get("sidebar_state")?.value !== "true"; + + return ( + + + + + }> + + + + + {children} + + + ); +} diff --git a/app/(chat)/opengraph-image.png b/app/(chat)/opengraph-image.png new file mode 100644 index 0000000000000000000000000000000000000000..c8402717a1d1cc0f37cffefe3a2b8930588e74f3 GIT binary patch literal 14418 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 literal 0 HcmV?d00001 diff --git a/app/(chat)/page.tsx b/app/(chat)/page.tsx new file mode 100644 index 0000000..67e0859 --- /dev/null +++ b/app/(chat)/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return null; +} diff --git a/app/(chat)/twitter-image.png b/app/(chat)/twitter-image.png new file mode 100644 index 0000000000000000000000000000000000000000..79fbc0f9c7a461a2982ade09f414bc8faa9ec60a GIT binary patch literal 52049 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 literal 0 HcmV?d00001 diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7452b5dc69b2a634c86779ee4cd4c13319549b43 GIT binary patch literal 15406 zcmeI2dF&O{6~`a@k3|ZWO{nl=#|Q{gb`S_4QZb@7ECrEG1VxFJfGh?BWs^-P#XpP{ zF?1tZ!jei%F;YMh1S=GwplGEuB3mi4FQ?zn_~vEa{AT9O@caF+Nj=H;&CI>`+;is6 zx#ymH?n|Xgq)Mf#R7vTYpBh^_m1>ttrSkK?eQs7YmHM^HTD3~oH%Xk5Fstq1I*yuxL-v})|eV}tFf4OqyZ2tWD)~#DN zZ=3DnDIqXcnfEPOvZS3jal)13v|qMtnJrne#7dMX;pOMPlfXD<%HcnE?p%g{`0(NO z$tRx}`UmjO6z_{+{HTN^mtTH4Q$B0fEURC?e&#vNGhbKi3fpk% z;6H!`pom*@YyTUepEAWk8 z5*Vw}LXi@FCST|-;GZI>p6c2Q?wCh3&8jM=r&|R4M0tVtyAeY6b>TB%p%A7^CDr{@ zcum+N{9C|2;l)oK7XB!V7j75g67P*{_uIO(TCjM_wXxaFvHf`G2 z9e3Q}`s~l813oQ`!oERbUGSAXd-k*kAAHd6zWZ+DSwg|z1Vsjc#5dr3yKQgy+M`Dg zn>cZzJ^JXQR;f}Ye?R!1pt)K@<=CyieWK^qty|kGue@S&=FG81jT!~<@{aukKh04= zQ2W?rwQJY5x8Hu-!9x!{6vg+EWO@A^zU5??{8FV#*`Y&+?CY<;HrBA9cIC^LxAp7S z+upryM$j0ZBJh(PoA`E*RC1szrQW<$BGpz?Bd0XwsYrByY<#v z3$*d`=y{dk(rda2<;f*~*sx)aZo~)e+qVzx$0y^cp51kCudgnvk0|+f-F26pJ$u%! zU%zf+$Bwl!Wy%arw_0EE?v6V(W6J5ai2YVb^#f@`|{<>t#jwj1?mE1G*=l! z+!ET}m5+_@-@o6Hy=l`X$JYIP&ph*ttzEm;`t<1&+FzQ7xSFwt#tWhCUHOh+{e1Do z7gn=o&A|3EX3Vf%yLQ>b4?i4O?~(Hkg40B75W4nwz?-t_S_RI7PjwP{KW3A-FZYix5TN$in;o@(fYIMn{ke!x~lwKxKSBleAsvYD8ZgO z;?#XYSYJuYdGENgT{VbPi4`N)2G!@%g_t+jn5#)3;hXW%e-(1c@z);{&mIXn zh!yV@h?$66!p4Mg-X!2#DhtIy#7x93#E*jo)=b0~tJH_eD+U{s)0MJwaf&Zum?z+7%D@UGZGZDFLaTOdE_=S%W@O8m73F1UAFC zhskB^J`?y(GljnhjB!}|q~&}czR5D7wGj2qRb7B8s$`20M{Z%qJ4(h??cWY5uI1a7 zEudv|0rno%cd^%zTO-GyoX7dnrAssai521!YZr5`*!2vOL2jTyg9Z-R!{{CO&Tk40 zg*b%GF;{Q8{_KM|FJdpk{j{24oc`st@_+O%mlVZsFG8!!)iy1UP;jjWYX#M(4dD0Um-ypVC{tak3)xz2lp zP3}7v%at76z<~p8_3G8Oa^*@}v}loa=+GffZt@%HHYiMa zl|LpF<+>X9{=Iwmws+rs*T^-ISLe(-?Qa6xODAtRF-xA{C(2^)=d`}qwr$(&@ZrPu z>8GDs%a$#@G6L_JujGZ_fB${kvuBTO-@e@zELh;=g(W+XC!icV8YuXQxe?p$@eyBs z`K6OOyL$DiwQburr+jipYu2oB@>t~BxW{)&Zc_Y7JYIjw+O;xCKS}jx>=!Ry?3_iA zqvgBx>(|eF2A1Ral+K3S&oV+r*R`j1ab7q|R8m15P_%XrV z&vn-QSH+<}zJ~egwKdKpIRE3DfFyF$ZCR7&3S|U{xFJ#O7I*yX)Tv`zwrp|E6qqc` zUCz4TXAJ-mYgW{|vERK#!1kXL68X(I`t$#tS6_YA(H*-va^#4Ue`n0mn=?beS-9jU z#e3q>KgXQBz${()Cb9d-r9Wdjb?Q{ReEG8DqsXt18#gW!+Ysk-rVIEUNo1$nexz$1 z0UIJukf@I+{Tbg!9(ly^lPHgWoi}e@*8ho!F?;pu<;;y-x%&{^4ywGafc}d^+JwGS zl>VF*eE8vqj@_X9h7B7E*a&eaF$9P@_lVHzkm~9Q#BB2f*0oT5{dKPX*tK{4;(u?% z1e{4z$2llxe||s1e&xw0pR}i*dMeJDRFGce!EYBz3DbltLQuQFGFN}sK3IQ=VK}4X z{1Dwaw|?M(2Lk;(am4iL)9r;9UT|zJiR^US9lF*MSOdlgs=u3bClKaTv*k@(FmPjFqQw(kC(oE~|x*!}qR#}4-G+vkisK8Ls% zTMFxwv$Qwfc*9n$T4m$Mj}L7Z!jG;0O2`wSrULn+*!_6*$5uG|#pWUY>%koM+q}1b ze9y*>8y#>i?X8P(&sxQr#o7h&9)A{M_v6*yQCdH|$MNIGty}j0W&``=JpF?YKCm4- zb~yj(Ng`WqIp^#xcswIW+&5<^MbEA>Hzr*N@e% zrr@O?>EW~xt1qwqj6XKd|8I#4;`G6PqfebWWv5S{HsZNBJf1fAdpGvfZHCCR#99Z4 zD~AjjVoyBrgagjibL_vfbYy-!{`liIX3QApKXHt?qj zMq4TbjlGJU{QELpWA7u?_GZ4r-zBe^5Jjz3No;X5^@=0wf^>RbaQ7^_4J6`;5kjK% zSncEJP?$FFNCx?-I7FWD7sCIF9UYb~P2wo-X;W4AZwiUxI<+s%82fqgg`W#ug&Uzp zVj};}y!ZEgGoI0XfRM}P)g+WSJ6JAU7H)>lt_*C3aW9PSVt0sK*HqyvA=dh)cCH>t zuNTG3Tuo~4Vu_@3s_iR~!zO=}RIgm^u~Gc{6rqk#TtxnTut2W2sJ;bp*GmF%d{Nko zy2(kLl@Aigr5+LZCUNFD?|ex(A$%;LH+~{4CQwze5_5w!`!RvIki6J?0^f%m2WvTd z?H$5;VTHin<6eOn_`A|uqO%A+`1a)V$ctgab%lDu?LsYqH6>4Q_f5L}p1vP}?`{PC E3($lMk^lez literal 0 HcmV?d00001 diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..4143774 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,500 @@ +@import "tailwindcss"; +@import "katex/dist/katex.min.css"; + +@source "../node_modules/streamdown/dist/index.js"; + +@custom-variant dark (&:is(.dark, .dark *)); + +@plugin "tailwindcss-animate"; +@plugin "@tailwindcss/typography"; + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(0.985 0 0); + --foreground: oklch(0.12 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.12 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.12 0 0); + --primary: oklch(0.12 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.965 0 0); + --secondary-foreground: oklch(0.38 0 0); + --muted: oklch(0.94 0 0); + --muted-foreground: oklch(0.58 0 0); + --accent: oklch(0.965 0 0); + --accent-foreground: oklch(0.12 0 0); + --destructive: oklch(0.55 0.15 25); + --border: oklch(0.9 0 0); + --input: oklch(0.9 0 0); + --ring: oklch(0.5 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.97 0 0); + --sidebar-foreground: oklch(0.38 0 0); + --sidebar-primary: oklch(0.12 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.12 0 0 / 0.06); + --sidebar-accent-foreground: oklch(0.12 0 0); + --sidebar-border: oklch(0.88 0 0); + --sidebar-ring: oklch(0.5 0 0); + + --shadow-card: 0 1px 3px oklch(0 0 0 / 0.05), 0 1px 1px oklch(0 0 0 / 0.03); + --shadow-float: + 0 8px 24px -6px oklch(0 0 0 / 0.1), 0 2px 8px -2px oklch(0 0 0 / 0.04); + --shadow-composer: 0 1px 2px oklch(0 0 0 / 0.04); + --shadow-composer-focus: + 0 0 0 1px oklch(0 0 0 / 0.06), 0 2px 8px -2px oklch(0 0 0 / 0.06); + --shadow-inset: inset 0 1px 1px oklch(0 0 0 / 0.03); + --shadow-glow: 0 0 20px oklch(0 0 0 / 0.08); + + --ease-spring: cubic-bezier(0.22, 1, 0.36, 1); + --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1); + --ease-smooth: cubic-bezier(0.4, 0, 0.2, 1); +} + +.dark { + --background: oklch(0.195 0 0); + --foreground: oklch(0.94 0 0); + --card: oklch(0.225 0 0); + --card-foreground: oklch(0.94 0 0); + --popover: oklch(0.225 0 0); + --popover-foreground: oklch(0.94 0 0); + --primary: oklch(0.94 0 0); + --primary-foreground: oklch(0.195 0 0); + --secondary: oklch(0.26 0 0); + --secondary-foreground: oklch(0.75 0 0); + --muted: oklch(0.165 0 0); + --muted-foreground: oklch(0.6 0 0); + --accent: oklch(0.26 0 0); + --accent-foreground: oklch(0.94 0 0); + --destructive: oklch(0.7 0.15 25); + --border: oklch(0.27 0 0); + --input: oklch(0.27 0 0); + --ring: oklch(0.45 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.175 0 0); + --sidebar-foreground: oklch(0.78 0 0); + --sidebar-primary: oklch(0.94 0 0); + --sidebar-primary-foreground: oklch(0.195 0 0); + --sidebar-accent: oklch(0.94 0 0 / 0.06); + --sidebar-accent-foreground: oklch(0.94 0 0); + --sidebar-border: oklch(0.25 0 0); + --sidebar-ring: oklch(0.45 0 0); + + --shadow-card: + inset 0 1px 0 oklch(1 0 0 / 0.04), 0 1px 2px oklch(0 0 0 / 0.2), + 0 0.5px 1px oklch(0 0 0 / 0.15); + --shadow-float: + 0 0 0 1px oklch(1 0 0 / 0.06), 0 16px 48px -6px oklch(0 0 0 / 0.35), + 0 6px 12px -2px oklch(0 0 0 / 0.2); + --shadow-composer: + 0 1px 3px oklch(0 0 0 / 0.2), inset 0 1px 0 oklch(1 0 0 / 0.03); + --shadow-composer-focus: + 0 0 0 1px oklch(1 0 0 / 0.1), 0 4px 16px -4px oklch(0 0 0 / 0.3), + inset 0 1px 0 oklch(1 0 0 / 0.04); +} + +@layer base { + * { + @apply border-border ring-0; + } + body { + @apply bg-background text-foreground; + font-feature-settings: "ss01", "ss02", "cv01"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + } +} + +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--border); + } +} + +@layer base { + body { + overflow-x: hidden; + position: relative; + } + + html { + overflow-x: hidden; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + letter-spacing: -0.025em; + line-height: 1.2; + } + + p { + line-height: 1.6; + } +} + +button:focus-visible, +select:focus-visible, +[role="button"]:focus-visible, +input:focus-visible, +textarea:focus-visible { + outline: none; +} + +@utility text-balance { + text-wrap: balance; +} + +@utility -webkit-overflow-scrolling-touch { + -webkit-overflow-scrolling: touch; +} + +@utility touch-pan-y { + touch-action: pan-y; +} + +@utility overscroll-behavior-contain { + overscroll-behavior: contain; +} + +@utility no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } +} + +@layer utilities { + :root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; + } + + @media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } + } +} + +.skeleton { + * { + pointer-events: none !important; + } + + *[class^="text-"] { + color: transparent; + @apply rounded-md bg-foreground/20 select-none animate-pulse; + } + + .skeleton-bg { + @apply bg-foreground/10; + } + + .skeleton-div { + @apply bg-foreground/20 animate-pulse; + } +} + +@keyframes fade-up { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +@keyframes dot-pulse { + 0%, + 60%, + 100% { + opacity: 0.3; + transform: translateY(0); + } + 30% { + opacity: 1; + transform: translateY(-3px); + } +} + +@keyframes message-in { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes thinking-dot { + 0%, + 60%, + 100% { + opacity: 0.3; + transform: translateY(0); + } + 30% { + opacity: 1; + transform: translateY(-3px); + } +} + +@keyframes glow-pulse { + 0%, + 100% { + box-shadow: 0 0 0 0 oklch(0.55 0.12 250 / 0%); + } + 50% { + box-shadow: 0 0 0 3px oklch(0.55 0.12 250 / 8%); + } +} + +@keyframes subtle-lift { + from { + transform: translateY(0); + box-shadow: var(--shadow-card); + } + to { + transform: translateY(-1px); + box-shadow: var(--shadow-float); + } +} + +@utility fade-up { + animation: fade-up 0.25s var(--ease-spring) both; +} + +@utility fade-in { + animation: fade-in 0.2s ease both; +} + +@utility shimmer { + animation: shimmer 2s linear infinite; + background: linear-gradient( + 90deg, + transparent, + oklch(1 0 0 / 0.04), + transparent + ); + background-size: 200% 100%; +} + +@utility dot-pulse { + animation: dot-pulse 1.4s ease-in-out infinite; +} + +@utility message-fade-in { + animation: message-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +@utility thinking-dot { + animation: thinking-dot 1.4s ease-in-out infinite; +} + +@utility composer-glow { + animation: glow-pulse 2s ease-in-out infinite; +} + +.ProseMirror { + outline: none; +} + +.cm-editor { + @apply bg-transparent! outline-hidden! text-[13px]! leading-[1.6]!; + font-family: + "SF Mono", "Cascadia Code", "Fira Code", "JetBrains Mono", ui-monospace, + monospace !important; +} + +.cm-gutters { + @apply bg-transparent! border-r-0! outline-hidden!; +} + +.cm-gutter.cm-lineNumbers { + @apply min-w-[3rem] text-muted-foreground/40 text-[11px]!; +} + +.cm-scroller { + @apply overflow-auto!; +} + +.ͼo.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, +.ͼo.cm-selectionBackground, +.ͼo.cm-content::selection { + background: oklch(0.55 0.12 250 / 0.15) !important; +} + +.dark + .ͼo.cm-focused + > .cm-scroller + > .cm-selectionLayer + .cm-selectionBackground, +.dark .ͼo.cm-selectionBackground, +.dark .ͼo.cm-content::selection { + background: oklch(0.55 0.12 250 / 0.2) !important; +} + +.cm-activeLine { + @apply bg-muted/50! rounded-sm!; +} + +.cm-activeLineGutter { + @apply bg-transparent!; +} + +.cm-activeLineGutter .cm-gutterElement { + color: var(--foreground) !important; + opacity: 0.7; +} + +.cm-gutter.cm-lineNumbers .cm-gutterElement { + padding-right: 12px !important; +} + +.cm-foldGutter { + @apply min-w-3; +} + +.cm-cursor { + border-left-color: oklch(0.55 0.12 250) !important; + border-left-width: 2px !important; +} + +.cm-matchingBracket { + background: oklch(0.55 0.12 250 / 0.12) !important; + outline: 1px solid oklch(0.55 0.12 250 / 0.3); + border-radius: 2px; +} + +.suggestion-highlight { + @apply cursor-pointer rounded-sm bg-blue-200 transition-colors hover:bg-blue-300 dark:bg-blue-500/40 dark:text-blue-50 dark:hover:bg-blue-400/50; + user-select: none; + -webkit-user-select: none; +} + +@layer base { + * { + scrollbar-width: thin; + scrollbar-color: oklch(0 0 0 / 0.12) transparent; + } + + .dark * { + scrollbar-color: oklch(1 0 0 / 0.1) transparent; + } + + *::-webkit-scrollbar { + width: 4px; + height: 4px; + } + + *::-webkit-scrollbar-track { + background: transparent; + } + + *::-webkit-scrollbar-thumb { + background: oklch(0 0 0 / 0.12); + border-radius: 9999px; + } + + *::-webkit-scrollbar-thumb:hover { + background: oklch(0 0 0 / 0.25); + } + + .dark *::-webkit-scrollbar-thumb { + background: oklch(1 0 0 / 0.1); + } + + .dark *::-webkit-scrollbar-thumb:hover { + background: oklch(1 0 0 / 0.2); + } + + *::-webkit-scrollbar-corner { + background: transparent; + } +} + +[data-testid="artifact"] { + isolation: isolate; +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..9e310aa --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,88 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import { ThemeProvider } from "@/components/theme-provider"; +import { TooltipProvider } from "@/components/ui/tooltip"; + +import "./globals.css"; +import { SessionProvider } from "next-auth/react"; + +const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000"; + +export const metadata: Metadata = { + metadataBase: new URL(baseUrl), + title: "EGBE Chatbot", + description: "Chatbot template wired to EGBE LiteLLM proxy.", +}; + +export const viewport = { + maximumScale: 1, +}; + +const geist = Geist({ + subsets: ["latin"], + display: "swap", + variable: "--font-geist", +}); + +const geistMono = Geist_Mono({ + subsets: ["latin"], + display: "swap", + variable: "--font-geist-mono", +}); + +const LIGHT_THEME_COLOR = "hsl(0 0% 100%)"; +const DARK_THEME_COLOR = "hsl(240deg 10% 3.92%)"; +const THEME_COLOR_SCRIPT = `\ +(function() { + var html = document.documentElement; + var meta = document.querySelector('meta[name="theme-color"]'); + if (!meta) { + meta = document.createElement('meta'); + meta.setAttribute('name', 'theme-color'); + document.head.appendChild(meta); + } + function updateThemeColor() { + var isDark = html.classList.contains('dark'); + meta.setAttribute('content', isDark ? '${DARK_THEME_COLOR}' : '${LIGHT_THEME_COLOR}'); + } + var observer = new MutationObserver(updateThemeColor); + observer.observe(html, { attributes: true, attributeFilter: ['class'] }); + updateThemeColor(); +})();`; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + +