Repository: vercel/chatbot Branch: main Commit: d83238b29b87 Files: 225 Total size: 1.3 MB Directory structure: gitextract_y5pipq23/ ├── .changeset/ │ └── config.json ├── .cursor/ │ └── rules/ │ └── ultracite.mdc ├── .github/ │ ├── CONTRIBUTING.md │ └── workflows/ │ ├── lint.yml │ ├── playwright.yml │ └── release.yml ├── .gitignore ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── LICENSE ├── README.md ├── app/ │ ├── (auth)/ │ │ ├── actions.ts │ │ ├── api/ │ │ │ └── auth/ │ │ │ ├── [...nextauth]/ │ │ │ │ └── route.ts │ │ │ └── guest/ │ │ │ └── route.ts │ │ ├── auth.config.ts │ │ ├── auth.ts │ │ ├── login/ │ │ │ └── page.tsx │ │ └── register/ │ │ └── page.tsx │ ├── (chat)/ │ │ ├── actions.ts │ │ ├── api/ │ │ │ ├── chat/ │ │ │ │ ├── [id]/ │ │ │ │ │ └── stream/ │ │ │ │ │ └── route.ts │ │ │ │ ├── route.ts │ │ │ │ └── schema.ts │ │ │ ├── document/ │ │ │ │ └── route.ts │ │ │ ├── files/ │ │ │ │ └── upload/ │ │ │ │ └── route.ts │ │ │ ├── history/ │ │ │ │ └── route.ts │ │ │ ├── suggestions/ │ │ │ │ └── route.ts │ │ │ └── vote/ │ │ │ └── route.ts │ │ ├── chat/ │ │ │ └── [id]/ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── globals.css │ └── layout.tsx ├── artifacts/ │ ├── actions.ts │ ├── code/ │ │ ├── client.tsx │ │ └── server.ts │ ├── image/ │ │ └── client.tsx │ ├── sheet/ │ │ ├── client.tsx │ │ └── server.ts │ └── text/ │ ├── client.tsx │ └── server.ts ├── biome.jsonc ├── components/ │ ├── ai-elements/ │ │ ├── artifact.tsx │ │ ├── canvas.tsx │ │ ├── chain-of-thought.tsx │ │ ├── checkpoint.tsx │ │ ├── confirmation.tsx │ │ ├── connection.tsx │ │ ├── controls.tsx │ │ ├── conversation.tsx │ │ ├── edge.tsx │ │ ├── image.tsx │ │ ├── inline-citation.tsx │ │ ├── loader.tsx │ │ ├── message.tsx │ │ ├── model-selector.tsx │ │ ├── node.tsx │ │ ├── open-in-chat.tsx │ │ ├── panel.tsx │ │ ├── plan.tsx │ │ ├── prompt-input.tsx │ │ ├── queue.tsx │ │ ├── reasoning.tsx │ │ ├── shimmer.tsx │ │ ├── sources.tsx │ │ ├── suggestion.tsx │ │ ├── task.tsx │ │ ├── tool.tsx │ │ ├── toolbar.tsx │ │ └── web-preview.tsx │ ├── app-sidebar.tsx │ ├── artifact-actions.tsx │ ├── artifact-close-button.tsx │ ├── artifact-messages.tsx │ ├── artifact.tsx │ ├── auth-form.tsx │ ├── chat-header.tsx │ ├── chat.tsx │ ├── code-editor.tsx │ ├── console.tsx │ ├── create-artifact.tsx │ ├── data-stream-handler.tsx │ ├── data-stream-provider.tsx │ ├── diffview.tsx │ ├── document-preview.tsx │ ├── document-skeleton.tsx │ ├── document.tsx │ ├── elements/ │ │ ├── actions.tsx │ │ ├── branch.tsx │ │ ├── conversation.tsx │ │ ├── image.tsx │ │ ├── inline-citation.tsx │ │ ├── loader.tsx │ │ ├── message.tsx │ │ ├── prompt-input.tsx │ │ ├── reasoning.tsx │ │ ├── response.tsx │ │ ├── source.tsx │ │ ├── suggestion.tsx │ │ ├── task.tsx │ │ ├── tool.tsx │ │ └── web-preview.tsx │ ├── greeting.tsx │ ├── icons.tsx │ ├── image-editor.tsx │ ├── message-actions.tsx │ ├── message-editor.tsx │ ├── message-reasoning.tsx │ ├── message.tsx │ ├── messages.tsx │ ├── multimodal-input.tsx │ ├── preview-attachment.tsx │ ├── sheet-editor.tsx │ ├── sidebar-history-item.tsx │ ├── sidebar-history.tsx │ ├── sidebar-toggle.tsx │ ├── sidebar-user-nav.tsx │ ├── sign-out-form.tsx │ ├── submit-button.tsx │ ├── suggested-actions.tsx │ ├── suggestion.tsx │ ├── text-editor.tsx │ ├── theme-provider.tsx │ ├── toast.tsx │ ├── toolbar.tsx │ ├── ui/ │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button-group.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── hover-card.tsx │ │ ├── input-group.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── progress.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx │ ├── version-footer.tsx │ ├── visibility-selector.tsx │ └── weather.tsx ├── components.json ├── drizzle.config.ts ├── hooks/ │ ├── use-artifact.ts │ ├── use-auto-resume.ts │ ├── use-chat-visibility.ts │ ├── use-messages.tsx │ ├── use-mobile.ts │ └── use-scroll-to-bottom.tsx ├── instrumentation-client.ts ├── instrumentation.ts ├── lib/ │ ├── ai/ │ │ ├── entitlements.ts │ │ ├── models.mock.ts │ │ ├── models.test.ts │ │ ├── models.ts │ │ ├── prompts.ts │ │ ├── providers.ts │ │ └── tools/ │ │ ├── create-document.ts │ │ ├── get-weather.ts │ │ ├── request-suggestions.ts │ │ └── update-document.ts │ ├── artifacts/ │ │ └── server.ts │ ├── constants.ts │ ├── db/ │ │ ├── helpers/ │ │ │ └── 01-core-to-parts.ts │ │ ├── migrate.ts │ │ ├── migrations/ │ │ │ ├── 0000_keen_devos.sql │ │ │ ├── 0001_sparkling_blue_marvel.sql │ │ │ ├── 0002_wandering_riptide.sql │ │ │ ├── 0003_cloudy_glorian.sql │ │ │ ├── 0004_odd_slayback.sql │ │ │ ├── 0005_wooden_whistler.sql │ │ │ ├── 0006_marvelous_frog_thor.sql │ │ │ ├── 0007_flowery_ben_parker.sql │ │ │ ├── 0008_flat_forgotten_one.sql │ │ │ └── meta/ │ │ │ ├── 0000_snapshot.json │ │ │ ├── 0001_snapshot.json │ │ │ ├── 0002_snapshot.json │ │ │ ├── 0003_snapshot.json │ │ │ ├── 0004_snapshot.json │ │ │ ├── 0005_snapshot.json │ │ │ ├── 0006_snapshot.json │ │ │ ├── 0007_snapshot.json │ │ │ ├── 0008_snapshot.json │ │ │ └── _journal.json │ │ ├── queries.ts │ │ ├── schema.ts │ │ └── utils.ts │ ├── editor/ │ │ ├── config.ts │ │ ├── diff.js │ │ ├── functions.tsx │ │ ├── react-renderer.tsx │ │ └── suggestions.tsx │ ├── errors.ts │ ├── ratelimit.ts │ ├── types.ts │ └── utils.ts ├── next-env.d.ts ├── next.config.ts ├── package.json ├── playwright.config.ts ├── postcss.config.mjs ├── proxy.ts ├── tests/ │ ├── e2e/ │ │ ├── api.test.ts │ │ ├── auth.test.ts │ │ ├── chat.test.ts │ │ └── model-selector.test.ts │ ├── fixtures.ts │ ├── helpers.ts │ ├── pages/ │ │ └── chat.ts │ └── prompts/ │ └── utils.ts ├── tsconfig.json ├── tsconfig.tsbuildinfo ├── vercel-template.json └── vercel.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", "changelog": [ "@changesets/changelog-github", { "repo": "vercel-labs/chatbot" } ], "commit": false, "fixed": [], "linked": [], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": [] } ================================================ FILE: .cursor/rules/ultracite.mdc ================================================ --- description: Ultracite Rules - AI-Ready Formatter and Linter globs: "**/*.{ts,tsx,js,jsx}" alwaysApply: true --- # Project Context Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter. ## Key Principles - Zero configuration required - Subsecond performance - Maximum type safety - AI-friendly code generation ## Before Writing Code 1. Analyze existing patterns in the codebase 2. Consider edge cases and error scenarios 3. Follow the rules below strictly 4. Validate accessibility requirements ## Rules ### Accessibility (a11y) - Don't use `accessKey` attribute on any HTML element. - Don't set `aria-hidden="true"` on focusable elements. - Don't add ARIA roles, states, and properties to elements that don't support them. - Don't use distracting elements like `` or ``. - Only use the `scope` prop on `` elements. - Don't assign non-interactive ARIA roles to interactive HTML elements. - Make sure label elements have text content and are associated with an input. - Don't assign interactive ARIA roles to non-interactive HTML elements. - Don't assign `tabIndex` to non-interactive HTML elements. - Don't use positive integers for `tabIndex` property. - Don't include "image", "picture", or "photo" in img alt prop. - Don't use explicit role property that's the same as the implicit/default role. - Make static elements with click handlers use a valid role attribute. - Always include a `title` element for SVG elements. - Give all elements requiring alt text meaningful information for screen readers. - Make sure anchors have content that's accessible to screen readers. - Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`. - Include all required ARIA attributes for elements with ARIA roles. - Make sure ARIA properties are valid for the element's supported roles. - Always include a `type` attribute for button elements. - Make elements with interactive roles and handlers focusable. - Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`). - Always include a `lang` attribute on the html element. - Always include a `title` attribute for iframe elements. - Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`. - Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`. - Include caption tracks for audio and video elements. - Use semantic elements instead of role attributes in JSX. - Make sure all anchors are valid and navigable. - Ensure all ARIA properties (`aria-*`) are valid. - Use valid, non-abstract ARIA roles for elements with ARIA roles. - Use valid ARIA state and property values. - Use valid values for the `autocomplete` attribute on input elements. - Use correct ISO language/country codes for the `lang` attribute. ### Code Complexity and Quality - Don't use consecutive spaces in regular expression literals. - Don't use the `arguments` object. - Don't use primitive type aliases or misleading types. - Don't use the comma operator. - Don't use empty type parameters in type aliases and interfaces. - Don't write functions that exceed a given Cognitive Complexity score. - Don't nest describe() blocks too deeply in test files. - Don't use unnecessary boolean casts. - Don't use unnecessary callbacks with flatMap. - Use for...of statements instead of Array.forEach. - Don't create classes that only have static members (like a static namespace). - Don't use this and super in static contexts. - Don't use unnecessary catch clauses. - Don't use unnecessary constructors. - Don't use unnecessary continue statements. - Don't export empty modules that don't change anything. - Don't use unnecessary escape sequences in regular expression literals. - Don't use unnecessary fragments. - Don't use unnecessary labels. - Don't use unnecessary nested block statements. - Don't rename imports, exports, and destructured assignments to the same name. - Don't use unnecessary string or template literal concatenation. - Don't use String.raw in template literals when there are no escape sequences. - Don't use useless case statements in switch statements. - Don't use ternary operators when simpler alternatives exist. - Don't use useless `this` aliasing. - Don't use any or unknown as type constraints. - Don't initialize variables to undefined. - Don't use the void operators (they're not familiar). - Use arrow functions instead of function expressions. - Use Date.now() to get milliseconds since the Unix Epoch. - Use .flatMap() instead of map().flat() when possible. - Use literal property access instead of computed property access. - Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work. - Use concise optional chaining instead of chained logical expressions. - Use regular expression literals instead of the RegExp constructor when possible. - Don't use number literal object member names that aren't base 10 or use underscore separators. - Remove redundant terms from logical expressions. - Use while loops instead of for loops when you don't need initializer and update expressions. - Don't pass children as props. - Don't reassign const variables. - Don't use constant expressions in conditions. - Don't use `Math.min` and `Math.max` to clamp values when the result is constant. - Don't return a value from a constructor. - Don't use empty character classes in regular expression literals. - Don't use empty destructuring patterns. - Don't call global object properties as functions. - Don't declare functions and vars that are accessible outside their block. - Make sure builtins are correctly instantiated. - Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors. - Don't use variables and function parameters before they're declared. - Don't use 8 and 9 escape sequences in string literals. - Don't use literal numbers that lose precision. ### React and JSX Best Practices - Don't use the return value of React.render. - Make sure all dependencies are correctly specified in React hooks. - Make sure all React hooks are called from the top level of component functions. - Don't forget key props in iterators and collection literals. - Don't destructure props inside JSX components in Solid projects. - Don't define React components inside other components. - Don't use event handlers on non-interactive elements. - Don't assign to React component props. - Don't use both `children` and `dangerouslySetInnerHTML` props on the same element. - Don't use dangerous JSX props. - Don't use Array index in keys. - Don't insert comments as text nodes. - Don't assign JSX properties multiple times. - Don't add extra closing tags for components without children. - Use `<>...` instead of `...`. - Watch out for possible "wrong" semicolons inside JSX elements. ### Correctness and Safety - Don't assign a value to itself. - Don't return a value from a setter. - Don't compare expressions that modify string case with non-compliant values. - Don't use lexical declarations in switch clauses. - Don't use variables that haven't been declared in the document. - Don't write unreachable code. - Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass. - Don't use control flow statements in finally blocks. - Don't use optional chaining where undefined values aren't allowed. - Don't have unused function parameters. - Don't have unused imports. - Don't have unused labels. - Don't have unused private class members. - Don't have unused variables. - Make sure void (self-closing) elements don't have children. - Don't return a value from a function with the return type 'void' - Use isNaN() when checking for NaN. - Make sure "for" loop update clauses move the counter in the right direction. - Make sure typeof expressions are compared to valid values. - Make sure generator functions contain yield. - Don't use await inside loops. - Don't use bitwise operators. - Don't use expressions where the operation doesn't change the value. - Make sure Promise-like statements are handled appropriately. - Don't use __dirname and __filename in the global scope. - Prevent import cycles. - Don't use configured elements. - Don't hardcode sensitive data like API keys and tokens. - Don't let variable declarations shadow variables from outer scopes. - Don't use the TypeScript directive @ts-ignore. - Prevent duplicate polyfills from Polyfill.io. - Don't use useless backreferences in regular expressions that always match empty strings. - Don't use unnecessary escapes in string literals. - Don't use useless undefined. - Make sure getters and setters for the same property are next to each other in class and object definitions. - Make sure object literals are declared consistently (defaults to explicit definitions). - Use static Response methods instead of new Response() constructor when possible. - Make sure switch-case statements are exhaustive. - Make sure the `preconnect` attribute is used when using Google Fonts. - Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item. - Make sure iterable callbacks return consistent values. - Use `with { type: "json" }` for JSON module imports. - Use numeric separators in numeric literals. - Use object spread instead of `Object.assign()` when constructing new objects. - Always use the radix argument when using `parseInt()`. - Make sure JSDoc comment lines start with a single asterisk, except for the first one. - Include a description parameter for `Symbol()`. - Don't use spread (`...`) syntax on accumulators. - Don't use the `delete` operator. - Don't access namespace imports dynamically. - Don't use namespace imports. - Declare regex literals at the top level. - Don't use `target="_blank"` without `rel="noopener"`. ### TypeScript Best Practices - Don't use TypeScript enums. - Don't export imported variables. - Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions. - Don't use TypeScript namespaces. - Don't use non-null assertions with the `!` postfix operator. - Don't use parameter properties in class constructors. - Don't use user-defined types. - Use `as const` instead of literal types and type annotations. - Use either `T[]` or `Array` consistently. - Initialize each enum member value explicitly. - Use `export type` for types. - Use `import type` for types. - Make sure all enum members are literal values. - Don't use TypeScript const enum. - Don't declare empty interfaces. - Don't let variables evolve into any type through reassignments. - Don't use the any type. - Don't misuse the non-null assertion operator (!) in TypeScript files. - Don't use implicit any type on variable declarations. - Don't merge interfaces and classes unsafely. - Don't use overload signatures that aren't next to each other. - Use the namespace keyword instead of the module keyword to declare TypeScript namespaces. ### Style and Consistency - Don't use global `eval()`. - Don't use callbacks in asynchronous tests and hooks. - Don't use negation in `if` statements that have `else` clauses. - Don't use nested ternary expressions. - Don't reassign function parameters. - This rule lets you specify global variable names you don't want to use in your application. - Don't use specified modules when loaded by import or require. - Don't use constants whose value is the upper-case version of their name. - Use `String.slice()` instead of `String.substr()` and `String.substring()`. - Don't use template literals if you don't need interpolation or special-character handling. - Don't use `else` blocks when the `if` block breaks early. - Don't use yoda expressions. - Don't use Array constructors. - Use `at()` instead of integer index access. - Follow curly brace conventions. - Use `else if` instead of nested `if` statements in `else` clauses. - Use single `if` statements instead of nested `if` clauses. - Use `new` for all builtins except `String`, `Number`, and `Boolean`. - Use consistent accessibility modifiers on class properties and methods. - Use `const` declarations for variables that are only assigned once. - Put default function parameters and optional function parameters last. - Include a `default` clause in switch statements. - Use the `**` operator instead of `Math.pow`. - Use `for-of` loops when you need the index to extract an item from the iterated array. - Use `node:assert/strict` over `node:assert`. - Use the `node:` protocol for Node.js builtin modules. - Use Number properties instead of global ones. - Use assignment operator shorthand where possible. - Use function types instead of object types with call signatures. - Use template literals over string concatenation. - Use `new` when throwing an error. - Don't throw non-Error values. - Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`. - Use standard constants instead of approximated literals. - Don't assign values in expressions. - Don't use async functions as Promise executors. - Don't reassign exceptions in catch clauses. - Don't reassign class members. - Don't compare against -0. - Don't use labeled statements that aren't loops. - Don't use void type outside of generic or return types. - Don't use console. - Don't use control characters and escape sequences that match control characters in regular expression literals. - Don't use debugger. - Don't assign directly to document.cookie. - Use `===` and `!==`. - Don't use duplicate case labels. - Don't use duplicate class members. - Don't use duplicate conditions in if-else-if chains. - Don't use two keys with the same name inside objects. - Don't use duplicate function parameter names. - Don't have duplicate hooks in describe blocks. - Don't use empty block statements and static blocks. - Don't let switch clauses fall through. - Don't reassign function declarations. - Don't allow assignments to native objects and read-only global variables. - Use Number.isFinite instead of global isFinite. - Use Number.isNaN instead of global isNaN. - Don't assign to imported bindings. - Don't use irregular whitespace characters. - Don't use labels that share a name with a variable. - Don't use characters made with multiple code points in character class syntax. - Make sure to use new and constructor properly. - Don't use shorthand assign when the variable appears on both sides. - Don't use octal escape sequences in string literals. - Don't use Object.prototype builtins directly. - Don't redeclare variables, functions, classes, and types in the same scope. - Don't have redundant "use strict". - Don't compare things where both sides are exactly the same. - Don't let identifiers shadow restricted names. - Don't use sparse arrays (arrays with holes). - Don't use template literal placeholder syntax in regular strings. - Don't use the then property. - Don't use unsafe negation. - Don't use var. - Don't use with statements in non-strict contexts. - Make sure async functions actually use await. - Make sure default clauses in switch statements come last. - Make sure to pass a message value when creating a built-in error. - Make sure get methods always return a value. - Use a recommended display strategy with Google Fonts. - Make sure for-in loops include an if statement. - Use Array.isArray() instead of instanceof Array. - Make sure to use the digits argument with Number#toFixed(). - Make sure to use the "use strict" directive in script files. ### Next.js Specific Rules - Don't use `` elements in Next.js projects. - Don't use `` elements in Next.js projects. - Don't import next/document outside of pages/_document.jsx in Next.js projects. - Don't use the next/head module in pages/_document.js on Next.js projects. ### Testing Best Practices - Don't use export or module.exports in test files. - Don't use focused tests. - Make sure the assertion function, like expect, is placed inside an it() function call. - Don't use disabled tests. ## Common Tasks - `npx ultracite init` - Initialize Ultracite in your project - `npx ultracite fix` - Format and fix code automatically - `npx ultracite check` - Check for issues without fixing ## Example: Error Handling ```typescript // ✅ Good: Comprehensive error handling try { const result = await fetchData(); return { success: true, data: result }; } catch (error) { console.error('API call failed:', error); return { success: false, error: error.message }; } // ❌ Bad: Swallowing errors try { return await fetchData(); } catch (e) { console.log(e); } ``` ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing Thanks for your interest in contributing to the chatbot template! Here's how to get started. ## Development 1. Fork and clone the repository 2. Install dependencies with `pnpm install` 3. Create a new branch from `demo` for your work ## Changesets This project uses [Changesets](https://github.com/changesets/changesets) to manage versioning and releases. When you make a change that should be included in the next release, you need to add a changeset. ### Adding a changeset Run the following command from the root of the repository: ```bash pnpm changeset ``` You'll be prompted to: 1. **Select the bump type** — `patch` for bug fixes, `minor` for new features, `major` for breaking changes 2. **Write a summary** — a short description of your change that will appear in the changelog This creates a Markdown file in the `.changeset` directory. Commit this file along with your code changes. ### When to add a changeset - Bug fixes, new features, breaking changes, dependency updates, and other user-facing changes should include a changeset - Internal refactors, test-only changes, and documentation updates typically don't need one ### What happens next When your changes land on `main`, the release workflow picks up any changeset files and opens a "Version Package" PR. Merging that PR bumps the version, updates the changelog, and creates a GitHub Release. ## Pull requests - Open PRs against the `demo` branch - Include a changeset if your change affects the release - Keep PRs focused — one feature or fix per PR ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: push: jobs: build: runs-on: ubuntu-22.04 strategy: matrix: node-version: [20] steps: - uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 9.12.3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: "pnpm" - name: Install dependencies run: pnpm install - name: Run check run: pnpm check ================================================ FILE: .github/workflows/playwright.yml ================================================ name: Playwright Tests on: push: branches: [main, master] pull_request: branches: [main, master] jobs: test: timeout-minutes: 30 runs-on: ubuntu-latest env: AUTH_SECRET: ${{ secrets.AUTH_SECRET }} POSTGRES_URL: ${{ secrets.POSTGRES_URL }} BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }} REDIS_URL: ${{ secrets.REDIS_URL }} steps: - uses: actions/checkout@v4 with: fetch-depth: 1 - uses: actions/setup-node@v4 with: node-version: lts/* - name: Install pnpm uses: pnpm/action-setup@v2 with: version: latest run_install: false - name: Get pnpm store directory id: pnpm-cache shell: bash run: | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - uses: actions/cache@v3 with: path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - uses: actions/setup-node@v4 with: node-version: lts/* cache: "pnpm" - name: Install dependencies run: pnpm install --frozen-lockfile - name: Cache Playwright browsers uses: actions/cache@v3 id: playwright-cache with: path: ~/.cache/ms-playwright key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }} - name: Install Playwright Browsers if: steps.playwright-cache.outputs.cache-hit != 'true' run: pnpm exec playwright install --with-deps chromium - name: Run Playwright tests run: pnpm test - uses: actions/upload-artifact@v4 if: always() && !cancelled() with: name: playwright-report path: playwright-report/ retention-days: 7 ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: - main concurrency: ${{ github.workflow }}-${{ github.ref }} permissions: contents: write pull-requests: write jobs: release: name: Release runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile - name: Create Release Pull Request or Tag id: changesets uses: changesets/action@v1 with: title: 'chore: version package' commit: 'chore: version package' version: pnpm version publish: pnpm release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create GitHub Release if: steps.changesets.outputs.published == 'true' uses: actions/github-script@v7 env: PUBLISHED_PACKAGES: ${{ steps.changesets.outputs.publishedPackages }} with: script: | const packages = JSON.parse(process.env.PUBLISHED_PACKAGES); for (const pkg of packages) { const tag = `v${pkg.version}`; await github.rest.repos.createRelease({ owner: context.repo.owner, repo: context.repo.repo, tag_name: tag, name: tag, generate_release_notes: true, }); } ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies node_modules .pnp .pnp.js # testing coverage # next.js .next/ out/ build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # local env files .env.local .env.development.local .env.test.local .env.production.local # turbo .turbo .env .vercel .env*.local # Playwright /test-results/ /playwright-report/ /blob-report/ /playwright/* ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": ["biomejs.biome"] } ================================================ FILE: .vscode/settings.json ================================================ { "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" } } ================================================ FILE: LICENSE ================================================ 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. ================================================ FILE: README.md ================================================ Chatbot

Chatbot

Chatbot (formerly AI Chatbot) is a free, open-source template built with Next.js and the AI SDK that helps you quickly build powerful chatbot applications.

Read Docs · Features · Model Providers · Deploy Your Own · Running locally


## Features - [Next.js](https://nextjs.org) App Router - Advanced routing for seamless navigation and performance - React Server Components (RSCs) and Server Actions for server-side rendering and increased performance - [AI SDK](https://ai-sdk.dev/docs/introduction) - Unified API for generating text, structured objects, and tool calls with LLMs - Hooks for building dynamic chat and generative user interfaces - Supports OpenAI, Anthropic, Google, xAI, and other model providers via AI Gateway - [shadcn/ui](https://ui.shadcn.com) - Styling with [Tailwind CSS](https://tailwindcss.com) - Component primitives from [Radix UI](https://radix-ui.com) for accessibility and flexibility - Data Persistence - [Neon Serverless Postgres](https://vercel.com/marketplace/neon) for saving chat history and user data - [Vercel Blob](https://vercel.com/storage/blob) for efficient file storage - [Auth.js](https://authjs.dev) - Simple and secure authentication ## Model Providers This template uses the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) to access multiple AI models through a unified interface. The default model is [OpenAI](https://openai.com) GPT-4.1 Mini, with support for Anthropic, Google, and xAI models. ### AI Gateway Authentication **For Vercel deployments**: Authentication is handled automatically via OIDC tokens. **For non-Vercel deployments**: You need to provide an AI Gateway API key by setting the `AI_GATEWAY_API_KEY` environment variable in your `.env.local` file. With the [AI SDK](https://ai-sdk.dev/docs/introduction), you can also switch to direct LLM providers like [OpenAI](https://openai.com), [Anthropic](https://anthropic.com), [Cohere](https://cohere.com/), and [many more](https://ai-sdk.dev/providers/ai-sdk-providers) with just a few lines of code. ## Deploy Your Own You can deploy your own version of Chatbot to Vercel with one click: [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/templates/next.js/chatbot) ## Running locally You will need to use the environment variables [defined in `.env.example`](.env.example) to run Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/projects/environment-variables) for this, but a `.env` file is all that is necessary. > Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various AI and authentication provider accounts. 1. Install Vercel CLI: `npm i -g vercel` 2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link` 3. Download your environment variables: `vercel env pull` ```bash pnpm install pnpm db:migrate # Setup database or apply latest database changes pnpm dev ``` Your app template should now be running on [localhost:3000](http://localhost:3000). ================================================ FILE: app/(auth)/actions.ts ================================================ "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" }; } }; ================================================ FILE: app/(auth)/api/auth/[...nextauth]/route.ts ================================================ export { GET, POST } from "@/app/(auth)/auth"; ================================================ FILE: app/(auth)/api/auth/guest/route.ts ================================================ 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 redirectUrl = searchParams.get("redirectUrl") || "/"; const token = await getToken({ req: request, secret: process.env.AUTH_SECRET, secureCookie: !isDevelopmentEnvironment, }); if (token) { return NextResponse.redirect(new URL("/", request.url)); } return signIn("guest", { redirect: true, redirectTo: redirectUrl }); } ================================================ FILE: app/(auth)/auth.config.ts ================================================ import type { NextAuthConfig } from "next-auth"; export const authConfig = { pages: { signIn: "/login", newUser: "/", }, providers: [ // added later in auth.ts since it requires bcrypt which is only compatible with Node.js // while this file is also used in non-Node.js environments ], callbacks: {}, } satisfies NextAuthConfig; ================================================ FILE: app/(auth)/auth.ts ================================================ 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: {}, async authorize({ email, password }: any) { 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; }, }, }); ================================================ FILE: app/(auth)/login/page.tsx ================================================ "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/auth-form"; import { SubmitButton } from "@/components/submit-button"; import { toast } from "@/components/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 (

Sign In

Use your email and password to sign in

Sign in

{"Don't have an account? "} Sign up {" for free."}

); } ================================================ FILE: app/(auth)/register/page.tsx ================================================ "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/auth-form"; import { SubmitButton } from "@/components/submit-button"; import { toast } from "@/components/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 successfully!" }); setIsSuccessful(true); updateSession(); router.refresh(); } }, [state.status]); const handleSubmit = (formData: FormData) => { setEmail(formData.get("email") as string); formAction(formData); }; return (

Sign Up

Create an account with your email and password

Sign Up

{"Already have an account? "} Sign in {" instead."}

); } ================================================ FILE: app/(chat)/actions.ts ================================================ "use server"; import { generateText, type UIMessage } from "ai"; import { cookies } from "next/headers"; import type { VisibilityType } from "@/components/visibility-selector"; import { titlePrompt } from "@/lib/ai/prompts"; import { getTitleModel } from "@/lib/ai/providers"; import { deleteMessagesByChatIdAfterTimestamp, 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 [message] = await getMessageById({ id }); await deleteMessagesByChatIdAfterTimestamp({ chatId: message.chatId, timestamp: message.createdAt, }); } export async function updateChatVisibility({ chatId, visibility, }: { chatId: string; visibility: VisibilityType; }) { await updateChatVisibilityById({ chatId, visibility }); } ================================================ FILE: app/(chat)/api/chat/[id]/stream/route.ts ================================================ export function GET() { return new Response(null, { status: 204 }); } ================================================ FILE: app/(chat)/api/chat/route.ts ================================================ import { geolocation, ipAddress } from "@vercel/functions"; import { convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse, generateId, stepCountIs, streamText, } from "ai"; import { checkBotId } from "botid/server"; import { after } from "next/server"; import { createResumableStreamContext } from "resumable-stream"; import { auth, type UserType } from "@/app/(auth)/auth"; import { entitlementsByUserType } from "@/lib/ai/entitlements"; import { allowedModelIds } from "@/lib/ai/models"; import { type RequestHints, systemPrompt } from "@/lib/ai/prompts"; import { getLanguageModel } from "@/lib/ai/providers"; import { createDocument } from "@/lib/ai/tools/create-document"; import { getWeather } from "@/lib/ai/tools/get-weather"; import { requestSuggestions } from "@/lib/ai/tools/request-suggestions"; import { updateDocument } from "@/lib/ai/tools/update-document"; import { isProductionEnvironment } from "@/lib/constants"; import { createStreamId, 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 getStreamContext() { try { return createResumableStreamContext({ waitUntil: after }); } catch (_) { return null; } } export { getStreamContext }; 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 [botResult, session] = await Promise.all([checkBotId(), auth()]); if (botResult.isBot) { return new ChatbotError("unauthorized:chat").toResponse(); } if (!session?.user) { return new ChatbotError("unauthorized:chat").toResponse(); } if (!allowedModelIds.has(selectedChatModel)) { return new ChatbotError("bad_request:api").toResponse(); } await checkIpRateLimit(ipAddress(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(); } if (!isToolApprovalFlow) { messagesFromDb = await getMessagesByChatId({ id }); } } else if (message?.role === "user") { await saveChat({ id, userId: session.user.id, title: "New chat", visibility: selectedVisibilityType, }); titlePromise = generateTitleFromUserMessage({ message }); } const uiMessages = isToolApprovalFlow ? (messages as ChatMessage[]) : [...convertToUIMessages(messagesFromDb), message as ChatMessage]; const { longitude, latitude, city, country } = geolocation(request); const requestHints: RequestHints = { longitude, latitude, city, country, }; if (message?.role === "user") { await saveMessages({ messages: [ { chatId: id, id: message.id, role: "user", parts: message.parts, attachments: [], createdAt: new Date(), }, ], }); } const isReasoningModel = selectedChatModel.endsWith("-thinking") || (selectedChatModel.includes("reasoning") && !selectedChatModel.includes("non-reasoning")); const modelMessages = await convertToModelMessages(uiMessages); const stream = createUIMessageStream({ originalMessages: isToolApprovalFlow ? uiMessages : undefined, execute: async ({ writer: dataStream }) => { const result = streamText({ model: getLanguageModel(selectedChatModel), system: systemPrompt({ selectedChatModel, requestHints }), messages: modelMessages, stopWhen: stepCountIs(5), experimental_activeTools: isReasoningModel ? [] : [ "getWeather", "createDocument", "updateDocument", "requestSuggestions", ], providerOptions: isReasoningModel ? { anthropic: { thinking: { type: "enabled", budgetTokens: 10_000 }, }, } : undefined, tools: { getWeather, createDocument: createDocument({ session, dataStream }), updateDocument: updateDocument({ session, dataStream }), requestSuggestions: requestSuggestions({ session, dataStream }), }, experimental_telemetry: { isEnabled: isProductionEnvironment, functionId: "stream-text", }, }); dataStream.merge( result.toUIMessageStream({ sendReasoning: isReasoningModel }) ); if (titlePromise) { const title = await titlePromise; dataStream.write({ type: "data-chat-title", data: title }); updateChatTitleById({ chatId: id, title }); } }, 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: (error) => { if ( error instanceof Error && error.message?.includes( "AI Gateway requires a valid credit card on file to service requests" ) ) { return "AI Gateway requires a valid credit card on file to service requests. Please visit https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dadd-credit-card to add a card and unlock your free credits."; } return "Oops, an error occurred!"; }, }); return createUIMessageStreamResponse({ stream, async consumeSseStream({ stream: sseStream }) { if (!process.env.REDIS_URL) { return; } try { const streamContext = getStreamContext(); if (streamContext) { const streamId = generateId(); await createStreamId({ streamId, chatId: id }); await streamContext.createNewResumableStream( streamId, () => sseStream ); } } catch (_) { // ignore redis errors } }, }); } catch (error) { const vercelId = request.headers.get("x-vercel-id"); if (error instanceof ChatbotError) { return error.toResponse(); } if ( error instanceof Error && error.message?.includes( "AI Gateway requires a valid credit card on file to service requests" ) ) { return new ChatbotError("bad_request:activate_gateway").toResponse(); } console.error("Unhandled error in chat API:", error, { vercelId }); 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 }); } ================================================ FILE: app/(chat)/api/chat/schema.ts ================================================ 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"]), name: z.string().min(1).max(100), url: z.string().url(), }); const partSchema = z.union([textPartSchema, filePartSchema]); const userMessageSchema = z.object({ id: z.string().uuid(), role: z.enum(["user"]), parts: z.array(partSchema), }); // For tool approval flows, we accept all messages (more permissive schema) const messageSchema = z.object({ id: z.string(), role: z.string(), parts: z.array(z.any()), }); export const postRequestBodySchema = z.object({ id: z.string().uuid(), // Either a single new message or all messages (for tool approvals) message: userMessageSchema.optional(), messages: z.array(messageSchema).optional(), selectedChatModel: z.string(), selectedVisibilityType: z.enum(["public", "private"]), }); export type PostRequestBody = z.infer; ================================================ FILE: app/(chat)/api/document/route.ts ================================================ import { auth } from "@/app/(auth)/auth"; import type { ArtifactKind } from "@/components/artifact"; import { deleteDocumentsByIdAfterTimestamp, getDocumentsById, saveDocument, } from "@/lib/db/queries"; import { ChatbotError } from "@/lib/errors"; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get("id"); if (!id) { return new ChatbotError( "bad_request:api", "Parameter id is missing" ).toResponse(); } const session = await auth(); if (!session?.user) { return new ChatbotError("unauthorized:document").toResponse(); } const documents = await getDocumentsById({ id }); const [document] = documents; if (!document) { return new ChatbotError("not_found:document").toResponse(); } if (document.userId !== session.user.id) { return new ChatbotError("forbidden:document").toResponse(); } return Response.json(documents, { status: 200 }); } export async function POST(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get("id"); if (!id) { return new ChatbotError( "bad_request:api", "Parameter id is required." ).toResponse(); } const session = await auth(); if (!session?.user) { return new ChatbotError("not_found:document").toResponse(); } const { content, title, kind, }: { content: string; title: string; kind: ArtifactKind } = await request.json(); const documents = await getDocumentsById({ id }); if (documents.length > 0) { const [doc] = documents; if (doc.userId !== session.user.id) { return new ChatbotError("forbidden:document").toResponse(); } } const document = await saveDocument({ id, content, title, kind, userId: session.user.id, }); return Response.json(document, { status: 200 }); } export async function DELETE(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get("id"); const timestamp = searchParams.get("timestamp"); if (!id) { return new ChatbotError( "bad_request:api", "Parameter id is required." ).toResponse(); } if (!timestamp) { return new ChatbotError( "bad_request:api", "Parameter timestamp is required." ).toResponse(); } const session = await auth(); if (!session?.user) { return new ChatbotError("unauthorized:document").toResponse(); } const documents = await getDocumentsById({ id }); const [document] = documents; if (document.userId !== session.user.id) { return new ChatbotError("forbidden:document").toResponse(); } const documentsDeleted = await deleteDocumentsByIdAfterTimestamp({ id, timestamp: new Date(timestamp), }); return Response.json(documentsDeleted, { status: 200 }); } ================================================ FILE: app/(chat)/api/files/upload/route.ts ================================================ import { put } from "@vercel/blob"; import { NextResponse } from "next/server"; import { z } from "zod"; import { auth } from "@/app/(auth)/auth"; // Use Blob instead of File since File is not available in Node.js environment const FileSchema = z.object({ file: z .instanceof(Blob) .refine((file) => file.size <= 5 * 1024 * 1024, { message: "File size should be less than 5MB", }) // Update the file type based on the kind of files you want to accept .refine((file) => ["image/jpeg", "image/png"].includes(file.type), { message: "File type should be JPEG or PNG", }), }); 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 }); } // Get filename from formData since Blob doesn't have name property const filename = (formData.get("file") as File).name; const fileBuffer = await file.arrayBuffer(); try { const data = await put(`${filename}`, fileBuffer, { access: "public", }); return NextResponse.json(data); } catch (_error) { return NextResponse.json({ error: "Upload failed" }, { status: 500 }); } } catch (_error) { return NextResponse.json( { error: "Failed to process request" }, { status: 500 } ); } } ================================================ FILE: app/(chat)/api/history/route.ts ================================================ 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 = Number.parseInt(searchParams.get("limit") || "10", 10); 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 }); } ================================================ FILE: app/(chat)/api/suggestions/route.ts ================================================ import { auth } from "@/app/(auth)/auth"; import { getSuggestionsByDocumentId } from "@/lib/db/queries"; import { ChatbotError } from "@/lib/errors"; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const documentId = searchParams.get("documentId"); if (!documentId) { return new ChatbotError( "bad_request:api", "Parameter documentId is required." ).toResponse(); } const session = await auth(); if (!session?.user) { return new ChatbotError("unauthorized:suggestions").toResponse(); } const suggestions = await getSuggestionsByDocumentId({ documentId, }); const [suggestion] = suggestions; if (!suggestion) { return Response.json([], { status: 200 }); } if (suggestion.userId !== session.user.id) { return new ChatbotError("forbidden:api").toResponse(); } return Response.json(suggestions, { status: 200 }); } ================================================ FILE: app/(chat)/api/vote/route.ts ================================================ import { auth } from "@/app/(auth)/auth"; import { getChatById, getVotesByChatId, voteMessage } from "@/lib/db/queries"; import { ChatbotError } from "@/lib/errors"; 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) { const { chatId, messageId, type, }: { chatId: string; messageId: string; type: "up" | "down" } = await request.json(); if (!chatId || !messageId || !type) { 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 }); } ================================================ FILE: app/(chat)/chat/[id]/page.tsx ================================================ import { cookies } from "next/headers"; import { notFound, redirect } from "next/navigation"; import { Suspense } from "react"; import { auth } from "@/app/(auth)/auth"; import { Chat } from "@/components/chat"; import { DataStreamHandler } from "@/components/data-stream-handler"; import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models"; import { getChatById, getMessagesByChatId } from "@/lib/db/queries"; import { convertToUIMessages } from "@/lib/utils"; export default function Page(props: { params: Promise<{ id: string }> }) { return ( }> ); } async function ChatPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; const chat = await getChatById({ id }); if (!chat) { redirect("/"); } const session = await auth(); if (!session) { redirect("/api/auth/guest"); } if (chat.visibility === "private") { if (!session.user) { return notFound(); } if (session.user.id !== chat.userId) { return notFound(); } } const messagesFromDb = await getMessagesByChatId({ id, }); const uiMessages = convertToUIMessages(messagesFromDb); const cookieStore = await cookies(); const chatModelFromCookie = cookieStore.get("chat-model"); if (!chatModelFromCookie) { return ( <> ); } return ( <> ); } ================================================ FILE: app/(chat)/layout.tsx ================================================ import { cookies } from "next/headers"; import Script from "next/script"; import { Suspense } from "react"; import { AppSidebar } from "@/components/app-sidebar"; import { DataStreamProvider } from "@/components/data-stream-provider"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { auth } from "../(auth)/auth"; export default function Layout({ children }: { children: React.ReactNode }) { return ( <>