From 0e320b391d6aabe74d601afd8576920b2b83bd0e Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Sat, 20 Sep 2025 12:47:10 -0700 Subject: [PATCH] Upgrade linter and formatter to Ultracite (#1224) --- .cursor/rules/ultracite.mdc | 333 ++ .vscode/settings.json | 28 +- app/(auth)/actions.ts | 60 +- app/(auth)/api/auth/[...nextauth]/route.ts | 3 +- app/(auth)/api/auth/guest/route.ts | 14 +- app/(auth)/auth.config.ts | 6 +- app/(auth)/auth.ts | 37 +- app/(auth)/login/page.tsx | 47 +- app/(auth)/register/page.tsx | 54 +- app/(chat)/actions.ts | 16 +- app/(chat)/api/chat/[id]/stream/route.ts | 43 +- app/(chat)/api/chat/route.ts | 162 +- app/(chat)/api/chat/schema.ts | 14 +- app/(chat)/api/document/route.ts | 50 +- app/(chat)/api/files/upload/route.ts | 38 +- app/(chat)/api/history/route.ts | 20 +- app/(chat)/api/suggestions/route.ts | 16 +- app/(chat)/api/vote/route.ts | 34 +- app/(chat)/chat/[id]/page.tsx | 36 +- app/(chat)/layout.tsx | 15 +- app/(chat)/page.tsx | 33 +- app/globals.css | 25 +- app/layout.tsx | 43 +- artifacts/actions.ts | 4 +- artifacts/code/client.tsx | 106 +- artifacts/code/server.ts | 36 +- artifacts/image/client.tsx | 36 +- artifacts/sheet/client.tsx | 60 +- artifacts/sheet/server.ts | 36 +- artifacts/text/client.tsx | 102 +- artifacts/text/server.ts | 38 +- biome.jsonc | 161 +- components/app-sidebar.tsx | 29 +- components/artifact-actions.tsx | 75 +- components/artifact-close-button.tsx | 16 +- components/artifact-messages.tsx | 83 +- components/artifact.tsx | 327 +- components/auth-form.tsx | 26 +- components/chat-header.tsx | 36 +- components/chat.tsx | 148 +- components/code-editor.tsx | 44 +- components/console.tsx | 113 +- components/create-artifact.tsx | 36 +- components/data-stream-handler.tsx | 45 +- components/data-stream-provider.tsx | 17 +- components/diffview.tsx | 48 +- components/document-preview.tsx | 126 +- components/document-skeleton.tsx | 6 +- components/document.tsx | 81 +- components/elements/actions.tsx | 22 +- components/elements/branch.tsx | 52 +- components/elements/code-block.tsx | 72 +- components/elements/context.tsx | 72 +- components/elements/conversation.tsx | 24 +- components/elements/image.tsx | 11 +- components/elements/inline-citation.tsx | 92 +- components/elements/loader.tsx | 10 +- components/elements/message.tsx | 32 +- components/elements/prompt-input.tsx | 88 +- components/elements/reasoning.tsx | 46 +- components/elements/response.tsx | 16 +- components/elements/source.tsx | 22 +- components/elements/suggestion.tsx | 20 +- components/elements/task.tsx | 30 +- components/elements/tool.tsx | 76 +- components/elements/web-preview.tsx | 62 +- components/greeting.tsx | 16 +- components/icons.tsx | 279 +- components/image-editor.tsx | 23 +- components/message-actions.tsx | 104 +- components/message-editor.tsx | 61 +- components/message-reasoning.tsx | 18 +- components/message.tsx | 227 +- components/messages.tsx | 101 +- components/model-selector.tsx | 48 +- components/multimodal-input.tsx | 280 +- components/preview-attachment.tsx | 24 +- components/sheet-editor.tsx | 75 +- components/sidebar-history-item.tsx | 46 +- components/sidebar-history.tsx | 76 +- components/sidebar-toggle.tsx | 14 +- components/sidebar-user-nav.tsx | 65 +- components/sign-out-form.tsx | 12 +- components/submit-button.tsx | 14 +- components/suggested-actions.tsx | 57 +- components/suggestion.tsx | 59 +- components/text-editor.tsx | 46 +- components/theme-provider.tsx | 6 +- components/toast.tsx | 44 +- components/toolbar.tsx | 239 +- components/ui/alert-dialog.tsx | 76 +- components/ui/avatar.tsx | 32 +- components/ui/badge.tsx | 26 +- components/ui/button.tsx | 52 +- components/ui/card.tsx | 53 +- components/ui/carousel.tsx | 200 +- components/ui/collapsible.tsx | 12 +- components/ui/dropdown-menu.tsx | 106 +- components/ui/hover-card.tsx | 24 +- components/ui/input.tsx | 20 +- components/ui/label.tsx | 20 +- components/ui/progress.tsx | 18 +- components/ui/scroll-area.tsx | 34 +- components/ui/select.tsx | 86 +- components/ui/separator.tsx | 26 +- components/ui/sheet.tsx | 82 +- components/ui/sidebar.tsx | 571 +-- components/ui/skeleton.tsx | 8 +- components/ui/textarea.tsx | 18 +- components/ui/tooltip.tsx | 24 +- components/version-footer.tsx | 62 +- components/visibility-selector.tsx | 44 +- components/weather.tsx | 306 +- drizzle.config.ts | 12 +- hooks/use-artifact.ts | 40 +- hooks/use-auto-resume.ts | 36 +- hooks/use-chat-visibility.ts | 32 +- hooks/use-messages.tsx | 14 +- hooks/use-mobile.ts | 19 + hooks/use-mobile.tsx | 21 - hooks/use-scroll-to-bottom.tsx | 34 +- instrumentation.ts | 4 +- lib/ai/entitlements.ts | 14 +- lib/ai/models.mock.ts | 20 +- lib/ai/models.test.ts | 32 +- lib/ai/models.ts | 20 +- lib/ai/prompts.ts | 53 +- lib/ai/providers.ts | 28 +- lib/ai/tools/create-document.ts | 32 +- lib/ai/tools/get-weather.ts | 8 +- lib/ai/tools/request-suggestions.ts | 53 +- lib/ai/tools/update-document.ts | 32 +- lib/artifacts/server.ts | 38 +- lib/constants.ts | 8 +- lib/db/migrate.ts | 20 +- lib/db/migrations/meta/0000_snapshot.json | 10 +- lib/db/migrations/meta/0001_snapshot.json | 45 +- lib/db/migrations/meta/0002_snapshot.json | 74 +- lib/db/migrations/meta/0003_snapshot.json | 74 +- lib/db/migrations/meta/0004_snapshot.json | 74 +- lib/db/migrations/meta/0005_snapshot.json | 103 +- lib/db/migrations/meta/0006_snapshot.json | 115 +- lib/db/migrations/meta/0007_snapshot.json | 115 +- lib/db/migrations/meta/_journal.json | 2 +- lib/db/queries.ts | 219 +- lib/db/schema.ts | 140 +- lib/db/utils.ts | 4 +- lib/editor/config.ts | 28 +- lib/editor/diff.js | 94 +- lib/editor/functions.tsx | 42 +- lib/editor/react-renderer.tsx | 3 +- lib/editor/suggestions.tsx | 47 +- lib/errors.ts | 136 +- lib/types.ts | 25 +- lib/usage.ts | 4 +- lib/utils.ts | 14 +- middleware.ts | 32 +- next.config.ts | 4 +- package.json | 32 +- playwright.config.ts | 22 +- pnpm-lock.yaml | 4471 +++++++------------- postcss.config.mjs | 2 +- tests/e2e/artifacts.test.ts | 30 +- tests/e2e/chat.test.ts | 76 +- tests/e2e/reasoning.test.ts | 28 +- tests/e2e/session.test.ts | 156 +- tests/fixtures.ts | 19 +- tests/helpers.ts | 36 +- tests/pages/artifact.ts | 76 +- tests/pages/auth.ts | 56 +- tests/pages/chat.ts | 149 +- tests/prompts/basic.ts | 142 +- tests/prompts/routes.ts | 18 +- tests/prompts/utils.ts | 224 +- tests/routes/chat.test.ts | 158 +- tests/routes/document.test.ts | 78 +- tsconfig.json | 3 +- 177 files changed, 6951 insertions(+), 8342 deletions(-) create mode 100644 .cursor/rules/ultracite.mdc create mode 100644 hooks/use-mobile.ts delete mode 100644 hooks/use-mobile.tsx diff --git a/.cursor/rules/ultracite.mdc b/.cursor/rules/ultracite.mdc new file mode 100644 index 0000000..f2da237 --- /dev/null +++ b/.cursor/rules/ultracite.mdc @@ -0,0 +1,333 @@ +--- +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); +} +``` \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index fb5a7ca..3e386d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,17 +1,35 @@ { - "editor.formatOnSave": true, + "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", - "eslint.workingDirectories": [ - { "pattern": "app/*" }, - { "pattern": "packages/*" } - ] + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "emmet.showExpandedAbbreviation": "never", + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + } } diff --git a/app/(auth)/actions.ts b/app/(auth)/actions.ts index 84f8ffd..024ff51 100644 --- a/app/(auth)/actions.ts +++ b/app/(auth)/actions.ts @@ -1,84 +1,84 @@ -'use server'; +"use server"; -import { z } from 'zod'; +import { z } from "zod"; -import { createUser, getUser } from '@/lib/db/queries'; +import { createUser, getUser } from "@/lib/db/queries"; -import { signIn } from './auth'; +import { signIn } from "./auth"; const authFormSchema = z.object({ email: z.string().email(), password: z.string().min(6), }); -export interface LoginActionState { - status: 'idle' | 'in_progress' | 'success' | 'failed' | 'invalid_data'; -} +export type LoginActionState = { + status: "idle" | "in_progress" | "success" | "failed" | "invalid_data"; +}; export const login = async ( _: LoginActionState, - formData: FormData, + formData: FormData ): Promise => { try { const validatedData = authFormSchema.parse({ - email: formData.get('email'), - password: formData.get('password'), + email: formData.get("email"), + password: formData.get("password"), }); - await signIn('credentials', { + await signIn("credentials", { email: validatedData.email, password: validatedData.password, redirect: false, }); - return { status: 'success' }; + return { status: "success" }; } catch (error) { if (error instanceof z.ZodError) { - return { status: 'invalid_data' }; + return { status: "invalid_data" }; } - return { status: 'failed' }; + return { status: "failed" }; } }; -export interface RegisterActionState { +export type RegisterActionState = { status: - | 'idle' - | 'in_progress' - | 'success' - | 'failed' - | 'user_exists' - | 'invalid_data'; -} + | "idle" + | "in_progress" + | "success" + | "failed" + | "user_exists" + | "invalid_data"; +}; export const register = async ( _: RegisterActionState, - formData: FormData, + formData: FormData ): Promise => { try { const validatedData = authFormSchema.parse({ - email: formData.get('email'), - password: formData.get('password'), + email: formData.get("email"), + password: formData.get("password"), }); const [user] = await getUser(validatedData.email); if (user) { - return { status: 'user_exists' } as RegisterActionState; + return { status: "user_exists" } as RegisterActionState; } await createUser(validatedData.email, validatedData.password); - await signIn('credentials', { + await signIn("credentials", { email: validatedData.email, password: validatedData.password, redirect: false, }); - return { status: 'success' }; + return { status: "success" }; } catch (error) { if (error instanceof z.ZodError) { - return { status: 'invalid_data' }; + return { status: "invalid_data" }; } - return { status: 'failed' }; + return { status: "failed" }; } }; diff --git a/app/(auth)/api/auth/[...nextauth]/route.ts b/app/(auth)/api/auth/[...nextauth]/route.ts index ba24234..588ff6a 100644 --- a/app/(auth)/api/auth/[...nextauth]/route.ts +++ b/app/(auth)/api/auth/[...nextauth]/route.ts @@ -1 +1,2 @@ -export { GET, POST } from '@/app/(auth)/auth'; +// biome-ignore lint/performance/noBarrelFile: "Required" +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 index 25af1fa..dca565c 100644 --- a/app/(auth)/api/auth/guest/route.ts +++ b/app/(auth)/api/auth/guest/route.ts @@ -1,11 +1,11 @@ -import { signIn } from '@/app/(auth)/auth'; -import { isDevelopmentEnvironment } from '@/lib/constants'; -import { getToken } from 'next-auth/jwt'; -import { NextResponse } from 'next/server'; +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 redirectUrl = searchParams.get("redirectUrl") || "/"; const token = await getToken({ req: request, @@ -14,8 +14,8 @@ export async function GET(request: Request) { }); if (token) { - return NextResponse.redirect(new URL('/', request.url)); + return NextResponse.redirect(new URL("/", request.url)); } - return signIn('guest', { redirect: true, redirectTo: redirectUrl }); + return signIn("guest", { redirect: true, redirectTo: redirectUrl }); } diff --git a/app/(auth)/auth.config.ts b/app/(auth)/auth.config.ts index b7d7d50..b8bc9e1 100644 --- a/app/(auth)/auth.config.ts +++ b/app/(auth)/auth.config.ts @@ -1,9 +1,9 @@ -import type { NextAuthConfig } from 'next-auth'; +import type { NextAuthConfig } from "next-auth"; export const authConfig = { pages: { - signIn: '/login', - newUser: '/', + signIn: "/login", + newUser: "/", }, providers: [ // added later in auth.ts since it requires bcrypt which is only compatible with Node.js diff --git a/app/(auth)/auth.ts b/app/(auth)/auth.ts index 9185248..dbebb1d 100644 --- a/app/(auth)/auth.ts +++ b/app/(auth)/auth.ts @@ -1,21 +1,22 @@ -import { compare } from 'bcrypt-ts'; -import NextAuth, { type DefaultSession } from 'next-auth'; -import Credentials from 'next-auth/providers/credentials'; -import { createGuestUser, getUser } from '@/lib/db/queries'; -import { authConfig } from './auth.config'; -import { DUMMY_PASSWORD } from '@/lib/constants'; -import type { DefaultJWT } from 'next-auth/jwt'; +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'; +export type UserType = "guest" | "regular"; -declare module 'next-auth' { +declare module "next-auth" { interface Session extends DefaultSession { user: { id: string; type: UserType; - } & DefaultSession['user']; + } & DefaultSession["user"]; } + // biome-ignore lint/nursery/useConsistentTypeDefinitions: "Required" interface User { id?: string; email?: string | null; @@ -23,7 +24,7 @@ declare module 'next-auth' { } } -declare module 'next-auth/jwt' { +declare module "next-auth/jwt" { interface JWT extends DefaultJWT { id: string; type: UserType; @@ -57,22 +58,24 @@ export const { const passwordsMatch = await compare(password, user.password); - if (!passwordsMatch) return null; + if (!passwordsMatch) { + return null; + } - return { ...user, type: 'regular' }; + return { ...user, type: "regular" }; }, }), Credentials({ - id: 'guest', + id: "guest", credentials: {}, async authorize() { const [guestUser] = await createGuestUser(); - return { ...guestUser, type: 'guest' }; + return { ...guestUser, type: "guest" }; }, }), ], callbacks: { - async jwt({ token, user }) { + jwt({ token, user }) { if (user) { token.id = user.id as string; token.type = user.type; @@ -80,7 +83,7 @@ export const { return token; }, - async session({ session, token }) { + session({ session, token }) { if (session.user) { session.user.id = token.id; session.user.type = token.type; diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index cd670ee..1140cbe 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,52 +1,51 @@ -'use client'; +"use client"; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { useActionState, useEffect, useState } from 'react'; -import { toast } from '@/components/toast'; +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 { login, type LoginActionState } from '../actions'; -import { useSession } from 'next-auth/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 [email, setEmail] = useState(""); const [isSuccessful, setIsSuccessful] = useState(false); const [state, formAction] = useActionState( login, { - status: 'idle', - }, + status: "idle", + } ); const { update: updateSession } = useSession(); useEffect(() => { - if (state.status === 'failed') { + if (state.status === "failed") { toast({ - type: 'error', - description: 'Invalid credentials!', + type: "error", + description: "Invalid credentials!", }); - } else if (state.status === 'invalid_data') { + } else if (state.status === "invalid_data") { toast({ - type: 'error', - description: 'Failed validating your submission!', + type: "error", + description: "Failed validating your submission!", }); - } else if (state.status === 'success') { + } else if (state.status === "success") { setIsSuccessful(true); updateSession(); router.refresh(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state.status]); + }, [state.status, router.refresh, updateSession]); const handleSubmit = (formData: FormData) => { - setEmail(formData.get('email') as string); + setEmail(formData.get("email") as string); formAction(formData); }; @@ -64,12 +63,12 @@ export default function Page() {

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

diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx index 74b4ff6..950f67b 100644 --- a/app/(auth)/register/page.tsx +++ b/app/(auth)/register/page.tsx @@ -1,53 +1,51 @@ -'use client'; +"use client"; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { useActionState, useEffect, useState } from 'react'; - -import { AuthForm } from '@/components/auth-form'; -import { SubmitButton } from '@/components/submit-button'; - -import { register, type RegisterActionState } from '../actions'; -import { toast } from '@/components/toast'; -import { useSession } from 'next-auth/react'; +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 [email, setEmail] = useState(""); const [isSuccessful, setIsSuccessful] = useState(false); const [state, formAction] = useActionState( register, { - status: 'idle', - }, + status: "idle", + } ); const { update: updateSession } = useSession(); 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') { + 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!', + type: "error", + description: "Failed validating your submission!", }); - } else if (state.status === 'success') { - toast({ type: 'success', description: 'Account created successfully!' }); + } else if (state.status === "success") { + toast({ type: "success", description: "Account created successfully!" }); setIsSuccessful(true); updateSession(); router.refresh(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state.status]); + }, [state.status, router.refresh, updateSession]); const handleSubmit = (formData: FormData) => { - setEmail(formData.get('email') as string); + setEmail(formData.get("email") as string); formAction(formData); }; @@ -63,14 +61,14 @@ export default function Page() { Sign Up

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

diff --git a/app/(chat)/actions.ts b/app/(chat)/actions.ts index 14ee7dc..4e8a3ee 100644 --- a/app/(chat)/actions.ts +++ b/app/(chat)/actions.ts @@ -1,18 +1,18 @@ -'use server'; +"use server"; -import { generateText, type UIMessage } from 'ai'; -import { cookies } from 'next/headers'; +import { generateText, type UIMessage } from "ai"; +import { cookies } from "next/headers"; +import type { VisibilityType } from "@/components/visibility-selector"; +import { myProvider } from "@/lib/ai/providers"; import { deleteMessagesByChatIdAfterTimestamp, getMessageById, updateChatVisiblityById, -} from '@/lib/db/queries'; -import type { VisibilityType } from '@/components/visibility-selector'; -import { myProvider } from '@/lib/ai/providers'; +} from "@/lib/db/queries"; export async function saveChatModelAsCookie(model: string) { const cookieStore = await cookies(); - cookieStore.set('chat-model', model); + cookieStore.set("chat-model", model); } export async function generateTitleFromUserMessage({ @@ -21,7 +21,7 @@ export async function generateTitleFromUserMessage({ message: UIMessage; }) { const { text: title } = await generateText({ - model: myProvider.languageModel('title-model'), + model: myProvider.languageModel("title-model"), system: `\n - you will generate a short title based on the first message a user begins a conversation with - ensure it is not more than 80 characters long diff --git a/app/(chat)/api/chat/[id]/stream/route.ts b/app/(chat)/api/chat/[id]/stream/route.ts index 58b4643..48352e9 100644 --- a/app/(chat)/api/chat/[id]/stream/route.ts +++ b/app/(chat)/api/chat/[id]/stream/route.ts @@ -1,19 +1,19 @@ -import { auth } from '@/app/(auth)/auth'; +import { createUIMessageStream, JsonToSseTransformStream } from "ai"; +import { differenceInSeconds } from "date-fns"; +import { auth } from "@/app/(auth)/auth"; import { getChatById, getMessagesByChatId, getStreamIdsByChatId, -} from '@/lib/db/queries'; -import type { Chat } from '@/lib/db/schema'; -import { ChatSDKError } from '@/lib/errors'; -import type { ChatMessage } from '@/lib/types'; -import { createUIMessageStream, JsonToSseTransformStream } from 'ai'; -import { getStreamContext } from '../../route'; -import { differenceInSeconds } from 'date-fns'; +} from "@/lib/db/queries"; +import type { Chat } from "@/lib/db/schema"; +import { ChatSDKError } from "@/lib/errors"; +import type { ChatMessage } from "@/lib/types"; +import { getStreamContext } from "../../route"; export async function GET( _: Request, - { params }: { params: Promise<{ id: string }> }, + { params }: { params: Promise<{ id: string }> } ) { const { id: chatId } = await params; @@ -25,13 +25,13 @@ export async function GET( } if (!chatId) { - return new ChatSDKError('bad_request:api').toResponse(); + return new ChatSDKError("bad_request:api").toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError('unauthorized:chat').toResponse(); + return new ChatSDKError("unauthorized:chat").toResponse(); } let chat: Chat | null; @@ -39,35 +39,36 @@ export async function GET( try { chat = await getChatById({ id: chatId }); } catch { - return new ChatSDKError('not_found:chat').toResponse(); + return new ChatSDKError("not_found:chat").toResponse(); } if (!chat) { - return new ChatSDKError('not_found:chat').toResponse(); + return new ChatSDKError("not_found:chat").toResponse(); } - if (chat.visibility === 'private' && chat.userId !== session.user.id) { - return new ChatSDKError('forbidden:chat').toResponse(); + if (chat.visibility === "private" && chat.userId !== session.user.id) { + return new ChatSDKError("forbidden:chat").toResponse(); } const streamIds = await getStreamIdsByChatId({ chatId }); if (!streamIds.length) { - return new ChatSDKError('not_found:stream').toResponse(); + return new ChatSDKError("not_found:stream").toResponse(); } const recentStreamId = streamIds.at(-1); if (!recentStreamId) { - return new ChatSDKError('not_found:stream').toResponse(); + return new ChatSDKError("not_found:stream").toResponse(); } const emptyDataStream = createUIMessageStream({ + // biome-ignore lint/suspicious/noEmptyBlockStatements: "Needs to exist" execute: () => {}, }); const stream = await streamContext.resumableStream(recentStreamId, () => - emptyDataStream.pipeThrough(new JsonToSseTransformStream()), + emptyDataStream.pipeThrough(new JsonToSseTransformStream()) ); /* @@ -82,7 +83,7 @@ export async function GET( return new Response(emptyDataStream, { status: 200 }); } - if (mostRecentMessage.role !== 'assistant') { + if (mostRecentMessage.role !== "assistant") { return new Response(emptyDataStream, { status: 200 }); } @@ -95,7 +96,7 @@ export async function GET( const restoredStream = createUIMessageStream({ execute: ({ writer }) => { writer.write({ - type: 'data-appendMessage', + type: "data-appendMessage", data: JSON.stringify(mostRecentMessage), transient: true, }); @@ -104,7 +105,7 @@ export async function GET( return new Response( restoredStream.pipeThrough(new JsonToSseTransformStream()), - { status: 200 }, + { status: 200 } ); } diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 7a62f5a..238fe84 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -1,3 +1,4 @@ +import { geolocation } from "@vercel/functions"; import { convertToModelMessages, createUIMessageStream, @@ -5,9 +6,27 @@ import { smoothStream, stepCountIs, streamText, -} from 'ai'; -import { auth, type UserType } from '@/app/(auth)/auth'; -import { type RequestHints, systemPrompt } from '@/lib/ai/prompts'; +} from "ai"; +import { unstable_cache as cache } from "next/cache"; +import { after } from "next/server"; +import { + createResumableStreamContext, + type ResumableStreamContext, +} from "resumable-stream"; +import type { ModelCatalog } from "tokenlens/core"; +import { fetchModels } from "tokenlens/fetch"; +import { getUsage } from "tokenlens/helpers"; +import { auth, type UserType } from "@/app/(auth)/auth"; +import type { VisibilityType } from "@/components/visibility-selector"; +import { entitlementsByUserType } from "@/lib/ai/entitlements"; +import type { ChatModel } from "@/lib/ai/models"; +import { type RequestHints, systemPrompt } from "@/lib/ai/prompts"; +import { myProvider } 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, @@ -16,33 +35,14 @@ import { getMessagesByChatId, saveChat, saveMessages, -} from '@/lib/db/queries'; -import { updateChatLastContextById } from '@/lib/db/queries'; -import { convertToUIMessages, generateUUID } from '@/lib/utils'; -import { generateTitleFromUserMessage } from '../../actions'; -import { createDocument } from '@/lib/ai/tools/create-document'; -import { updateDocument } from '@/lib/ai/tools/update-document'; -import { requestSuggestions } from '@/lib/ai/tools/request-suggestions'; -import { getWeather } from '@/lib/ai/tools/get-weather'; -import { isProductionEnvironment } from '@/lib/constants'; -import { myProvider } from '@/lib/ai/providers'; -import { entitlementsByUserType } from '@/lib/ai/entitlements'; -import { postRequestBodySchema, type PostRequestBody } from './schema'; -import { geolocation } from '@vercel/functions'; -import { - createResumableStreamContext, - type ResumableStreamContext, -} from 'resumable-stream'; -import { after } from 'next/server'; -import { ChatSDKError } from '@/lib/errors'; -import type { ChatMessage } from '@/lib/types'; -import type { ChatModel } from '@/lib/ai/models'; -import type { VisibilityType } from '@/components/visibility-selector'; -import { unstable_cache as cache } from 'next/cache'; -import { fetchModels } from 'tokenlens/fetch'; -import { getUsage } from 'tokenlens/helpers'; -import type { ModelCatalog } from 'tokenlens/core'; -import type { AppUsage } from '@/lib/usage'; + updateChatLastContextById, +} from "@/lib/db/queries"; +import { ChatSDKError } from "@/lib/errors"; +import type { ChatMessage } from "@/lib/types"; +import type { AppUsage } from "@/lib/usage"; +import { convertToUIMessages, generateUUID } from "@/lib/utils"; +import { generateTitleFromUserMessage } from "../../actions"; +import { type PostRequestBody, postRequestBodySchema } from "./schema"; export const maxDuration = 60; @@ -54,14 +54,14 @@ const getTokenlensCatalog = cache( return await fetchModels(); } catch (err) { console.warn( - 'TokenLens: catalog fetch failed, using default catalog', - err, + "TokenLens: catalog fetch failed, using default catalog", + err ); - return undefined; // tokenlens helpers will fall back to defaultCatalog + return; // tokenlens helpers will fall back to defaultCatalog } }, - ['tokenlens-catalog'], - { revalidate: 24 * 60 * 60 }, // 24 hours + ["tokenlens-catalog"], + { revalidate: 24 * 60 * 60 } // 24 hours ); export function getStreamContext() { @@ -71,9 +71,9 @@ export function getStreamContext() { waitUntil: after, }); } catch (error: any) { - if (error.message.includes('REDIS_URL')) { + if (error.message.includes("REDIS_URL")) { console.log( - ' > Resumable streams are disabled due to missing REDIS_URL', + " > Resumable streams are disabled due to missing REDIS_URL" ); } else { console.error(error); @@ -91,7 +91,7 @@ export async function POST(request: Request) { const json = await request.json(); requestBody = postRequestBodySchema.parse(json); } catch (_) { - return new ChatSDKError('bad_request:api').toResponse(); + return new ChatSDKError("bad_request:api").toResponse(); } try { @@ -103,14 +103,14 @@ export async function POST(request: Request) { }: { id: string; message: ChatMessage; - selectedChatModel: ChatModel['id']; + selectedChatModel: ChatModel["id"]; selectedVisibilityType: VisibilityType; } = requestBody; const session = await auth(); if (!session?.user) { - return new ChatSDKError('unauthorized:chat').toResponse(); + return new ChatSDKError("unauthorized:chat").toResponse(); } const userType: UserType = session.user.type; @@ -121,12 +121,16 @@ export async function POST(request: Request) { }); if (messageCount > entitlementsByUserType[userType].maxMessagesPerDay) { - return new ChatSDKError('rate_limit:chat').toResponse(); + return new ChatSDKError("rate_limit:chat").toResponse(); } const chat = await getChatById({ id }); - if (!chat) { + if (chat) { + if (chat.userId !== session.user.id) { + return new ChatSDKError("forbidden:chat").toResponse(); + } + } else { const title = await generateTitleFromUserMessage({ message, }); @@ -137,10 +141,6 @@ export async function POST(request: Request) { title, visibility: selectedVisibilityType, }); - } else { - if (chat.userId !== session.user.id) { - return new ChatSDKError('forbidden:chat').toResponse(); - } } const messagesFromDb = await getMessagesByChatId({ id }); @@ -160,7 +160,7 @@ export async function POST(request: Request) { { chatId: id, id: message.id, - role: 'user', + role: "user", parts: message.parts, attachments: [], createdAt: new Date(), @@ -181,15 +181,15 @@ export async function POST(request: Request) { messages: convertToModelMessages(uiMessages), stopWhen: stepCountIs(5), experimental_activeTools: - selectedChatModel === 'chat-model-reasoning' + selectedChatModel === "chat-model-reasoning" ? [] : [ - 'getWeather', - 'createDocument', - 'updateDocument', - 'requestSuggestions', + "getWeather", + "createDocument", + "updateDocument", + "requestSuggestions", ], - experimental_transform: smoothStream({ chunking: 'word' }), + experimental_transform: smoothStream({ chunking: "word" }), tools: { getWeather, createDocument: createDocument({ session, dataStream }), @@ -201,7 +201,7 @@ export async function POST(request: Request) { }, experimental_telemetry: { isEnabled: isProductionEnvironment, - functionId: 'stream-text', + functionId: "stream-text", }, onFinish: async ({ usage }) => { try { @@ -210,23 +210,29 @@ export async function POST(request: Request) { myProvider.languageModel(selectedChatModel).modelId; if (!modelId) { finalMergedUsage = usage; - dataStream.write({ type: 'data-usage', data: finalMergedUsage }); + dataStream.write({ + type: "data-usage", + data: finalMergedUsage, + }); return; } if (!providers) { finalMergedUsage = usage; - dataStream.write({ type: 'data-usage', data: finalMergedUsage }); + dataStream.write({ + type: "data-usage", + data: finalMergedUsage, + }); return; } const summary = getUsage({ modelId, usage, providers }); finalMergedUsage = { ...usage, ...summary, modelId } as AppUsage; - dataStream.write({ type: 'data-usage', data: finalMergedUsage }); + dataStream.write({ type: "data-usage", data: finalMergedUsage }); } catch (err) { - console.warn('TokenLens enrichment failed', err); + console.warn("TokenLens enrichment failed", err); finalMergedUsage = usage; - dataStream.write({ type: 'data-usage', data: finalMergedUsage }); + dataStream.write({ type: "data-usage", data: finalMergedUsage }); } }, }); @@ -236,16 +242,16 @@ export async function POST(request: Request) { dataStream.merge( result.toUIMessageStream({ sendReasoning: true, - }), + }) ); }, generateId: generateUUID, onFinish: async ({ messages }) => { await saveMessages({ - messages: messages.map((message) => ({ - id: message.id, - role: message.role, - parts: message.parts, + messages: messages.map((currentMessage) => ({ + id: currentMessage.id, + role: currentMessage.role, + parts: currentMessage.parts, createdAt: new Date(), attachments: [], chatId: id, @@ -259,12 +265,12 @@ export async function POST(request: Request) { context: finalMergedUsage, }); } catch (err) { - console.warn('Unable to persist last usage for chat', id, err); + console.warn("Unable to persist last usage for chat", id, err); } } }, onError: () => { - return 'Oops, an error occurred!'; + return "Oops, an error occurred!"; }, }); @@ -273,12 +279,12 @@ export async function POST(request: Request) { if (streamContext) { return new Response( await streamContext.resumableStream(streamId, () => - stream.pipeThrough(new JsonToSseTransformStream()), - ), + stream.pipeThrough(new JsonToSseTransformStream()) + ) ); - } else { - return new Response(stream.pipeThrough(new JsonToSseTransformStream())); } + + return new Response(stream.pipeThrough(new JsonToSseTransformStream())); } catch (error) { if (error instanceof ChatSDKError) { return error.toResponse(); @@ -288,35 +294,35 @@ export async function POST(request: Request) { if ( error instanceof Error && error.message?.includes( - 'AI Gateway requires a valid credit card on file to service requests', + "AI Gateway requires a valid credit card on file to service requests" ) ) { - return new ChatSDKError('bad_request:activate_gateway').toResponse(); + return new ChatSDKError("bad_request:activate_gateway").toResponse(); } - console.error('Unhandled error in chat API:', error); - return new ChatSDKError('offline:chat').toResponse(); + console.error("Unhandled error in chat API:", error); + return new ChatSDKError("offline:chat").toResponse(); } } export async function DELETE(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); + const id = searchParams.get("id"); if (!id) { - return new ChatSDKError('bad_request:api').toResponse(); + return new ChatSDKError("bad_request:api").toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError('unauthorized:chat').toResponse(); + return new ChatSDKError("unauthorized:chat").toResponse(); } const chat = await getChatById({ id }); if (chat?.userId !== session.user.id) { - return new ChatSDKError('forbidden:chat').toResponse(); + return new ChatSDKError("forbidden:chat").toResponse(); } const deletedChat = await deleteChatById({ id }); diff --git a/app/(chat)/api/chat/schema.ts b/app/(chat)/api/chat/schema.ts index 555ef8b..090aa63 100644 --- a/app/(chat)/api/chat/schema.ts +++ b/app/(chat)/api/chat/schema.ts @@ -1,13 +1,13 @@ -import { z } from 'zod'; +import { z } from "zod"; const textPartSchema = z.object({ - type: z.enum(['text']), + 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']), + type: z.enum(["file"]), + mediaType: z.enum(["image/jpeg", "image/png"]), name: z.string().min(1).max(100), url: z.string().url(), }); @@ -18,11 +18,11 @@ export const postRequestBodySchema = z.object({ id: z.string().uuid(), message: z.object({ id: z.string().uuid(), - role: z.enum(['user']), + role: z.enum(["user"]), parts: z.array(partSchema), }), - selectedChatModel: z.enum(['chat-model', 'chat-model-reasoning']), - selectedVisibilityType: z.enum(['public', 'private']), + selectedChatModel: z.enum(["chat-model", "chat-model-reasoning"]), + selectedVisibilityType: z.enum(["public", "private"]), }); export type PostRequestBody = z.infer; diff --git a/app/(chat)/api/document/route.ts b/app/(chat)/api/document/route.ts index de41836..0ea78ff 100644 --- a/app/(chat)/api/document/route.ts +++ b/app/(chat)/api/document/route.ts @@ -1,27 +1,27 @@ -import { auth } from '@/app/(auth)/auth'; -import type { ArtifactKind } from '@/components/artifact'; +import { auth } from "@/app/(auth)/auth"; +import type { ArtifactKind } from "@/components/artifact"; import { deleteDocumentsByIdAfterTimestamp, getDocumentsById, saveDocument, -} from '@/lib/db/queries'; -import { ChatSDKError } from '@/lib/errors'; +} from "@/lib/db/queries"; +import { ChatSDKError } from "@/lib/errors"; export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); + const id = searchParams.get("id"); if (!id) { return new ChatSDKError( - 'bad_request:api', - 'Parameter id is missing', + "bad_request:api", + "Parameter id is missing" ).toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError('unauthorized:document').toResponse(); + return new ChatSDKError("unauthorized:document").toResponse(); } const documents = await getDocumentsById({ id }); @@ -29,11 +29,11 @@ export async function GET(request: Request) { const [document] = documents; if (!document) { - return new ChatSDKError('not_found:document').toResponse(); + return new ChatSDKError("not_found:document").toResponse(); } if (document.userId !== session.user.id) { - return new ChatSDKError('forbidden:document').toResponse(); + return new ChatSDKError("forbidden:document").toResponse(); } return Response.json(documents, { status: 200 }); @@ -41,19 +41,19 @@ export async function GET(request: Request) { export async function POST(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); + const id = searchParams.get("id"); if (!id) { return new ChatSDKError( - 'bad_request:api', - 'Parameter id is required.', + "bad_request:api", + "Parameter id is required." ).toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError('not_found:document').toResponse(); + return new ChatSDKError("not_found:document").toResponse(); } const { @@ -66,10 +66,10 @@ export async function POST(request: Request) { const documents = await getDocumentsById({ id }); if (documents.length > 0) { - const [document] = documents; + const [doc] = documents; - if (document.userId !== session.user.id) { - return new ChatSDKError('forbidden:document').toResponse(); + if (doc.userId !== session.user.id) { + return new ChatSDKError("forbidden:document").toResponse(); } } @@ -86,27 +86,27 @@ export async function POST(request: Request) { export async function DELETE(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); - const timestamp = searchParams.get('timestamp'); + const id = searchParams.get("id"); + const timestamp = searchParams.get("timestamp"); if (!id) { return new ChatSDKError( - 'bad_request:api', - 'Parameter id is required.', + "bad_request:api", + "Parameter id is required." ).toResponse(); } if (!timestamp) { return new ChatSDKError( - 'bad_request:api', - 'Parameter timestamp is required.', + "bad_request:api", + "Parameter timestamp is required." ).toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError('unauthorized:document').toResponse(); + return new ChatSDKError("unauthorized:document").toResponse(); } const documents = await getDocumentsById({ id }); @@ -114,7 +114,7 @@ export async function DELETE(request: Request) { const [document] = documents; if (document.userId !== session.user.id) { - return new ChatSDKError('forbidden:document').toResponse(); + return new ChatSDKError("forbidden:document").toResponse(); } const documentsDeleted = await deleteDocumentsByIdAfterTimestamp({ diff --git a/app/(chat)/api/files/upload/route.ts b/app/(chat)/api/files/upload/route.ts index 699a4cb..4e4e4f3 100644 --- a/app/(chat)/api/files/upload/route.ts +++ b/app/(chat)/api/files/upload/route.ts @@ -1,19 +1,19 @@ -import { put } from '@vercel/blob'; -import { NextResponse } from 'next/server'; -import { z } from 'zod'; +import { put } from "@vercel/blob"; +import { NextResponse } from "next/server"; +import { z } from "zod"; -import { auth } from '@/app/(auth)/auth'; +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', + 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', + .refine((file) => ["image/jpeg", "image/png"].includes(file.type), { + message: "File type should be JPEG or PNG", }), }); @@ -21,19 +21,19 @@ export async function POST(request: Request) { const session = await auth(); if (!session) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } if (request.body === null) { - return new Response('Request body is empty', { status: 400 }); + return new Response("Request body is empty", { status: 400 }); } try { const formData = await request.formData(); - const file = formData.get('file') as Blob; + const file = formData.get("file") as Blob; if (!file) { - return NextResponse.json({ error: 'No file uploaded' }, { status: 400 }); + return NextResponse.json({ error: "No file uploaded" }, { status: 400 }); } const validatedFile = FileSchema.safeParse({ file }); @@ -41,28 +41,28 @@ export async function POST(request: Request) { if (!validatedFile.success) { const errorMessage = validatedFile.error.errors .map((error) => error.message) - .join(', '); + .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 filename = (formData.get("file") as File).name; const fileBuffer = await file.arrayBuffer(); try { const data = await put(`${filename}`, fileBuffer, { - access: 'public', + access: "public", }); return NextResponse.json(data); - } catch (error) { - return NextResponse.json({ error: 'Upload failed' }, { status: 500 }); + } catch (_error) { + return NextResponse.json({ error: "Upload failed" }, { status: 500 }); } - } catch (error) { + } catch (_error) { return NextResponse.json( - { error: 'Failed to process request' }, - { status: 500 }, + { error: "Failed to process request" }, + { status: 500 } ); } } diff --git a/app/(chat)/api/history/route.ts b/app/(chat)/api/history/route.ts index e28290c..412f10f 100644 --- a/app/(chat)/api/history/route.ts +++ b/app/(chat)/api/history/route.ts @@ -1,26 +1,26 @@ -import { auth } from '@/app/(auth)/auth'; -import type { NextRequest } from 'next/server'; -import { getChatsByUserId } from '@/lib/db/queries'; -import { ChatSDKError } from '@/lib/errors'; +import type { NextRequest } from "next/server"; +import { auth } from "@/app/(auth)/auth"; +import { getChatsByUserId } from "@/lib/db/queries"; +import { ChatSDKError } from "@/lib/errors"; export async function GET(request: NextRequest) { const { searchParams } = request.nextUrl; - const limit = Number.parseInt(searchParams.get('limit') || '10'); - const startingAfter = searchParams.get('starting_after'); - const endingBefore = searchParams.get('ending_before'); + 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 ChatSDKError( - 'bad_request:api', - 'Only one of starting_after or ending_before can be provided.', + "bad_request:api", + "Only one of starting_after or ending_before can be provided." ).toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError('unauthorized:chat').toResponse(); + return new ChatSDKError("unauthorized:chat").toResponse(); } const chats = await getChatsByUserId({ diff --git a/app/(chat)/api/suggestions/route.ts b/app/(chat)/api/suggestions/route.ts index ffbda2f..8801004 100644 --- a/app/(chat)/api/suggestions/route.ts +++ b/app/(chat)/api/suggestions/route.ts @@ -1,22 +1,22 @@ -import { auth } from '@/app/(auth)/auth'; -import { getSuggestionsByDocumentId } from '@/lib/db/queries'; -import { ChatSDKError } from '@/lib/errors'; +import { auth } from "@/app/(auth)/auth"; +import { getSuggestionsByDocumentId } from "@/lib/db/queries"; +import { ChatSDKError } from "@/lib/errors"; export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const documentId = searchParams.get('documentId'); + const documentId = searchParams.get("documentId"); if (!documentId) { return new ChatSDKError( - 'bad_request:api', - 'Parameter documentId is required.', + "bad_request:api", + "Parameter documentId is required." ).toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError('unauthorized:suggestions').toResponse(); + return new ChatSDKError("unauthorized:suggestions").toResponse(); } const suggestions = await getSuggestionsByDocumentId({ @@ -30,7 +30,7 @@ export async function GET(request: Request) { } if (suggestion.userId !== session.user.id) { - return new ChatSDKError('forbidden:api').toResponse(); + return new ChatSDKError("forbidden:api").toResponse(); } return Response.json(suggestions, { status: 200 }); diff --git a/app/(chat)/api/vote/route.ts b/app/(chat)/api/vote/route.ts index b3d7e45..2c0ce3f 100644 --- a/app/(chat)/api/vote/route.ts +++ b/app/(chat)/api/vote/route.ts @@ -1,32 +1,32 @@ -import { auth } from '@/app/(auth)/auth'; -import { getChatById, getVotesByChatId, voteMessage } from '@/lib/db/queries'; -import { ChatSDKError } from '@/lib/errors'; +import { auth } from "@/app/(auth)/auth"; +import { getChatById, getVotesByChatId, voteMessage } from "@/lib/db/queries"; +import { ChatSDKError } from "@/lib/errors"; export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const chatId = searchParams.get('chatId'); + const chatId = searchParams.get("chatId"); if (!chatId) { return new ChatSDKError( - 'bad_request:api', - 'Parameter chatId is required.', + "bad_request:api", + "Parameter chatId is required." ).toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError('unauthorized:vote').toResponse(); + return new ChatSDKError("unauthorized:vote").toResponse(); } const chat = await getChatById({ id: chatId }); if (!chat) { - return new ChatSDKError('not_found:chat').toResponse(); + return new ChatSDKError("not_found:chat").toResponse(); } if (chat.userId !== session.user.id) { - return new ChatSDKError('forbidden:vote').toResponse(); + return new ChatSDKError("forbidden:vote").toResponse(); } const votes = await getVotesByChatId({ id: chatId }); @@ -39,37 +39,37 @@ export async function PATCH(request: Request) { chatId, messageId, type, - }: { chatId: string; messageId: string; type: 'up' | 'down' } = + }: { chatId: string; messageId: string; type: "up" | "down" } = await request.json(); if (!chatId || !messageId || !type) { return new ChatSDKError( - 'bad_request:api', - 'Parameters chatId, messageId, and type are required.', + "bad_request:api", + "Parameters chatId, messageId, and type are required." ).toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError('unauthorized:vote').toResponse(); + return new ChatSDKError("unauthorized:vote").toResponse(); } const chat = await getChatById({ id: chatId }); if (!chat) { - return new ChatSDKError('not_found:vote').toResponse(); + return new ChatSDKError("not_found:vote").toResponse(); } if (chat.userId !== session.user.id) { - return new ChatSDKError('forbidden:vote').toResponse(); + return new ChatSDKError("forbidden:vote").toResponse(); } await voteMessage({ chatId, messageId, - type: type, + type, }); - return new Response('Message voted', { status: 200 }); + return new Response("Message voted", { status: 200 }); } diff --git a/app/(chat)/chat/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx index cf3b71e..f208fac 100644 --- a/app/(chat)/chat/[id]/page.tsx +++ b/app/(chat)/chat/[id]/page.tsx @@ -1,12 +1,12 @@ -import { cookies } from 'next/headers'; -import { notFound, redirect } from 'next/navigation'; +import { cookies } from "next/headers"; +import { notFound, redirect } from "next/navigation"; -import { auth } from '@/app/(auth)/auth'; -import { Chat } from '@/components/chat'; -import { getChatById, getMessagesByChatId } from '@/lib/db/queries'; -import { DataStreamHandler } from '@/components/data-stream-handler'; -import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models'; -import { convertToUIMessages } from '@/lib/utils'; +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 async function Page(props: { params: Promise<{ id: string }> }) { const params = await props.params; @@ -20,10 +20,10 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { const session = await auth(); if (!session) { - redirect('/api/auth/guest'); + redirect("/api/auth/guest"); } - if (chat.visibility === 'private') { + if (chat.visibility === "private") { if (!session.user) { return notFound(); } @@ -40,20 +40,19 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { const uiMessages = convertToUIMessages(messagesFromDb); const cookieStore = await cookies(); - const chatModelFromCookie = cookieStore.get('chat-model'); + const chatModelFromCookie = cookieStore.get("chat-model"); if (!chatModelFromCookie) { return ( <> @@ -63,14 +62,13 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { return ( <> diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx index dfd9da2..6900400 100644 --- a/app/(chat)/layout.tsx +++ b/app/(chat)/layout.tsx @@ -1,10 +1,9 @@ -import { cookies } from 'next/headers'; - -import { AppSidebar } from '@/components/app-sidebar'; -import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; -import { auth } from '../(auth)/auth'; -import Script from 'next/script'; -import { DataStreamProvider } from '@/components/data-stream-provider'; +import { cookies } from "next/headers"; +import Script from "next/script"; +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 const experimental_ppr = true; @@ -14,7 +13,7 @@ export default async function Layout({ children: React.ReactNode; }) { const [session, cookieStore] = await Promise.all([auth(), cookies()]); - const isCollapsed = cookieStore.get('sidebar:state')?.value !== 'true'; + const isCollapsed = cookieStore.get("sidebar:state")?.value !== "true"; return ( <> diff --git a/app/(chat)/page.tsx b/app/(chat)/page.tsx index 41328cb..225d2ac 100644 --- a/app/(chat)/page.tsx +++ b/app/(chat)/page.tsx @@ -1,36 +1,34 @@ -import { cookies } from 'next/headers'; - -import { Chat } from '@/components/chat'; -import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models'; -import { generateUUID } from '@/lib/utils'; -import { DataStreamHandler } from '@/components/data-stream-handler'; -import { auth } from '../(auth)/auth'; -import { redirect } from 'next/navigation'; +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { Chat } from "@/components/chat"; +import { DataStreamHandler } from "@/components/data-stream-handler"; +import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models"; +import { generateUUID } from "@/lib/utils"; +import { auth } from "../(auth)/auth"; export default async function Page() { const session = await auth(); if (!session) { - redirect('/api/auth/guest'); + redirect("/api/auth/guest"); } const id = generateUUID(); const cookieStore = await cookies(); - const modelIdFromCookie = cookieStore.get('chat-model'); + const modelIdFromCookie = cookieStore.get("chat-model"); if (!modelIdFromCookie) { return ( <> @@ -40,14 +38,13 @@ export default async function Page() { return ( <> diff --git a/app/globals.css b/app/globals.css index 4be4d70..d0851ff 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,5 +1,4 @@ -/* set base path to root directory to include utility classes from app, components, pages, etc. */ -@import "tailwindcss" source(".."); +@import "tailwindcss"; /* include utility classes in streamdown */ @source '../node_modules/streamdown/dist/index.js'; @@ -47,6 +46,7 @@ --sidebar-ring: hsl(217.2 91.2% 59.8%); /* border radius unit */ --radius: 0.5rem; + --sidebar: hsl(0 0% 98%); } /* define design tokens (dark mode) */ @@ -83,6 +83,7 @@ --sidebar-accent-foreground: hsl(240 4.8% 95.9%); --sidebar-border: hsl(240 3.7% 15.9%); --sidebar-ring: hsl(217.2 91.2% 59.8%); + --sidebar: hsl(240 5.9% 10%); } /* define theme */ @@ -294,3 +295,23 @@ scrollbar-width: thin; scrollbar-color: var(--border) transparent; } + +@theme inline { + --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); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 1813e0f..66db5da 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,15 +1,15 @@ -import { Toaster } from 'sonner'; -import type { Metadata } from 'next'; -import { Geist, Geist_Mono } from 'next/font/google'; -import { ThemeProvider } from '@/components/theme-provider'; +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import { Toaster } from "sonner"; +import { ThemeProvider } from "@/components/theme-provider"; -import './globals.css'; -import { SessionProvider } from 'next-auth/react'; +import "./globals.css"; +import { SessionProvider } from "next-auth/react"; export const metadata: Metadata = { - metadataBase: new URL('https://chat.vercel.ai'), - title: 'Next.js Chatbot Template', - description: 'Next.js chatbot template using the AI SDK.', + metadataBase: new URL("https://chat.vercel.ai"), + title: "Next.js Chatbot Template", + description: "Next.js chatbot template using the AI SDK.", }; export const viewport = { @@ -17,19 +17,19 @@ export const viewport = { }; const geist = Geist({ - subsets: ['latin'], - display: 'swap', - variable: '--font-geist', + subsets: ["latin"], + display: "swap", + variable: "--font-geist", }); const geistMono = Geist_Mono({ - subsets: ['latin'], - display: 'swap', - variable: '--font-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 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; @@ -48,23 +48,24 @@ const THEME_COLOR_SCRIPT = `\ updateThemeColor(); })();`; -export default async function RootLayout({ +export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return (