Restore Ultracite + fix sidebar (#1233)
This commit is contained in:
parent
8fbfc253fa
commit
947ed094a6
177 changed files with 6908 additions and 8306 deletions
333
.cursor/rules/ultracite.mdc
Normal file
333
.cursor/rules/ultracite.mdc
Normal file
|
|
@ -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 `<marquee>` or `<blink>`.
|
||||
- Only use the `scope` prop on `<th>` 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 `<Fragment>...</Fragment>`.
|
||||
- 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<T>` 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 `<img>` elements in Next.js projects.
|
||||
- Don't use `<head>` 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);
|
||||
}
|
||||
```
|
||||
28
.vscode/settings.json
vendored
28
.vscode/settings.json
vendored
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<LoginActionState> => {
|
||||
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<RegisterActionState> => {
|
||||
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" };
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export { GET, POST } from '@/app/(auth)/auth';
|
||||
// biome-ignore lint/performance/noBarrelFile: "Required"
|
||||
export { GET, POST } from "@/app/(auth)/auth";
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<LoginActionState, FormData>(
|
||||
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() {
|
|||
<p className="mt-4 text-center text-gray-600 text-sm dark:text-zinc-400">
|
||||
{"Don't have an account? "}
|
||||
<Link
|
||||
href="/register"
|
||||
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
|
||||
href="/register"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
{' for free.'}
|
||||
{" for free."}
|
||||
</p>
|
||||
</AuthForm>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<RegisterActionState, FormData>(
|
||||
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() {
|
|||
<AuthForm action={handleSubmit} defaultEmail={email}>
|
||||
<SubmitButton isSuccessful={isSuccessful}>Sign Up</SubmitButton>
|
||||
<p className="mt-4 text-center text-gray-600 text-sm dark:text-zinc-400">
|
||||
{'Already have an account? '}
|
||||
{"Already have an account? "}
|
||||
<Link
|
||||
href="/login"
|
||||
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
|
||||
href="/login"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
{' instead.'}
|
||||
{" instead."}
|
||||
</p>
|
||||
</AuthForm>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<ChatMessage>({
|
||||
// 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<ChatMessage>({
|
||||
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 }
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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<typeof postRequestBodySchema>;
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<Chat
|
||||
autoResume={true}
|
||||
id={chat.id}
|
||||
initialMessages={uiMessages}
|
||||
initialChatModel={DEFAULT_CHAT_MODEL}
|
||||
initialLastContext={chat.lastContext ?? undefined}
|
||||
initialMessages={uiMessages}
|
||||
initialVisibilityType={chat.visibility}
|
||||
isReadonly={session?.user?.id !== chat.userId}
|
||||
session={session}
|
||||
autoResume={true}
|
||||
initialLastContext={chat.lastContext ?? undefined}
|
||||
/>
|
||||
<DataStreamHandler />
|
||||
</>
|
||||
|
|
@ -63,14 +62,13 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
|
|||
return (
|
||||
<>
|
||||
<Chat
|
||||
autoResume={true}
|
||||
id={chat.id}
|
||||
initialMessages={uiMessages}
|
||||
initialChatModel={chatModelFromCookie.value}
|
||||
initialLastContext={chat.lastContext ?? undefined}
|
||||
initialMessages={uiMessages}
|
||||
initialVisibilityType={chat.visibility}
|
||||
isReadonly={session?.user?.id !== chat.userId}
|
||||
session={session}
|
||||
autoResume={true}
|
||||
initialLastContext={chat.lastContext ?? undefined}
|
||||
/>
|
||||
<DataStreamHandler />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<Chat
|
||||
key={id}
|
||||
autoResume={false}
|
||||
id={id}
|
||||
initialMessages={[]}
|
||||
initialChatModel={DEFAULT_CHAT_MODEL}
|
||||
initialMessages={[]}
|
||||
initialVisibilityType="private"
|
||||
isReadonly={false}
|
||||
session={session}
|
||||
autoResume={false}
|
||||
key={id}
|
||||
/>
|
||||
<DataStreamHandler />
|
||||
</>
|
||||
|
|
@ -40,14 +38,13 @@ export default async function Page() {
|
|||
return (
|
||||
<>
|
||||
<Chat
|
||||
key={id}
|
||||
autoResume={false}
|
||||
id={id}
|
||||
initialMessages={[]}
|
||||
initialChatModel={modelIdFromCookie.value}
|
||||
initialMessages={[]}
|
||||
initialVisibilityType="private"
|
||||
isReadonly={false}
|
||||
session={session}
|
||||
autoResume={false}
|
||||
key={id}
|
||||
/>
|
||||
<DataStreamHandler />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geist.variable} ${geistMono.variable}`}
|
||||
// `next-themes` injects an extra classname to the body element to avoid
|
||||
// visual flicker before hydration. Hence the `suppressHydrationWarning`
|
||||
// prop is necessary to avoid the React hydration mismatch warning.
|
||||
// https://github.com/pacocoursey/next-themes?tab=readme-ov-file#with-app
|
||||
lang="en"
|
||||
suppressHydrationWarning
|
||||
className={`${geist.variable} ${geistMono.variable}`}
|
||||
>
|
||||
<head>
|
||||
<script
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: "Required"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: THEME_COLOR_SCRIPT,
|
||||
}}
|
||||
|
|
@ -74,8 +75,8 @@ export default async function RootLayout({
|
|||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
enableSystem
|
||||
>
|
||||
<Toaster position="top-center" />
|
||||
<SessionProvider>{children}</SessionProvider>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use server';
|
||||
"use server";
|
||||
|
||||
import { getSuggestionsByDocumentId } from '@/lib/db/queries';
|
||||
import { getSuggestionsByDocumentId } from "@/lib/db/queries";
|
||||
|
||||
export async function getSuggestions({ documentId }: { documentId: string }) {
|
||||
const suggestions = await getSuggestionsByDocumentId({ documentId });
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import { Artifact } from '@/components/create-artifact';
|
||||
import { CodeEditor } from '@/components/code-editor';
|
||||
import { toast } from "sonner";
|
||||
import { CodeEditor } from "@/components/code-editor";
|
||||
import {
|
||||
Console,
|
||||
type ConsoleOutput,
|
||||
type ConsoleOutputContent,
|
||||
} from "@/components/console";
|
||||
import { Artifact } from "@/components/create-artifact";
|
||||
import {
|
||||
CopyIcon,
|
||||
LogsIcon,
|
||||
|
|
@ -7,14 +13,8 @@ import {
|
|||
PlayIcon,
|
||||
RedoIcon,
|
||||
UndoIcon,
|
||||
} from '@/components/icons';
|
||||
import { toast } from 'sonner';
|
||||
import { generateUUID } from '@/lib/utils';
|
||||
import {
|
||||
Console,
|
||||
type ConsoleOutput,
|
||||
type ConsoleOutputContent,
|
||||
} from '@/components/console';
|
||||
} from "@/components/icons";
|
||||
import { generateUUID } from "@/lib/utils";
|
||||
|
||||
const OUTPUT_HANDLERS = {
|
||||
matplotlib: `
|
||||
|
|
@ -53,40 +53,40 @@ const OUTPUT_HANDLERS = {
|
|||
};
|
||||
|
||||
function detectRequiredHandlers(code: string): string[] {
|
||||
const handlers: string[] = ['basic'];
|
||||
const handlers: string[] = ["basic"];
|
||||
|
||||
if (code.includes('matplotlib') || code.includes('plt.')) {
|
||||
handlers.push('matplotlib');
|
||||
if (code.includes("matplotlib") || code.includes("plt.")) {
|
||||
handlers.push("matplotlib");
|
||||
}
|
||||
|
||||
return handlers;
|
||||
}
|
||||
|
||||
interface Metadata {
|
||||
outputs: Array<ConsoleOutput>;
|
||||
}
|
||||
type Metadata = {
|
||||
outputs: ConsoleOutput[];
|
||||
};
|
||||
|
||||
export const codeArtifact = new Artifact<'code', Metadata>({
|
||||
kind: 'code',
|
||||
export const codeArtifact = new Artifact<"code", Metadata>({
|
||||
kind: "code",
|
||||
description:
|
||||
'Useful for code generation; Code execution is only available for python code.',
|
||||
initialize: async ({ setMetadata }) => {
|
||||
"Useful for code generation; Code execution is only available for python code.",
|
||||
initialize: ({ setMetadata }) => {
|
||||
setMetadata({
|
||||
outputs: [],
|
||||
});
|
||||
},
|
||||
onStreamPart: ({ streamPart, setArtifact }) => {
|
||||
if (streamPart.type === 'data-codeDelta') {
|
||||
if (streamPart.type === "data-codeDelta") {
|
||||
setArtifact((draftArtifact) => ({
|
||||
...draftArtifact,
|
||||
content: streamPart.data,
|
||||
isVisible:
|
||||
draftArtifact.status === 'streaming' &&
|
||||
draftArtifact.status === "streaming" &&
|
||||
draftArtifact.content.length > 300 &&
|
||||
draftArtifact.content.length < 310
|
||||
? true
|
||||
: draftArtifact.isVisible,
|
||||
status: 'streaming',
|
||||
status: "streaming",
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
|
@ -114,11 +114,11 @@ export const codeArtifact = new Artifact<'code', Metadata>({
|
|||
actions: [
|
||||
{
|
||||
icon: <PlayIcon size={18} />,
|
||||
label: 'Run',
|
||||
description: 'Execute code',
|
||||
label: "Run",
|
||||
description: "Execute code",
|
||||
onClick: async ({ content, setMetadata }) => {
|
||||
const runId = generateUUID();
|
||||
const outputContent: Array<ConsoleOutputContent> = [];
|
||||
const outputContent: ConsoleOutputContent[] = [];
|
||||
|
||||
setMetadata((metadata) => ({
|
||||
...metadata,
|
||||
|
|
@ -127,7 +127,7 @@ export const codeArtifact = new Artifact<'code', Metadata>({
|
|||
{
|
||||
id: runId,
|
||||
contents: [],
|
||||
status: 'in_progress',
|
||||
status: "in_progress",
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
|
@ -135,15 +135,15 @@ export const codeArtifact = new Artifact<'code', Metadata>({
|
|||
try {
|
||||
// @ts-expect-error - loadPyodide is not defined
|
||||
const currentPyodideInstance = await globalThis.loadPyodide({
|
||||
indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.23.4/full/',
|
||||
indexURL: "https://cdn.jsdelivr.net/pyodide/v0.23.4/full/",
|
||||
});
|
||||
|
||||
currentPyodideInstance.setStdout({
|
||||
batched: (output: string) => {
|
||||
outputContent.push({
|
||||
type: output.startsWith('data:image/png;base64')
|
||||
? 'image'
|
||||
: 'text',
|
||||
type: output.startsWith("data:image/png;base64")
|
||||
? "image"
|
||||
: "text",
|
||||
value: output,
|
||||
});
|
||||
},
|
||||
|
|
@ -157,8 +157,8 @@ export const codeArtifact = new Artifact<'code', Metadata>({
|
|||
...metadata.outputs.filter((output) => output.id !== runId),
|
||||
{
|
||||
id: runId,
|
||||
contents: [{ type: 'text', value: message }],
|
||||
status: 'loading_packages',
|
||||
contents: [{ type: "text", value: message }],
|
||||
status: "loading_packages",
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
|
@ -169,12 +169,12 @@ export const codeArtifact = new Artifact<'code', Metadata>({
|
|||
for (const handler of requiredHandlers) {
|
||||
if (OUTPUT_HANDLERS[handler as keyof typeof OUTPUT_HANDLERS]) {
|
||||
await currentPyodideInstance.runPythonAsync(
|
||||
OUTPUT_HANDLERS[handler as keyof typeof OUTPUT_HANDLERS],
|
||||
OUTPUT_HANDLERS[handler as keyof typeof OUTPUT_HANDLERS]
|
||||
);
|
||||
|
||||
if (handler === 'matplotlib') {
|
||||
if (handler === "matplotlib") {
|
||||
await currentPyodideInstance.runPythonAsync(
|
||||
'setup_matplotlib_output()',
|
||||
"setup_matplotlib_output()"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -189,7 +189,7 @@ export const codeArtifact = new Artifact<'code', Metadata>({
|
|||
{
|
||||
id: runId,
|
||||
contents: outputContent,
|
||||
status: 'completed',
|
||||
status: "completed",
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
|
@ -200,8 +200,8 @@ export const codeArtifact = new Artifact<'code', Metadata>({
|
|||
...metadata.outputs.filter((output) => output.id !== runId),
|
||||
{
|
||||
id: runId,
|
||||
contents: [{ type: 'text', value: error.message }],
|
||||
status: 'failed',
|
||||
contents: [{ type: "text", value: error.message }],
|
||||
status: "failed",
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
|
@ -210,9 +210,9 @@ export const codeArtifact = new Artifact<'code', Metadata>({
|
|||
},
|
||||
{
|
||||
icon: <UndoIcon size={18} />,
|
||||
description: 'View Previous version',
|
||||
description: "View Previous version",
|
||||
onClick: ({ handleVersionChange }) => {
|
||||
handleVersionChange('prev');
|
||||
handleVersionChange("prev");
|
||||
},
|
||||
isDisabled: ({ currentVersionIndex }) => {
|
||||
if (currentVersionIndex === 0) {
|
||||
|
|
@ -224,9 +224,9 @@ export const codeArtifact = new Artifact<'code', Metadata>({
|
|||
},
|
||||
{
|
||||
icon: <RedoIcon size={18} />,
|
||||
description: 'View Next version',
|
||||
description: "View Next version",
|
||||
onClick: ({ handleVersionChange }) => {
|
||||
handleVersionChange('next');
|
||||
handleVersionChange("next");
|
||||
},
|
||||
isDisabled: ({ isCurrentVersion }) => {
|
||||
if (isCurrentVersion) {
|
||||
|
|
@ -238,24 +238,24 @@ export const codeArtifact = new Artifact<'code', Metadata>({
|
|||
},
|
||||
{
|
||||
icon: <CopyIcon size={18} />,
|
||||
description: 'Copy code to clipboard',
|
||||
description: "Copy code to clipboard",
|
||||
onClick: ({ content }) => {
|
||||
navigator.clipboard.writeText(content);
|
||||
toast.success('Copied to clipboard!');
|
||||
toast.success("Copied to clipboard!");
|
||||
},
|
||||
},
|
||||
],
|
||||
toolbar: [
|
||||
{
|
||||
icon: <MessageIcon />,
|
||||
description: 'Add comments',
|
||||
description: "Add comments",
|
||||
onClick: ({ sendMessage }) => {
|
||||
sendMessage({
|
||||
role: 'user',
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Add comments to the code snippet for understanding',
|
||||
type: "text",
|
||||
text: "Add comments to the code snippet for understanding",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -263,14 +263,14 @@ export const codeArtifact = new Artifact<'code', Metadata>({
|
|||
},
|
||||
{
|
||||
icon: <LogsIcon />,
|
||||
description: 'Add logs',
|
||||
description: "Add logs",
|
||||
onClick: ({ sendMessage }) => {
|
||||
sendMessage({
|
||||
role: 'user',
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Add logs to the code snippet for debugging',
|
||||
type: "text",
|
||||
text: "Add logs to the code snippet for debugging",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { z } from 'zod';
|
||||
import { streamObject } from 'ai';
|
||||
import { myProvider } from '@/lib/ai/providers';
|
||||
import { codePrompt, updateDocumentPrompt } from '@/lib/ai/prompts';
|
||||
import { createDocumentHandler } from '@/lib/artifacts/server';
|
||||
import { streamObject } from "ai";
|
||||
import { z } from "zod";
|
||||
import { codePrompt, updateDocumentPrompt } from "@/lib/ai/prompts";
|
||||
import { myProvider } from "@/lib/ai/providers";
|
||||
import { createDocumentHandler } from "@/lib/artifacts/server";
|
||||
|
||||
export const codeDocumentHandler = createDocumentHandler<'code'>({
|
||||
kind: 'code',
|
||||
export const codeDocumentHandler = createDocumentHandler<"code">({
|
||||
kind: "code",
|
||||
onCreateDocument: async ({ title, dataStream }) => {
|
||||
let draftContent = '';
|
||||
let draftContent = "";
|
||||
|
||||
const { fullStream } = streamObject({
|
||||
model: myProvider.languageModel('artifact-model'),
|
||||
model: myProvider.languageModel("artifact-model"),
|
||||
system: codePrompt,
|
||||
prompt: title,
|
||||
schema: z.object({
|
||||
|
|
@ -21,14 +21,14 @@ export const codeDocumentHandler = createDocumentHandler<'code'>({
|
|||
for await (const delta of fullStream) {
|
||||
const { type } = delta;
|
||||
|
||||
if (type === 'object') {
|
||||
if (type === "object") {
|
||||
const { object } = delta;
|
||||
const { code } = object;
|
||||
|
||||
if (code) {
|
||||
dataStream.write({
|
||||
type: 'data-codeDelta',
|
||||
data: code ?? '',
|
||||
type: "data-codeDelta",
|
||||
data: code ?? "",
|
||||
transient: true,
|
||||
});
|
||||
|
||||
|
|
@ -40,11 +40,11 @@ export const codeDocumentHandler = createDocumentHandler<'code'>({
|
|||
return draftContent;
|
||||
},
|
||||
onUpdateDocument: async ({ document, description, dataStream }) => {
|
||||
let draftContent = '';
|
||||
let draftContent = "";
|
||||
|
||||
const { fullStream } = streamObject({
|
||||
model: myProvider.languageModel('artifact-model'),
|
||||
system: updateDocumentPrompt(document.content, 'code'),
|
||||
model: myProvider.languageModel("artifact-model"),
|
||||
system: updateDocumentPrompt(document.content, "code"),
|
||||
prompt: description,
|
||||
schema: z.object({
|
||||
code: z.string(),
|
||||
|
|
@ -54,14 +54,14 @@ export const codeDocumentHandler = createDocumentHandler<'code'>({
|
|||
for await (const delta of fullStream) {
|
||||
const { type } = delta;
|
||||
|
||||
if (type === 'object') {
|
||||
if (type === "object") {
|
||||
const { object } = delta;
|
||||
const { code } = object;
|
||||
|
||||
if (code) {
|
||||
dataStream.write({
|
||||
type: 'data-codeDelta',
|
||||
data: code ?? '',
|
||||
type: "data-codeDelta",
|
||||
data: code ?? "",
|
||||
transient: true,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { Artifact } from '@/components/create-artifact';
|
||||
import { CopyIcon, RedoIcon, UndoIcon } from '@/components/icons';
|
||||
import { ImageEditor } from '@/components/image-editor';
|
||||
import { toast } from 'sonner';
|
||||
import { toast } from "sonner";
|
||||
import { Artifact } from "@/components/create-artifact";
|
||||
import { CopyIcon, RedoIcon, UndoIcon } from "@/components/icons";
|
||||
import { ImageEditor } from "@/components/image-editor";
|
||||
|
||||
export const imageArtifact = new Artifact({
|
||||
kind: 'image',
|
||||
description: 'Useful for image generation',
|
||||
kind: "image",
|
||||
description: "Useful for image generation",
|
||||
onStreamPart: ({ streamPart, setArtifact }) => {
|
||||
if (streamPart.type === 'data-imageDelta') {
|
||||
if (streamPart.type === "data-imageDelta") {
|
||||
setArtifact((draftArtifact) => ({
|
||||
...draftArtifact,
|
||||
content: streamPart.data,
|
||||
isVisible: true,
|
||||
status: 'streaming',
|
||||
status: "streaming",
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
|
@ -20,9 +20,9 @@ export const imageArtifact = new Artifact({
|
|||
actions: [
|
||||
{
|
||||
icon: <UndoIcon size={18} />,
|
||||
description: 'View Previous version',
|
||||
description: "View Previous version",
|
||||
onClick: ({ handleVersionChange }) => {
|
||||
handleVersionChange('prev');
|
||||
handleVersionChange("prev");
|
||||
},
|
||||
isDisabled: ({ currentVersionIndex }) => {
|
||||
if (currentVersionIndex === 0) {
|
||||
|
|
@ -34,9 +34,9 @@ export const imageArtifact = new Artifact({
|
|||
},
|
||||
{
|
||||
icon: <RedoIcon size={18} />,
|
||||
description: 'View Next version',
|
||||
description: "View Next version",
|
||||
onClick: ({ handleVersionChange }) => {
|
||||
handleVersionChange('next');
|
||||
handleVersionChange("next");
|
||||
},
|
||||
isDisabled: ({ isCurrentVersion }) => {
|
||||
if (isCurrentVersion) {
|
||||
|
|
@ -48,27 +48,27 @@ export const imageArtifact = new Artifact({
|
|||
},
|
||||
{
|
||||
icon: <CopyIcon size={18} />,
|
||||
description: 'Copy image to clipboard',
|
||||
description: "Copy image to clipboard",
|
||||
onClick: ({ content }) => {
|
||||
const img = new Image();
|
||||
img.src = `data:image/png;base64,${content}`;
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx?.drawImage(img, 0, 0);
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
navigator.clipboard.write([
|
||||
new ClipboardItem({ 'image/png': blob }),
|
||||
new ClipboardItem({ "image/png": blob }),
|
||||
]);
|
||||
}
|
||||
}, 'image/png');
|
||||
}, "image/png");
|
||||
};
|
||||
|
||||
toast.success('Copied image to clipboard!');
|
||||
toast.success("Copied image to clipboard!");
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,43 +1,37 @@
|
|||
import { Artifact } from '@/components/create-artifact';
|
||||
import { parse, unparse } from "papaparse";
|
||||
import { toast } from "sonner";
|
||||
import { Artifact } from "@/components/create-artifact";
|
||||
import {
|
||||
CopyIcon,
|
||||
LineChartIcon,
|
||||
RedoIcon,
|
||||
SparklesIcon,
|
||||
UndoIcon,
|
||||
} from '@/components/icons';
|
||||
import { SpreadsheetEditor } from '@/components/sheet-editor';
|
||||
import { parse, unparse } from 'papaparse';
|
||||
import { toast } from 'sonner';
|
||||
} from "@/components/icons";
|
||||
import { SpreadsheetEditor } from "@/components/sheet-editor";
|
||||
|
||||
type Metadata = any;
|
||||
|
||||
export const sheetArtifact = new Artifact<'sheet', Metadata>({
|
||||
kind: 'sheet',
|
||||
description: 'Useful for working with spreadsheets',
|
||||
initialize: async () => {},
|
||||
export const sheetArtifact = new Artifact<"sheet", Metadata>({
|
||||
kind: "sheet",
|
||||
description: "Useful for working with spreadsheets",
|
||||
initialize: () => null,
|
||||
onStreamPart: ({ setArtifact, streamPart }) => {
|
||||
if (streamPart.type === 'data-sheetDelta') {
|
||||
if (streamPart.type === "data-sheetDelta") {
|
||||
setArtifact((draftArtifact) => ({
|
||||
...draftArtifact,
|
||||
content: streamPart.data,
|
||||
isVisible: true,
|
||||
status: 'streaming',
|
||||
status: "streaming",
|
||||
}));
|
||||
}
|
||||
},
|
||||
content: ({
|
||||
content,
|
||||
currentVersionIndex,
|
||||
isCurrentVersion,
|
||||
onSaveContent,
|
||||
status,
|
||||
}) => {
|
||||
content: ({ content, currentVersionIndex, onSaveContent, status }) => {
|
||||
return (
|
||||
<SpreadsheetEditor
|
||||
content={content}
|
||||
currentVersionIndex={currentVersionIndex}
|
||||
isCurrentVersion={isCurrentVersion}
|
||||
isCurrentVersion={true}
|
||||
saveContent={onSaveContent}
|
||||
status={status}
|
||||
/>
|
||||
|
|
@ -46,9 +40,9 @@ export const sheetArtifact = new Artifact<'sheet', Metadata>({
|
|||
actions: [
|
||||
{
|
||||
icon: <UndoIcon size={18} />,
|
||||
description: 'View Previous version',
|
||||
description: "View Previous version",
|
||||
onClick: ({ handleVersionChange }) => {
|
||||
handleVersionChange('prev');
|
||||
handleVersionChange("prev");
|
||||
},
|
||||
isDisabled: ({ currentVersionIndex }) => {
|
||||
if (currentVersionIndex === 0) {
|
||||
|
|
@ -60,9 +54,9 @@ export const sheetArtifact = new Artifact<'sheet', Metadata>({
|
|||
},
|
||||
{
|
||||
icon: <RedoIcon size={18} />,
|
||||
description: 'View Next version',
|
||||
description: "View Next version",
|
||||
onClick: ({ handleVersionChange }) => {
|
||||
handleVersionChange('next');
|
||||
handleVersionChange("next");
|
||||
},
|
||||
isDisabled: ({ isCurrentVersion }) => {
|
||||
if (isCurrentVersion) {
|
||||
|
|
@ -74,44 +68,44 @@ export const sheetArtifact = new Artifact<'sheet', Metadata>({
|
|||
},
|
||||
{
|
||||
icon: <CopyIcon />,
|
||||
description: 'Copy as .csv',
|
||||
description: "Copy as .csv",
|
||||
onClick: ({ content }) => {
|
||||
const parsed = parse<string[]>(content, { skipEmptyLines: true });
|
||||
|
||||
const nonEmptyRows = parsed.data.filter((row) =>
|
||||
row.some((cell) => cell.trim() !== ''),
|
||||
row.some((cell) => cell.trim() !== "")
|
||||
);
|
||||
|
||||
const cleanedCsv = unparse(nonEmptyRows);
|
||||
|
||||
navigator.clipboard.writeText(cleanedCsv);
|
||||
toast.success('Copied csv to clipboard!');
|
||||
toast.success("Copied csv to clipboard!");
|
||||
},
|
||||
},
|
||||
],
|
||||
toolbar: [
|
||||
{
|
||||
description: 'Format and clean data',
|
||||
description: "Format and clean data",
|
||||
icon: <SparklesIcon />,
|
||||
onClick: ({ sendMessage }) => {
|
||||
sendMessage({
|
||||
role: 'user',
|
||||
role: "user",
|
||||
parts: [
|
||||
{ type: 'text', text: 'Can you please format and clean the data?' },
|
||||
{ type: "text", text: "Can you please format and clean the data?" },
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Analyze and visualize data',
|
||||
description: "Analyze and visualize data",
|
||||
icon: <LineChartIcon />,
|
||||
onClick: ({ sendMessage }) => {
|
||||
sendMessage({
|
||||
role: 'user',
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Can you please analyze and visualize the data by creating a new code artifact in python?',
|
||||
type: "text",
|
||||
text: "Can you please analyze and visualize the data by creating a new code artifact in python?",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
import { myProvider } from '@/lib/ai/providers';
|
||||
import { sheetPrompt, updateDocumentPrompt } from '@/lib/ai/prompts';
|
||||
import { createDocumentHandler } from '@/lib/artifacts/server';
|
||||
import { streamObject } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { streamObject } from "ai";
|
||||
import { z } from "zod";
|
||||
import { sheetPrompt, updateDocumentPrompt } from "@/lib/ai/prompts";
|
||||
import { myProvider } from "@/lib/ai/providers";
|
||||
import { createDocumentHandler } from "@/lib/artifacts/server";
|
||||
|
||||
export const sheetDocumentHandler = createDocumentHandler<'sheet'>({
|
||||
kind: 'sheet',
|
||||
export const sheetDocumentHandler = createDocumentHandler<"sheet">({
|
||||
kind: "sheet",
|
||||
onCreateDocument: async ({ title, dataStream }) => {
|
||||
let draftContent = '';
|
||||
let draftContent = "";
|
||||
|
||||
const { fullStream } = streamObject({
|
||||
model: myProvider.languageModel('artifact-model'),
|
||||
model: myProvider.languageModel("artifact-model"),
|
||||
system: sheetPrompt,
|
||||
prompt: title,
|
||||
schema: z.object({
|
||||
csv: z.string().describe('CSV data'),
|
||||
csv: z.string().describe("CSV data"),
|
||||
}),
|
||||
});
|
||||
|
||||
for await (const delta of fullStream) {
|
||||
const { type } = delta;
|
||||
|
||||
if (type === 'object') {
|
||||
if (type === "object") {
|
||||
const { object } = delta;
|
||||
const { csv } = object;
|
||||
|
||||
if (csv) {
|
||||
dataStream.write({
|
||||
type: 'data-sheetDelta',
|
||||
type: "data-sheetDelta",
|
||||
data: csv,
|
||||
transient: true,
|
||||
});
|
||||
|
|
@ -38,7 +38,7 @@ export const sheetDocumentHandler = createDocumentHandler<'sheet'>({
|
|||
}
|
||||
|
||||
dataStream.write({
|
||||
type: 'data-sheetDelta',
|
||||
type: "data-sheetDelta",
|
||||
data: draftContent,
|
||||
transient: true,
|
||||
});
|
||||
|
|
@ -46,11 +46,11 @@ export const sheetDocumentHandler = createDocumentHandler<'sheet'>({
|
|||
return draftContent;
|
||||
},
|
||||
onUpdateDocument: async ({ document, description, dataStream }) => {
|
||||
let draftContent = '';
|
||||
let draftContent = "";
|
||||
|
||||
const { fullStream } = streamObject({
|
||||
model: myProvider.languageModel('artifact-model'),
|
||||
system: updateDocumentPrompt(document.content, 'sheet'),
|
||||
model: myProvider.languageModel("artifact-model"),
|
||||
system: updateDocumentPrompt(document.content, "sheet"),
|
||||
prompt: description,
|
||||
schema: z.object({
|
||||
csv: z.string(),
|
||||
|
|
@ -60,13 +60,13 @@ export const sheetDocumentHandler = createDocumentHandler<'sheet'>({
|
|||
for await (const delta of fullStream) {
|
||||
const { type } = delta;
|
||||
|
||||
if (type === 'object') {
|
||||
if (type === "object") {
|
||||
const { object } = delta;
|
||||
const { csv } = object;
|
||||
|
||||
if (csv) {
|
||||
dataStream.write({
|
||||
type: 'data-sheetDelta',
|
||||
type: "data-sheetDelta",
|
||||
data: csv,
|
||||
transient: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Artifact } from '@/components/create-artifact';
|
||||
import { DiffView } from '@/components/diffview';
|
||||
import { DocumentSkeleton } from '@/components/document-skeleton';
|
||||
import { Editor } from '@/components/text-editor';
|
||||
import { toast } from "sonner";
|
||||
import { Artifact } from "@/components/create-artifact";
|
||||
import { DiffView } from "@/components/diffview";
|
||||
import { DocumentSkeleton } from "@/components/document-skeleton";
|
||||
import {
|
||||
ClockRewind,
|
||||
CopyIcon,
|
||||
|
|
@ -9,18 +9,18 @@ import {
|
|||
PenIcon,
|
||||
RedoIcon,
|
||||
UndoIcon,
|
||||
} from '@/components/icons';
|
||||
import type { Suggestion } from '@/lib/db/schema';
|
||||
import { toast } from 'sonner';
|
||||
import { getSuggestions } from '../actions';
|
||||
} from "@/components/icons";
|
||||
import { Editor } from "@/components/text-editor";
|
||||
import type { Suggestion } from "@/lib/db/schema";
|
||||
import { getSuggestions } from "../actions";
|
||||
|
||||
interface TextArtifactMetadata {
|
||||
suggestions: Array<Suggestion>;
|
||||
}
|
||||
type TextArtifactMetadata = {
|
||||
suggestions: Suggestion[];
|
||||
};
|
||||
|
||||
export const textArtifact = new Artifact<'text', TextArtifactMetadata>({
|
||||
kind: 'text',
|
||||
description: 'Useful for text content, like drafting essays and emails.',
|
||||
export const textArtifact = new Artifact<"text", TextArtifactMetadata>({
|
||||
kind: "text",
|
||||
description: "Useful for text content, like drafting essays and emails.",
|
||||
initialize: async ({ documentId, setMetadata }) => {
|
||||
const suggestions = await getSuggestions({ documentId });
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ export const textArtifact = new Artifact<'text', TextArtifactMetadata>({
|
|||
});
|
||||
},
|
||||
onStreamPart: ({ streamPart, setMetadata, setArtifact }) => {
|
||||
if (streamPart.type === 'data-suggestion') {
|
||||
if (streamPart.type === "data-suggestion") {
|
||||
setMetadata((metadata) => {
|
||||
return {
|
||||
suggestions: [...metadata.suggestions, streamPart.data],
|
||||
|
|
@ -37,18 +37,18 @@ export const textArtifact = new Artifact<'text', TextArtifactMetadata>({
|
|||
});
|
||||
}
|
||||
|
||||
if (streamPart.type === 'data-textDelta') {
|
||||
if (streamPart.type === "data-textDelta") {
|
||||
setArtifact((draftArtifact) => {
|
||||
return {
|
||||
...draftArtifact,
|
||||
content: draftArtifact.content + streamPart.data,
|
||||
isVisible:
|
||||
draftArtifact.status === 'streaming' &&
|
||||
draftArtifact.status === "streaming" &&
|
||||
draftArtifact.content.length > 400 &&
|
||||
draftArtifact.content.length < 450
|
||||
? true
|
||||
: draftArtifact.isVisible,
|
||||
status: 'streaming',
|
||||
status: "streaming",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -68,40 +68,38 @@ export const textArtifact = new Artifact<'text', TextArtifactMetadata>({
|
|||
return <DocumentSkeleton artifactKind="text" />;
|
||||
}
|
||||
|
||||
if (mode === 'diff') {
|
||||
if (mode === "diff") {
|
||||
const oldContent = getDocumentContentById(currentVersionIndex - 1);
|
||||
const newContent = getDocumentContentById(currentVersionIndex);
|
||||
|
||||
return <DiffView oldContent={oldContent} newContent={newContent} />;
|
||||
return <DiffView newContent={newContent} oldContent={oldContent} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row px-4 py-8 md:p-20">
|
||||
<Editor
|
||||
content={content}
|
||||
suggestions={metadata ? metadata.suggestions : []}
|
||||
isCurrentVersion={isCurrentVersion}
|
||||
currentVersionIndex={currentVersionIndex}
|
||||
status={status}
|
||||
onSaveContent={onSaveContent}
|
||||
/>
|
||||
<div className="flex flex-row px-4 py-8 md:p-20">
|
||||
<Editor
|
||||
content={content}
|
||||
currentVersionIndex={currentVersionIndex}
|
||||
isCurrentVersion={isCurrentVersion}
|
||||
onSaveContent={onSaveContent}
|
||||
status={status}
|
||||
suggestions={metadata ? metadata.suggestions : []}
|
||||
/>
|
||||
|
||||
{metadata?.suggestions && metadata.suggestions.length > 0 ? (
|
||||
<div className="h-dvh w-12 shrink-0 md:hidden" />
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
{metadata?.suggestions && metadata.suggestions.length > 0 ? (
|
||||
<div className="h-dvh w-12 shrink-0 md:hidden" />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
icon: <ClockRewind size={18} />,
|
||||
description: 'View changes',
|
||||
description: "View changes",
|
||||
onClick: ({ handleVersionChange }) => {
|
||||
handleVersionChange('toggle');
|
||||
handleVersionChange("toggle");
|
||||
},
|
||||
isDisabled: ({ currentVersionIndex, setMetadata }) => {
|
||||
isDisabled: ({ currentVersionIndex }) => {
|
||||
if (currentVersionIndex === 0) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -111,9 +109,9 @@ export const textArtifact = new Artifact<'text', TextArtifactMetadata>({
|
|||
},
|
||||
{
|
||||
icon: <UndoIcon size={18} />,
|
||||
description: 'View Previous version',
|
||||
description: "View Previous version",
|
||||
onClick: ({ handleVersionChange }) => {
|
||||
handleVersionChange('prev');
|
||||
handleVersionChange("prev");
|
||||
},
|
||||
isDisabled: ({ currentVersionIndex }) => {
|
||||
if (currentVersionIndex === 0) {
|
||||
|
|
@ -125,9 +123,9 @@ export const textArtifact = new Artifact<'text', TextArtifactMetadata>({
|
|||
},
|
||||
{
|
||||
icon: <RedoIcon size={18} />,
|
||||
description: 'View Next version',
|
||||
description: "View Next version",
|
||||
onClick: ({ handleVersionChange }) => {
|
||||
handleVersionChange('next');
|
||||
handleVersionChange("next");
|
||||
},
|
||||
isDisabled: ({ isCurrentVersion }) => {
|
||||
if (isCurrentVersion) {
|
||||
|
|
@ -139,24 +137,24 @@ export const textArtifact = new Artifact<'text', TextArtifactMetadata>({
|
|||
},
|
||||
{
|
||||
icon: <CopyIcon size={18} />,
|
||||
description: 'Copy to clipboard',
|
||||
description: "Copy to clipboard",
|
||||
onClick: ({ content }) => {
|
||||
navigator.clipboard.writeText(content);
|
||||
toast.success('Copied to clipboard!');
|
||||
toast.success("Copied to clipboard!");
|
||||
},
|
||||
},
|
||||
],
|
||||
toolbar: [
|
||||
{
|
||||
icon: <PenIcon />,
|
||||
description: 'Add final polish',
|
||||
description: "Add final polish",
|
||||
onClick: ({ sendMessage }) => {
|
||||
sendMessage({
|
||||
role: 'user',
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Please add final polish and check for grammar, add section titles for better structure, and ensure everything reads smoothly.',
|
||||
type: "text",
|
||||
text: "Please add final polish and check for grammar, add section titles for better structure, and ensure everything reads smoothly.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -164,14 +162,14 @@ export const textArtifact = new Artifact<'text', TextArtifactMetadata>({
|
|||
},
|
||||
{
|
||||
icon: <MessageIcon />,
|
||||
description: 'Request suggestions',
|
||||
description: "Request suggestions",
|
||||
onClick: ({ sendMessage }) => {
|
||||
sendMessage({
|
||||
role: 'user',
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Please add suggestions you have that could improve the writing.',
|
||||
type: "text",
|
||||
text: "Please add suggestions you have that could improve the writing.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
import { smoothStream, streamText } from 'ai';
|
||||
import { myProvider } from '@/lib/ai/providers';
|
||||
import { createDocumentHandler } from '@/lib/artifacts/server';
|
||||
import { updateDocumentPrompt } from '@/lib/ai/prompts';
|
||||
import { smoothStream, streamText } from "ai";
|
||||
import { updateDocumentPrompt } from "@/lib/ai/prompts";
|
||||
import { myProvider } from "@/lib/ai/providers";
|
||||
import { createDocumentHandler } from "@/lib/artifacts/server";
|
||||
|
||||
export const textDocumentHandler = createDocumentHandler<'text'>({
|
||||
kind: 'text',
|
||||
export const textDocumentHandler = createDocumentHandler<"text">({
|
||||
kind: "text",
|
||||
onCreateDocument: async ({ title, dataStream }) => {
|
||||
let draftContent = '';
|
||||
let draftContent = "";
|
||||
|
||||
const { fullStream } = streamText({
|
||||
model: myProvider.languageModel('artifact-model'),
|
||||
model: myProvider.languageModel("artifact-model"),
|
||||
system:
|
||||
'Write about the given topic. Markdown is supported. Use headings wherever appropriate.',
|
||||
experimental_transform: smoothStream({ chunking: 'word' }),
|
||||
"Write about the given topic. Markdown is supported. Use headings wherever appropriate.",
|
||||
experimental_transform: smoothStream({ chunking: "word" }),
|
||||
prompt: title,
|
||||
});
|
||||
|
||||
for await (const delta of fullStream) {
|
||||
const { type } = delta;
|
||||
|
||||
if (type === 'text-delta') {
|
||||
if (type === "text-delta") {
|
||||
const { text } = delta;
|
||||
|
||||
draftContent += text;
|
||||
|
||||
dataStream.write({
|
||||
type: 'data-textDelta',
|
||||
type: "data-textDelta",
|
||||
data: text,
|
||||
transient: true,
|
||||
});
|
||||
|
|
@ -35,17 +35,17 @@ export const textDocumentHandler = createDocumentHandler<'text'>({
|
|||
return draftContent;
|
||||
},
|
||||
onUpdateDocument: async ({ document, description, dataStream }) => {
|
||||
let draftContent = '';
|
||||
let draftContent = "";
|
||||
|
||||
const { fullStream } = streamText({
|
||||
model: myProvider.languageModel('artifact-model'),
|
||||
system: updateDocumentPrompt(document.content, 'text'),
|
||||
experimental_transform: smoothStream({ chunking: 'word' }),
|
||||
model: myProvider.languageModel("artifact-model"),
|
||||
system: updateDocumentPrompt(document.content, "text"),
|
||||
experimental_transform: smoothStream({ chunking: "word" }),
|
||||
prompt: description,
|
||||
providerOptions: {
|
||||
openai: {
|
||||
prediction: {
|
||||
type: 'content',
|
||||
type: "content",
|
||||
content: document.content,
|
||||
},
|
||||
},
|
||||
|
|
@ -55,13 +55,13 @@ export const textDocumentHandler = createDocumentHandler<'text'>({
|
|||
for await (const delta of fullStream) {
|
||||
const { type } = delta;
|
||||
|
||||
if (type === 'text-delta') {
|
||||
if (type === "text-delta") {
|
||||
const { text } = delta;
|
||||
|
||||
draftContent += text;
|
||||
|
||||
dataStream.write({
|
||||
type: 'data-textDelta',
|
||||
type: "data-textDelta",
|
||||
data: text,
|
||||
transient: true,
|
||||
});
|
||||
|
|
|
|||
161
biome.jsonc
161
biome.jsonc
|
|
@ -1,136 +1,51 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"extends": ["ultracite"],
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": [
|
||||
"**/pnpm-lock.yaml",
|
||||
"lib/db/migrations",
|
||||
"lib/editor/react-renderer.tsx",
|
||||
"node_modules",
|
||||
".next",
|
||||
"public",
|
||||
".vercel"
|
||||
"includes": [
|
||||
"**/*",
|
||||
"!components/ui",
|
||||
"!lib/utils.ts",
|
||||
"!hooks/use-mobile.ts"
|
||||
]
|
||||
},
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"defaultBranch": "main",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 80,
|
||||
"attributePosition": "auto"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"a11y": {
|
||||
"useHtmlLang": "warn", // Not in recommended ruleset, turning on manually
|
||||
"noHeaderScope": "warn", // Not in recommended ruleset, turning on manually
|
||||
"useValidAriaRole": {
|
||||
"level": "warn",
|
||||
"options": {
|
||||
"ignoreNonDom": false,
|
||||
"allowInvalidRoles": ["none", "text"]
|
||||
}
|
||||
},
|
||||
"useSemanticElements": "off", // Rule is buggy, revisit later
|
||||
"noSvgWithoutTitle": "off", // We do not intend to adhere to this rule
|
||||
"useMediaCaption": "off", // We would need a cultural change to turn this on
|
||||
"noAutofocus": "off", // We're highly intentional about when we use autofocus
|
||||
"noBlankTarget": "off", // Covered by Conformance
|
||||
"useFocusableInteractive": "off", // Disable focusable interactive element requirement
|
||||
"useAriaPropsForRole": "off", // Disable required ARIA attributes check
|
||||
"useKeyWithClickEvents": "off" // Disable keyboard event requirement with click events
|
||||
},
|
||||
"complexity": {
|
||||
"noUselessStringConcat": "warn", // Not in recommended ruleset, turning on manually
|
||||
"noForEach": "off", // forEach is too familiar to ban
|
||||
"noUselessSwitchCase": "off", // Turned off due to developer preferences
|
||||
"noUselessThisAlias": "off", // Turned off due to developer preferences
|
||||
"noBannedTypes": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedImports": "warn", // Not in recommended ruleset, turning on manually
|
||||
"useArrayLiterals": "warn", // Not in recommended ruleset, turning on manually
|
||||
"noNewSymbol": "warn", // Not in recommended ruleset, turning on manually
|
||||
"useJsxKeyInIterable": "off", // Rule is buggy, revisit later
|
||||
"useExhaustiveDependencies": "warn", // Errors by default, switching to warn instead
|
||||
"noUnnecessaryContinue": "off" // Turned off due to developer preferences
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "off" // Covered by Conformance
|
||||
"suspicious": {
|
||||
/* Needs more work to fix */
|
||||
"noExplicitAny": "off",
|
||||
|
||||
/* Allow for Tailwind @ rules */
|
||||
"noUnknownAtRules": "off",
|
||||
|
||||
/* Allowing console for debugging */
|
||||
"noConsole": "off",
|
||||
|
||||
/* Needed for generateUUID() */
|
||||
"noBitwiseOperators": "off"
|
||||
},
|
||||
"style": {
|
||||
"useFragmentSyntax": "warn", // Not in recommended ruleset, turning on manually
|
||||
"noYodaExpression": "warn", // Not in recommended ruleset, turning on manually
|
||||
"useDefaultParameterLast": "warn", // Not in recommended ruleset, turning on manually
|
||||
"useExponentiationOperator": "off", // Obscure and arguably not easily readable
|
||||
"noUnusedTemplateLiteral": "off", // Stylistic opinion
|
||||
"noUselessElse": "off" // Stylistic opinion
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off" // We trust Vercelians to use any only when necessary
|
||||
/* Allowing magic numbers */
|
||||
"noMagicNumbers": "off",
|
||||
|
||||
/* Needs more work to fix */
|
||||
"noNestedTernary": "off"
|
||||
},
|
||||
"nursery": {
|
||||
"noDocumentImportInPage": "warn",
|
||||
"noDuplicateElseIf": "warn",
|
||||
"noHeadImportInDocument": "warn",
|
||||
"noIrregularWhitespace": "warn",
|
||||
"noStaticElementInteractions": "warn",
|
||||
"useSortedClasses": "error",
|
||||
"useValidAutocomplete": "warn"
|
||||
/* Too many false positives */
|
||||
"noUnnecessaryConditions": "off"
|
||||
},
|
||||
"complexity": {
|
||||
/* Needs more work to fix */
|
||||
"noExcessiveCognitiveComplexity": "off",
|
||||
|
||||
/* This one has false positives. It's a bit... iffy 😉 */
|
||||
"useSimplifiedLogicExpression": "off"
|
||||
},
|
||||
"a11y": {
|
||||
/* Needs more work to fix */
|
||||
"noSvgWithoutTitle": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"jsxRuntime": "reactClassic",
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingCommas": "all",
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "single",
|
||||
"attributePosition": "auto"
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"trailingCommas": "none"
|
||||
},
|
||||
"parser": {
|
||||
"allowComments": true,
|
||||
"allowTrailingCommas": false
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"formatter": { "enabled": false },
|
||||
"linter": { "enabled": false }
|
||||
},
|
||||
"organizeImports": { "enabled": false },
|
||||
"overrides": [
|
||||
// Playwright requires an object destructure, even if empty
|
||||
// https://github.com/microsoft/playwright/issues/30007
|
||||
{
|
||||
"include": ["playwright/**"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"correctness": {
|
||||
"noEmptyPattern": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import type { User } from 'next-auth';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { PlusIcon } from '@/components/icons';
|
||||
import { SidebarHistory } from '@/components/sidebar-history';
|
||||
import { SidebarUserNav } from '@/components/sidebar-user-nav';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { User } from "next-auth";
|
||||
import { PlusIcon } from "@/components/icons";
|
||||
import { SidebarHistory } from "@/components/sidebar-history";
|
||||
import { SidebarUserNav } from "@/components/sidebar-user-nav";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
|
|
@ -14,9 +14,8 @@ import {
|
|||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar';
|
||||
import Link from 'next/link';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||
|
||||
export function AppSidebar({ user }: { user: User | undefined }) {
|
||||
const router = useRouter();
|
||||
|
|
@ -28,11 +27,11 @@ export function AppSidebar({ user }: { user: User | undefined }) {
|
|||
<SidebarMenu>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<Link
|
||||
className="flex flex-row items-center gap-3"
|
||||
href="/"
|
||||
onClick={() => {
|
||||
setOpenMobile(false);
|
||||
}}
|
||||
className="flex flex-row items-center gap-3"
|
||||
>
|
||||
<span className="cursor-pointer rounded-md px-2 font-semibold text-lg hover:bg-muted">
|
||||
Chatbot
|
||||
|
|
@ -41,14 +40,14 @@ export function AppSidebar({ user }: { user: User | undefined }) {
|
|||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
className="h-8 p-1 md:h-fit md:p-2"
|
||||
onClick={() => {
|
||||
setOpenMobile(false);
|
||||
router.push('/');
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
}}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import { Button } from './ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
import { artifactDefinitions, type UIArtifact } from './artifact';
|
||||
import { type Dispatch, memo, type SetStateAction, useState } from 'react';
|
||||
import type { ArtifactActionContext } from './create-artifact';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { type Dispatch, memo, type SetStateAction, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { artifactDefinitions, type UIArtifact } from "./artifact";
|
||||
import type { ArtifactActionContext } from "./create-artifact";
|
||||
import { Button } from "./ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||
|
||||
interface ArtifactActionsProps {
|
||||
type ArtifactActionsProps = {
|
||||
artifact: UIArtifact;
|
||||
handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void;
|
||||
handleVersionChange: (type: "next" | "prev" | "toggle" | "latest") => void;
|
||||
currentVersionIndex: number;
|
||||
isCurrentVersion: boolean;
|
||||
mode: 'edit' | 'diff';
|
||||
mode: "edit" | "diff";
|
||||
metadata: any;
|
||||
setMetadata: Dispatch<SetStateAction<any>>;
|
||||
}
|
||||
};
|
||||
|
||||
function PureArtifactActions({
|
||||
artifact,
|
||||
|
|
@ -28,11 +28,11 @@ function PureArtifactActions({
|
|||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const artifactDefinition = artifactDefinitions.find(
|
||||
(definition) => definition.kind === artifact.kind,
|
||||
(definition) => definition.kind === artifact.kind
|
||||
);
|
||||
|
||||
if (!artifactDefinition) {
|
||||
throw new Error('Artifact definition not found!');
|
||||
throw new Error("Artifact definition not found!");
|
||||
}
|
||||
|
||||
const actionContext: ArtifactActionContext = {
|
||||
|
|
@ -51,29 +51,29 @@ function PureArtifactActions({
|
|||
<Tooltip key={action.description}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn('h-fit dark:hover:bg-zinc-700', {
|
||||
'p-2': !action.label,
|
||||
'px-2 py-1.5': action.label,
|
||||
className={cn("h-fit dark:hover:bg-zinc-700", {
|
||||
"p-2": !action.label,
|
||||
"px-2 py-1.5": action.label,
|
||||
})}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await Promise.resolve(action.onClick(actionContext));
|
||||
} catch (error) {
|
||||
toast.error('Failed to execute action');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
isLoading || artifact.status === 'streaming'
|
||||
isLoading || artifact.status === "streaming"
|
||||
? true
|
||||
: action.isDisabled
|
||||
? action.isDisabled(actionContext)
|
||||
: false
|
||||
}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await Promise.resolve(action.onClick(actionContext));
|
||||
} catch (_error) {
|
||||
toast.error("Failed to execute action");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
{action.icon}
|
||||
{action.label}
|
||||
|
|
@ -89,12 +89,19 @@ function PureArtifactActions({
|
|||
export const ArtifactActions = memo(
|
||||
PureArtifactActions,
|
||||
(prevProps, nextProps) => {
|
||||
if (prevProps.artifact.status !== nextProps.artifact.status) return false;
|
||||
if (prevProps.currentVersionIndex !== nextProps.currentVersionIndex)
|
||||
if (prevProps.artifact.status !== nextProps.artifact.status) {
|
||||
return false;
|
||||
if (prevProps.isCurrentVersion !== nextProps.isCurrentVersion) return false;
|
||||
if (prevProps.artifact.content !== nextProps.artifact.content) return false;
|
||||
}
|
||||
if (prevProps.currentVersionIndex !== nextProps.currentVersionIndex) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.isCurrentVersion !== nextProps.isCurrentVersion) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.artifact.content !== nextProps.artifact.content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { memo } from 'react';
|
||||
import { CrossIcon } from './icons';
|
||||
import { Button } from './ui/button';
|
||||
import { initialArtifactData, useArtifact } from '@/hooks/use-artifact';
|
||||
import { memo } from "react";
|
||||
import { initialArtifactData, useArtifact } from "@/hooks/use-artifact";
|
||||
import { CrossIcon } from "./icons";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
function PureArtifactCloseButton() {
|
||||
const { setArtifact } = useArtifact();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-testid="artifact-close-button"
|
||||
variant="outline"
|
||||
className="h-fit p-2 dark:hover:bg-zinc-700"
|
||||
data-testid="artifact-close-button"
|
||||
onClick={() => {
|
||||
setArtifact((currentArtifact) =>
|
||||
currentArtifact.status === 'streaming'
|
||||
currentArtifact.status === "streaming"
|
||||
? {
|
||||
...currentArtifact,
|
||||
isVisible: false,
|
||||
}
|
||||
: { ...initialArtifactData, status: 'idle' },
|
||||
: { ...initialArtifactData, status: "idle" }
|
||||
);
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<CrossIcon size={18} />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
import { PreviewMessage, ThinkingMessage } from './message';
|
||||
import type { Vote } from '@/lib/db/schema';
|
||||
import { memo } from 'react';
|
||||
import equal from 'fast-deep-equal';
|
||||
import type { UIArtifact } from './artifact';
|
||||
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useMessages } from '@/hooks/use-messages';
|
||||
import type { ChatMessage } from '@/lib/types';
|
||||
import type { UseChatHelpers } from "@ai-sdk/react";
|
||||
import equal from "fast-deep-equal";
|
||||
import { motion } from "framer-motion";
|
||||
import { memo } from "react";
|
||||
import { useMessages } from "@/hooks/use-messages";
|
||||
import type { Vote } from "@/lib/db/schema";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import type { UIArtifact } from "./artifact";
|
||||
import { PreviewMessage, ThinkingMessage } from "./message";
|
||||
|
||||
interface ArtifactMessagesProps {
|
||||
type ArtifactMessagesProps = {
|
||||
chatId: string;
|
||||
status: UseChatHelpers<ChatMessage>['status'];
|
||||
votes: Array<Vote> | undefined;
|
||||
status: UseChatHelpers<ChatMessage>["status"];
|
||||
votes: Vote[] | undefined;
|
||||
messages: ChatMessage[];
|
||||
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
|
||||
regenerate: UseChatHelpers<ChatMessage>['regenerate'];
|
||||
setMessages: UseChatHelpers<ChatMessage>["setMessages"];
|
||||
regenerate: UseChatHelpers<ChatMessage>["regenerate"];
|
||||
isReadonly: boolean;
|
||||
artifactStatus: UIArtifact['status'];
|
||||
}
|
||||
artifactStatus: UIArtifact["status"];
|
||||
};
|
||||
|
||||
function PureArtifactMessages({
|
||||
chatId,
|
||||
|
|
@ -35,45 +35,43 @@ function PureArtifactMessages({
|
|||
onViewportLeave,
|
||||
hasSentMessage,
|
||||
} = useMessages({
|
||||
chatId,
|
||||
status,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
className="flex h-full flex-col items-center gap-4 overflow-y-scroll px-4 pt-20"
|
||||
ref={messagesContainerRef}
|
||||
>
|
||||
{messages.map((message, index) => (
|
||||
<PreviewMessage
|
||||
chatId={chatId}
|
||||
isLoading={status === "streaming" && index === messages.length - 1}
|
||||
isReadonly={isReadonly}
|
||||
key={message.id}
|
||||
message={message}
|
||||
isLoading={status === 'streaming' && index === messages.length - 1}
|
||||
regenerate={regenerate}
|
||||
requiresScrollPadding={
|
||||
hasSentMessage && index === messages.length - 1
|
||||
}
|
||||
setMessages={setMessages}
|
||||
vote={
|
||||
votes
|
||||
? votes.find((vote) => vote.messageId === message.id)
|
||||
: undefined
|
||||
}
|
||||
setMessages={setMessages}
|
||||
regenerate={regenerate}
|
||||
isReadonly={isReadonly}
|
||||
requiresScrollPadding={
|
||||
hasSentMessage && index === messages.length - 1
|
||||
}
|
||||
isArtifactVisible={true}
|
||||
/>
|
||||
))}
|
||||
|
||||
{status === 'submitted' &&
|
||||
{status === "submitted" &&
|
||||
messages.length > 0 &&
|
||||
messages[messages.length - 1].role === 'user' && <ThinkingMessage />}
|
||||
messages.at(-1)?.role === "user" && <ThinkingMessage />}
|
||||
|
||||
<motion.div
|
||||
ref={messagesEndRef}
|
||||
className="min-h-[24px] min-w-[24px] shrink-0"
|
||||
onViewportLeave={onViewportLeave}
|
||||
onViewportEnter={onViewportEnter}
|
||||
onViewportLeave={onViewportLeave}
|
||||
ref={messagesEndRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -81,18 +79,27 @@ function PureArtifactMessages({
|
|||
|
||||
function areEqual(
|
||||
prevProps: ArtifactMessagesProps,
|
||||
nextProps: ArtifactMessagesProps,
|
||||
nextProps: ArtifactMessagesProps
|
||||
) {
|
||||
if (
|
||||
prevProps.artifactStatus === 'streaming' &&
|
||||
nextProps.artifactStatus === 'streaming'
|
||||
)
|
||||
prevProps.artifactStatus === "streaming" &&
|
||||
nextProps.artifactStatus === "streaming"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (prevProps.status !== nextProps.status) return false;
|
||||
if (prevProps.status && nextProps.status) return false;
|
||||
if (prevProps.messages.length !== nextProps.messages.length) return false;
|
||||
if (!equal(prevProps.votes, nextProps.votes)) return false;
|
||||
if (prevProps.status !== nextProps.status) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.status && nextProps.status) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.messages.length !== nextProps.messages.length) {
|
||||
return false;
|
||||
}
|
||||
if (!equal(prevProps.votes, nextProps.votes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { formatDistance } from 'date-fns';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import type { UseChatHelpers } from "@ai-sdk/react";
|
||||
import { formatDistance } from "date-fns";
|
||||
import equal from "fast-deep-equal";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import {
|
||||
type Dispatch,
|
||||
memo,
|
||||
|
|
@ -7,27 +9,25 @@ import {
|
|||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import useSWR, { useSWRConfig } from 'swr';
|
||||
import { useDebounceCallback, useWindowSize } from 'usehooks-ts';
|
||||
import type { Document, Vote } from '@/lib/db/schema';
|
||||
import { fetcher } from '@/lib/utils';
|
||||
import { MultimodalInput } from './multimodal-input';
|
||||
import { Toolbar } from './toolbar';
|
||||
import { VersionFooter } from './version-footer';
|
||||
import { ArtifactActions } from './artifact-actions';
|
||||
import { ArtifactCloseButton } from './artifact-close-button';
|
||||
import { ArtifactMessages } from './artifact-messages';
|
||||
import { useSidebar } from './ui/sidebar';
|
||||
import { useArtifact } from '@/hooks/use-artifact';
|
||||
import { imageArtifact } from '@/artifacts/image/client';
|
||||
import { codeArtifact } from '@/artifacts/code/client';
|
||||
import { sheetArtifact } from '@/artifacts/sheet/client';
|
||||
import { textArtifact } from '@/artifacts/text/client';
|
||||
import equal from 'fast-deep-equal';
|
||||
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||
import type { VisibilityType } from './visibility-selector';
|
||||
import type { Attachment, ChatMessage } from '@/lib/types';
|
||||
} from "react";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import { useDebounceCallback, useWindowSize } from "usehooks-ts";
|
||||
import { codeArtifact } from "@/artifacts/code/client";
|
||||
import { imageArtifact } from "@/artifacts/image/client";
|
||||
import { sheetArtifact } from "@/artifacts/sheet/client";
|
||||
import { textArtifact } from "@/artifacts/text/client";
|
||||
import { useArtifact } from "@/hooks/use-artifact";
|
||||
import type { Document, Vote } from "@/lib/db/schema";
|
||||
import type { Attachment, ChatMessage } from "@/lib/types";
|
||||
import { fetcher } from "@/lib/utils";
|
||||
import { ArtifactActions } from "./artifact-actions";
|
||||
import { ArtifactCloseButton } from "./artifact-close-button";
|
||||
import { ArtifactMessages } from "./artifact-messages";
|
||||
import { MultimodalInput } from "./multimodal-input";
|
||||
import { Toolbar } from "./toolbar";
|
||||
import { useSidebar } from "./ui/sidebar";
|
||||
import { VersionFooter } from "./version-footer";
|
||||
import type { VisibilityType } from "./visibility-selector";
|
||||
|
||||
export const artifactDefinitions = [
|
||||
textArtifact,
|
||||
|
|
@ -35,22 +35,22 @@ export const artifactDefinitions = [
|
|||
imageArtifact,
|
||||
sheetArtifact,
|
||||
];
|
||||
export type ArtifactKind = (typeof artifactDefinitions)[number]['kind'];
|
||||
export type ArtifactKind = (typeof artifactDefinitions)[number]["kind"];
|
||||
|
||||
export interface UIArtifact {
|
||||
export type UIArtifact = {
|
||||
title: string;
|
||||
documentId: string;
|
||||
kind: ArtifactKind;
|
||||
content: string;
|
||||
isVisible: boolean;
|
||||
status: 'streaming' | 'idle';
|
||||
status: "streaming" | "idle";
|
||||
boundingBox: {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function PureArtifact({
|
||||
chatId,
|
||||
|
|
@ -72,15 +72,15 @@ function PureArtifact({
|
|||
chatId: string;
|
||||
input: string;
|
||||
setInput: Dispatch<SetStateAction<string>>;
|
||||
status: UseChatHelpers<ChatMessage>['status'];
|
||||
stop: UseChatHelpers<ChatMessage>['stop'];
|
||||
status: UseChatHelpers<ChatMessage>["status"];
|
||||
stop: UseChatHelpers<ChatMessage>["stop"];
|
||||
attachments: Attachment[];
|
||||
setAttachments: Dispatch<SetStateAction<Attachment[]>>;
|
||||
messages: ChatMessage[];
|
||||
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
|
||||
votes: Array<Vote> | undefined;
|
||||
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
|
||||
regenerate: UseChatHelpers<ChatMessage>['regenerate'];
|
||||
setMessages: UseChatHelpers<ChatMessage>["setMessages"];
|
||||
votes: Vote[] | undefined;
|
||||
sendMessage: UseChatHelpers<ChatMessage>["sendMessage"];
|
||||
regenerate: UseChatHelpers<ChatMessage>["regenerate"];
|
||||
isReadonly: boolean;
|
||||
selectedVisibilityType: VisibilityType;
|
||||
selectedModelId: string;
|
||||
|
|
@ -91,14 +91,14 @@ function PureArtifact({
|
|||
data: documents,
|
||||
isLoading: isDocumentsFetching,
|
||||
mutate: mutateDocuments,
|
||||
} = useSWR<Array<Document>>(
|
||||
artifact.documentId !== 'init' && artifact.status !== 'streaming'
|
||||
} = useSWR<Document[]>(
|
||||
artifact.documentId !== "init" && artifact.status !== "streaming"
|
||||
? `/api/document?id=${artifact.documentId}`
|
||||
: null,
|
||||
fetcher,
|
||||
fetcher
|
||||
);
|
||||
|
||||
const [mode, setMode] = useState<'edit' | 'diff'>('edit');
|
||||
const [mode, setMode] = useState<"edit" | "diff">("edit");
|
||||
const [document, setDocument] = useState<Document | null>(null);
|
||||
const [currentVersionIndex, setCurrentVersionIndex] = useState(-1);
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ function PureArtifact({
|
|||
setCurrentVersionIndex(documents.length - 1);
|
||||
setArtifact((currentArtifact) => ({
|
||||
...currentArtifact,
|
||||
content: mostRecentDocument.content ?? '',
|
||||
content: mostRecentDocument.content ?? "",
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -121,19 +121,23 @@ function PureArtifact({
|
|||
|
||||
useEffect(() => {
|
||||
mutateDocuments();
|
||||
}, [artifact.status, mutateDocuments]);
|
||||
}, [mutateDocuments]);
|
||||
|
||||
const { mutate } = useSWRConfig();
|
||||
const [isContentDirty, setIsContentDirty] = useState(false);
|
||||
|
||||
const handleContentChange = useCallback(
|
||||
(updatedContent: string) => {
|
||||
if (!artifact) return;
|
||||
if (!artifact) {
|
||||
return;
|
||||
}
|
||||
|
||||
mutate<Array<Document>>(
|
||||
mutate<Document[]>(
|
||||
`/api/document?id=${artifact.documentId}`,
|
||||
async (currentDocuments) => {
|
||||
if (!currentDocuments) return undefined;
|
||||
if (!currentDocuments) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentDocument = currentDocuments.at(-1);
|
||||
|
||||
|
|
@ -144,7 +148,7 @@ function PureArtifact({
|
|||
|
||||
if (currentDocument.content !== updatedContent) {
|
||||
await fetch(`/api/document?id=${artifact.documentId}`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
title: artifact.title,
|
||||
content: updatedContent,
|
||||
|
|
@ -164,15 +168,15 @@ function PureArtifact({
|
|||
}
|
||||
return currentDocuments;
|
||||
},
|
||||
{ revalidate: false },
|
||||
{ revalidate: false }
|
||||
);
|
||||
},
|
||||
[artifact, mutate],
|
||||
[artifact, mutate]
|
||||
);
|
||||
|
||||
const debouncedHandleContentChange = useDebounceCallback(
|
||||
handleContentChange,
|
||||
2000,
|
||||
2000
|
||||
);
|
||||
|
||||
const saveContent = useCallback(
|
||||
|
|
@ -187,35 +191,39 @@ function PureArtifact({
|
|||
}
|
||||
}
|
||||
},
|
||||
[document, debouncedHandleContentChange, handleContentChange],
|
||||
[document, debouncedHandleContentChange, handleContentChange]
|
||||
);
|
||||
|
||||
function getDocumentContentById(index: number) {
|
||||
if (!documents) return '';
|
||||
if (!documents[index]) return '';
|
||||
return documents[index].content ?? '';
|
||||
if (!documents) {
|
||||
return "";
|
||||
}
|
||||
if (!documents[index]) {
|
||||
return "";
|
||||
}
|
||||
return documents[index].content ?? "";
|
||||
}
|
||||
|
||||
const handleVersionChange = (type: 'next' | 'prev' | 'toggle' | 'latest') => {
|
||||
if (!documents) return;
|
||||
const handleVersionChange = (type: "next" | "prev" | "toggle" | "latest") => {
|
||||
if (!documents) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'latest') {
|
||||
if (type === "latest") {
|
||||
setCurrentVersionIndex(documents.length - 1);
|
||||
setMode('edit');
|
||||
setMode("edit");
|
||||
}
|
||||
|
||||
if (type === 'toggle') {
|
||||
setMode((mode) => (mode === 'edit' ? 'diff' : 'edit'));
|
||||
if (type === "toggle") {
|
||||
setMode((currentMode) => (currentMode === "edit" ? "diff" : "edit"));
|
||||
}
|
||||
|
||||
if (type === 'prev') {
|
||||
if (type === "prev") {
|
||||
if (currentVersionIndex > 0) {
|
||||
setCurrentVersionIndex((index) => index - 1);
|
||||
}
|
||||
} else if (type === 'next') {
|
||||
if (currentVersionIndex < documents.length - 1) {
|
||||
setCurrentVersionIndex((index) => index + 1);
|
||||
}
|
||||
} else if (type === "next" && currentVersionIndex < documents.length - 1) {
|
||||
setCurrentVersionIndex((index) => index + 1);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -236,21 +244,19 @@ function PureArtifact({
|
|||
const isMobile = windowWidth ? windowWidth < 768 : false;
|
||||
|
||||
const artifactDefinition = artifactDefinitions.find(
|
||||
(definition) => definition.kind === artifact.kind,
|
||||
(definition) => definition.kind === artifact.kind
|
||||
);
|
||||
|
||||
if (!artifactDefinition) {
|
||||
throw new Error('Artifact definition not found!');
|
||||
throw new Error("Artifact definition not found!");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (artifact.documentId !== 'init') {
|
||||
if (artifactDefinition.initialize) {
|
||||
artifactDefinition.initialize({
|
||||
documentId: artifact.documentId,
|
||||
setMetadata,
|
||||
});
|
||||
}
|
||||
if (artifact.documentId !== "init" && artifactDefinition.initialize) {
|
||||
artifactDefinition.initialize({
|
||||
documentId: artifact.documentId,
|
||||
setMetadata,
|
||||
});
|
||||
}
|
||||
}, [artifact.documentId, artifactDefinition, setMetadata]);
|
||||
|
||||
|
|
@ -258,21 +264,21 @@ function PureArtifact({
|
|||
<AnimatePresence>
|
||||
{artifact.isVisible && (
|
||||
<motion.div
|
||||
data-testid="artifact"
|
||||
className="fixed top-0 left-0 z-50 flex h-dvh w-dvw flex-row bg-transparent"
|
||||
initial={{ opacity: 1 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="fixed top-0 left-0 z-50 flex h-dvh w-dvw flex-row bg-transparent"
|
||||
data-testid="artifact"
|
||||
exit={{ opacity: 0, transition: { delay: 0.4 } }}
|
||||
initial={{ opacity: 1 }}
|
||||
>
|
||||
{!isMobile && (
|
||||
<motion.div
|
||||
animate={{ width: windowWidth, right: 0 }}
|
||||
className="fixed h-dvh bg-background"
|
||||
initial={{
|
||||
exit={{
|
||||
width: isSidebarOpen ? windowWidth - 256 : windowWidth,
|
||||
right: 0,
|
||||
}}
|
||||
animate={{ width: windowWidth, right: 0 }}
|
||||
exit={{
|
||||
initial={{
|
||||
width: isSidebarOpen ? windowWidth - 256 : windowWidth,
|
||||
right: 0,
|
||||
}}
|
||||
|
|
@ -281,64 +287,64 @@ function PureArtifact({
|
|||
|
||||
{!isMobile && (
|
||||
<motion.div
|
||||
className="relative h-dvh w-[400px] shrink-0 bg-muted dark:bg-background"
|
||||
initial={{ opacity: 0, x: 10, scale: 1 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
delay: 0.1,
|
||||
type: 'spring',
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
},
|
||||
}}
|
||||
className="relative h-dvh w-[400px] shrink-0 bg-muted dark:bg-background"
|
||||
exit={{
|
||||
opacity: 0,
|
||||
x: 0,
|
||||
scale: 1,
|
||||
transition: { duration: 0 },
|
||||
}}
|
||||
initial={{ opacity: 0, x: 10, scale: 1 }}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{!isCurrentVersion && (
|
||||
<motion.div
|
||||
className="absolute top-0 left-0 z-50 h-dvh w-[400px] bg-zinc-900/50"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="absolute top-0 left-0 z-50 h-dvh w-[400px] bg-zinc-900/50"
|
||||
exit={{ opacity: 0 }}
|
||||
initial={{ opacity: 0 }}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="flex h-full flex-col items-center justify-between">
|
||||
<ArtifactMessages
|
||||
artifactStatus={artifact.status}
|
||||
chatId={chatId}
|
||||
isReadonly={isReadonly}
|
||||
messages={messages}
|
||||
regenerate={regenerate}
|
||||
setMessages={setMessages}
|
||||
status={status}
|
||||
votes={votes}
|
||||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
regenerate={regenerate}
|
||||
isReadonly={isReadonly}
|
||||
artifactStatus={artifact.status}
|
||||
/>
|
||||
|
||||
<div className="relative flex w-full flex-row items-end gap-2 px-4 pb-4">
|
||||
<MultimodalInput
|
||||
attachments={attachments}
|
||||
chatId={chatId}
|
||||
className="bg-background dark:bg-muted"
|
||||
input={input}
|
||||
messages={messages}
|
||||
selectedModelId={selectedModelId}
|
||||
selectedVisibilityType={selectedVisibilityType}
|
||||
sendMessage={sendMessage}
|
||||
setAttachments={setAttachments}
|
||||
setInput={setInput}
|
||||
setMessages={setMessages}
|
||||
status={status}
|
||||
stop={stop}
|
||||
attachments={attachments}
|
||||
setAttachments={setAttachments}
|
||||
messages={messages}
|
||||
sendMessage={sendMessage}
|
||||
className="bg-background dark:bg-muted"
|
||||
setMessages={setMessages}
|
||||
selectedVisibilityType={selectedVisibilityType}
|
||||
selectedModelId={selectedModelId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -346,7 +352,52 @@ function PureArtifact({
|
|||
)}
|
||||
|
||||
<motion.div
|
||||
animate={
|
||||
isMobile
|
||||
? {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: windowHeight,
|
||||
width: windowWidth ? windowWidth : "calc(100dvw)",
|
||||
borderRadius: 0,
|
||||
transition: {
|
||||
delay: 0,
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
duration: 0.8,
|
||||
},
|
||||
}
|
||||
: {
|
||||
opacity: 1,
|
||||
x: 400,
|
||||
y: 0,
|
||||
height: windowHeight,
|
||||
width: windowWidth
|
||||
? windowWidth - 400
|
||||
: "calc(100dvw-400px)",
|
||||
borderRadius: 0,
|
||||
transition: {
|
||||
delay: 0,
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
duration: 0.8,
|
||||
},
|
||||
}
|
||||
}
|
||||
className="fixed flex h-dvh flex-col overflow-y-scroll border-zinc-200 bg-background md:border-l dark:border-zinc-700 dark:bg-muted"
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 0.5,
|
||||
transition: {
|
||||
delay: 0.1,
|
||||
type: "spring",
|
||||
stiffness: 600,
|
||||
damping: 30,
|
||||
},
|
||||
}}
|
||||
initial={
|
||||
isMobile
|
||||
? {
|
||||
|
|
@ -366,51 +417,6 @@ function PureArtifact({
|
|||
borderRadius: 50,
|
||||
}
|
||||
}
|
||||
animate={
|
||||
isMobile
|
||||
? {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: windowHeight,
|
||||
width: windowWidth ? windowWidth : 'calc(100dvw)',
|
||||
borderRadius: 0,
|
||||
transition: {
|
||||
delay: 0,
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
duration: 0.8,
|
||||
},
|
||||
}
|
||||
: {
|
||||
opacity: 1,
|
||||
x: 400,
|
||||
y: 0,
|
||||
height: windowHeight,
|
||||
width: windowWidth
|
||||
? windowWidth - 400
|
||||
: 'calc(100dvw-400px)',
|
||||
borderRadius: 0,
|
||||
transition: {
|
||||
delay: 0,
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
duration: 0.8,
|
||||
},
|
||||
}
|
||||
}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 0.5,
|
||||
transition: {
|
||||
delay: 0.1,
|
||||
type: 'spring',
|
||||
stiffness: 600,
|
||||
damping: 30,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-start justify-between p-2">
|
||||
<div className="flex flex-row items-start gap-4">
|
||||
|
|
@ -430,7 +436,7 @@ function PureArtifact({
|
|||
new Date(),
|
||||
{
|
||||
addSuffix: true,
|
||||
},
|
||||
}
|
||||
)}`}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -444,43 +450,43 @@ function PureArtifact({
|
|||
currentVersionIndex={currentVersionIndex}
|
||||
handleVersionChange={handleVersionChange}
|
||||
isCurrentVersion={isCurrentVersion}
|
||||
mode={mode}
|
||||
metadata={metadata}
|
||||
mode={mode}
|
||||
setMetadata={setMetadata}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-full max-w-full! items-center overflow-y-scroll bg-background dark:bg-muted">
|
||||
<artifactDefinition.content
|
||||
title={artifact.title}
|
||||
content={
|
||||
isCurrentVersion
|
||||
? artifact.content
|
||||
: getDocumentContentById(currentVersionIndex)
|
||||
}
|
||||
mode={mode}
|
||||
status={artifact.status}
|
||||
currentVersionIndex={currentVersionIndex}
|
||||
suggestions={[]}
|
||||
onSaveContent={saveContent}
|
||||
isInline={false}
|
||||
isCurrentVersion={isCurrentVersion}
|
||||
getDocumentContentById={getDocumentContentById}
|
||||
isCurrentVersion={isCurrentVersion}
|
||||
isInline={false}
|
||||
isLoading={isDocumentsFetching && !artifact.content}
|
||||
metadata={metadata}
|
||||
mode={mode}
|
||||
onSaveContent={saveContent}
|
||||
setMetadata={setMetadata}
|
||||
status={artifact.status}
|
||||
suggestions={[]}
|
||||
title={artifact.title}
|
||||
/>
|
||||
|
||||
<AnimatePresence>
|
||||
{isCurrentVersion && (
|
||||
<Toolbar
|
||||
artifactKind={artifact.kind}
|
||||
isToolbarVisible={isToolbarVisible}
|
||||
setIsToolbarVisible={setIsToolbarVisible}
|
||||
sendMessage={sendMessage}
|
||||
setIsToolbarVisible={setIsToolbarVisible}
|
||||
setMessages={setMessages}
|
||||
status={status}
|
||||
stop={stop}
|
||||
setMessages={setMessages}
|
||||
artifactKind={artifact.kind}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
@ -503,12 +509,21 @@ function PureArtifact({
|
|||
}
|
||||
|
||||
export const Artifact = memo(PureArtifact, (prevProps, nextProps) => {
|
||||
if (prevProps.status !== nextProps.status) return false;
|
||||
if (!equal(prevProps.votes, nextProps.votes)) return false;
|
||||
if (prevProps.input !== nextProps.input) return false;
|
||||
if (!equal(prevProps.messages, nextProps.messages.length)) return false;
|
||||
if (prevProps.selectedVisibilityType !== nextProps.selectedVisibilityType)
|
||||
if (prevProps.status !== nextProps.status) {
|
||||
return false;
|
||||
}
|
||||
if (!equal(prevProps.votes, nextProps.votes)) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.input !== nextProps.input) {
|
||||
return false;
|
||||
}
|
||||
if (!equal(prevProps.messages, nextProps.messages.length)) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.selectedVisibilityType !== nextProps.selectedVisibilityType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import Form from 'next/form';
|
||||
import Form from "next/form";
|
||||
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
|
||||
export function AuthForm({
|
||||
action,
|
||||
children,
|
||||
defaultEmail = '',
|
||||
defaultEmail = "",
|
||||
}: {
|
||||
action: NonNullable<
|
||||
string | ((formData: FormData) => void | Promise<void>) | undefined
|
||||
|
|
@ -18,39 +18,39 @@ export function AuthForm({
|
|||
<Form action={action} className="flex flex-col gap-4 px-4 sm:px-16">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label
|
||||
htmlFor="email"
|
||||
className="font-normal text-zinc-600 dark:text-zinc-400"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email Address
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
autoComplete="email"
|
||||
autoFocus
|
||||
className="bg-muted text-md md:text-sm"
|
||||
defaultValue={defaultEmail}
|
||||
id="email"
|
||||
name="email"
|
||||
className="bg-muted text-md md:text-sm"
|
||||
type="email"
|
||||
placeholder="user@acme.com"
|
||||
autoComplete="email"
|
||||
required
|
||||
autoFocus
|
||||
defaultValue={defaultEmail}
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label
|
||||
htmlFor="password"
|
||||
className="font-normal text-zinc-600 dark:text-zinc-400"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
className="bg-muted text-md md:text-sm"
|
||||
id="password"
|
||||
name="password"
|
||||
className="bg-muted text-md md:text-sm"
|
||||
type="password"
|
||||
required
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,23 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useWindowSize } from 'usehooks-ts';
|
||||
|
||||
import { SidebarToggle } from '@/components/sidebar-toggle';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { PlusIcon, VercelIcon } from './icons';
|
||||
import { useSidebar } from './ui/sidebar';
|
||||
import { memo } from 'react';
|
||||
import { type VisibilityType, VisibilitySelector } from './visibility-selector';
|
||||
import type { Session } from 'next-auth';
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { memo } from "react";
|
||||
import { useWindowSize } from "usehooks-ts";
|
||||
import { SidebarToggle } from "@/components/sidebar-toggle";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PlusIcon, VercelIcon } from "./icons";
|
||||
import { useSidebar } from "./ui/sidebar";
|
||||
import { VisibilitySelector, type VisibilityType } from "./visibility-selector";
|
||||
|
||||
function PureChatHeader({
|
||||
chatId,
|
||||
selectedVisibilityType,
|
||||
isReadonly,
|
||||
session,
|
||||
}: {
|
||||
chatId: string;
|
||||
selectedVisibilityType: VisibilityType;
|
||||
isReadonly: boolean;
|
||||
session: Session;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { open } = useSidebar();
|
||||
|
|
@ -34,12 +30,12 @@ function PureChatHeader({
|
|||
|
||||
{(!open || windowWidth < 768) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="order-2 ml-auto h-8 px-2 md:order-1 md:ml-0 md:h-fit md:px-2"
|
||||
onClick={() => {
|
||||
router.push('/');
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<PlusIcon />
|
||||
<span className="md:sr-only">New Chat</span>
|
||||
|
|
@ -49,19 +45,19 @@ function PureChatHeader({
|
|||
{!isReadonly && (
|
||||
<VisibilitySelector
|
||||
chatId={chatId}
|
||||
selectedVisibilityType={selectedVisibilityType}
|
||||
className="order-1 md:order-2"
|
||||
selectedVisibilityType={selectedVisibilityType}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="order-3 hidden bg-zinc-900 px-2 text-zinc-50 hover:bg-zinc-800 md:ml-auto md:flex md:h-fit dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
|
||||
asChild
|
||||
className="order-3 hidden bg-zinc-900 px-2 text-zinc-50 hover:bg-zinc-800 md:ml-auto md:flex md:h-fit dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
|
||||
>
|
||||
<Link
|
||||
href={`https://vercel.com/templates/next.js/nextjs-ai-chatbot`}
|
||||
target="_noblank"
|
||||
href={"https://vercel.com/templates/next.js/nextjs-ai-chatbot"}
|
||||
rel="noreferrer"
|
||||
target="_noblank"
|
||||
>
|
||||
<VercelIcon size={16} />
|
||||
Deploy with Vercel
|
||||
|
|
|
|||
|
|
@ -1,28 +1,12 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { DefaultChatTransport } from 'ai';
|
||||
import { useChat } from '@ai-sdk/react';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import useSWR, { useSWRConfig } from 'swr';
|
||||
import { ChatHeader } from '@/components/chat-header';
|
||||
import type { Vote } from '@/lib/db/schema';
|
||||
import { fetcher, fetchWithErrorHandlers, generateUUID } from '@/lib/utils';
|
||||
import { Artifact } from './artifact';
|
||||
import { MultimodalInput } from './multimodal-input';
|
||||
import { Messages } from './messages';
|
||||
import type { VisibilityType } from './visibility-selector';
|
||||
import { useArtifactSelector } from '@/hooks/use-artifact';
|
||||
import { unstable_serialize } from 'swr/infinite';
|
||||
import { getChatHistoryPaginationKey } from './sidebar-history';
|
||||
import { toast } from './toast';
|
||||
import type { Session } from 'next-auth';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useChatVisibility } from '@/hooks/use-chat-visibility';
|
||||
import { useAutoResume } from '@/hooks/use-auto-resume';
|
||||
import { ChatSDKError } from '@/lib/errors';
|
||||
import type { Attachment, ChatMessage } from '@/lib/types';
|
||||
import type { AppUsage } from '@/lib/usage';
|
||||
import { useDataStream } from './data-stream-provider';
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import { unstable_serialize } from "swr/infinite";
|
||||
import { ChatHeader } from "@/components/chat-header";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
|
@ -32,7 +16,22 @@ import {
|
|||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { useArtifactSelector } from "@/hooks/use-artifact";
|
||||
import { useAutoResume } from "@/hooks/use-auto-resume";
|
||||
import { useChatVisibility } from "@/hooks/use-chat-visibility";
|
||||
import type { Vote } from "@/lib/db/schema";
|
||||
import { ChatSDKError } from "@/lib/errors";
|
||||
import type { Attachment, ChatMessage } from "@/lib/types";
|
||||
import type { AppUsage } from "@/lib/usage";
|
||||
import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils";
|
||||
import { Artifact } from "./artifact";
|
||||
import { useDataStream } from "./data-stream-provider";
|
||||
import { Messages } from "./messages";
|
||||
import { MultimodalInput } from "./multimodal-input";
|
||||
import { getChatHistoryPaginationKey } from "./sidebar-history";
|
||||
import { toast } from "./toast";
|
||||
import type { VisibilityType } from "./visibility-selector";
|
||||
|
||||
export function Chat({
|
||||
id,
|
||||
|
|
@ -40,7 +39,6 @@ export function Chat({
|
|||
initialChatModel,
|
||||
initialVisibilityType,
|
||||
isReadonly,
|
||||
session,
|
||||
autoResume,
|
||||
initialLastContext,
|
||||
}: {
|
||||
|
|
@ -49,7 +47,6 @@ export function Chat({
|
|||
initialChatModel: string;
|
||||
initialVisibilityType: VisibilityType;
|
||||
isReadonly: boolean;
|
||||
session: Session;
|
||||
autoResume: boolean;
|
||||
initialLastContext?: AppUsage;
|
||||
}) {
|
||||
|
|
@ -61,12 +58,12 @@ export function Chat({
|
|||
const { mutate } = useSWRConfig();
|
||||
const { setDataStream } = useDataStream();
|
||||
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [input, setInput] = useState<string>("");
|
||||
const [usage, setUsage] = useState<AppUsage | undefined>(initialLastContext);
|
||||
const [showCreditCardAlert, setShowCreditCardAlert] = useState(false);
|
||||
const [currentModelId, setCurrentModelId] = useState(initialChatModel);
|
||||
const currentModelIdRef = useRef(currentModelId);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
currentModelIdRef.current = currentModelId;
|
||||
}, [currentModelId]);
|
||||
|
|
@ -85,23 +82,25 @@ export function Chat({
|
|||
experimental_throttle: 100,
|
||||
generateId: generateUUID,
|
||||
transport: new DefaultChatTransport({
|
||||
api: '/api/chat',
|
||||
api: "/api/chat",
|
||||
fetch: fetchWithErrorHandlers,
|
||||
prepareSendMessagesRequest({ messages, id, body }) {
|
||||
prepareSendMessagesRequest(request) {
|
||||
return {
|
||||
body: {
|
||||
id,
|
||||
message: messages.at(-1),
|
||||
id: request.id,
|
||||
message: request.messages.at(-1),
|
||||
selectedChatModel: currentModelIdRef.current,
|
||||
selectedVisibilityType: visibilityType,
|
||||
...body,
|
||||
...request.body,
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
onData: (dataPart) => {
|
||||
setDataStream((ds) => (ds ? [...ds, dataPart] : []));
|
||||
if (dataPart.type === 'data-usage') setUsage(dataPart.data);
|
||||
if (dataPart.type === "data-usage") {
|
||||
setUsage(dataPart.data);
|
||||
}
|
||||
},
|
||||
onFinish: () => {
|
||||
mutate(unstable_serialize(getChatHistoryPaginationKey));
|
||||
|
|
@ -110,12 +109,12 @@ export function Chat({
|
|||
if (error instanceof ChatSDKError) {
|
||||
// Check if it's a credit card error
|
||||
if (
|
||||
error.message?.includes('AI Gateway requires a valid credit card')
|
||||
error.message?.includes("AI Gateway requires a valid credit card")
|
||||
) {
|
||||
setShowCreditCardAlert(true);
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
type: "error",
|
||||
description: error.message,
|
||||
});
|
||||
}
|
||||
|
|
@ -124,28 +123,28 @@ export function Chat({
|
|||
});
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const query = searchParams.get('query');
|
||||
const query = searchParams.get("query");
|
||||
|
||||
const [hasAppendedQuery, setHasAppendedQuery] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (query && !hasAppendedQuery) {
|
||||
sendMessage({
|
||||
role: 'user' as const,
|
||||
parts: [{ type: 'text', text: query }],
|
||||
role: "user" as const,
|
||||
parts: [{ type: "text", text: query }],
|
||||
});
|
||||
|
||||
setHasAppendedQuery(true);
|
||||
window.history.replaceState({}, '', `/chat/${id}`);
|
||||
window.history.replaceState({}, "", `/chat/${id}`);
|
||||
}
|
||||
}, [query, sendMessage, hasAppendedQuery, id]);
|
||||
|
||||
const { data: votes } = useSWR<Array<Vote>>(
|
||||
const { data: votes } = useSWR<Vote[]>(
|
||||
messages.length >= 2 ? `/api/vote?chatId=${id}` : null,
|
||||
fetcher,
|
||||
fetcher
|
||||
);
|
||||
|
||||
const [attachments, setAttachments] = useState<Array<Attachment>>([]);
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const isArtifactVisible = useArtifactSelector((state) => state.isVisible);
|
||||
|
||||
useAutoResume({
|
||||
|
|
@ -160,39 +159,38 @@ export function Chat({
|
|||
<div className="overscroll-behavior-contain flex h-dvh min-w-0 touch-pan-y flex-col bg-background">
|
||||
<ChatHeader
|
||||
chatId={id}
|
||||
selectedVisibilityType={initialVisibilityType}
|
||||
isReadonly={isReadonly}
|
||||
session={session}
|
||||
selectedVisibilityType={initialVisibilityType}
|
||||
/>
|
||||
|
||||
<Messages
|
||||
chatId={id}
|
||||
isArtifactVisible={isArtifactVisible}
|
||||
isReadonly={isReadonly}
|
||||
messages={messages}
|
||||
regenerate={regenerate}
|
||||
selectedModelId={initialChatModel}
|
||||
setMessages={setMessages}
|
||||
status={status}
|
||||
votes={votes}
|
||||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
regenerate={regenerate}
|
||||
isReadonly={isReadonly}
|
||||
isArtifactVisible={isArtifactVisible}
|
||||
selectedModelId={initialChatModel}
|
||||
/>
|
||||
|
||||
<div className="sticky bottom-0 z-1 mx-auto flex w-full max-w-4xl gap-2 border-t-0 bg-background px-2 pb-3 md:px-4 md:pb-4">
|
||||
{!isReadonly && (
|
||||
<MultimodalInput
|
||||
attachments={attachments}
|
||||
chatId={id}
|
||||
input={input}
|
||||
messages={messages}
|
||||
onModelChange={setCurrentModelId}
|
||||
selectedModelId={currentModelId}
|
||||
selectedVisibilityType={visibilityType}
|
||||
sendMessage={sendMessage}
|
||||
setAttachments={setAttachments}
|
||||
setInput={setInput}
|
||||
setMessages={setMessages}
|
||||
status={status}
|
||||
stop={stop}
|
||||
attachments={attachments}
|
||||
setAttachments={setAttachments}
|
||||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
sendMessage={sendMessage}
|
||||
selectedVisibilityType={visibilityType}
|
||||
selectedModelId={currentModelId}
|
||||
onModelChange={setCurrentModelId}
|
||||
usage={usage}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -200,33 +198,33 @@ export function Chat({
|
|||
</div>
|
||||
|
||||
<Artifact
|
||||
attachments={attachments}
|
||||
chatId={id}
|
||||
input={input}
|
||||
isReadonly={isReadonly}
|
||||
messages={messages}
|
||||
regenerate={regenerate}
|
||||
selectedModelId={currentModelId}
|
||||
selectedVisibilityType={visibilityType}
|
||||
sendMessage={sendMessage}
|
||||
setAttachments={setAttachments}
|
||||
setInput={setInput}
|
||||
setMessages={setMessages}
|
||||
status={status}
|
||||
stop={stop}
|
||||
attachments={attachments}
|
||||
setAttachments={setAttachments}
|
||||
sendMessage={sendMessage}
|
||||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
regenerate={regenerate}
|
||||
votes={votes}
|
||||
isReadonly={isReadonly}
|
||||
selectedVisibilityType={visibilityType}
|
||||
selectedModelId={currentModelId}
|
||||
/>
|
||||
|
||||
<AlertDialog
|
||||
open={showCreditCardAlert}
|
||||
onOpenChange={setShowCreditCardAlert}
|
||||
open={showCreditCardAlert}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Activate AI Gateway</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This application requires{' '}
|
||||
{process.env.NODE_ENV === 'production' ? 'the owner' : 'you'} to
|
||||
This application requires{" "}
|
||||
{process.env.NODE_ENV === "production" ? "the owner" : "you"} to
|
||||
activate Vercel AI Gateway.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
|
@ -235,10 +233,10 @@ export function Chat({
|
|||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
window.open(
|
||||
'https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dadd-credit-card',
|
||||
'_blank',
|
||||
"https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dadd-credit-card",
|
||||
"_blank"
|
||||
);
|
||||
window.location.href = '/';
|
||||
window.location.href = "/";
|
||||
}}
|
||||
>
|
||||
Activate
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { EditorState, Transaction } from '@codemirror/state';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { basicSetup } from 'codemirror';
|
||||
import React, { memo, useEffect, useRef } from 'react';
|
||||
import type { Suggestion } from '@/lib/db/schema';
|
||||
import { python } from "@codemirror/lang-python";
|
||||
import { EditorState, Transaction } from "@codemirror/state";
|
||||
import { oneDark } from "@codemirror/theme-one-dark";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { basicSetup } from "codemirror";
|
||||
import { memo, useEffect, useRef } from "react";
|
||||
import type { Suggestion } from "@/lib/db/schema";
|
||||
|
||||
type EditorProps = {
|
||||
content: string;
|
||||
onSaveContent: (updatedContent: string, debounce: boolean) => void;
|
||||
status: 'streaming' | 'idle';
|
||||
status: "streaming" | "idle";
|
||||
isCurrentVersion: boolean;
|
||||
currentVersionIndex: number;
|
||||
suggestions: Array<Suggestion>;
|
||||
suggestions: Suggestion[];
|
||||
};
|
||||
|
||||
function PureCodeEditor({ content, onSaveContent, status }: EditorProps) {
|
||||
|
|
@ -42,14 +42,14 @@ function PureCodeEditor({ content, onSaveContent, status }: EditorProps) {
|
|||
};
|
||||
// NOTE: we only want to run this effect once
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
}, [content]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
const updateListener = EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
const transaction = update.transactions.find(
|
||||
(tr) => !tr.annotation(Transaction.remote),
|
||||
(tr) => !tr.annotation(Transaction.remote)
|
||||
);
|
||||
|
||||
if (transaction) {
|
||||
|
|
@ -75,7 +75,7 @@ function PureCodeEditor({ content, onSaveContent, status }: EditorProps) {
|
|||
if (editorRef.current && content) {
|
||||
const currentContent = editorRef.current.state.doc.toString();
|
||||
|
||||
if (status === 'streaming' || currentContent !== content) {
|
||||
if (status === "streaming" || currentContent !== content) {
|
||||
const transaction = editorRef.current.state.update({
|
||||
changes: {
|
||||
from: 0,
|
||||
|
|
@ -99,13 +99,21 @@ function PureCodeEditor({ content, onSaveContent, status }: EditorProps) {
|
|||
}
|
||||
|
||||
function areEqual(prevProps: EditorProps, nextProps: EditorProps) {
|
||||
if (prevProps.suggestions !== nextProps.suggestions) return false;
|
||||
if (prevProps.currentVersionIndex !== nextProps.currentVersionIndex)
|
||||
if (prevProps.suggestions !== nextProps.suggestions) {
|
||||
return false;
|
||||
if (prevProps.isCurrentVersion !== nextProps.isCurrentVersion) return false;
|
||||
if (prevProps.status === 'streaming' && nextProps.status === 'streaming')
|
||||
}
|
||||
if (prevProps.currentVersionIndex !== nextProps.currentVersionIndex) {
|
||||
return false;
|
||||
if (prevProps.content !== nextProps.content) return false;
|
||||
}
|
||||
if (prevProps.isCurrentVersion !== nextProps.isCurrentVersion) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.status === "streaming" && nextProps.status === "streaming") {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.content !== nextProps.content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
import { TerminalWindowIcon, CrossSmallIcon } from './icons';
|
||||
import { Loader } from './elements/loader';
|
||||
import { Button } from './ui/button';
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
|
|
@ -8,25 +5,28 @@ import {
|
|||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useArtifactSelector } from '@/hooks/use-artifact';
|
||||
} from "react";
|
||||
import { useArtifactSelector } from "@/hooks/use-artifact";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Loader } from "./elements/loader";
|
||||
import { CrossSmallIcon, TerminalWindowIcon } from "./icons";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export interface ConsoleOutputContent {
|
||||
type: 'text' | 'image';
|
||||
export type ConsoleOutputContent = {
|
||||
type: "text" | "image";
|
||||
value: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ConsoleOutput {
|
||||
export type ConsoleOutput = {
|
||||
id: string;
|
||||
status: 'in_progress' | 'loading_packages' | 'completed' | 'failed';
|
||||
contents: Array<ConsoleOutputContent>;
|
||||
}
|
||||
status: "in_progress" | "loading_packages" | "completed" | "failed";
|
||||
contents: ConsoleOutputContent[];
|
||||
};
|
||||
|
||||
interface ConsoleProps {
|
||||
consoleOutputs: Array<ConsoleOutput>;
|
||||
setConsoleOutputs: Dispatch<SetStateAction<Array<ConsoleOutput>>>;
|
||||
}
|
||||
type ConsoleProps = {
|
||||
consoleOutputs: ConsoleOutput[];
|
||||
setConsoleOutputs: Dispatch<SetStateAction<ConsoleOutput[]>>;
|
||||
};
|
||||
|
||||
export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
|
||||
const [height, setHeight] = useState<number>(300);
|
||||
|
|
@ -55,21 +55,21 @@ export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
|
|||
}
|
||||
}
|
||||
},
|
||||
[isResizing],
|
||||
[isResizing]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mousemove', resize);
|
||||
window.addEventListener('mouseup', stopResizing);
|
||||
window.addEventListener("mousemove", resize);
|
||||
window.addEventListener("mouseup", stopResizing);
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', resize);
|
||||
window.removeEventListener('mouseup', stopResizing);
|
||||
window.removeEventListener("mousemove", resize);
|
||||
window.removeEventListener("mouseup", stopResizing);
|
||||
};
|
||||
}, [resize, stopResizing]);
|
||||
|
||||
useEffect(() => {
|
||||
consoleEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [consoleOutputs]);
|
||||
consoleEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isArtifactVisible) {
|
||||
|
|
@ -80,19 +80,31 @@ export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
|
|||
return consoleOutputs.length > 0 ? (
|
||||
<>
|
||||
<div
|
||||
aria-label="Resize console"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuemax={maxHeight}
|
||||
aria-valuemin={minHeight}
|
||||
aria-valuenow={height}
|
||||
className="fixed z-50 h-2 w-full cursor-ns-resize"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowUp") {
|
||||
setHeight((prev) => Math.min(prev + 10, maxHeight));
|
||||
} else if (e.key === "ArrowDown") {
|
||||
setHeight((prev) => Math.max(prev - 10, minHeight));
|
||||
}
|
||||
}}
|
||||
onMouseDown={startResizing}
|
||||
style={{ bottom: height - 4 }}
|
||||
role="slider"
|
||||
aria-valuenow={minHeight}
|
||||
style={{ bottom: height - 4 }}
|
||||
tabIndex={0}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'fixed bottom-0 z-40 flex w-full flex-col overflow-x-hidden overflow-y-scroll border-zinc-200 border-t bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900',
|
||||
"fixed bottom-0 z-40 flex w-full flex-col overflow-x-hidden overflow-y-scroll border-zinc-200 border-t bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900",
|
||||
{
|
||||
'select-none': isResizing,
|
||||
},
|
||||
"select-none": isResizing,
|
||||
}
|
||||
)}
|
||||
style={{ height }}
|
||||
>
|
||||
|
|
@ -104,10 +116,10 @@ export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
|
|||
<div>Console</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="size-fit p-1 hover:bg-zinc-200 dark:hover:bg-zinc-700"
|
||||
size="icon"
|
||||
onClick={() => setConsoleOutputs([])}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<CrossSmallIcon />
|
||||
</Button>
|
||||
|
|
@ -116,57 +128,58 @@ export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
|
|||
<div>
|
||||
{consoleOutputs.map((consoleOutput, index) => (
|
||||
<div
|
||||
key={consoleOutput.id}
|
||||
className="flex flex-row border-zinc-200 border-b bg-zinc-50 px-4 py-2 font-mono text-sm dark:border-zinc-700 dark:bg-zinc-900"
|
||||
key={consoleOutput.id}
|
||||
>
|
||||
<div
|
||||
className={cn('w-12 shrink-0', {
|
||||
'text-muted-foreground': [
|
||||
'in_progress',
|
||||
'loading_packages',
|
||||
className={cn("w-12 shrink-0", {
|
||||
"text-muted-foreground": [
|
||||
"in_progress",
|
||||
"loading_packages",
|
||||
].includes(consoleOutput.status),
|
||||
'text-emerald-500': consoleOutput.status === 'completed',
|
||||
'text-red-400': consoleOutput.status === 'failed',
|
||||
"text-emerald-500": consoleOutput.status === "completed",
|
||||
"text-red-400": consoleOutput.status === "failed",
|
||||
})}
|
||||
>
|
||||
[{index + 1}]
|
||||
</div>
|
||||
{['in_progress', 'loading_packages'].includes(
|
||||
consoleOutput.status,
|
||||
{["in_progress", "loading_packages"].includes(
|
||||
consoleOutput.status
|
||||
) ? (
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="mt-0.5 mb-auto size-fit self-center">
|
||||
<Loader size={16} />
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
{consoleOutput.status === 'in_progress'
|
||||
? 'Initializing...'
|
||||
: consoleOutput.status === 'loading_packages'
|
||||
{consoleOutput.status === "in_progress"
|
||||
? "Initializing..."
|
||||
: consoleOutput.status === "loading_packages"
|
||||
? consoleOutput.contents.map((content) =>
|
||||
content.type === 'text' ? content.value : null,
|
||||
content.type === "text" ? content.value : null
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full flex-col gap-2 overflow-x-scroll text-zinc-900 dark:text-zinc-50">
|
||||
{consoleOutput.contents.map((content, index) =>
|
||||
content.type === 'image' ? (
|
||||
<picture key={`${consoleOutput.id}-${index}`}>
|
||||
{consoleOutput.contents.map((content, contentIndex) =>
|
||||
content.type === "image" ? (
|
||||
<picture key={`${consoleOutput.id}-${contentIndex}`}>
|
||||
{/** biome-ignore lint/nursery/useImageSize: "Generated image without explicit size" */}
|
||||
<img
|
||||
src={content.value}
|
||||
alt="output"
|
||||
className="w-full max-w-(--breakpoint-toast-mobile) rounded-md"
|
||||
src={content.value}
|
||||
/>
|
||||
</picture>
|
||||
) : (
|
||||
<div
|
||||
key={`${consoleOutput.id}-${index}`}
|
||||
className="w-full whitespace-pre-line break-words"
|
||||
key={`${consoleOutput.id}-${contentIndex}`}
|
||||
>
|
||||
{content.value}
|
||||
</div>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import type { Suggestion } from '@/lib/db/schema';
|
||||
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||
import type { ComponentType, Dispatch, ReactNode, SetStateAction } from 'react';
|
||||
import type { UIArtifact } from './artifact';
|
||||
import type { ChatMessage, CustomUIDataTypes } from '@/lib/types';
|
||||
import type { DataUIPart } from 'ai';
|
||||
import type { UseChatHelpers } from "@ai-sdk/react";
|
||||
import type { DataUIPart } from "ai";
|
||||
import type { ComponentType, Dispatch, ReactNode, SetStateAction } from "react";
|
||||
import type { Suggestion } from "@/lib/db/schema";
|
||||
import type { ChatMessage, CustomUIDataTypes } from "@/lib/types";
|
||||
import type { UIArtifact } from "./artifact";
|
||||
|
||||
export type ArtifactActionContext<M = any> = {
|
||||
content: string;
|
||||
handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void;
|
||||
handleVersionChange: (type: "next" | "prev" | "toggle" | "latest") => void;
|
||||
currentVersionIndex: number;
|
||||
isCurrentVersion: boolean;
|
||||
mode: 'edit' | 'diff';
|
||||
mode: "edit" | "diff";
|
||||
metadata: M;
|
||||
setMetadata: Dispatch<SetStateAction<M>>;
|
||||
};
|
||||
|
|
@ -24,7 +24,7 @@ type ArtifactAction<M = any> = {
|
|||
};
|
||||
|
||||
export type ArtifactToolbarContext = {
|
||||
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
|
||||
sendMessage: UseChatHelpers<ChatMessage>["sendMessage"];
|
||||
};
|
||||
|
||||
export type ArtifactToolbarItem = {
|
||||
|
|
@ -33,32 +33,32 @@ export type ArtifactToolbarItem = {
|
|||
onClick: (context: ArtifactToolbarContext) => void;
|
||||
};
|
||||
|
||||
interface ArtifactContent<M = any> {
|
||||
type ArtifactContent<M = any> = {
|
||||
title: string;
|
||||
content: string;
|
||||
mode: 'edit' | 'diff';
|
||||
mode: "edit" | "diff";
|
||||
isCurrentVersion: boolean;
|
||||
currentVersionIndex: number;
|
||||
status: 'streaming' | 'idle';
|
||||
suggestions: Array<Suggestion>;
|
||||
status: "streaming" | "idle";
|
||||
suggestions: Suggestion[];
|
||||
onSaveContent: (updatedContent: string, debounce: boolean) => void;
|
||||
isInline: boolean;
|
||||
getDocumentContentById: (index: number) => string;
|
||||
isLoading: boolean;
|
||||
metadata: M;
|
||||
setMetadata: Dispatch<SetStateAction<M>>;
|
||||
}
|
||||
};
|
||||
|
||||
interface InitializeParameters<M = any> {
|
||||
type InitializeParameters<M = any> = {
|
||||
documentId: string;
|
||||
setMetadata: Dispatch<SetStateAction<M>>;
|
||||
}
|
||||
};
|
||||
|
||||
type ArtifactConfig<T extends string, M = any> = {
|
||||
kind: T;
|
||||
description: string;
|
||||
content: ComponentType<ArtifactContent<M>>;
|
||||
actions: Array<ArtifactAction<M>>;
|
||||
actions: ArtifactAction<M>[];
|
||||
toolbar: ArtifactToolbarItem[];
|
||||
initialize?: (parameters: InitializeParameters<M>) => void;
|
||||
onStreamPart: (args: {
|
||||
|
|
@ -72,7 +72,7 @@ export class Artifact<T extends string, M = any> {
|
|||
readonly kind: T;
|
||||
readonly description: string;
|
||||
readonly content: ComponentType<ArtifactContent<M>>;
|
||||
readonly actions: Array<ArtifactAction<M>>;
|
||||
readonly actions: ArtifactAction<M>[];
|
||||
readonly toolbar: ArtifactToolbarItem[];
|
||||
readonly initialize?: (parameters: InitializeParameters) => void;
|
||||
readonly onStreamPart: (args: {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { artifactDefinitions } from './artifact';
|
||||
import { initialArtifactData, useArtifact } from '@/hooks/use-artifact';
|
||||
import { useDataStream } from './data-stream-provider';
|
||||
import { useEffect, useRef } from "react";
|
||||
import { initialArtifactData, useArtifact } from "@/hooks/use-artifact";
|
||||
import { artifactDefinitions } from "./artifact";
|
||||
import { useDataStream } from "./data-stream-provider";
|
||||
|
||||
export function DataStreamHandler() {
|
||||
const { dataStream } = useDataStream();
|
||||
|
|
@ -12,14 +12,17 @@ export function DataStreamHandler() {
|
|||
const lastProcessedIndex = useRef(-1);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataStream?.length) return;
|
||||
if (!dataStream?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newDeltas = dataStream.slice(lastProcessedIndex.current + 1);
|
||||
lastProcessedIndex.current = dataStream.length - 1;
|
||||
|
||||
newDeltas.forEach((delta) => {
|
||||
for (const delta of newDeltas) {
|
||||
const artifactDefinition = artifactDefinitions.find(
|
||||
(artifactDefinition) => artifactDefinition.kind === artifact.kind,
|
||||
(currentArtifactDefinition) =>
|
||||
currentArtifactDefinition.kind === artifact.kind
|
||||
);
|
||||
|
||||
if (artifactDefinition?.onStreamPart) {
|
||||
|
|
@ -32,49 +35,49 @@ export function DataStreamHandler() {
|
|||
|
||||
setArtifact((draftArtifact) => {
|
||||
if (!draftArtifact) {
|
||||
return { ...initialArtifactData, status: 'streaming' };
|
||||
return { ...initialArtifactData, status: "streaming" };
|
||||
}
|
||||
|
||||
switch (delta.type) {
|
||||
case 'data-id':
|
||||
case "data-id":
|
||||
return {
|
||||
...draftArtifact,
|
||||
documentId: delta.data,
|
||||
status: 'streaming',
|
||||
status: "streaming",
|
||||
};
|
||||
|
||||
case 'data-title':
|
||||
case "data-title":
|
||||
return {
|
||||
...draftArtifact,
|
||||
title: delta.data,
|
||||
status: 'streaming',
|
||||
status: "streaming",
|
||||
};
|
||||
|
||||
case 'data-kind':
|
||||
case "data-kind":
|
||||
return {
|
||||
...draftArtifact,
|
||||
kind: delta.data,
|
||||
status: 'streaming',
|
||||
status: "streaming",
|
||||
};
|
||||
|
||||
case 'data-clear':
|
||||
case "data-clear":
|
||||
return {
|
||||
...draftArtifact,
|
||||
content: '',
|
||||
status: 'streaming',
|
||||
content: "",
|
||||
status: "streaming",
|
||||
};
|
||||
|
||||
case 'data-finish':
|
||||
case "data-finish":
|
||||
return {
|
||||
...draftArtifact,
|
||||
status: 'idle',
|
||||
status: "idle",
|
||||
};
|
||||
|
||||
default:
|
||||
return draftArtifact;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [dataStream, setArtifact, setMetadata, artifact]);
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useMemo, useState } from 'react';
|
||||
import type { DataUIPart } from 'ai';
|
||||
import type { CustomUIDataTypes } from '@/lib/types';
|
||||
import type { DataUIPart } from "ai";
|
||||
import type React from "react";
|
||||
import { createContext, useContext, useMemo, useState } from "react";
|
||||
import type { CustomUIDataTypes } from "@/lib/types";
|
||||
|
||||
interface DataStreamContextValue {
|
||||
type DataStreamContextValue = {
|
||||
dataStream: DataUIPart<CustomUIDataTypes>[];
|
||||
setDataStream: React.Dispatch<
|
||||
React.SetStateAction<DataUIPart<CustomUIDataTypes>[]>
|
||||
>;
|
||||
}
|
||||
};
|
||||
|
||||
const DataStreamContext = createContext<DataStreamContextValue | null>(null);
|
||||
|
||||
|
|
@ -19,7 +20,7 @@ export function DataStreamProvider({
|
|||
children: React.ReactNode;
|
||||
}) {
|
||||
const [dataStream, setDataStream] = useState<DataUIPart<CustomUIDataTypes>[]>(
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
const value = useMemo(() => ({ dataStream, setDataStream }), [dataStream]);
|
||||
|
|
@ -34,7 +35,7 @@ export function DataStreamProvider({
|
|||
export function useDataStream() {
|
||||
const context = useContext(DataStreamContext);
|
||||
if (!context) {
|
||||
throw new Error('useDataStream must be used within a DataStreamProvider');
|
||||
throw new Error("useDataStream must be used within a DataStreamProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,42 @@
|
|||
import OrderedMap from 'orderedmap';
|
||||
import OrderedMap from "orderedmap";
|
||||
import {
|
||||
Schema,
|
||||
type Node as ProsemirrorNode,
|
||||
type MarkSpec,
|
||||
DOMParser,
|
||||
} from 'prosemirror-model';
|
||||
import { schema } from 'prosemirror-schema-basic';
|
||||
import { addListNodes } from 'prosemirror-schema-list';
|
||||
import { EditorState } from 'prosemirror-state';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { Streamdown } from 'streamdown';
|
||||
type MarkSpec,
|
||||
type Node as ProsemirrorNode,
|
||||
Schema,
|
||||
} from "prosemirror-model";
|
||||
import { schema } from "prosemirror-schema-basic";
|
||||
import { addListNodes } from "prosemirror-schema-list";
|
||||
import { EditorState } from "prosemirror-state";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
import { diffEditor, DiffType } from '@/lib/editor/diff';
|
||||
import { DiffType, diffEditor } from "@/lib/editor/diff";
|
||||
|
||||
const diffSchema = new Schema({
|
||||
nodes: addListNodes(schema.spec.nodes, 'paragraph block*', 'block'),
|
||||
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
|
||||
marks: OrderedMap.from({
|
||||
...schema.spec.marks.toObject(),
|
||||
diffMark: {
|
||||
attrs: { type: { default: '' } },
|
||||
attrs: { type: { default: "" } },
|
||||
toDOM(mark) {
|
||||
let className = '';
|
||||
let className = "";
|
||||
|
||||
switch (mark.attrs.type) {
|
||||
case DiffType.Inserted:
|
||||
className =
|
||||
'bg-green-100 text-green-700 dark:bg-green-500/70 dark:text-green-300';
|
||||
"bg-green-100 text-green-700 dark:bg-green-500/70 dark:text-green-300";
|
||||
break;
|
||||
case DiffType.Deleted:
|
||||
className =
|
||||
'bg-red-100 line-through text-red-600 dark:bg-red-500/70 dark:text-red-300';
|
||||
"bg-red-100 line-through text-red-600 dark:bg-red-500/70 dark:text-red-300";
|
||||
break;
|
||||
default:
|
||||
className = '';
|
||||
className = "";
|
||||
}
|
||||
return ['span', { class: className }, 0];
|
||||
return ["span", { class: className }, 0];
|
||||
},
|
||||
} as MarkSpec,
|
||||
}),
|
||||
|
|
@ -60,16 +60,16 @@ export const DiffView = ({ oldContent, newContent }: DiffEditorProps) => {
|
|||
const parser = DOMParser.fromSchema(diffSchema);
|
||||
|
||||
const oldHtmlContent = renderToString(
|
||||
<Streamdown>{oldContent}</Streamdown>,
|
||||
<Streamdown>{oldContent}</Streamdown>
|
||||
);
|
||||
const newHtmlContent = renderToString(
|
||||
<Streamdown>{newContent}</Streamdown>,
|
||||
<Streamdown>{newContent}</Streamdown>
|
||||
);
|
||||
|
||||
const oldContainer = document.createElement('div');
|
||||
const oldContainer = document.createElement("div");
|
||||
oldContainer.innerHTML = oldHtmlContent;
|
||||
|
||||
const newContainer = document.createElement('div');
|
||||
const newContainer = document.createElement("div");
|
||||
newContainer.innerHTML = newHtmlContent;
|
||||
|
||||
const oldDoc = parser.parse(oldContainer);
|
||||
|
|
|
|||
|
|
@ -1,32 +1,32 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import equal from "fast-deep-equal";
|
||||
import {
|
||||
memo,
|
||||
type MouseEvent,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import type { ArtifactKind, UIArtifact } from './artifact';
|
||||
import { FileIcon, FullscreenIcon, ImageIcon, LoaderIcon } from './icons';
|
||||
import { cn, fetcher } from '@/lib/utils';
|
||||
import type { Document } from '@/lib/db/schema';
|
||||
import { InlineDocumentSkeleton } from './document-skeleton';
|
||||
import useSWR from 'swr';
|
||||
import { Editor } from './text-editor';
|
||||
import { DocumentToolCall, DocumentToolResult } from './document';
|
||||
import { CodeEditor } from './code-editor';
|
||||
import { useArtifact } from '@/hooks/use-artifact';
|
||||
import equal from 'fast-deep-equal';
|
||||
import { SpreadsheetEditor } from './sheet-editor';
|
||||
import { ImageEditor } from './image-editor';
|
||||
} from "react";
|
||||
import useSWR from "swr";
|
||||
import { useArtifact } from "@/hooks/use-artifact";
|
||||
import type { Document } from "@/lib/db/schema";
|
||||
import { cn, fetcher } from "@/lib/utils";
|
||||
import type { ArtifactKind, UIArtifact } from "./artifact";
|
||||
import { CodeEditor } from "./code-editor";
|
||||
import { DocumentToolCall, DocumentToolResult } from "./document";
|
||||
import { InlineDocumentSkeleton } from "./document-skeleton";
|
||||
import { FileIcon, FullscreenIcon, ImageIcon, LoaderIcon } from "./icons";
|
||||
import { ImageEditor } from "./image-editor";
|
||||
import { SpreadsheetEditor } from "./sheet-editor";
|
||||
import { Editor } from "./text-editor";
|
||||
|
||||
interface DocumentPreviewProps {
|
||||
type DocumentPreviewProps = {
|
||||
isReadonly: boolean;
|
||||
result?: any;
|
||||
args?: any;
|
||||
}
|
||||
};
|
||||
|
||||
export function DocumentPreview({
|
||||
isReadonly,
|
||||
|
|
@ -36,7 +36,7 @@ export function DocumentPreview({
|
|||
const { artifact, setArtifact } = useArtifact();
|
||||
|
||||
const { data: documents, isLoading: isDocumentsFetching } = useSWR<
|
||||
Array<Document>
|
||||
Document[]
|
||||
>(result ? `/api/document?id=${result.id}` : null, fetcher);
|
||||
|
||||
const previewDocument = useMemo(() => documents?.[0], [documents]);
|
||||
|
|
@ -46,8 +46,8 @@ export function DocumentPreview({
|
|||
const boundingBox = hitboxRef.current?.getBoundingClientRect();
|
||||
|
||||
if (artifact.documentId && boundingBox) {
|
||||
setArtifact((artifact) => ({
|
||||
...artifact,
|
||||
setArtifact((currentArtifact) => ({
|
||||
...currentArtifact,
|
||||
boundingBox: {
|
||||
left: boundingBox.x,
|
||||
top: boundingBox.y,
|
||||
|
|
@ -62,9 +62,9 @@ export function DocumentPreview({
|
|||
if (result) {
|
||||
return (
|
||||
<DocumentToolResult
|
||||
type="create"
|
||||
result={{ id: result.id, title: result.title, kind: result.kind }}
|
||||
isReadonly={isReadonly}
|
||||
result={{ id: result.id, title: result.title, kind: result.kind }}
|
||||
type="create"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -72,9 +72,9 @@ export function DocumentPreview({
|
|||
if (args) {
|
||||
return (
|
||||
<DocumentToolCall
|
||||
type="create"
|
||||
args={{ title: args.title, kind: args.kind }}
|
||||
isReadonly={isReadonly}
|
||||
type="create"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -86,18 +86,20 @@ export function DocumentPreview({
|
|||
|
||||
const document: Document | null = previewDocument
|
||||
? previewDocument
|
||||
: artifact.status === 'streaming'
|
||||
: artifact.status === "streaming"
|
||||
? {
|
||||
title: artifact.title,
|
||||
kind: artifact.kind,
|
||||
content: artifact.content,
|
||||
id: artifact.documentId,
|
||||
createdAt: new Date(),
|
||||
userId: 'noop',
|
||||
userId: "noop",
|
||||
}
|
||||
: null;
|
||||
|
||||
if (!document) return <LoadingSkeleton artifactKind={artifact.kind} />;
|
||||
if (!document) {
|
||||
return <LoadingSkeleton artifactKind={artifact.kind} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full cursor-pointer">
|
||||
|
|
@ -107,9 +109,9 @@ export function DocumentPreview({
|
|||
setArtifact={setArtifact}
|
||||
/>
|
||||
<DocumentHeader
|
||||
title={document.title}
|
||||
isStreaming={artifact.status === "streaming"}
|
||||
kind={document.kind}
|
||||
isStreaming={artifact.status === 'streaming'}
|
||||
title={document.title}
|
||||
/>
|
||||
<DocumentContent document={document} />
|
||||
</div>
|
||||
|
|
@ -129,7 +131,7 @@ const LoadingSkeleton = ({ artifactKind }: { artifactKind: ArtifactKind }) => (
|
|||
<FullscreenIcon />
|
||||
</div>
|
||||
</div>
|
||||
{artifactKind === 'image' ? (
|
||||
{artifactKind === "image" ? (
|
||||
<div className="overflow-y-scroll rounded-b-2xl border border-t-0 bg-muted dark:border-zinc-700">
|
||||
<div className="h-[257px] w-full animate-pulse bg-muted-foreground/20" />
|
||||
</div>
|
||||
|
|
@ -149,7 +151,7 @@ const PureHitboxLayer = ({
|
|||
hitboxRef: React.RefObject<HTMLDivElement>;
|
||||
result: any;
|
||||
setArtifact: (
|
||||
updaterFn: UIArtifact | ((currentArtifact: UIArtifact) => UIArtifact),
|
||||
updaterFn: UIArtifact | ((currentArtifact: UIArtifact) => UIArtifact)
|
||||
) => void;
|
||||
}) => {
|
||||
const handleClick = useCallback(
|
||||
|
|
@ -157,7 +159,7 @@ const PureHitboxLayer = ({
|
|||
const boundingBox = event.currentTarget.getBoundingClientRect();
|
||||
|
||||
setArtifact((artifact) =>
|
||||
artifact.status === 'streaming'
|
||||
artifact.status === "streaming"
|
||||
? { ...artifact, isVisible: true }
|
||||
: {
|
||||
...artifact,
|
||||
|
|
@ -171,19 +173,19 @@ const PureHitboxLayer = ({
|
|||
width: boundingBox.width,
|
||||
height: boundingBox.height,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
[setArtifact, result],
|
||||
[setArtifact, result]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute top-0 left-0 z-10 size-full rounded-xl"
|
||||
ref={hitboxRef}
|
||||
onClick={handleClick}
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className="absolute top-0 left-0 z-10 size-full rounded-xl"
|
||||
onClick={handleClick}
|
||||
ref={hitboxRef}
|
||||
role="presentation"
|
||||
>
|
||||
<div className="flex w-full items-center justify-end p-4">
|
||||
<div className="absolute top-[13px] right-[9px] rounded-md p-2 hover:bg-zinc-100 dark:hover:bg-zinc-700">
|
||||
|
|
@ -195,7 +197,9 @@ const PureHitboxLayer = ({
|
|||
};
|
||||
|
||||
const HitboxLayer = memo(PureHitboxLayer, (prevProps, nextProps) => {
|
||||
if (!equal(prevProps.result, nextProps.result)) return false;
|
||||
if (!equal(prevProps.result, nextProps.result)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
|
|
@ -215,7 +219,7 @@ const PureDocumentHeader = ({
|
|||
<div className="animate-spin">
|
||||
<LoaderIcon />
|
||||
</div>
|
||||
) : kind === 'image' ? (
|
||||
) : kind === "image" ? (
|
||||
<ImageIcon />
|
||||
) : (
|
||||
<FileIcon />
|
||||
|
|
@ -228,8 +232,12 @@ const PureDocumentHeader = ({
|
|||
);
|
||||
|
||||
const DocumentHeader = memo(PureDocumentHeader, (prevProps, nextProps) => {
|
||||
if (prevProps.title !== nextProps.title) return false;
|
||||
if (prevProps.isStreaming !== nextProps.isStreaming) return false;
|
||||
if (prevProps.title !== nextProps.title) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.isStreaming !== nextProps.isStreaming) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
|
@ -238,46 +246,48 @@ const DocumentContent = ({ document }: { document: Document }) => {
|
|||
const { artifact } = useArtifact();
|
||||
|
||||
const containerClassName = cn(
|
||||
'h-[257px] overflow-y-scroll border rounded-b-2xl dark:bg-muted border-t-0 dark:border-zinc-700',
|
||||
"h-[257px] overflow-y-scroll rounded-b-2xl border border-t-0 dark:border-zinc-700 dark:bg-muted",
|
||||
{
|
||||
'p-4 sm:px-14 sm:py-16': document.kind === 'text',
|
||||
'p-0': document.kind === 'code',
|
||||
},
|
||||
"p-4 sm:px-14 sm:py-16": document.kind === "text",
|
||||
"p-0": document.kind === "code",
|
||||
}
|
||||
);
|
||||
|
||||
const commonProps = {
|
||||
content: document.content ?? '',
|
||||
content: document.content ?? "",
|
||||
isCurrentVersion: true,
|
||||
currentVersionIndex: 0,
|
||||
status: artifact.status,
|
||||
saveContent: () => {},
|
||||
saveContent: () => null,
|
||||
suggestions: [],
|
||||
};
|
||||
|
||||
const handleSaveContent = () => null;
|
||||
|
||||
return (
|
||||
<div className={containerClassName}>
|
||||
{document.kind === 'text' ? (
|
||||
<Editor {...commonProps} onSaveContent={() => {}} />
|
||||
) : document.kind === 'code' ? (
|
||||
{document.kind === "text" ? (
|
||||
<Editor {...commonProps} onSaveContent={handleSaveContent} />
|
||||
) : document.kind === "code" ? (
|
||||
<div className="relative flex w-full flex-1">
|
||||
<div className="absolute inset-0">
|
||||
<CodeEditor {...commonProps} onSaveContent={() => {}} />
|
||||
<CodeEditor {...commonProps} onSaveContent={handleSaveContent} />
|
||||
</div>
|
||||
</div>
|
||||
) : document.kind === 'sheet' ? (
|
||||
) : document.kind === "sheet" ? (
|
||||
<div className="relative flex size-full flex-1 p-4">
|
||||
<div className="absolute inset-0">
|
||||
<SpreadsheetEditor {...commonProps} />
|
||||
</div>
|
||||
</div>
|
||||
) : document.kind === 'image' ? (
|
||||
) : document.kind === "image" ? (
|
||||
<ImageEditor
|
||||
title={document.title}
|
||||
content={document.content ?? ''}
|
||||
isCurrentVersion={true}
|
||||
content={document.content ?? ""}
|
||||
currentVersionIndex={0}
|
||||
status={artifact.status}
|
||||
isCurrentVersion={true}
|
||||
isInline={true}
|
||||
status={artifact.status}
|
||||
title={document.title}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import type { ArtifactKind } from './artifact';
|
||||
import type { ArtifactKind } from "./artifact";
|
||||
|
||||
export const DocumentSkeleton = ({
|
||||
artifactKind,
|
||||
}: {
|
||||
artifactKind: ArtifactKind;
|
||||
}) => {
|
||||
return artifactKind === 'image' ? (
|
||||
return artifactKind === "image" ? (
|
||||
<div className="flex h-[calc(100dvh-60px)] w-full flex-col items-center justify-center gap-4">
|
||||
<div className="size-96 animate-pulse rounded-lg bg-muted-foreground/20" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,33 +1,32 @@
|
|||
import { memo } from 'react';
|
||||
|
||||
import type { ArtifactKind } from './artifact';
|
||||
import { FileIcon, LoaderIcon, MessageIcon, PencilEditIcon } from './icons';
|
||||
import { toast } from 'sonner';
|
||||
import { useArtifact } from '@/hooks/use-artifact';
|
||||
import { memo } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useArtifact } from "@/hooks/use-artifact";
|
||||
import type { ArtifactKind } from "./artifact";
|
||||
import { FileIcon, LoaderIcon, MessageIcon, PencilEditIcon } from "./icons";
|
||||
|
||||
const getActionText = (
|
||||
type: 'create' | 'update' | 'request-suggestions',
|
||||
tense: 'present' | 'past',
|
||||
type: "create" | "update" | "request-suggestions",
|
||||
tense: "present" | "past"
|
||||
) => {
|
||||
switch (type) {
|
||||
case 'create':
|
||||
return tense === 'present' ? 'Creating' : 'Created';
|
||||
case 'update':
|
||||
return tense === 'present' ? 'Updating' : 'Updated';
|
||||
case 'request-suggestions':
|
||||
return tense === 'present'
|
||||
? 'Adding suggestions'
|
||||
: 'Added suggestions to';
|
||||
case "create":
|
||||
return tense === "present" ? "Creating" : "Created";
|
||||
case "update":
|
||||
return tense === "present" ? "Updating" : "Updated";
|
||||
case "request-suggestions":
|
||||
return tense === "present"
|
||||
? "Adding suggestions"
|
||||
: "Added suggestions to";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
interface DocumentToolResultProps {
|
||||
type: 'create' | 'update' | 'request-suggestions';
|
||||
type DocumentToolResultProps = {
|
||||
type: "create" | "update" | "request-suggestions";
|
||||
result: { id: string; title: string; kind: ArtifactKind };
|
||||
isReadonly: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
function PureDocumentToolResult({
|
||||
type,
|
||||
|
|
@ -38,12 +37,11 @@ function PureDocumentToolResult({
|
|||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-fit cursor-pointer flex-row items-start gap-3 rounded-xl border bg-background px-3 py-2"
|
||||
onClick={(event) => {
|
||||
if (isReadonly) {
|
||||
toast.error(
|
||||
'Viewing files in shared chats is currently not supported.',
|
||||
"Viewing files in shared chats is currently not supported."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -60,25 +58,26 @@ function PureDocumentToolResult({
|
|||
setArtifact({
|
||||
documentId: result.id,
|
||||
kind: result.kind,
|
||||
content: '',
|
||||
content: "",
|
||||
title: result.title,
|
||||
isVisible: true,
|
||||
status: 'idle',
|
||||
status: "idle",
|
||||
boundingBox,
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div className="mt-1 text-muted-foreground">
|
||||
{type === 'create' ? (
|
||||
{type === "create" ? (
|
||||
<FileIcon />
|
||||
) : type === 'update' ? (
|
||||
) : type === "update" ? (
|
||||
<PencilEditIcon />
|
||||
) : type === 'request-suggestions' ? (
|
||||
) : type === "request-suggestions" ? (
|
||||
<MessageIcon />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
{`${getActionText(type, 'past')} "${result.title}"`}
|
||||
{`${getActionText(type, "past")} "${result.title}"`}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
|
@ -86,14 +85,14 @@ function PureDocumentToolResult({
|
|||
|
||||
export const DocumentToolResult = memo(PureDocumentToolResult, () => true);
|
||||
|
||||
interface DocumentToolCallProps {
|
||||
type: 'create' | 'update' | 'request-suggestions';
|
||||
type DocumentToolCallProps = {
|
||||
type: "create" | "update" | "request-suggestions";
|
||||
args:
|
||||
| { title: string; kind: ArtifactKind } // for create
|
||||
| { id: string; description: string } // for update
|
||||
| { documentId: string }; // for request-suggestions
|
||||
isReadonly: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
function PureDocumentToolCall({
|
||||
type,
|
||||
|
|
@ -104,12 +103,11 @@ function PureDocumentToolCall({
|
|||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="cursor pointer flex w-fit flex-row items-start justify-between gap-3 rounded-xl border px-3 py-2"
|
||||
onClick={(event) => {
|
||||
if (isReadonly) {
|
||||
toast.error(
|
||||
'Viewing files in shared chats is currently not supported.',
|
||||
"Viewing files in shared chats is currently not supported."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -129,27 +127,28 @@ function PureDocumentToolCall({
|
|||
boundingBox,
|
||||
}));
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex flex-row items-start gap-3">
|
||||
<div className="mt-1 text-zinc-500">
|
||||
{type === 'create' ? (
|
||||
{type === "create" ? (
|
||||
<FileIcon />
|
||||
) : type === 'update' ? (
|
||||
) : type === "update" ? (
|
||||
<PencilEditIcon />
|
||||
) : type === 'request-suggestions' ? (
|
||||
) : type === "request-suggestions" ? (
|
||||
<MessageIcon />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="text-left">
|
||||
{`${getActionText(type, 'present')} ${
|
||||
type === 'create' && 'title' in args && args.title
|
||||
{`${getActionText(type, "present")} ${
|
||||
type === "create" && "title" in args && args.title
|
||||
? `"${args.title}"`
|
||||
: type === 'update' && 'description' in args
|
||||
: type === "update" && "description" in args
|
||||
? `"${args.description}"`
|
||||
: type === 'request-suggestions'
|
||||
? 'for document'
|
||||
: ''
|
||||
: type === "request-suggestions"
|
||||
? "for document"
|
||||
: ""
|
||||
}`}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { ComponentProps } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ComponentProps } from 'react';
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type ActionsProps = ComponentProps<'div'>;
|
||||
export type ActionsProps = ComponentProps<"div">;
|
||||
|
||||
export const Actions = ({ className, children, ...props }: ActionsProps) => (
|
||||
<div className={cn('flex items-center gap-1', className)} {...props}>
|
||||
<div className={cn("flex items-center gap-1", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -28,15 +28,15 @@ export const Action = ({
|
|||
children,
|
||||
label,
|
||||
className,
|
||||
variant = 'ghost',
|
||||
size = 'sm',
|
||||
variant = "ghost",
|
||||
size = "sm",
|
||||
...props
|
||||
}: ActionProps) => {
|
||||
const button = (
|
||||
<Button
|
||||
className={cn(
|
||||
'relative size-9 p-1.5 text-muted-foreground hover:text-foreground',
|
||||
className,
|
||||
"relative size-9 p-1.5 text-muted-foreground hover:text-foreground",
|
||||
className
|
||||
)}
|
||||
size={size}
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { UIMessage } from 'ai';
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
||||
import type { ComponentProps, HTMLAttributes, ReactElement } from 'react';
|
||||
import { createContext, useContext, useEffect, useState, useMemo } from 'react';
|
||||
import type { UIMessage } from "ai";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type BranchContextType = {
|
||||
currentBranch: number;
|
||||
|
|
@ -22,7 +22,7 @@ const useBranch = () => {
|
|||
const context = useContext(BranchContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Branch components must be used within Branch');
|
||||
throw new Error("Branch components must be used within Branch");
|
||||
}
|
||||
|
||||
return context;
|
||||
|
|
@ -71,7 +71,7 @@ export const Branch = ({
|
|||
return (
|
||||
<BranchContext.Provider value={contextValue}>
|
||||
<div
|
||||
className={cn('grid w-full gap-2 [&>div]:pb-0', className)}
|
||||
className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
</BranchContext.Provider>
|
||||
|
|
@ -84,7 +84,7 @@ export const BranchMessages = ({ children, ...props }: BranchMessagesProps) => {
|
|||
const { currentBranch, setBranches, branches } = useBranch();
|
||||
const childrenArray = useMemo(
|
||||
() => (Array.isArray(children) ? children : [children]),
|
||||
[children],
|
||||
[children]
|
||||
);
|
||||
|
||||
// Use useEffect to update branches when they change
|
||||
|
|
@ -97,8 +97,8 @@ export const BranchMessages = ({ children, ...props }: BranchMessagesProps) => {
|
|||
return childrenArray.map((branch, index) => (
|
||||
<div
|
||||
className={cn(
|
||||
'grid gap-2 overflow-hidden [&>div]:pb-0',
|
||||
index === currentBranch ? 'block' : 'hidden',
|
||||
"grid gap-2 overflow-hidden [&>div]:pb-0",
|
||||
index === currentBranch ? "block" : "hidden"
|
||||
)}
|
||||
key={branch.key}
|
||||
{...props}
|
||||
|
|
@ -109,7 +109,7 @@ export const BranchMessages = ({ children, ...props }: BranchMessagesProps) => {
|
|||
};
|
||||
|
||||
export type BranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
|
||||
from: UIMessage['role'];
|
||||
from: UIMessage["role"];
|
||||
};
|
||||
|
||||
export const BranchSelector = ({
|
||||
|
|
@ -127,9 +127,9 @@ export const BranchSelector = ({
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 self-end px-10',
|
||||
from === 'assistant' ? 'justify-start' : 'justify-end',
|
||||
className,
|
||||
"flex items-center gap-2 self-end px-10",
|
||||
from === "assistant" ? "justify-start" : "justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
|
@ -149,10 +149,10 @@ export const BranchPrevious = ({
|
|||
<Button
|
||||
aria-label="Previous branch"
|
||||
className={cn(
|
||||
'size-7 shrink-0 rounded-full text-muted-foreground transition-colors',
|
||||
'hover:bg-accent hover:text-foreground',
|
||||
'disabled:pointer-events-none disabled:opacity-50',
|
||||
className,
|
||||
"size-7 shrink-0 rounded-full text-muted-foreground transition-colors",
|
||||
"hover:bg-accent hover:text-foreground",
|
||||
"disabled:pointer-events-none disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
disabled={totalBranches <= 1}
|
||||
onClick={goToPrevious}
|
||||
|
|
@ -179,10 +179,10 @@ export const BranchNext = ({
|
|||
<Button
|
||||
aria-label="Next branch"
|
||||
className={cn(
|
||||
'size-7 shrink-0 rounded-full text-muted-foreground transition-colors',
|
||||
'hover:bg-accent hover:text-foreground',
|
||||
'disabled:pointer-events-none disabled:opacity-50',
|
||||
className,
|
||||
"size-7 shrink-0 rounded-full text-muted-foreground transition-colors",
|
||||
"hover:bg-accent hover:text-foreground",
|
||||
"disabled:pointer-events-none disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
disabled={totalBranches <= 1}
|
||||
onClick={goToNext}
|
||||
|
|
@ -204,8 +204,8 @@ export const BranchPage = ({ className, ...props }: BranchPageProps) => {
|
|||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'font-medium text-muted-foreground text-xs tabular-nums',
|
||||
className,
|
||||
"font-medium text-muted-foreground text-xs tabular-nums",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckIcon, CopyIcon } from 'lucide-react';
|
||||
import type { ComponentProps, HTMLAttributes, ReactNode } from 'react';
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
|
||||
import { createContext, useContext, useState } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import {
|
||||
oneDark,
|
||||
oneLight,
|
||||
} from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
} from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type CodeBlockContextType = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
const CodeBlockContext = createContext<CodeBlockContextType>({
|
||||
code: '',
|
||||
code: "",
|
||||
});
|
||||
|
||||
export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
||||
|
|
@ -37,8 +37,8 @@ export const CodeBlock = ({
|
|||
<CodeBlockContext.Provider value={{ code }}>
|
||||
<div
|
||||
className={cn(
|
||||
'relative w-full overflow-hidden rounded-md border bg-background text-foreground',
|
||||
className,
|
||||
"relative w-full overflow-hidden rounded-md border bg-background text-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -46,23 +46,23 @@ export const CodeBlock = ({
|
|||
<SyntaxHighlighter
|
||||
className="overflow-hidden dark:hidden"
|
||||
codeTagProps={{
|
||||
className: 'font-mono text-sm',
|
||||
className: "font-mono text-sm",
|
||||
}}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: '1rem',
|
||||
fontSize: '0.875rem',
|
||||
background: 'hsl(var(--background))',
|
||||
color: 'hsl(var(--foreground))',
|
||||
overflowX: 'auto',
|
||||
overflowWrap: 'break-word',
|
||||
wordBreak: 'break-all',
|
||||
padding: "1rem",
|
||||
fontSize: "0.875rem",
|
||||
background: "hsl(var(--background))",
|
||||
color: "hsl(var(--foreground))",
|
||||
overflowX: "auto",
|
||||
overflowWrap: "break-word",
|
||||
wordBreak: "break-all",
|
||||
}}
|
||||
language={language}
|
||||
lineNumberStyle={{
|
||||
color: 'hsl(var(--muted-foreground))',
|
||||
paddingRight: '1rem',
|
||||
minWidth: '2.5rem',
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
paddingRight: "1rem",
|
||||
minWidth: "2.5rem",
|
||||
}}
|
||||
showLineNumbers={showLineNumbers}
|
||||
style={oneLight}
|
||||
|
|
@ -72,23 +72,23 @@ export const CodeBlock = ({
|
|||
<SyntaxHighlighter
|
||||
className="hidden overflow-hidden dark:block"
|
||||
codeTagProps={{
|
||||
className: 'font-mono text-sm',
|
||||
className: "font-mono text-sm",
|
||||
}}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: '1rem',
|
||||
fontSize: '0.875rem',
|
||||
background: 'hsl(var(--background))',
|
||||
color: 'hsl(var(--foreground))',
|
||||
overflowX: 'auto',
|
||||
overflowWrap: 'break-word',
|
||||
wordBreak: 'break-all',
|
||||
padding: "1rem",
|
||||
fontSize: "0.875rem",
|
||||
background: "hsl(var(--background))",
|
||||
color: "hsl(var(--foreground))",
|
||||
overflowX: "auto",
|
||||
overflowWrap: "break-word",
|
||||
wordBreak: "break-all",
|
||||
}}
|
||||
language={language}
|
||||
lineNumberStyle={{
|
||||
color: 'hsl(var(--muted-foreground))',
|
||||
paddingRight: '1rem',
|
||||
minWidth: '2.5rem',
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
paddingRight: "1rem",
|
||||
minWidth: "2.5rem",
|
||||
}}
|
||||
showLineNumbers={showLineNumbers}
|
||||
style={oneDark}
|
||||
|
|
@ -123,8 +123,8 @@ export const CodeBlockCopyButton = ({
|
|||
const { code } = useContext(CodeBlockContext);
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
if (typeof window === 'undefined' || !navigator.clipboard.writeText) {
|
||||
onError?.(new Error('Clipboard API not available'));
|
||||
if (typeof window === "undefined" || !navigator.clipboard.writeText) {
|
||||
onError?.(new Error("Clipboard API not available"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ export const CodeBlockCopyButton = ({
|
|||
|
||||
return (
|
||||
<Button
|
||||
className={cn('shrink-0', className)}
|
||||
className={cn("shrink-0", className)}
|
||||
onClick={copyToClipboard}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import type { ComponentProps } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import type { AppUsage } from '@/lib/usage';
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import type { AppUsage } from "@/lib/usage";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type ContextProps = ComponentProps<'button'> & {
|
||||
export type ContextProps = ComponentProps<"button"> & {
|
||||
/** Optional full usage payload to enable breakdown view */
|
||||
usage?: AppUsage;
|
||||
};
|
||||
|
||||
const THOUSAND = 1000;
|
||||
const MILLION = 1_000_000;
|
||||
const BILLION = 1_000_000_000;
|
||||
const _THOUSAND = 1000;
|
||||
const _MILLION = 1_000_000;
|
||||
const _BILLION = 1_000_000_000;
|
||||
const PERCENT_MAX = 100;
|
||||
|
||||
// Lucide CircleIcon geometry
|
||||
|
|
@ -41,7 +41,7 @@ export const ContextIcon = ({ percent }: ContextIconProps) => {
|
|||
aria-label={`${percent.toFixed(2)}% of model context used`}
|
||||
height="28"
|
||||
role="img"
|
||||
style={{ color: 'currentcolor' }}
|
||||
style={{ color: "currentcolor" }}
|
||||
viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
|
||||
width="28"
|
||||
>
|
||||
|
|
@ -71,7 +71,6 @@ export const ContextIcon = ({ percent }: ContextIconProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
|
||||
function InfoRow({
|
||||
label,
|
||||
tokens,
|
||||
|
|
@ -85,14 +84,16 @@ function InfoRow({
|
|||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<div className="flex items-center gap-2 font-mono">
|
||||
<span className="text-right min-w-[4ch]">
|
||||
{tokens === undefined ? '—' : tokens.toLocaleString()}
|
||||
<span className="min-w-[4ch] text-right">
|
||||
{tokens === undefined ? "—" : tokens.toLocaleString()}
|
||||
</span>
|
||||
{costText !== undefined && costText !== null && !isNaN(parseFloat(costText)) && (
|
||||
<span className="text-muted-foreground">
|
||||
${parseFloat(costText).toFixed(6)}
|
||||
</span>
|
||||
)}
|
||||
{costText !== undefined &&
|
||||
costText !== null &&
|
||||
!Number.isNaN(Number.parseFloat(costText)) && (
|
||||
<span className="text-muted-foreground">
|
||||
${Number.parseFloat(costText).toFixed(6)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -104,16 +105,16 @@ export const Context = ({ className, usage, ...props }: ContextProps) => {
|
|||
usage?.context?.totalMax ??
|
||||
usage?.context?.combinedMax ??
|
||||
usage?.context?.inputMax;
|
||||
const hasMax = typeof max === 'number' && Number.isFinite(max) && max > 0;
|
||||
const hasMax = typeof max === "number" && Number.isFinite(max) && max > 0;
|
||||
const usedPercent = hasMax ? Math.min(100, (used / max) * 100) : 0;
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 select-none rounded-md text-sm',
|
||||
'cursor-pointer bg-background text-foreground',
|
||||
className,
|
||||
"inline-flex select-none items-center gap-1 rounded-md text-sm",
|
||||
"cursor-pointer bg-background text-foreground",
|
||||
className
|
||||
)}
|
||||
type="button"
|
||||
{...props}
|
||||
|
|
@ -124,7 +125,7 @@ export const Context = ({ className, usage, ...props }: ContextProps) => {
|
|||
<ContextIcon percent={usedPercent} />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="top" className="w-fit p-3">
|
||||
<DropdownMenuContent align="end" className="w-fit p-3" side="top">
|
||||
<div className="min-w-[240px] space-y-2">
|
||||
<div className="flex items-start justify-between text-sm">
|
||||
<span>{usedPercent.toFixed(1)}%</span>
|
||||
|
|
@ -138,42 +139,43 @@ export const Context = ({ className, usage, ...props }: ContextProps) => {
|
|||
<div className="mt-1 space-y-1">
|
||||
{usage?.cachedInputTokens && usage.cachedInputTokens > 0 && (
|
||||
<InfoRow
|
||||
costText={usage?.costUSD?.cacheReadUSD?.toString()}
|
||||
label="Cache Hits"
|
||||
tokens={usage?.cachedInputTokens}
|
||||
costText={usage?.costUSD?.cacheReadUSD?.toString()}
|
||||
/>
|
||||
)}
|
||||
<InfoRow
|
||||
costText={usage?.costUSD?.inputUSD?.toString()}
|
||||
label="Input"
|
||||
tokens={usage?.inputTokens}
|
||||
costText={usage?.costUSD?.inputUSD?.toString()}
|
||||
/>
|
||||
<InfoRow
|
||||
costText={usage?.costUSD?.outputUSD?.toString()}
|
||||
label="Output"
|
||||
tokens={usage?.outputTokens}
|
||||
costText={usage?.costUSD?.outputUSD?.toString()}
|
||||
/>
|
||||
<InfoRow
|
||||
costText={usage?.costUSD?.reasoningUSD?.toString()}
|
||||
label="Reasoning"
|
||||
tokens={
|
||||
usage?.reasoningTokens && usage.reasoningTokens > 0
|
||||
? usage.reasoningTokens
|
||||
: undefined
|
||||
}
|
||||
costText={usage?.costUSD?.reasoningUSD?.toString()}
|
||||
/>
|
||||
{usage?.costUSD?.totalUSD !== undefined && (
|
||||
<>
|
||||
<Separator className="mt-1" />
|
||||
<div className="flex justify-between items-center pt-1 text-xs">
|
||||
<div className="flex items-center justify-between pt-1 text-xs">
|
||||
<span className="text-muted-foreground">Total cost</span>
|
||||
<div className="flex items-center gap-2 font-mono">
|
||||
<span className="text-right min-w-[4ch]"></span>
|
||||
<span className="min-w-[4ch] text-right" />
|
||||
<span>
|
||||
{!isNaN(parseFloat(usage.costUSD.totalUSD.toString()))
|
||||
? `$${parseFloat(usage.costUSD.totalUSD.toString()).toFixed(6)}`
|
||||
: '—'
|
||||
}
|
||||
{Number.isNaN(
|
||||
Number.parseFloat(usage.costUSD.totalUSD.toString())
|
||||
)
|
||||
? "—"
|
||||
: `$${Number.parseFloat(usage.costUSD.totalUSD.toString()).toFixed(6)}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ArrowDownIcon } from 'lucide-react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom';
|
||||
import { ArrowDownIcon } from "lucide-react";
|
||||
import type { ComponentProps } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
||||
|
||||
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
||||
<StickToBottom
|
||||
className={cn(
|
||||
'relative flex-1 touch-pan-y overflow-y-auto will-change-scroll',
|
||||
className,
|
||||
"relative flex-1 touch-pan-y overflow-y-auto will-change-scroll",
|
||||
className
|
||||
)}
|
||||
initial="smooth"
|
||||
resize="smooth"
|
||||
|
|
@ -30,7 +30,7 @@ export const ConversationContent = ({
|
|||
className,
|
||||
...props
|
||||
}: ConversationContentProps) => (
|
||||
<StickToBottom.Content className={cn('p-4', className)} {...props} />
|
||||
<StickToBottom.Content className={cn("p-4", className)} {...props} />
|
||||
);
|
||||
|
||||
export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
|
||||
|
|
@ -49,8 +49,8 @@ export const ConversationScrollButton = ({
|
|||
!isAtBottom && (
|
||||
<Button
|
||||
className={cn(
|
||||
'-translate-x-1/2 absolute bottom-4 left-1/2 z-10 rounded-full shadow-lg',
|
||||
className,
|
||||
"-translate-x-1/2 absolute bottom-4 left-1/2 z-10 rounded-full shadow-lg",
|
||||
className
|
||||
)}
|
||||
onClick={handleScrollToBottom}
|
||||
size="icon"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import type { Experimental_GeneratedImage } from 'ai';
|
||||
import type { Experimental_GeneratedImage } from "ai";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type ImageProps = Experimental_GeneratedImage & {
|
||||
className?: string;
|
||||
|
|
@ -12,13 +12,14 @@ export const Image = ({
|
|||
mediaType,
|
||||
...props
|
||||
}: ImageProps) => (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
// biome-ignore lint/nursery/useImageSize: "Generated image without explicit size"
|
||||
// biome-ignore lint/performance/noImgElement: "Generated image without explicit size"
|
||||
<img
|
||||
{...props}
|
||||
alt={props.alt}
|
||||
className={cn(
|
||||
'h-auto max-w-full overflow-hidden rounded-md',
|
||||
props.className,
|
||||
"h-auto max-w-full overflow-hidden rounded-md",
|
||||
props.className
|
||||
)}
|
||||
src={`data:${mediaType};base64,${base64}`}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,6 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
type CarouselApi,
|
||||
} from '@/components/ui/carousel';
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '@/components/ui/hover-card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react';
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
|
|
@ -21,28 +8,41 @@ import {
|
|||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
} from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Carousel,
|
||||
type CarouselApi,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
} from "@/components/ui/carousel";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type InlineCitationProps = ComponentProps<'span'>;
|
||||
export type InlineCitationProps = ComponentProps<"span">;
|
||||
|
||||
export const InlineCitation = ({
|
||||
className,
|
||||
...props
|
||||
}: InlineCitationProps) => (
|
||||
<span
|
||||
className={cn('group inline items-center gap-1', className)}
|
||||
className={cn("group inline items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type InlineCitationTextProps = ComponentProps<'span'>;
|
||||
export type InlineCitationTextProps = ComponentProps<"span">;
|
||||
|
||||
export const InlineCitationText = ({
|
||||
className,
|
||||
...props
|
||||
}: InlineCitationTextProps) => (
|
||||
<span
|
||||
className={cn('transition-colors group-hover:bg-accent', className)}
|
||||
className={cn("transition-colors group-hover:bg-accent", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -64,29 +64,29 @@ export const InlineCitationCardTrigger = ({
|
|||
}: InlineCitationCardTriggerProps) => (
|
||||
<HoverCardTrigger asChild>
|
||||
<Badge
|
||||
className={cn('ml-1 rounded-full', className)}
|
||||
className={cn("ml-1 rounded-full", className)}
|
||||
variant="secondary"
|
||||
{...props}
|
||||
>
|
||||
{sources.length ? (
|
||||
<>
|
||||
{new URL(sources[0]).hostname}{' '}
|
||||
{new URL(sources[0]).hostname}{" "}
|
||||
{sources.length > 1 && `+${sources.length - 1}`}
|
||||
</>
|
||||
) : (
|
||||
'unknown'
|
||||
"unknown"
|
||||
)}
|
||||
</Badge>
|
||||
</HoverCardTrigger>
|
||||
);
|
||||
|
||||
export type InlineCitationCardBodyProps = ComponentProps<'div'>;
|
||||
export type InlineCitationCardBodyProps = ComponentProps<"div">;
|
||||
|
||||
export const InlineCitationCardBody = ({
|
||||
className,
|
||||
...props
|
||||
}: InlineCitationCardBodyProps) => (
|
||||
<HoverCardContent className={cn('relative w-80 p-0', className)} {...props} />
|
||||
<HoverCardContent className={cn("relative w-80 p-0", className)} {...props} />
|
||||
);
|
||||
|
||||
const CarouselApiContext = createContext<CarouselApi | undefined>(undefined);
|
||||
|
|
@ -107,32 +107,32 @@ export const InlineCitationCarousel = ({
|
|||
|
||||
return (
|
||||
<CarouselApiContext.Provider value={api}>
|
||||
<Carousel className={cn('w-full', className)} setApi={setApi} {...props}>
|
||||
<Carousel className={cn("w-full", className)} setApi={setApi} {...props}>
|
||||
{children}
|
||||
</Carousel>
|
||||
</CarouselApiContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export type InlineCitationCarouselContentProps = ComponentProps<'div'>;
|
||||
export type InlineCitationCarouselContentProps = ComponentProps<"div">;
|
||||
|
||||
export const InlineCitationCarouselContent = (
|
||||
props: InlineCitationCarouselContentProps,
|
||||
props: InlineCitationCarouselContentProps
|
||||
) => <CarouselContent {...props} />;
|
||||
|
||||
export type InlineCitationCarouselItemProps = ComponentProps<'div'>;
|
||||
export type InlineCitationCarouselItemProps = ComponentProps<"div">;
|
||||
|
||||
export const InlineCitationCarouselItem = ({
|
||||
className,
|
||||
...props
|
||||
}: InlineCitationCarouselItemProps) => (
|
||||
<CarouselItem
|
||||
className={cn('w-full space-y-2 p-4 pl-8', className)}
|
||||
className={cn("w-full space-y-2 p-4 pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type InlineCitationCarouselHeaderProps = ComponentProps<'div'>;
|
||||
export type InlineCitationCarouselHeaderProps = ComponentProps<"div">;
|
||||
|
||||
export const InlineCitationCarouselHeader = ({
|
||||
className,
|
||||
|
|
@ -140,14 +140,14 @@ export const InlineCitationCarouselHeader = ({
|
|||
}: InlineCitationCarouselHeaderProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between gap-2 rounded-t-md bg-secondary p-2',
|
||||
className,
|
||||
"flex items-center justify-between gap-2 rounded-t-md bg-secondary p-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type InlineCitationCarouselIndexProps = ComponentProps<'div'>;
|
||||
export type InlineCitationCarouselIndexProps = ComponentProps<"div">;
|
||||
|
||||
export const InlineCitationCarouselIndex = ({
|
||||
children,
|
||||
|
|
@ -166,7 +166,7 @@ export const InlineCitationCarouselIndex = ({
|
|||
setCount(api.scrollSnapList().length);
|
||||
setCurrent(api.selectedScrollSnap() + 1);
|
||||
|
||||
api.on('select', () => {
|
||||
api.on("select", () => {
|
||||
setCurrent(api.selectedScrollSnap() + 1);
|
||||
});
|
||||
}, [api]);
|
||||
|
|
@ -174,8 +174,8 @@ export const InlineCitationCarouselIndex = ({
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-xs',
|
||||
className,
|
||||
"flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -184,7 +184,7 @@ export const InlineCitationCarouselIndex = ({
|
|||
);
|
||||
};
|
||||
|
||||
export type InlineCitationCarouselPrevProps = ComponentProps<'button'>;
|
||||
export type InlineCitationCarouselPrevProps = ComponentProps<"button">;
|
||||
|
||||
export const InlineCitationCarouselPrev = ({
|
||||
className,
|
||||
|
|
@ -201,7 +201,7 @@ export const InlineCitationCarouselPrev = ({
|
|||
return (
|
||||
<button
|
||||
aria-label="Previous"
|
||||
className={cn('shrink-0', className)}
|
||||
className={cn("shrink-0", className)}
|
||||
onClick={handleClick}
|
||||
type="button"
|
||||
{...props}
|
||||
|
|
@ -211,7 +211,7 @@ export const InlineCitationCarouselPrev = ({
|
|||
);
|
||||
};
|
||||
|
||||
export type InlineCitationCarouselNextProps = ComponentProps<'button'>;
|
||||
export type InlineCitationCarouselNextProps = ComponentProps<"button">;
|
||||
|
||||
export const InlineCitationCarouselNext = ({
|
||||
className,
|
||||
|
|
@ -228,7 +228,7 @@ export const InlineCitationCarouselNext = ({
|
|||
return (
|
||||
<button
|
||||
aria-label="Next"
|
||||
className={cn('shrink-0', className)}
|
||||
className={cn("shrink-0", className)}
|
||||
onClick={handleClick}
|
||||
type="button"
|
||||
{...props}
|
||||
|
|
@ -238,7 +238,7 @@ export const InlineCitationCarouselNext = ({
|
|||
);
|
||||
};
|
||||
|
||||
export type InlineCitationSourceProps = ComponentProps<'div'> & {
|
||||
export type InlineCitationSourceProps = ComponentProps<"div"> & {
|
||||
title?: string;
|
||||
url?: string;
|
||||
description?: string;
|
||||
|
|
@ -252,7 +252,7 @@ export const InlineCitationSource = ({
|
|||
children,
|
||||
...props
|
||||
}: InlineCitationSourceProps) => (
|
||||
<div className={cn('space-y-1', className)} {...props}>
|
||||
<div className={cn("space-y-1", className)} {...props}>
|
||||
{title && (
|
||||
<h4 className="truncate font-medium text-sm leading-tight">{title}</h4>
|
||||
)}
|
||||
|
|
@ -268,7 +268,7 @@ export const InlineCitationSource = ({
|
|||
</div>
|
||||
);
|
||||
|
||||
export type InlineCitationQuoteProps = ComponentProps<'blockquote'>;
|
||||
export type InlineCitationQuoteProps = ComponentProps<"blockquote">;
|
||||
|
||||
export const InlineCitationQuote = ({
|
||||
children,
|
||||
|
|
@ -277,8 +277,8 @@ export const InlineCitationQuote = ({
|
|||
}: InlineCitationQuoteProps) => (
|
||||
<blockquote
|
||||
className={cn(
|
||||
'border-muted border-l-2 pl-3 text-muted-foreground text-sm italic',
|
||||
className,
|
||||
"border-muted border-l-2 pl-3 text-muted-foreground text-sm italic",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import type { HTMLAttributes } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type LoaderIconProps = {
|
||||
size?: number;
|
||||
|
|
@ -9,7 +9,7 @@ const LoaderIcon = ({ size = 16 }: LoaderIconProps) => (
|
|||
<svg
|
||||
height={size}
|
||||
strokeLinejoin="round"
|
||||
style={{ color: 'currentcolor' }}
|
||||
style={{ color: "currentcolor" }}
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
>
|
||||
|
|
@ -86,8 +86,8 @@ export type LoaderProps = HTMLAttributes<HTMLDivElement> & {
|
|||
export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex animate-spin items-center justify-center',
|
||||
className,
|
||||
"inline-flex animate-spin items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { UIMessage } from 'ai';
|
||||
import type { ComponentProps, HTMLAttributes } from 'react';
|
||||
import type { UIMessage } from "ai";
|
||||
import type { ComponentProps, HTMLAttributes } from "react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
||||
from: UIMessage['role'];
|
||||
from: UIMessage["role"];
|
||||
};
|
||||
|
||||
export const Message = ({ className, from, ...props }: MessageProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
'group flex w-full items-end justify-end gap-2 py-4',
|
||||
from === 'user' ? 'is-user' : 'is-assistant flex-row-reverse justify-end',
|
||||
'[&>div]:max-w-[80%]',
|
||||
className,
|
||||
"group flex w-full items-end justify-end gap-2 py-4",
|
||||
from === "user" ? "is-user" : "is-assistant flex-row-reverse justify-end",
|
||||
"[&>div]:max-w-[80%]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
|
@ -28,11 +28,11 @@ export const MessageContent = ({
|
|||
}: MessageContentProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-2 overflow-hidden rounded-lg px-4 py-3 text-foreground text-sm',
|
||||
'group-[.is-user]:bg-primary group-[.is-user]:text-primary-foreground',
|
||||
'group-[.is-assistant]:bg-secondary group-[.is-assistant]:text-foreground',
|
||||
'is-user:dark',
|
||||
className,
|
||||
"flex flex-col gap-2 overflow-hidden rounded-lg px-4 py-3 text-foreground text-sm",
|
||||
"group-[.is-user]:bg-primary group-[.is-user]:text-primary-foreground",
|
||||
"group-[.is-assistant]:bg-secondary group-[.is-assistant]:text-foreground",
|
||||
"is-user:dark",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -51,8 +51,8 @@ export const MessageAvatar = ({
|
|||
className,
|
||||
...props
|
||||
}: MessageAvatarProps) => (
|
||||
<Avatar className={cn('size-8 ring-1 ring-border', className)} {...props}>
|
||||
<Avatar className={cn("size-8 ring-1 ring-border", className)} {...props}>
|
||||
<AvatarImage alt="" className="my-0" src={src} />
|
||||
<AvatarFallback>{name?.slice(0, 2) || 'ME'}</AvatarFallback>
|
||||
<AvatarFallback>{name?.slice(0, 2) || "ME"}</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { ChatStatus } from "ai";
|
||||
import { Loader2Icon, SendIcon, SquareIcon, XIcon } from "lucide-react";
|
||||
import type {
|
||||
ComponentProps,
|
||||
HTMLAttributes,
|
||||
KeyboardEventHandler,
|
||||
} from "react";
|
||||
import { Children } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ChatStatus } from 'ai';
|
||||
import { Loader2Icon, SendIcon, SquareIcon, XIcon } from 'lucide-react';
|
||||
import type {
|
||||
ComponentProps,
|
||||
HTMLAttributes,
|
||||
KeyboardEventHandler,
|
||||
} from 'react';
|
||||
import { Children } from 'react';
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type PromptInputProps = HTMLAttributes<HTMLFormElement>;
|
||||
|
||||
export const PromptInput = ({ className, ...props }: PromptInputProps) => (
|
||||
<form
|
||||
className={cn(
|
||||
'w-full overflow-hidden rounded-xl border bg-background shadow-xs',
|
||||
className,
|
||||
"w-full overflow-hidden rounded-xl border bg-background shadow-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
|
@ -41,7 +41,7 @@ export type PromptInputTextareaProps = ComponentProps<typeof Textarea> & {
|
|||
export const PromptInputTextarea = ({
|
||||
onChange,
|
||||
className,
|
||||
placeholder = 'What would you like to know?',
|
||||
placeholder = "What would you like to know?",
|
||||
minHeight = 48,
|
||||
maxHeight = 164,
|
||||
disableAutoResize = false,
|
||||
|
|
@ -49,7 +49,7 @@ export const PromptInputTextarea = ({
|
|||
...props
|
||||
}: PromptInputTextareaProps) => {
|
||||
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === "Enter") {
|
||||
// Don't submit if IME composition is in progress
|
||||
if (e.nativeEvent.isComposing) {
|
||||
return;
|
||||
|
|
@ -72,15 +72,15 @@ export const PromptInputTextarea = ({
|
|||
return (
|
||||
<Textarea
|
||||
className={cn(
|
||||
'w-full resize-none rounded-none border-none p-3 shadow-none outline-hidden ring-0',
|
||||
"w-full resize-none rounded-none border-none p-3 shadow-none outline-hidden ring-0",
|
||||
disableAutoResize
|
||||
? 'field-sizing-fixed'
|
||||
? "field-sizing-fixed"
|
||||
: resizeOnNewLinesOnly
|
||||
? 'field-sizing-fixed'
|
||||
: 'field-sizing-content max-h-[6lh]',
|
||||
'bg-transparent dark:bg-transparent',
|
||||
'focus-visible:ring-0',
|
||||
className,
|
||||
? "field-sizing-fixed"
|
||||
: "field-sizing-content max-h-[6lh]",
|
||||
"bg-transparent dark:bg-transparent",
|
||||
"focus-visible:ring-0",
|
||||
className
|
||||
)}
|
||||
name="message"
|
||||
onChange={(e) => {
|
||||
|
|
@ -100,7 +100,7 @@ export const PromptInputToolbar = ({
|
|||
...props
|
||||
}: PromptInputToolbarProps) => (
|
||||
<div
|
||||
className={cn('flex items-center justify-between p-1', className)}
|
||||
className={cn("flex items-center justify-between p-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -113,9 +113,9 @@ export const PromptInputTools = ({
|
|||
}: PromptInputToolsProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1',
|
||||
'[&_button:first-child]:rounded-bl-xl',
|
||||
className,
|
||||
"flex items-center gap-1",
|
||||
"[&_button:first-child]:rounded-bl-xl",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
|
@ -124,21 +124,21 @@ export const PromptInputTools = ({
|
|||
export type PromptInputButtonProps = ComponentProps<typeof Button>;
|
||||
|
||||
export const PromptInputButton = ({
|
||||
variant = 'ghost',
|
||||
variant = "ghost",
|
||||
className,
|
||||
size,
|
||||
...props
|
||||
}: PromptInputButtonProps) => {
|
||||
const newSize =
|
||||
(size ?? Children.count(props.children) > 1) ? 'default' : 'icon';
|
||||
(size ?? Children.count(props.children) > 1) ? "default" : "icon";
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
'shrink-0 gap-1.5 rounded-lg',
|
||||
variant === 'ghost' && 'text-muted-foreground',
|
||||
newSize === 'default' && 'px-3',
|
||||
className,
|
||||
"shrink-0 gap-1.5 rounded-lg",
|
||||
variant === "ghost" && "text-muted-foreground",
|
||||
newSize === "default" && "px-3",
|
||||
className
|
||||
)}
|
||||
size={newSize}
|
||||
type="button"
|
||||
|
|
@ -154,25 +154,25 @@ export type PromptInputSubmitProps = ComponentProps<typeof Button> & {
|
|||
|
||||
export const PromptInputSubmit = ({
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'icon',
|
||||
variant = "default",
|
||||
size = "icon",
|
||||
status,
|
||||
children,
|
||||
...props
|
||||
}: PromptInputSubmitProps) => {
|
||||
let Icon = <SendIcon className="size-4" />;
|
||||
|
||||
if (status === 'submitted') {
|
||||
if (status === "submitted") {
|
||||
Icon = <Loader2Icon className="size-4 animate-spin" />;
|
||||
} else if (status === 'streaming') {
|
||||
} else if (status === "streaming") {
|
||||
Icon = <SquareIcon className="size-4" />;
|
||||
} else if (status === 'error') {
|
||||
} else if (status === "error") {
|
||||
Icon = <XIcon className="size-4" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={cn('gap-1.5 rounded-lg', className)}
|
||||
className={cn("gap-1.5 rounded-lg", className)}
|
||||
size={size}
|
||||
type="submit"
|
||||
variant={variant}
|
||||
|
|
@ -199,10 +199,10 @@ export const PromptInputModelSelectTrigger = ({
|
|||
}: PromptInputModelSelectTriggerProps) => (
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
'border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors',
|
||||
'hover:bg-accent hover:text-foreground aria-expanded:bg-accent aria-expanded:text-foreground',
|
||||
'h-auto px-2 py-1.5',
|
||||
className,
|
||||
"border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors",
|
||||
"hover:bg-accent hover:text-foreground aria-expanded:bg-accent aria-expanded:text-foreground",
|
||||
"h-auto px-2 py-1.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
||||
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
||||
import { BrainIcon, ChevronDownIcon } from "lucide-react";
|
||||
import type { ComponentProps } from "react";
|
||||
import { createContext, memo, useContext, useEffect, useState } from "react";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { BrainIcon, ChevronDownIcon } from 'lucide-react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { createContext, memo, useContext, useEffect, useState } from 'react';
|
||||
import { Response } from './response';
|
||||
} from "@/components/ui/collapsible";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Response } from "./response";
|
||||
|
||||
type ReasoningContextValue = {
|
||||
isStreaming: boolean;
|
||||
|
|
@ -24,7 +24,7 @@ const ReasoningContext = createContext<ReasoningContextValue | null>(null);
|
|||
const useReasoning = () => {
|
||||
const context = useContext(ReasoningContext);
|
||||
if (!context) {
|
||||
throw new Error('Reasoning components must be used within Reasoning');
|
||||
throw new Error("Reasoning components must be used within Reasoning");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
|
@ -98,7 +98,7 @@ export const Reasoning = memo(
|
|||
value={{ isStreaming, isOpen, setIsOpen, duration }}
|
||||
>
|
||||
<Collapsible
|
||||
className={cn('not-prose', className)}
|
||||
className={cn("not-prose", className)}
|
||||
onOpenChange={handleOpenChange}
|
||||
open={isOpen}
|
||||
{...props}
|
||||
|
|
@ -107,7 +107,7 @@ export const Reasoning = memo(
|
|||
</Collapsible>
|
||||
</ReasoningContext.Provider>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
|
||||
|
|
@ -119,8 +119,8 @@ export const ReasoningTrigger = memo(
|
|||
return (
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 text-muted-foreground text-xs transition-colors hover:text-foreground',
|
||||
className,
|
||||
"flex items-center gap-1.5 text-muted-foreground text-xs transition-colors hover:text-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -134,15 +134,15 @@ export const ReasoningTrigger = memo(
|
|||
)}
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
'size-3 text-muted-foreground transition-transform',
|
||||
isOpen ? 'rotate-180' : 'rotate-0',
|
||||
"size-3 text-muted-foreground transition-transform",
|
||||
isOpen ? "rotate-180" : "rotate-0"
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CollapsibleTrigger>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export type ReasoningContentProps = ComponentProps<
|
||||
|
|
@ -155,17 +155,17 @@ export const ReasoningContent = memo(
|
|||
({ className, children, ...props }: ReasoningContentProps) => (
|
||||
<CollapsibleContent
|
||||
className={cn(
|
||||
'mt-2 text-muted-foreground text-xs',
|
||||
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||
className,
|
||||
"mt-2 text-muted-foreground text-xs",
|
||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Response className="grid gap-2">{children}</Response>
|
||||
</CollapsibleContent>
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
Reasoning.displayName = 'Reasoning';
|
||||
ReasoningTrigger.displayName = 'ReasoningTrigger';
|
||||
ReasoningContent.displayName = 'ReasoningContent';
|
||||
Reasoning.displayName = "Reasoning";
|
||||
ReasoningTrigger.displayName = "ReasoningTrigger";
|
||||
ReasoningContent.displayName = "ReasoningContent";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type ComponentProps, memo } from 'react';
|
||||
import { Streamdown } from 'streamdown';
|
||||
import { type ComponentProps, memo } from "react";
|
||||
import { Streamdown } from "streamdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ResponseProps = ComponentProps<typeof Streamdown>;
|
||||
|
||||
|
|
@ -10,13 +10,13 @@ export const Response = memo(
|
|||
({ className, ...props }: ResponseProps) => (
|
||||
<Streamdown
|
||||
className={cn(
|
||||
'size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_code]:whitespace-pre-wrap [&_code]:break-words [&_pre]:max-w-full [&_pre]:overflow-x-auto',
|
||||
className,
|
||||
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_code]:whitespace-pre-wrap [&_code]:break-words [&_pre]:max-w-full [&_pre]:overflow-x-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
(prevProps, nextProps) => prevProps.children === nextProps.children,
|
||||
(prevProps, nextProps) => prevProps.children === nextProps.children
|
||||
);
|
||||
|
||||
Response.displayName = 'Response';
|
||||
Response.displayName = "Response";
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { BookIcon, ChevronDownIcon } from "lucide-react";
|
||||
import type { ComponentProps } from "react";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { BookIcon, ChevronDownIcon } from 'lucide-react';
|
||||
import type { ComponentProps } from 'react';
|
||||
} from "@/components/ui/collapsible";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type SourcesProps = ComponentProps<'div'>;
|
||||
export type SourcesProps = ComponentProps<"div">;
|
||||
|
||||
export const Sources = ({ className, ...props }: SourcesProps) => (
|
||||
<Collapsible
|
||||
className={cn('not-prose mb-4 text-primary text-xs', className)}
|
||||
className={cn("not-prose mb-4 text-primary text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -46,15 +46,15 @@ export const SourcesContent = ({
|
|||
}: SourcesContentProps) => (
|
||||
<CollapsibleContent
|
||||
className={cn(
|
||||
'mt-3 flex w-fit flex-col gap-2',
|
||||
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||
className,
|
||||
"mt-3 flex w-fit flex-col gap-2",
|
||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type SourceProps = ComponentProps<'a'>;
|
||||
export type SourceProps = ComponentProps<"a">;
|
||||
|
||||
export const Source = ({ href, title, children, ...props }: SourceProps) => (
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ComponentProps } from 'react';
|
||||
import type { ComponentProps } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
|
||||
|
||||
|
|
@ -13,14 +13,14 @@ export const Suggestions = ({
|
|||
...props
|
||||
}: SuggestionsProps) => (
|
||||
<ScrollArea className="w-full overflow-x-auto whitespace-nowrap" {...props}>
|
||||
<div className={cn('flex w-max flex-nowrap items-center gap-2', className)}>
|
||||
<div className={cn("flex w-max flex-nowrap items-center gap-2", className)}>
|
||||
{children}
|
||||
</div>
|
||||
<ScrollBar className="hidden" orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
);
|
||||
|
||||
export type SuggestionProps = Omit<ComponentProps<typeof Button>, 'onClick'> & {
|
||||
export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
|
||||
suggestion: string;
|
||||
onClick?: (suggestion: string) => void;
|
||||
};
|
||||
|
|
@ -29,8 +29,8 @@ export const Suggestion = ({
|
|||
suggestion,
|
||||
onClick,
|
||||
className,
|
||||
variant = 'outline',
|
||||
size = 'sm',
|
||||
variant = "outline",
|
||||
size = "sm",
|
||||
children,
|
||||
...props
|
||||
}: SuggestionProps) => {
|
||||
|
|
@ -40,7 +40,7 @@ export const Suggestion = ({
|
|||
|
||||
return (
|
||||
<Button
|
||||
className={cn('cursor-pointer rounded-full px-4', className)}
|
||||
className={cn("cursor-pointer rounded-full px-4", className)}
|
||||
onClick={handleClick}
|
||||
size={size}
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { ChevronDownIcon, SearchIcon } from "lucide-react";
|
||||
import type { ComponentProps } from "react";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ChevronDownIcon, SearchIcon } from 'lucide-react';
|
||||
import type { ComponentProps } from 'react';
|
||||
} from "@/components/ui/collapsible";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type TaskItemFileProps = ComponentProps<'div'>;
|
||||
export type TaskItemFileProps = ComponentProps<"div">;
|
||||
|
||||
export const TaskItemFile = ({
|
||||
children,
|
||||
|
|
@ -18,8 +18,8 @@ export const TaskItemFile = ({
|
|||
}: TaskItemFileProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-foreground text-xs',
|
||||
className,
|
||||
"inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-foreground text-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -27,10 +27,10 @@ export const TaskItemFile = ({
|
|||
</div>
|
||||
);
|
||||
|
||||
export type TaskItemProps = ComponentProps<'div'>;
|
||||
export type TaskItemProps = ComponentProps<"div">;
|
||||
|
||||
export const TaskItem = ({ children, className, ...props }: TaskItemProps) => (
|
||||
<div className={cn('text-muted-foreground text-sm', className)} {...props}>
|
||||
<div className={cn("text-muted-foreground text-sm", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -44,8 +44,8 @@ export const Task = ({
|
|||
}: TaskProps) => (
|
||||
<Collapsible
|
||||
className={cn(
|
||||
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||
className,
|
||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className
|
||||
)}
|
||||
defaultOpen={defaultOpen}
|
||||
{...props}
|
||||
|
|
@ -62,7 +62,7 @@ export const TaskTrigger = ({
|
|||
title,
|
||||
...props
|
||||
}: TaskTriggerProps) => (
|
||||
<CollapsibleTrigger asChild className={cn('group', className)} {...props}>
|
||||
<CollapsibleTrigger asChild className={cn("group", className)} {...props}>
|
||||
{children ?? (
|
||||
<div className="flex cursor-pointer items-center gap-2 text-muted-foreground hover:text-foreground">
|
||||
<SearchIcon className="size-4" />
|
||||
|
|
@ -82,8 +82,8 @@ export const TaskContent = ({
|
|||
}: TaskContentProps) => (
|
||||
<CollapsibleContent
|
||||
className={cn(
|
||||
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||
className,
|
||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,6 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ToolUIPart } from 'ai';
|
||||
import type { ToolUIPart } from "ai";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ChevronDownIcon,
|
||||
|
|
@ -15,38 +8,45 @@ import {
|
|||
ClockIcon,
|
||||
WrenchIcon,
|
||||
XCircleIcon,
|
||||
} from 'lucide-react';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { CodeBlock } from './code-block';
|
||||
} from "lucide-react";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CodeBlock } from "./code-block";
|
||||
|
||||
export type ToolProps = ComponentProps<typeof Collapsible>;
|
||||
|
||||
export const Tool = ({ className, ...props }: ToolProps) => (
|
||||
<Collapsible
|
||||
className={cn('not-prose mb-4 w-full rounded-md border', className)}
|
||||
className={cn("not-prose mb-4 w-full rounded-md border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type ToolHeaderProps = {
|
||||
type: ToolUIPart['type'];
|
||||
state: ToolUIPart['state'];
|
||||
type: ToolUIPart["type"];
|
||||
state: ToolUIPart["state"];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: ToolUIPart['state']) => {
|
||||
const getStatusBadge = (status: ToolUIPart["state"]) => {
|
||||
const labels = {
|
||||
'input-streaming': 'Pending',
|
||||
'input-available': 'Running',
|
||||
'output-available': 'Completed',
|
||||
'output-error': 'Error',
|
||||
"input-streaming": "Pending",
|
||||
"input-available": "Running",
|
||||
"output-available": "Completed",
|
||||
"output-error": "Error",
|
||||
} as const;
|
||||
|
||||
const icons = {
|
||||
'input-streaming': <CircleIcon className="size-4" />,
|
||||
'input-available': <ClockIcon className="size-4 animate-pulse" />,
|
||||
'output-available': <CheckCircleIcon className="size-4 text-green-600" />,
|
||||
'output-error': <XCircleIcon className="size-4 text-red-600" />,
|
||||
"input-streaming": <CircleIcon className="size-4" />,
|
||||
"input-available": <ClockIcon className="size-4 animate-pulse" />,
|
||||
"output-available": <CheckCircleIcon className="size-4 text-green-600" />,
|
||||
"output-error": <XCircleIcon className="size-4 text-red-600" />,
|
||||
} as const;
|
||||
|
||||
return (
|
||||
|
|
@ -68,8 +68,8 @@ export const ToolHeader = ({
|
|||
}: ToolHeaderProps) => (
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
'flex w-full min-w-0 items-center justify-between gap-2 p-3',
|
||||
className,
|
||||
"flex w-full min-w-0 items-center justify-between gap-2 p-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -89,19 +89,19 @@ export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
|
|||
export const ToolContent = ({ className, ...props }: ToolContentProps) => (
|
||||
<CollapsibleContent
|
||||
className={cn(
|
||||
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||
className,
|
||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type ToolInputProps = ComponentProps<'div'> & {
|
||||
input: ToolUIPart['input'];
|
||||
export type ToolInputProps = ComponentProps<"div"> & {
|
||||
input: ToolUIPart["input"];
|
||||
};
|
||||
|
||||
export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
|
||||
<div className={cn('space-y-2 overflow-hidden p-4', className)} {...props}>
|
||||
<div className={cn("space-y-2 overflow-hidden p-4", className)} {...props}>
|
||||
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||
Parameters
|
||||
</h4>
|
||||
|
|
@ -111,9 +111,9 @@ export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
|
|||
</div>
|
||||
);
|
||||
|
||||
export type ToolOutputProps = ComponentProps<'div'> & {
|
||||
export type ToolOutputProps = ComponentProps<"div"> & {
|
||||
output: ReactNode;
|
||||
errorText: ToolUIPart['errorText'];
|
||||
errorText: ToolUIPart["errorText"];
|
||||
};
|
||||
|
||||
export const ToolOutput = ({
|
||||
|
|
@ -127,16 +127,16 @@ export const ToolOutput = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-2 p-4', className)} {...props}>
|
||||
<div className={cn("space-y-2 p-4", className)} {...props}>
|
||||
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||
{errorText ? 'Error' : 'Result'}
|
||||
{errorText ? "Error" : "Result"}
|
||||
</h4>
|
||||
<div
|
||||
className={cn(
|
||||
'overflow-x-auto rounded-md text-xs [&_table]:w-full',
|
||||
"overflow-x-auto rounded-md text-xs [&_table]:w-full",
|
||||
errorText
|
||||
? 'bg-destructive/10 text-destructive'
|
||||
: 'bg-muted/50 text-foreground',
|
||||
? "bg-destructive/10 text-destructive"
|
||||
: "bg-muted/50 text-foreground"
|
||||
)}
|
||||
>
|
||||
{errorText && <div>{errorText}</div>}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { createContext, useContext, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import { Input } from '@/components/ui/input';
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type WebPreviewContextValue = {
|
||||
url: string;
|
||||
|
|
@ -30,12 +30,12 @@ const WebPreviewContext = createContext<WebPreviewContextValue | null>(null);
|
|||
const useWebPreview = () => {
|
||||
const context = useContext(WebPreviewContext);
|
||||
if (!context) {
|
||||
throw new Error('WebPreview components must be used within a WebPreview');
|
||||
throw new Error("WebPreview components must be used within a WebPreview");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export type WebPreviewProps = ComponentProps<'div'> & {
|
||||
export type WebPreviewProps = ComponentProps<"div"> & {
|
||||
defaultUrl?: string;
|
||||
onUrlChange?: (url: string) => void;
|
||||
};
|
||||
|
|
@ -43,7 +43,7 @@ export type WebPreviewProps = ComponentProps<'div'> & {
|
|||
export const WebPreview = ({
|
||||
className,
|
||||
children,
|
||||
defaultUrl = '',
|
||||
defaultUrl = "",
|
||||
onUrlChange,
|
||||
...props
|
||||
}: WebPreviewProps) => {
|
||||
|
|
@ -66,8 +66,8 @@ export const WebPreview = ({
|
|||
<WebPreviewContext.Provider value={contextValue}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex size-full flex-col rounded-lg border bg-card',
|
||||
className,
|
||||
"flex size-full flex-col rounded-lg border bg-card",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -77,7 +77,7 @@ export const WebPreview = ({
|
|||
);
|
||||
};
|
||||
|
||||
export type WebPreviewNavigationProps = ComponentProps<'div'>;
|
||||
export type WebPreviewNavigationProps = ComponentProps<"div">;
|
||||
|
||||
export const WebPreviewNavigation = ({
|
||||
className,
|
||||
|
|
@ -85,7 +85,7 @@ export const WebPreviewNavigation = ({
|
|||
...props
|
||||
}: WebPreviewNavigationProps) => (
|
||||
<div
|
||||
className={cn('flex items-center gap-1 border-b p-2', className)}
|
||||
className={cn("flex items-center gap-1 border-b p-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
|
@ -135,7 +135,7 @@ export const WebPreviewUrl = ({
|
|||
const { url, setUrl } = useWebPreview();
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === "Enter") {
|
||||
const target = event.target as HTMLInputElement;
|
||||
setUrl(target.value);
|
||||
}
|
||||
|
|
@ -154,7 +154,7 @@ export const WebPreviewUrl = ({
|
|||
);
|
||||
};
|
||||
|
||||
export type WebPreviewBodyProps = ComponentProps<'iframe'> & {
|
||||
export type WebPreviewBodyProps = ComponentProps<"iframe"> & {
|
||||
loading?: ReactNode;
|
||||
};
|
||||
|
||||
|
|
@ -169,7 +169,7 @@ export const WebPreviewBody = ({
|
|||
return (
|
||||
<div className="flex-1">
|
||||
<iframe
|
||||
className={cn('size-full', className)}
|
||||
className={cn("size-full", className)}
|
||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-presentation"
|
||||
src={(src ?? url) || undefined}
|
||||
title="Preview"
|
||||
|
|
@ -180,9 +180,9 @@ export const WebPreviewBody = ({
|
|||
);
|
||||
};
|
||||
|
||||
export type WebPreviewConsoleProps = ComponentProps<'div'> & {
|
||||
export type WebPreviewConsoleProps = ComponentProps<"div"> & {
|
||||
logs?: Array<{
|
||||
level: 'log' | 'warn' | 'error';
|
||||
level: "log" | "warn" | "error";
|
||||
message: string;
|
||||
timestamp: Date;
|
||||
}>;
|
||||
|
|
@ -198,7 +198,7 @@ export const WebPreviewConsole = ({
|
|||
|
||||
return (
|
||||
<Collapsible
|
||||
className={cn('border-t bg-muted/50 font-mono text-sm', className)}
|
||||
className={cn("border-t bg-muted/50 font-mono text-sm", className)}
|
||||
onOpenChange={setConsoleOpen}
|
||||
open={consoleOpen}
|
||||
{...props}
|
||||
|
|
@ -211,16 +211,16 @@ export const WebPreviewConsole = ({
|
|||
Console
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
'h-4 w-4 transition-transform duration-200',
|
||||
consoleOpen && 'rotate-180',
|
||||
"h-4 w-4 transition-transform duration-200",
|
||||
consoleOpen && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent
|
||||
className={cn(
|
||||
'px-4 pb-4',
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||
"px-4 pb-4",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in"
|
||||
)}
|
||||
>
|
||||
<div className="max-h-48 space-y-1 overflow-y-auto">
|
||||
|
|
@ -230,16 +230,16 @@ export const WebPreviewConsole = ({
|
|||
logs.map((log, index) => (
|
||||
<div
|
||||
className={cn(
|
||||
'text-xs',
|
||||
log.level === 'error' && 'text-destructive',
|
||||
log.level === 'warn' && 'text-yellow-600',
|
||||
log.level === 'log' && 'text-foreground',
|
||||
"text-xs",
|
||||
log.level === "error" && "text-destructive",
|
||||
log.level === "warn" && "text-yellow-600",
|
||||
log.level === "log" && "text-foreground"
|
||||
)}
|
||||
key={`${log.timestamp.getTime()}-${index}`}
|
||||
>
|
||||
<span className="text-muted-foreground">
|
||||
{log.timestamp.toLocaleTimeString()}
|
||||
</span>{' '}
|
||||
</span>{" "}
|
||||
{log.message}
|
||||
</div>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export const Greeting = () => {
|
||||
return (
|
||||
<div
|
||||
key="overview"
|
||||
className="mx-auto mt-4 flex size-full max-w-3xl flex-col justify-center px-4 md:mt-16 md:px-8"
|
||||
key="overview"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="font-semibold text-xl md:text-2xl"
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
>
|
||||
Hello there!
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
className="text-xl text-zinc-500 md:text-2xl"
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
>
|
||||
How can I help you today?
|
||||
</motion.div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,14 @@
|
|||
import { LoaderIcon } from './icons';
|
||||
import cn from 'classnames';
|
||||
import cn from "classnames";
|
||||
import { LoaderIcon } from "./icons";
|
||||
|
||||
interface ImageEditorProps {
|
||||
type ImageEditorProps = {
|
||||
title: string;
|
||||
content: string;
|
||||
isCurrentVersion: boolean;
|
||||
currentVersionIndex: number;
|
||||
status: string;
|
||||
isInline: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export function ImageEditor({
|
||||
title,
|
||||
|
|
@ -18,12 +18,12 @@ export function ImageEditor({
|
|||
}: ImageEditorProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn('flex w-full flex-row items-center justify-center', {
|
||||
'h-[calc(100dvh-60px)]': !isInline,
|
||||
'h-[200px]': isInline,
|
||||
className={cn("flex w-full flex-row items-center justify-center", {
|
||||
"h-[calc(100dvh-60px)]": !isInline,
|
||||
"h-[200px]": isInline,
|
||||
})}
|
||||
>
|
||||
{status === 'streaming' ? (
|
||||
{status === "streaming" ? (
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
{!isInline && (
|
||||
<div className="animate-spin">
|
||||
|
|
@ -34,12 +34,13 @@ export function ImageEditor({
|
|||
</div>
|
||||
) : (
|
||||
<picture>
|
||||
{/** biome-ignore lint/nursery/useImageSize: "Generated image without explicit size" */}
|
||||
<img
|
||||
className={cn('h-fit w-full max-w-[800px]', {
|
||||
'p-0 md:p-20': !isInline,
|
||||
alt={title}
|
||||
className={cn("h-fit w-full max-w-[800px]", {
|
||||
"p-0 md:p-20": !isInline,
|
||||
})}
|
||||
src={`data:image/png;base64,${content}`}
|
||||
alt={title}
|
||||
/>
|
||||
</picture>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import { useSWRConfig } from 'swr';
|
||||
import { useCopyToClipboard } from 'usehooks-ts';
|
||||
|
||||
import type { Vote } from '@/lib/db/schema';
|
||||
|
||||
import { CopyIcon, ThumbDownIcon, ThumbUpIcon, PencilEditIcon } from './icons';
|
||||
import { Actions, Action } from './elements/actions';
|
||||
import { memo } from 'react';
|
||||
import equal from 'fast-deep-equal';
|
||||
import { toast } from 'sonner';
|
||||
import type { ChatMessage } from '@/lib/types';
|
||||
import equal from "fast-deep-equal";
|
||||
import { memo } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useCopyToClipboard } from "usehooks-ts";
|
||||
import type { Vote } from "@/lib/db/schema";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import { Action, Actions } from "./elements/actions";
|
||||
import { CopyIcon, PencilEditIcon, ThumbDownIcon, ThumbUpIcon } from "./icons";
|
||||
|
||||
export function PureMessageActions({
|
||||
chatId,
|
||||
|
|
@ -21,17 +19,19 @@ export function PureMessageActions({
|
|||
message: ChatMessage;
|
||||
vote: Vote | undefined;
|
||||
isLoading: boolean;
|
||||
setMode?: (mode: 'view' | 'edit') => void;
|
||||
setMode?: (mode: "view" | "edit") => void;
|
||||
}) {
|
||||
const { mutate } = useSWRConfig();
|
||||
const [_, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
if (isLoading) return null;
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const textFromParts = message.parts
|
||||
?.filter((part) => part.type === 'text')
|
||||
?.filter((part) => part.type === "text")
|
||||
.map((part) => part.text)
|
||||
.join('\n')
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
||||
const handleCopy = async () => {
|
||||
|
|
@ -41,24 +41,24 @@ export function PureMessageActions({
|
|||
}
|
||||
|
||||
await copyToClipboard(textFromParts);
|
||||
toast.success('Copied to clipboard!');
|
||||
toast.success("Copied to clipboard!");
|
||||
};
|
||||
|
||||
// User messages get edit (on hover) and copy actions
|
||||
if (message.role === 'user') {
|
||||
if (message.role === "user") {
|
||||
return (
|
||||
<Actions className="-mr-0.5 justify-end">
|
||||
<div className="relative">
|
||||
{setMode && (
|
||||
<Action
|
||||
tooltip="Edit"
|
||||
onClick={() => setMode('edit')}
|
||||
className="-left-10 absolute top-0 opacity-0 transition-opacity group-hover/message:opacity-100"
|
||||
onClick={() => setMode("edit")}
|
||||
tooltip="Edit"
|
||||
>
|
||||
<PencilEditIcon />
|
||||
</Action>
|
||||
)}
|
||||
<Action tooltip="Copy" onClick={handleCopy}>
|
||||
<Action onClick={handleCopy} tooltip="Copy">
|
||||
<CopyIcon />
|
||||
</Action>
|
||||
</div>
|
||||
|
|
@ -68,34 +68,35 @@ export function PureMessageActions({
|
|||
|
||||
return (
|
||||
<Actions className="-ml-0.5">
|
||||
<Action tooltip="Copy" onClick={handleCopy}>
|
||||
<Action onClick={handleCopy} tooltip="Copy">
|
||||
<CopyIcon />
|
||||
</Action>
|
||||
|
||||
<Action
|
||||
tooltip="Upvote Response"
|
||||
data-testid="message-upvote"
|
||||
disabled={vote?.isUpvoted}
|
||||
onClick={async () => {
|
||||
const upvote = fetch('/api/vote', {
|
||||
method: 'PATCH',
|
||||
onClick={() => {
|
||||
const upvote = fetch("/api/vote", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
type: 'up',
|
||||
type: "up",
|
||||
}),
|
||||
});
|
||||
|
||||
toast.promise(upvote, {
|
||||
loading: 'Upvoting Response...',
|
||||
loading: "Upvoting Response...",
|
||||
success: () => {
|
||||
mutate<Array<Vote>>(
|
||||
mutate<Vote[]>(
|
||||
`/api/vote?chatId=${chatId}`,
|
||||
(currentVotes) => {
|
||||
if (!currentVotes) return [];
|
||||
if (!currentVotes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const votesWithoutCurrent = currentVotes.filter(
|
||||
(vote) => vote.messageId !== message.id,
|
||||
(currentVote) => currentVote.messageId !== message.id
|
||||
);
|
||||
|
||||
return [
|
||||
|
|
@ -107,42 +108,44 @@ export function PureMessageActions({
|
|||
},
|
||||
];
|
||||
},
|
||||
{ revalidate: false },
|
||||
{ revalidate: false }
|
||||
);
|
||||
|
||||
return 'Upvoted Response!';
|
||||
return "Upvoted Response!";
|
||||
},
|
||||
error: 'Failed to upvote response.',
|
||||
error: "Failed to upvote response.",
|
||||
});
|
||||
}}
|
||||
tooltip="Upvote Response"
|
||||
>
|
||||
<ThumbUpIcon />
|
||||
</Action>
|
||||
|
||||
<Action
|
||||
tooltip="Downvote Response"
|
||||
data-testid="message-downvote"
|
||||
disabled={vote && !vote.isUpvoted}
|
||||
onClick={async () => {
|
||||
const downvote = fetch('/api/vote', {
|
||||
method: 'PATCH',
|
||||
onClick={() => {
|
||||
const downvote = fetch("/api/vote", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
type: 'down',
|
||||
type: "down",
|
||||
}),
|
||||
});
|
||||
|
||||
toast.promise(downvote, {
|
||||
loading: 'Downvoting Response...',
|
||||
loading: "Downvoting Response...",
|
||||
success: () => {
|
||||
mutate<Array<Vote>>(
|
||||
mutate<Vote[]>(
|
||||
`/api/vote?chatId=${chatId}`,
|
||||
(currentVotes) => {
|
||||
if (!currentVotes) return [];
|
||||
if (!currentVotes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const votesWithoutCurrent = currentVotes.filter(
|
||||
(vote) => vote.messageId !== message.id,
|
||||
(currentVote) => currentVote.messageId !== message.id
|
||||
);
|
||||
|
||||
return [
|
||||
|
|
@ -154,14 +157,15 @@ export function PureMessageActions({
|
|||
},
|
||||
];
|
||||
},
|
||||
{ revalidate: false },
|
||||
{ revalidate: false }
|
||||
);
|
||||
|
||||
return 'Downvoted Response!';
|
||||
return "Downvoted Response!";
|
||||
},
|
||||
error: 'Failed to downvote response.',
|
||||
error: "Failed to downvote response.",
|
||||
});
|
||||
}}
|
||||
tooltip="Downvote Response"
|
||||
>
|
||||
<ThumbDownIcon />
|
||||
</Action>
|
||||
|
|
@ -172,9 +176,13 @@ export function PureMessageActions({
|
|||
export const MessageActions = memo(
|
||||
PureMessageActions,
|
||||
(prevProps, nextProps) => {
|
||||
if (!equal(prevProps.vote, nextProps.vote)) return false;
|
||||
if (prevProps.isLoading !== nextProps.isLoading) return false;
|
||||
if (!equal(prevProps.vote, nextProps.vote)) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.isLoading !== nextProps.isLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { Button } from './ui/button';
|
||||
import type { UseChatHelpers } from "@ai-sdk/react";
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Textarea } from './ui/textarea';
|
||||
import { deleteTrailingMessages } from '@/app/(chat)/actions';
|
||||
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||
import type { ChatMessage } from '@/lib/types';
|
||||
import { getTextFromMessage } from '@/lib/utils';
|
||||
} from "react";
|
||||
import { deleteTrailingMessages } from "@/app/(chat)/actions";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import { getTextFromMessage } from "@/lib/utils";
|
||||
import { Button } from "./ui/button";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
|
||||
export type MessageEditorProps = {
|
||||
message: ChatMessage;
|
||||
setMode: Dispatch<SetStateAction<'view' | 'edit'>>;
|
||||
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
|
||||
regenerate: UseChatHelpers<ChatMessage>['regenerate'];
|
||||
setMode: Dispatch<SetStateAction<"view" | "edit">>;
|
||||
setMessages: UseChatHelpers<ChatMessage>["setMessages"];
|
||||
regenerate: UseChatHelpers<ChatMessage>["regenerate"];
|
||||
};
|
||||
|
||||
export function MessageEditor({
|
||||
|
|
@ -30,22 +31,22 @@ export function MessageEditor({
|
|||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
|
||||
const [draftContent, setDraftContent] = useState<string>(
|
||||
getTextFromMessage(message),
|
||||
getTextFromMessage(message)
|
||||
);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const adjustHeight = useCallback(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = "auto";
|
||||
textareaRef.current.style.height = `${textareaRef.current.scrollHeight + 2}px`;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
adjustHeight();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const adjustHeight = () => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto';
|
||||
textareaRef.current.style.height = `${textareaRef.current.scrollHeight + 2}px`;
|
||||
}
|
||||
};
|
||||
}, [adjustHeight]);
|
||||
|
||||
const handleInput = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setDraftContent(event.target.value);
|
||||
|
|
@ -55,27 +56,26 @@ export function MessageEditor({
|
|||
return (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<Textarea
|
||||
data-testid="message-editor"
|
||||
ref={textareaRef}
|
||||
className="w-full resize-none overflow-hidden rounded-xl bg-transparent text-base! outline-hidden"
|
||||
value={draftContent}
|
||||
data-testid="message-editor"
|
||||
onChange={handleInput}
|
||||
ref={textareaRef}
|
||||
value={draftContent}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-fit px-3 py-2"
|
||||
onClick={() => {
|
||||
setMode('view');
|
||||
setMode("view");
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="message-editor-send-button"
|
||||
variant="default"
|
||||
className="h-fit px-3 py-2"
|
||||
data-testid="message-editor-send-button"
|
||||
disabled={isSubmitting}
|
||||
onClick={async () => {
|
||||
setIsSubmitting(true);
|
||||
|
|
@ -90,7 +90,7 @@ export function MessageEditor({
|
|||
if (index !== -1) {
|
||||
const updatedMessage: ChatMessage = {
|
||||
...message,
|
||||
parts: [{ type: 'text', text: draftContent }],
|
||||
parts: [{ type: "text", text: draftContent }],
|
||||
};
|
||||
|
||||
return [...messages.slice(0, index), updatedMessage];
|
||||
|
|
@ -99,11 +99,12 @@ export function MessageEditor({
|
|||
return messages;
|
||||
});
|
||||
|
||||
setMode('view');
|
||||
setMode("view");
|
||||
regenerate();
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
{isSubmitting ? 'Sending...' : 'Send'}
|
||||
{isSubmitting ? "Sending..." : "Send"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Reasoning,
|
||||
ReasoningTrigger,
|
||||
ReasoningContent,
|
||||
} from './elements/reasoning';
|
||||
ReasoningTrigger,
|
||||
} from "./elements/reasoning";
|
||||
|
||||
interface MessageReasoningProps {
|
||||
type MessageReasoningProps = {
|
||||
isLoading: boolean;
|
||||
reasoning: string;
|
||||
}
|
||||
};
|
||||
|
||||
export function MessageReasoning({
|
||||
isLoading,
|
||||
|
|
@ -23,12 +23,12 @@ export function MessageReasoning({
|
|||
setHasBeenStreaming(true);
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
|
||||
return (
|
||||
<Reasoning
|
||||
isStreaming={isLoading}
|
||||
defaultOpen={hasBeenStreaming}
|
||||
data-testid="message-reasoning"
|
||||
defaultOpen={hasBeenStreaming}
|
||||
isStreaming={isLoading}
|
||||
>
|
||||
<ReasoningTrigger />
|
||||
<ReasoningContent>{reasoning}</ReasoningContent>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
'use client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { memo, useState } from 'react';
|
||||
import type { Vote } from '@/lib/db/schema';
|
||||
import { DocumentToolResult } from './document';
|
||||
import { SparklesIcon } from './icons';
|
||||
import { Response } from './elements/response';
|
||||
import { MessageContent } from './elements/message';
|
||||
"use client";
|
||||
import type { UseChatHelpers } from "@ai-sdk/react";
|
||||
import equal from "fast-deep-equal";
|
||||
import { motion } from "framer-motion";
|
||||
import { memo, useState } from "react";
|
||||
import type { Vote } from "@/lib/db/schema";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import { cn, sanitizeText } from "@/lib/utils";
|
||||
import { useDataStream } from "./data-stream-provider";
|
||||
import { DocumentToolResult } from "./document";
|
||||
import { DocumentPreview } from "./document-preview";
|
||||
import { MessageContent } from "./elements/message";
|
||||
import { Response } from "./elements/response";
|
||||
import {
|
||||
Tool,
|
||||
ToolHeader,
|
||||
ToolContent,
|
||||
ToolHeader,
|
||||
ToolInput,
|
||||
ToolOutput,
|
||||
} from './elements/tool';
|
||||
import { MessageActions } from './message-actions';
|
||||
import { PreviewAttachment } from './preview-attachment';
|
||||
import { Weather } from './weather';
|
||||
import equal from 'fast-deep-equal';
|
||||
import { cn, sanitizeText } from '@/lib/utils';
|
||||
import { MessageEditor } from './message-editor';
|
||||
import { DocumentPreview } from './document-preview';
|
||||
import { MessageReasoning } from './message-reasoning';
|
||||
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||
import type { ChatMessage } from '@/lib/types';
|
||||
import { useDataStream } from './data-stream-provider';
|
||||
} from "./elements/tool";
|
||||
import { SparklesIcon } from "./icons";
|
||||
import { MessageActions } from "./message-actions";
|
||||
import { MessageEditor } from "./message-editor";
|
||||
import { MessageReasoning } from "./message-reasoning";
|
||||
import { PreviewAttachment } from "./preview-attachment";
|
||||
import { Weather } from "./weather";
|
||||
|
||||
const PurePreviewMessage = ({
|
||||
chatId,
|
||||
|
|
@ -34,75 +34,73 @@ const PurePreviewMessage = ({
|
|||
regenerate,
|
||||
isReadonly,
|
||||
requiresScrollPadding,
|
||||
isArtifactVisible,
|
||||
}: {
|
||||
chatId: string;
|
||||
message: ChatMessage;
|
||||
vote: Vote | undefined;
|
||||
isLoading: boolean;
|
||||
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
|
||||
regenerate: UseChatHelpers<ChatMessage>['regenerate'];
|
||||
setMessages: UseChatHelpers<ChatMessage>["setMessages"];
|
||||
regenerate: UseChatHelpers<ChatMessage>["regenerate"];
|
||||
isReadonly: boolean;
|
||||
requiresScrollPadding: boolean;
|
||||
isArtifactVisible: boolean;
|
||||
}) => {
|
||||
const [mode, setMode] = useState<'view' | 'edit'>('view');
|
||||
const [mode, setMode] = useState<"view" | "edit">("view");
|
||||
|
||||
const attachmentsFromMessage = message.parts.filter(
|
||||
(part) => part.type === 'file',
|
||||
(part) => part.type === "file"
|
||||
);
|
||||
|
||||
useDataStream();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
data-testid={`message-${message.role}`}
|
||||
className="group/message w-full"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="group/message w-full"
|
||||
data-role={message.role}
|
||||
data-testid={`message-${message.role}`}
|
||||
initial={{ opacity: 0 }}
|
||||
>
|
||||
<div
|
||||
className={cn('flex w-full items-start gap-2 md:gap-3', {
|
||||
'justify-end': message.role === 'user' && mode !== 'edit',
|
||||
'justify-start': message.role === 'assistant',
|
||||
className={cn("flex w-full items-start gap-2 md:gap-3", {
|
||||
"justify-end": message.role === "user" && mode !== "edit",
|
||||
"justify-start": message.role === "assistant",
|
||||
})}
|
||||
>
|
||||
{message.role === 'assistant' && (
|
||||
{message.role === "assistant" && (
|
||||
<div className="-mt-1 flex size-8 shrink-0 items-center justify-center rounded-full bg-background ring-1 ring-border">
|
||||
<SparklesIcon size={14} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn('flex flex-col', {
|
||||
'gap-2 md:gap-4': message.parts?.some(
|
||||
(p) => p.type === 'text' && p.text?.trim(),
|
||||
className={cn("flex flex-col", {
|
||||
"gap-2 md:gap-4": message.parts?.some(
|
||||
(p) => p.type === "text" && p.text?.trim()
|
||||
),
|
||||
'min-h-96': message.role === 'assistant' && requiresScrollPadding,
|
||||
'w-full':
|
||||
(message.role === 'assistant' &&
|
||||
"min-h-96": message.role === "assistant" && requiresScrollPadding,
|
||||
"w-full":
|
||||
(message.role === "assistant" &&
|
||||
message.parts?.some(
|
||||
(p) => p.type === 'text' && p.text?.trim(),
|
||||
(p) => p.type === "text" && p.text?.trim()
|
||||
)) ||
|
||||
mode === 'edit',
|
||||
'max-w-[calc(100%-2.5rem)] sm:max-w-[min(fit-content,80%)]':
|
||||
message.role === 'user' && mode !== 'edit',
|
||||
mode === "edit",
|
||||
"max-w-[calc(100%-2.5rem)] sm:max-w-[min(fit-content,80%)]":
|
||||
message.role === "user" && mode !== "edit",
|
||||
})}
|
||||
>
|
||||
{attachmentsFromMessage.length > 0 && (
|
||||
<div
|
||||
data-testid={`message-attachments`}
|
||||
className="flex flex-row justify-end gap-2"
|
||||
data-testid={"message-attachments"}
|
||||
>
|
||||
{attachmentsFromMessage.map((attachment) => (
|
||||
<PreviewAttachment
|
||||
key={attachment.url}
|
||||
attachment={{
|
||||
name: attachment.filename ?? 'file',
|
||||
name: attachment.filename ?? "file",
|
||||
contentType: attachment.mediaType,
|
||||
url: attachment.url,
|
||||
}}
|
||||
key={attachment.url}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -112,31 +110,31 @@ const PurePreviewMessage = ({
|
|||
const { type } = part;
|
||||
const key = `message-${message.id}-part-${index}`;
|
||||
|
||||
if (type === 'reasoning' && part.text?.trim().length > 0) {
|
||||
if (type === "reasoning" && part.text?.trim().length > 0) {
|
||||
return (
|
||||
<MessageReasoning
|
||||
key={key}
|
||||
isLoading={isLoading}
|
||||
key={key}
|
||||
reasoning={part.text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'text') {
|
||||
if (mode === 'view') {
|
||||
if (type === "text") {
|
||||
if (mode === "view") {
|
||||
return (
|
||||
<div key={key}>
|
||||
<MessageContent
|
||||
data-testid="message-content"
|
||||
className={cn({
|
||||
'w-fit break-words rounded-2xl px-3 py-2 text-right text-white':
|
||||
message.role === 'user',
|
||||
'bg-transparent px-0 py-0 text-left':
|
||||
message.role === 'assistant',
|
||||
"w-fit break-words rounded-2xl px-3 py-2 text-right text-white":
|
||||
message.role === "user",
|
||||
"bg-transparent px-0 py-0 text-left":
|
||||
message.role === "assistant",
|
||||
})}
|
||||
data-testid="message-content"
|
||||
style={
|
||||
message.role === 'user'
|
||||
? { backgroundColor: '#006cff' }
|
||||
message.role === "user"
|
||||
? { backgroundColor: "#006cff" }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
|
|
@ -146,20 +144,20 @@ const PurePreviewMessage = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (mode === 'edit') {
|
||||
if (mode === "edit") {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex w-full flex-row items-start gap-3"
|
||||
key={key}
|
||||
>
|
||||
<div className="size-8" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<MessageEditor
|
||||
key={message.id}
|
||||
message={message}
|
||||
setMode={setMode}
|
||||
setMessages={setMessages}
|
||||
regenerate={regenerate}
|
||||
setMessages={setMessages}
|
||||
setMode={setMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -167,20 +165,20 @@ const PurePreviewMessage = ({
|
|||
}
|
||||
}
|
||||
|
||||
if (type === 'tool-getWeather') {
|
||||
if (type === "tool-getWeather") {
|
||||
const { toolCallId, state } = part;
|
||||
|
||||
return (
|
||||
<Tool key={toolCallId} defaultOpen={true}>
|
||||
<ToolHeader type="tool-getWeather" state={state} />
|
||||
<Tool defaultOpen={true} key={toolCallId}>
|
||||
<ToolHeader state={state} type="tool-getWeather" />
|
||||
<ToolContent>
|
||||
{state === 'input-available' && (
|
||||
{state === "input-available" && (
|
||||
<ToolInput input={part.input} />
|
||||
)}
|
||||
{state === 'output-available' && (
|
||||
{state === "output-available" && (
|
||||
<ToolOutput
|
||||
output={<Weather weatherAtLocation={part.output} />}
|
||||
errorText={undefined}
|
||||
output={<Weather weatherAtLocation={part.output} />}
|
||||
/>
|
||||
)}
|
||||
</ToolContent>
|
||||
|
|
@ -188,14 +186,14 @@ const PurePreviewMessage = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (type === 'tool-createDocument') {
|
||||
if (type === "tool-createDocument") {
|
||||
const { toolCallId } = part;
|
||||
|
||||
if (part.output && 'error' in part.output) {
|
||||
if (part.output && "error" in part.output) {
|
||||
return (
|
||||
<div
|
||||
key={toolCallId}
|
||||
className="rounded-lg border border-red-200 bg-red-50 p-4 text-red-500 dark:bg-red-950/50"
|
||||
key={toolCallId}
|
||||
>
|
||||
Error creating document: {String(part.output.error)}
|
||||
</div>
|
||||
|
|
@ -204,21 +202,21 @@ const PurePreviewMessage = ({
|
|||
|
||||
return (
|
||||
<DocumentPreview
|
||||
key={toolCallId}
|
||||
isReadonly={isReadonly}
|
||||
key={toolCallId}
|
||||
result={part.output}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'tool-updateDocument') {
|
||||
if (type === "tool-updateDocument") {
|
||||
const { toolCallId } = part;
|
||||
|
||||
if (part.output && 'error' in part.output) {
|
||||
if (part.output && "error" in part.output) {
|
||||
return (
|
||||
<div
|
||||
key={toolCallId}
|
||||
className="rounded-lg border border-red-200 bg-red-50 p-4 text-red-500 dark:bg-red-950/50"
|
||||
key={toolCallId}
|
||||
>
|
||||
Error updating document: {String(part.output.error)}
|
||||
</div>
|
||||
|
|
@ -226,58 +224,60 @@ const PurePreviewMessage = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div key={toolCallId} className="relative">
|
||||
<div className="relative" key={toolCallId}>
|
||||
<DocumentPreview
|
||||
args={{ ...part.output, isUpdate: true }}
|
||||
isReadonly={isReadonly}
|
||||
result={part.output}
|
||||
args={{ ...part.output, isUpdate: true }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'tool-requestSuggestions') {
|
||||
if (type === "tool-requestSuggestions") {
|
||||
const { toolCallId, state } = part;
|
||||
|
||||
return (
|
||||
<Tool key={toolCallId} defaultOpen={true}>
|
||||
<ToolHeader type="tool-requestSuggestions" state={state} />
|
||||
<Tool defaultOpen={true} key={toolCallId}>
|
||||
<ToolHeader state={state} type="tool-requestSuggestions" />
|
||||
<ToolContent>
|
||||
{state === 'input-available' && (
|
||||
{state === "input-available" && (
|
||||
<ToolInput input={part.input} />
|
||||
)}
|
||||
{state === 'output-available' && (
|
||||
{state === "output-available" && (
|
||||
<ToolOutput
|
||||
errorText={undefined}
|
||||
output={
|
||||
'error' in part.output ? (
|
||||
"error" in part.output ? (
|
||||
<div className="rounded border p-2 text-red-500">
|
||||
Error: {String(part.output.error)}
|
||||
</div>
|
||||
) : (
|
||||
<DocumentToolResult
|
||||
type="request-suggestions"
|
||||
result={part.output}
|
||||
isReadonly={isReadonly}
|
||||
result={part.output}
|
||||
type="request-suggestions"
|
||||
/>
|
||||
)
|
||||
}
|
||||
errorText={undefined}
|
||||
/>
|
||||
)}
|
||||
</ToolContent>
|
||||
</Tool>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
|
||||
{!isReadonly && (
|
||||
<MessageActions
|
||||
key={`action-${message.id}`}
|
||||
chatId={chatId}
|
||||
message={message}
|
||||
vote={vote}
|
||||
isLoading={isLoading}
|
||||
key={`action-${message.id}`}
|
||||
message={message}
|
||||
setMode={setMode}
|
||||
vote={vote}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -289,27 +289,36 @@ const PurePreviewMessage = ({
|
|||
export const PreviewMessage = memo(
|
||||
PurePreviewMessage,
|
||||
(prevProps, nextProps) => {
|
||||
if (prevProps.isLoading !== nextProps.isLoading) return false;
|
||||
if (prevProps.message.id !== nextProps.message.id) return false;
|
||||
if (prevProps.requiresScrollPadding !== nextProps.requiresScrollPadding)
|
||||
if (prevProps.isLoading !== nextProps.isLoading) {
|
||||
return false;
|
||||
if (!equal(prevProps.message.parts, nextProps.message.parts)) return false;
|
||||
if (!equal(prevProps.vote, nextProps.vote)) return false;
|
||||
}
|
||||
if (prevProps.message.id !== nextProps.message.id) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.requiresScrollPadding !== nextProps.requiresScrollPadding) {
|
||||
return false;
|
||||
}
|
||||
if (!equal(prevProps.message.parts, nextProps.message.parts)) {
|
||||
return false;
|
||||
}
|
||||
if (!equal(prevProps.vote, nextProps.vote)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const ThinkingMessage = () => {
|
||||
const role = 'assistant';
|
||||
const role = "assistant";
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
data-testid="message-assistant-loading"
|
||||
className="group/message w-full"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="group/message w-full"
|
||||
data-role={role}
|
||||
data-testid="message-assistant-loading"
|
||||
initial={{ opacity: 0 }}
|
||||
>
|
||||
<div className="flex items-start justify-start gap-3">
|
||||
<div className="-mt-1 flex size-8 shrink-0 items-center justify-center rounded-full bg-background ring-1 ring-border">
|
||||
|
|
@ -329,20 +338,20 @@ export const ThinkingMessage = () => {
|
|||
const LoadingText = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<motion.div
|
||||
animate={{ backgroundPosition: ['100% 50%', '-100% 50%'] }}
|
||||
animate={{ backgroundPosition: ["100% 50%", "-100% 50%"] }}
|
||||
className="flex items-center text-transparent"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(90deg, hsl(var(--muted-foreground)) 0%, hsl(var(--muted-foreground)) 35%, hsl(var(--foreground)) 50%, hsl(var(--muted-foreground)) 65%, hsl(var(--muted-foreground)) 100%)",
|
||||
backgroundSize: "200% 100%",
|
||||
WebkitBackgroundClip: "text",
|
||||
backgroundClip: "text",
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
ease: 'linear',
|
||||
ease: "linear",
|
||||
}}
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(90deg, hsl(var(--muted-foreground)) 0%, hsl(var(--muted-foreground)) 35%, hsl(var(--foreground)) 50%, hsl(var(--muted-foreground)) 65%, hsl(var(--muted-foreground)) 100%)',
|
||||
backgroundSize: '200% 100%',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
}}
|
||||
className="flex items-center text-transparent"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { PreviewMessage, ThinkingMessage } from './message';
|
||||
import { Greeting } from './greeting';
|
||||
import { memo, useEffect } from 'react';
|
||||
import type { Vote } from '@/lib/db/schema';
|
||||
import equal from 'fast-deep-equal';
|
||||
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||
import { useMessages } from '@/hooks/use-messages';
|
||||
import type { ChatMessage } from '@/lib/types';
|
||||
import { useDataStream } from './data-stream-provider';
|
||||
import { Conversation, ConversationContent } from './elements/conversation';
|
||||
import { ArrowDownIcon } from 'lucide-react';
|
||||
import type { UseChatHelpers } from "@ai-sdk/react";
|
||||
import equal from "fast-deep-equal";
|
||||
import { ArrowDownIcon } from "lucide-react";
|
||||
import { memo, useEffect } from "react";
|
||||
import { useMessages } from "@/hooks/use-messages";
|
||||
import type { Vote } from "@/lib/db/schema";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import { useDataStream } from "./data-stream-provider";
|
||||
import { Conversation, ConversationContent } from "./elements/conversation";
|
||||
import { Greeting } from "./greeting";
|
||||
import { PreviewMessage, ThinkingMessage } from "./message";
|
||||
|
||||
interface MessagesProps {
|
||||
type MessagesProps = {
|
||||
chatId: string;
|
||||
status: UseChatHelpers<ChatMessage>['status'];
|
||||
votes: Array<Vote> | undefined;
|
||||
status: UseChatHelpers<ChatMessage>["status"];
|
||||
votes: Vote[] | undefined;
|
||||
messages: ChatMessage[];
|
||||
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
|
||||
regenerate: UseChatHelpers<ChatMessage>['regenerate'];
|
||||
setMessages: UseChatHelpers<ChatMessage>["setMessages"];
|
||||
regenerate: UseChatHelpers<ChatMessage>["regenerate"];
|
||||
isReadonly: boolean;
|
||||
isArtifactVisible: boolean;
|
||||
selectedModelId: string;
|
||||
}
|
||||
};
|
||||
|
||||
function PureMessages({
|
||||
chatId,
|
||||
|
|
@ -30,7 +30,6 @@ function PureMessages({
|
|||
setMessages,
|
||||
regenerate,
|
||||
isReadonly,
|
||||
isArtifactVisible,
|
||||
selectedModelId,
|
||||
}: MessagesProps) {
|
||||
const {
|
||||
|
|
@ -40,20 +39,19 @@ function PureMessages({
|
|||
scrollToBottom,
|
||||
hasSentMessage,
|
||||
} = useMessages({
|
||||
chatId,
|
||||
status,
|
||||
});
|
||||
|
||||
useDataStream();
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'submitted') {
|
||||
if (status === "submitted") {
|
||||
requestAnimationFrame(() => {
|
||||
const container = messagesContainerRef.current;
|
||||
if (container) {
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -62,9 +60,9 @@ function PureMessages({
|
|||
|
||||
return (
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
className="overscroll-behavior-contain -webkit-overflow-scrolling-touch flex-1 touch-pan-y overflow-y-scroll"
|
||||
style={{ overflowAnchor: 'none' }}
|
||||
ref={messagesContainerRef}
|
||||
style={{ overflowAnchor: "none" }}
|
||||
>
|
||||
<Conversation className="mx-auto flex min-w-0 max-w-4xl flex-col gap-4 md:gap-6">
|
||||
<ConversationContent className="flex flex-col gap-4 px-2 py-4 md:gap-6 md:px-4">
|
||||
|
|
@ -72,45 +70,44 @@ function PureMessages({
|
|||
|
||||
{messages.map((message, index) => (
|
||||
<PreviewMessage
|
||||
key={message.id}
|
||||
chatId={chatId}
|
||||
message={message}
|
||||
isLoading={
|
||||
status === 'streaming' && messages.length - 1 === index
|
||||
status === "streaming" && messages.length - 1 === index
|
||||
}
|
||||
isReadonly={isReadonly}
|
||||
key={message.id}
|
||||
message={message}
|
||||
regenerate={regenerate}
|
||||
requiresScrollPadding={
|
||||
hasSentMessage && index === messages.length - 1
|
||||
}
|
||||
setMessages={setMessages}
|
||||
vote={
|
||||
votes
|
||||
? votes.find((vote) => vote.messageId === message.id)
|
||||
: undefined
|
||||
}
|
||||
setMessages={setMessages}
|
||||
regenerate={regenerate}
|
||||
isReadonly={isReadonly}
|
||||
requiresScrollPadding={
|
||||
hasSentMessage && index === messages.length - 1
|
||||
}
|
||||
isArtifactVisible={isArtifactVisible}
|
||||
/>
|
||||
))}
|
||||
|
||||
{status === 'submitted' &&
|
||||
{status === "submitted" &&
|
||||
messages.length > 0 &&
|
||||
messages[messages.length - 1].role === 'user' &&
|
||||
selectedModelId !== 'chat-model-reasoning' && <ThinkingMessage />}
|
||||
messages.at(-1)?.role === "user" &&
|
||||
selectedModelId !== "chat-model-reasoning" && <ThinkingMessage />}
|
||||
|
||||
<div
|
||||
ref={messagesEndRef}
|
||||
className="min-h-[24px] min-w-[24px] shrink-0"
|
||||
ref={messagesEndRef}
|
||||
/>
|
||||
</ConversationContent>
|
||||
</Conversation>
|
||||
|
||||
{!isAtBottom && (
|
||||
<button
|
||||
className="-translate-x-1/2 absolute bottom-40 left-1/2 z-10 rounded-full border bg-background p-2 shadow-lg transition-colors hover:bg-muted"
|
||||
onClick={() => scrollToBottom('smooth')}
|
||||
type="button"
|
||||
aria-label="Scroll to bottom"
|
||||
className="-translate-x-1/2 absolute bottom-40 left-1/2 z-10 rounded-full border bg-background p-2 shadow-lg transition-colors hover:bg-muted"
|
||||
onClick={() => scrollToBottom("smooth")}
|
||||
type="button"
|
||||
>
|
||||
<ArrowDownIcon className="size-4" />
|
||||
</button>
|
||||
|
|
@ -120,13 +117,25 @@ function PureMessages({
|
|||
}
|
||||
|
||||
export const Messages = memo(PureMessages, (prevProps, nextProps) => {
|
||||
if (prevProps.isArtifactVisible && nextProps.isArtifactVisible) return true;
|
||||
if (prevProps.isArtifactVisible && nextProps.isArtifactVisible) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (prevProps.status !== nextProps.status) return false;
|
||||
if (prevProps.selectedModelId !== nextProps.selectedModelId) return false;
|
||||
if (prevProps.messages.length !== nextProps.messages.length) return false;
|
||||
if (!equal(prevProps.messages, nextProps.messages)) return false;
|
||||
if (!equal(prevProps.votes, nextProps.votes)) return false;
|
||||
if (prevProps.status !== nextProps.status) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.selectedModelId !== nextProps.selectedModelId) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.messages.length !== nextProps.messages.length) {
|
||||
return false;
|
||||
}
|
||||
if (!equal(prevProps.messages, nextProps.messages)) {
|
||||
return false;
|
||||
}
|
||||
if (!equal(prevProps.votes, nextProps.votes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { startTransition, useMemo, useOptimistic, useState } from 'react';
|
||||
|
||||
import { saveChatModelAsCookie } from '@/app/(chat)/actions';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { Session } from "next-auth";
|
||||
import { startTransition, useMemo, useOptimistic, useState } from "react";
|
||||
import { saveChatModelAsCookie } from "@/app/(chat)/actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { chatModels } from '@/lib/ai/models';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { CheckCircleFillIcon, ChevronDownIcon } from './icons';
|
||||
import { entitlementsByUserType } from '@/lib/ai/entitlements';
|
||||
import type { Session } from 'next-auth';
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { entitlementsByUserType } from "@/lib/ai/entitlements";
|
||||
import { chatModels } from "@/lib/ai/models";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckCircleFillIcon, ChevronDownIcon } from "./icons";
|
||||
|
||||
export function ModelSelector({
|
||||
session,
|
||||
|
|
@ -33,30 +31,30 @@ export function ModelSelector({
|
|||
const { availableChatModelIds } = entitlementsByUserType[userType];
|
||||
|
||||
const availableChatModels = chatModels.filter((chatModel) =>
|
||||
availableChatModelIds.includes(chatModel.id),
|
||||
availableChatModelIds.includes(chatModel.id)
|
||||
);
|
||||
|
||||
const selectedChatModel = useMemo(
|
||||
() =>
|
||||
availableChatModels.find(
|
||||
(chatModel) => chatModel.id === optimisticModelId,
|
||||
(chatModel) => chatModel.id === optimisticModelId
|
||||
),
|
||||
[optimisticModelId, availableChatModels],
|
||||
[optimisticModelId, availableChatModels]
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenu onOpenChange={setOpen} open={open}>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
className={cn(
|
||||
'w-fit data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
|
||||
className,
|
||||
"w-fit data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
className="md:h-[34px] md:px-2"
|
||||
data-testid="model-selector"
|
||||
variant="outline"
|
||||
className="md:h-[34px] md:px-2"
|
||||
>
|
||||
{selectedChatModel?.name}
|
||||
<ChevronDownIcon />
|
||||
|
|
@ -71,6 +69,8 @@ export function ModelSelector({
|
|||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
data-active={id === optimisticModelId}
|
||||
data-testid={`model-selector-item-${id}`}
|
||||
key={id}
|
||||
onSelect={() => {
|
||||
|
|
@ -81,16 +81,14 @@ export function ModelSelector({
|
|||
saveChatModelAsCookie(id);
|
||||
});
|
||||
}}
|
||||
data-active={id === optimisticModelId}
|
||||
asChild
|
||||
>
|
||||
<button
|
||||
className="group/item flex w-full flex-row items-center justify-between gap-2 sm:gap-4"
|
||||
type="button"
|
||||
className="flex flex-row gap-2 justify-between items-center w-full group/item sm:gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-1 items-start">
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<div className="text-sm sm:text-base">{chatModel.name}</div>
|
||||
<div className="text-xs line-clamp-2 text-muted-foreground">
|
||||
<div className="line-clamp-2 text-muted-foreground text-xs">
|
||||
{chatModel.description}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -105,4 +103,4 @@ export function ModelSelector({
|
|||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,51 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import type { UIMessage } from 'ai';
|
||||
import type { UseChatHelpers } from "@ai-sdk/react";
|
||||
import { Trigger } from "@radix-ui/react-select";
|
||||
import type { UIMessage } from "ai";
|
||||
import equal from "fast-deep-equal";
|
||||
import {
|
||||
useRef,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
type ChangeEvent,
|
||||
type Dispatch,
|
||||
memo,
|
||||
type SetStateAction,
|
||||
startTransition,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useLocalStorage, useWindowSize } from 'usehooks-ts';
|
||||
|
||||
import {
|
||||
ArrowUpIcon,
|
||||
PaperclipIcon,
|
||||
CpuIcon,
|
||||
StopIcon,
|
||||
ChevronDownIcon,
|
||||
} from './icons';
|
||||
import { PreviewAttachment } from './preview-attachment';
|
||||
import { Button } from './ui/button';
|
||||
import { SuggestedActions } from './suggested-actions';
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useLocalStorage, useWindowSize } from "usehooks-ts";
|
||||
import { saveChatModelAsCookie } from "@/app/(chat)/actions";
|
||||
import { SelectItem } from "@/components/ui/select";
|
||||
import { chatModels } from "@/lib/ai/models";
|
||||
import { myProvider } from "@/lib/ai/providers";
|
||||
import type { Attachment, ChatMessage } from "@/lib/types";
|
||||
import type { AppUsage } from "@/lib/usage";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Context } from "./elements/context";
|
||||
import {
|
||||
PromptInput,
|
||||
PromptInputModelSelect,
|
||||
PromptInputModelSelectContent,
|
||||
PromptInputSubmit,
|
||||
PromptInputTextarea,
|
||||
PromptInputToolbar,
|
||||
PromptInputTools,
|
||||
PromptInputSubmit,
|
||||
PromptInputModelSelect,
|
||||
PromptInputModelSelectContent,
|
||||
} from './elements/prompt-input';
|
||||
import { SelectItem } from '@/components/ui/select';
|
||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
import equal from 'fast-deep-equal';
|
||||
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||
import type { VisibilityType } from './visibility-selector';
|
||||
import type { Attachment, ChatMessage } from '@/lib/types';
|
||||
import type { AppUsage } from '@/lib/usage';
|
||||
import { chatModels } from '@/lib/ai/models';
|
||||
import { saveChatModelAsCookie } from '@/app/(chat)/actions';
|
||||
import { startTransition } from 'react';
|
||||
import { Context } from './elements/context';
|
||||
import { myProvider } from '@/lib/ai/providers';
|
||||
} from "./elements/prompt-input";
|
||||
import {
|
||||
ArrowUpIcon,
|
||||
ChevronDownIcon,
|
||||
CpuIcon,
|
||||
PaperclipIcon,
|
||||
StopIcon,
|
||||
} from "./icons";
|
||||
import { PreviewAttachment } from "./preview-attachment";
|
||||
import { SuggestedActions } from "./suggested-actions";
|
||||
import { Button } from "./ui/button";
|
||||
import type { VisibilityType } from "./visibility-selector";
|
||||
|
||||
function PureMultimodalInput({
|
||||
chatId,
|
||||
|
|
@ -67,13 +67,13 @@ function PureMultimodalInput({
|
|||
chatId: string;
|
||||
input: string;
|
||||
setInput: Dispatch<SetStateAction<string>>;
|
||||
status: UseChatHelpers<ChatMessage>['status'];
|
||||
status: UseChatHelpers<ChatMessage>["status"];
|
||||
stop: () => void;
|
||||
attachments: Array<Attachment>;
|
||||
setAttachments: Dispatch<SetStateAction<Array<Attachment>>>;
|
||||
messages: Array<UIMessage>;
|
||||
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
|
||||
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
|
||||
attachments: Attachment[];
|
||||
setAttachments: Dispatch<SetStateAction<Attachment[]>>;
|
||||
messages: UIMessage[];
|
||||
setMessages: UseChatHelpers<ChatMessage>["setMessages"];
|
||||
sendMessage: UseChatHelpers<ChatMessage>["sendMessage"];
|
||||
className?: string;
|
||||
selectedVisibilityType: VisibilityType;
|
||||
selectedModelId: string;
|
||||
|
|
@ -83,40 +83,40 @@ function PureMultimodalInput({
|
|||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const { width } = useWindowSize();
|
||||
|
||||
const adjustHeight = useCallback(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = "44px";
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
adjustHeight();
|
||||
}
|
||||
}, [adjustHeight]);
|
||||
|
||||
const resetHeight = useCallback(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = "44px";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const adjustHeight = () => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = '44px';
|
||||
}
|
||||
};
|
||||
|
||||
const resetHeight = () => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = '44px';
|
||||
}
|
||||
};
|
||||
|
||||
const [localStorageInput, setLocalStorageInput] = useLocalStorage(
|
||||
'input',
|
||||
'',
|
||||
"input",
|
||||
""
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
const domValue = textareaRef.current.value;
|
||||
// Prefer DOM value over localStorage to handle hydration
|
||||
const finalValue = domValue || localStorageInput || '';
|
||||
const finalValue = domValue || localStorageInput || "";
|
||||
setInput(finalValue);
|
||||
adjustHeight();
|
||||
}
|
||||
// Only run once after hydration
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [adjustHeight, localStorageInput, setInput]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalStorageInput(input);
|
||||
|
|
@ -127,31 +127,31 @@ function PureMultimodalInput({
|
|||
};
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [uploadQueue, setUploadQueue] = useState<Array<string>>([]);
|
||||
const [uploadQueue, setUploadQueue] = useState<string[]>([]);
|
||||
|
||||
const submitForm = useCallback(() => {
|
||||
window.history.replaceState({}, '', `/chat/${chatId}`);
|
||||
window.history.replaceState({}, "", `/chat/${chatId}`);
|
||||
|
||||
sendMessage({
|
||||
role: 'user',
|
||||
role: "user",
|
||||
parts: [
|
||||
...attachments.map((attachment) => ({
|
||||
type: 'file' as const,
|
||||
type: "file" as const,
|
||||
url: attachment.url,
|
||||
name: attachment.name,
|
||||
mediaType: attachment.contentType,
|
||||
})),
|
||||
{
|
||||
type: 'text',
|
||||
type: "text",
|
||||
text: input,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
setAttachments([]);
|
||||
setLocalStorageInput('');
|
||||
setLocalStorageInput("");
|
||||
resetHeight();
|
||||
setInput('');
|
||||
setInput("");
|
||||
|
||||
if (width && width > 768) {
|
||||
textareaRef.current?.focus();
|
||||
|
|
@ -165,15 +165,16 @@ function PureMultimodalInput({
|
|||
setLocalStorageInput,
|
||||
width,
|
||||
chatId,
|
||||
resetHeight,
|
||||
]);
|
||||
|
||||
const uploadFile = async (file: File) => {
|
||||
const uploadFile = useCallback(async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append("file", file);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/files/upload', {
|
||||
method: 'POST',
|
||||
const response = await fetch("/api/files/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
|
|
@ -184,17 +185,17 @@ function PureMultimodalInput({
|
|||
return {
|
||||
url,
|
||||
name: pathname,
|
||||
contentType: contentType,
|
||||
contentType,
|
||||
};
|
||||
}
|
||||
const { error } = await response.json();
|
||||
toast.error(error);
|
||||
} catch (error) {
|
||||
toast.error('Failed to upload file, please try again!');
|
||||
} catch (_error) {
|
||||
toast.error("Failed to upload file, please try again!");
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const modelResolver = useMemo(() => {
|
||||
const _modelResolver = useMemo(() => {
|
||||
return myProvider.languageModel(selectedModelId);
|
||||
}, [selectedModelId]);
|
||||
|
||||
|
|
@ -202,7 +203,7 @@ function PureMultimodalInput({
|
|||
() => ({
|
||||
usage,
|
||||
}),
|
||||
[usage],
|
||||
[usage]
|
||||
);
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
|
|
@ -215,7 +216,7 @@ function PureMultimodalInput({
|
|||
const uploadPromises = files.map((file) => uploadFile(file));
|
||||
const uploadedAttachments = await Promise.all(uploadPromises);
|
||||
const successfullyUploadedAttachments = uploadedAttachments.filter(
|
||||
(attachment) => attachment !== undefined,
|
||||
(attachment) => attachment !== undefined
|
||||
);
|
||||
|
||||
setAttachments((currentAttachments) => [
|
||||
|
|
@ -223,42 +224,41 @@ function PureMultimodalInput({
|
|||
...successfullyUploadedAttachments,
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error uploading files!', error);
|
||||
console.error("Error uploading files!", error);
|
||||
} finally {
|
||||
setUploadQueue([]);
|
||||
}
|
||||
},
|
||||
[setAttachments],
|
||||
[setAttachments, uploadFile]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex relative flex-col gap-4 w-full">
|
||||
|
||||
<div className={cn("relative flex w-full flex-col gap-4", className)}>
|
||||
{messages.length === 0 &&
|
||||
attachments.length === 0 &&
|
||||
uploadQueue.length === 0 && (
|
||||
<SuggestedActions
|
||||
sendMessage={sendMessage}
|
||||
chatId={chatId}
|
||||
selectedVisibilityType={selectedVisibilityType}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="file"
|
||||
className="-top-4 -left-4 pointer-events-none fixed size-0.5 opacity-0"
|
||||
ref={fileInputRef}
|
||||
multiple
|
||||
onChange={handleFileChange}
|
||||
ref={fileInputRef}
|
||||
tabIndex={-1}
|
||||
type="file"
|
||||
/>
|
||||
|
||||
<PromptInput
|
||||
className="p-3 rounded-xl border transition-all duration-200 border-border bg-background shadow-xs focus-within:border-border hover:border-muted-foreground/50"
|
||||
className="rounded-xl border border-border bg-background p-3 shadow-xs transition-all duration-200 focus-within:border-border hover:border-muted-foreground/50"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (status !== 'ready') {
|
||||
toast.error('Please wait for the model to finish its response!');
|
||||
if (status !== "ready") {
|
||||
toast.error("Please wait for the model to finish its response!");
|
||||
} else {
|
||||
submitForm();
|
||||
}
|
||||
|
|
@ -266,19 +266,19 @@ function PureMultimodalInput({
|
|||
>
|
||||
{(attachments.length > 0 || uploadQueue.length > 0) && (
|
||||
<div
|
||||
className="flex flex-row items-end gap-2 overflow-x-scroll"
|
||||
data-testid="attachments-preview"
|
||||
className="flex overflow-x-scroll flex-row gap-2 items-end"
|
||||
>
|
||||
{attachments.map((attachment) => (
|
||||
<PreviewAttachment
|
||||
key={attachment.url}
|
||||
attachment={attachment}
|
||||
key={attachment.url}
|
||||
onRemove={() => {
|
||||
setAttachments((currentAttachments) =>
|
||||
currentAttachments.filter((a) => a.url !== attachment.url),
|
||||
currentAttachments.filter((a) => a.url !== attachment.url)
|
||||
);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -286,50 +286,53 @@ function PureMultimodalInput({
|
|||
|
||||
{uploadQueue.map((filename) => (
|
||||
<PreviewAttachment
|
||||
key={filename}
|
||||
attachment={{
|
||||
url: '',
|
||||
url: "",
|
||||
name: filename,
|
||||
contentType: '',
|
||||
contentType: "",
|
||||
}}
|
||||
isUploading={true}
|
||||
key={filename}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row gap-1 items-start sm:gap-2">
|
||||
<div className="flex flex-row items-start gap-1 sm:gap-2">
|
||||
<PromptInputTextarea
|
||||
data-testid="multimodal-input"
|
||||
ref={textareaRef}
|
||||
placeholder="Send a message..."
|
||||
value={input}
|
||||
onChange={handleInput}
|
||||
minHeight={44}
|
||||
maxHeight={200}
|
||||
disableAutoResize={true}
|
||||
className="grow resize-none border-0! p-2 border-none! bg-transparent text-sm outline-none ring-0 [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 [&::-webkit-scrollbar]:hidden"
|
||||
rows={1}
|
||||
autoFocus
|
||||
/>{' '}
|
||||
className="grow resize-none border-0! border-none! bg-transparent p-2 text-sm outline-none ring-0 [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 [&::-webkit-scrollbar]:hidden"
|
||||
data-testid="multimodal-input"
|
||||
disableAutoResize={true}
|
||||
maxHeight={200}
|
||||
minHeight={44}
|
||||
onChange={handleInput}
|
||||
placeholder="Send a message..."
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
value={input}
|
||||
/>{" "}
|
||||
<Context {...contextProps} />
|
||||
</div>
|
||||
<PromptInputToolbar className="!border-top-0 border-t-0! p-0 shadow-none dark:border-0 dark:border-transparent!">
|
||||
<PromptInputTools className="gap-0 sm:gap-0.5">
|
||||
<AttachmentsButton
|
||||
fileInputRef={fileInputRef}
|
||||
selectedModelId={selectedModelId}
|
||||
status={status}
|
||||
/>
|
||||
<ModelSelectorCompact
|
||||
onModelChange={onModelChange}
|
||||
selectedModelId={selectedModelId}
|
||||
/>
|
||||
<ModelSelectorCompact selectedModelId={selectedModelId} onModelChange={onModelChange} />
|
||||
</PromptInputTools>
|
||||
|
||||
{status === 'submitted' ? (
|
||||
<StopButton stop={stop} setMessages={setMessages} />
|
||||
{status === "submitted" ? (
|
||||
<StopButton setMessages={setMessages} stop={stop} />
|
||||
) : (
|
||||
<PromptInputSubmit
|
||||
status={status}
|
||||
className="size-8 rounded-full bg-primary text-primary-foreground transition-colors duration-200 hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground"
|
||||
disabled={!input.trim() || uploadQueue.length > 0}
|
||||
className="rounded-full transition-colors duration-200 size-8 bg-primary text-primary-foreground hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground"
|
||||
status={status}
|
||||
>
|
||||
<ArrowUpIcon size={14} />
|
||||
</PromptInputSubmit>
|
||||
|
|
@ -343,15 +346,24 @@ function PureMultimodalInput({
|
|||
export const MultimodalInput = memo(
|
||||
PureMultimodalInput,
|
||||
(prevProps, nextProps) => {
|
||||
if (prevProps.input !== nextProps.input) return false;
|
||||
if (prevProps.status !== nextProps.status) return false;
|
||||
if (!equal(prevProps.attachments, nextProps.attachments)) return false;
|
||||
if (prevProps.selectedVisibilityType !== nextProps.selectedVisibilityType)
|
||||
if (prevProps.input !== nextProps.input) {
|
||||
return false;
|
||||
if (prevProps.selectedModelId !== nextProps.selectedModelId) return false;
|
||||
}
|
||||
if (prevProps.status !== nextProps.status) {
|
||||
return false;
|
||||
}
|
||||
if (!equal(prevProps.attachments, nextProps.attachments)) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.selectedVisibilityType !== nextProps.selectedVisibilityType) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.selectedModelId !== nextProps.selectedModelId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function PureAttachmentsButton({
|
||||
|
|
@ -360,20 +372,20 @@ function PureAttachmentsButton({
|
|||
selectedModelId,
|
||||
}: {
|
||||
fileInputRef: React.MutableRefObject<HTMLInputElement | null>;
|
||||
status: UseChatHelpers<ChatMessage>['status'];
|
||||
status: UseChatHelpers<ChatMessage>["status"];
|
||||
selectedModelId: string;
|
||||
}) {
|
||||
const isReasoningModel = selectedModelId === 'chat-model-reasoning';
|
||||
const isReasoningModel = selectedModelId === "chat-model-reasoning";
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="aspect-square h-8 rounded-lg p-1 transition-colors hover:bg-accent"
|
||||
data-testid="attachments-button"
|
||||
className="p-1 h-8 rounded-lg transition-colors aspect-square hover:bg-accent"
|
||||
disabled={status !== "ready" || isReasoningModel}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
fileInputRef.current?.click();
|
||||
}}
|
||||
disabled={status !== 'ready' || isReasoningModel}
|
||||
variant="ghost"
|
||||
>
|
||||
<PaperclipIcon size={14} style={{ width: 14, height: 14 }} />
|
||||
|
|
@ -397,12 +409,11 @@ function PureModelSelectorCompact({
|
|||
}, [selectedModelId]);
|
||||
|
||||
const selectedModel = chatModels.find(
|
||||
(model) => model.id === optimisticModelId,
|
||||
(model) => model.id === optimisticModelId
|
||||
);
|
||||
|
||||
return (
|
||||
<PromptInputModelSelect
|
||||
value={selectedModel?.name}
|
||||
onValueChange={(modelName) => {
|
||||
const model = chatModels.find((m) => m.name === modelName);
|
||||
if (model) {
|
||||
|
|
@ -413,30 +424,25 @@ function PureModelSelectorCompact({
|
|||
});
|
||||
}
|
||||
}}
|
||||
value={selectedModel?.name}
|
||||
>
|
||||
<SelectPrimitive.Trigger
|
||||
<Trigger
|
||||
className="flex h-8 items-center gap-2 rounded-lg border-0 bg-background px-2 text-foreground shadow-none transition-colors hover:bg-accent focus:outline-none focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
type="button"
|
||||
className="flex gap-2 items-center px-2 h-8 rounded-lg border-0 shadow-none transition-colors bg-background text-foreground hover:bg-accent focus:outline-none focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
<CpuIcon size={16} />
|
||||
<span className="hidden text-xs font-medium sm:block">
|
||||
<span className="hidden font-medium text-xs sm:block">
|
||||
{selectedModel?.name}
|
||||
</span>
|
||||
<ChevronDownIcon size={16} />
|
||||
</SelectPrimitive.Trigger>
|
||||
</Trigger>
|
||||
<PromptInputModelSelectContent className="min-w-[260px] p-0">
|
||||
<div className="flex flex-col gap-px">
|
||||
{chatModels.map((model) => (
|
||||
<SelectItem
|
||||
key={model.id}
|
||||
value={model.name}
|
||||
className="px-3 py-2 text-xs"
|
||||
>
|
||||
<div className="flex flex-col flex-1 gap-1 min-w-0">
|
||||
<div className="text-xs font-medium truncate">{model.name}</div>
|
||||
<div className="text-[10px] text-muted-foreground truncate leading-tight">
|
||||
{model.description}
|
||||
</div>
|
||||
<SelectItem key={model.id} value={model.name}>
|
||||
<div className="truncate font-medium text-xs">{model.name}</div>
|
||||
<div className="mt-px truncate text-[10px] text-muted-foreground leading-tight">
|
||||
{model.description}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
|
|
@ -453,12 +459,12 @@ function PureStopButton({
|
|||
setMessages,
|
||||
}: {
|
||||
stop: () => void;
|
||||
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
|
||||
setMessages: UseChatHelpers<ChatMessage>["setMessages"];
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
className="size-7 rounded-full bg-foreground p-1 text-background transition-colors duration-200 hover:bg-foreground/90 disabled:bg-muted disabled:text-muted-foreground"
|
||||
data-testid="stop-button"
|
||||
className="p-1 rounded-full transition-colors duration-200 size-7 bg-foreground text-background hover:bg-foreground/90 disabled:bg-muted disabled:text-muted-foreground"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
stop();
|
||||
|
|
|
|||
|
|
@ -1,34 +1,32 @@
|
|||
import type { Attachment } from '@/lib/types';
|
||||
import { Loader } from './elements/loader';
|
||||
import { CrossSmallIcon } from './icons';
|
||||
import { Button } from './ui/button';
|
||||
import Image from 'next/image';
|
||||
import Image from "next/image";
|
||||
import type { Attachment } from "@/lib/types";
|
||||
import { Loader } from "./elements/loader";
|
||||
import { CrossSmallIcon } from "./icons";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export const PreviewAttachment = ({
|
||||
attachment,
|
||||
isUploading = false,
|
||||
onRemove,
|
||||
onEdit,
|
||||
}: {
|
||||
attachment: Attachment;
|
||||
isUploading?: boolean;
|
||||
onRemove?: () => void;
|
||||
onEdit?: () => void;
|
||||
}) => {
|
||||
const { name, url, contentType } = attachment;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="input-attachment-preview"
|
||||
className="group relative size-16 overflow-hidden rounded-lg border bg-muted"
|
||||
data-testid="input-attachment-preview"
|
||||
>
|
||||
{contentType?.startsWith('image') ? (
|
||||
{contentType?.startsWith("image") ? (
|
||||
<Image
|
||||
src={url}
|
||||
alt={name ?? 'An image attachment'}
|
||||
alt={name ?? "An image attachment"}
|
||||
className="size-full object-cover"
|
||||
width={64}
|
||||
height={64}
|
||||
src={url}
|
||||
width={64}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex size-full items-center justify-center text-muted-foreground text-xs">
|
||||
|
|
@ -44,10 +42,10 @@ export const PreviewAttachment = ({
|
|||
|
||||
{onRemove && !isUploading && (
|
||||
<Button
|
||||
className="absolute top-0.5 right-0.5 size-4 rounded-full p-0 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
onClick={onRemove}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="absolute top-0.5 right-0.5 size-4 rounded-full p-0 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
>
|
||||
<CrossSmallIcon size={8} />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,46 +1,43 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import React, { memo, useEffect, useMemo, useState } from 'react';
|
||||
import DataGrid, { textEditor } from 'react-data-grid';
|
||||
import { parse, unparse } from 'papaparse';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTheme } from "next-themes";
|
||||
import { parse, unparse } from "papaparse";
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
import DataGrid, { textEditor } from "react-data-grid";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import 'react-data-grid/lib/styles.css';
|
||||
import "react-data-grid/lib/styles.css";
|
||||
|
||||
type SheetEditorProps = {
|
||||
content: string;
|
||||
saveContent: (content: string, isCurrentVersion: boolean) => void;
|
||||
status: string;
|
||||
isCurrentVersion: boolean;
|
||||
currentVersionIndex: number;
|
||||
isCurrentVersion: boolean;
|
||||
status: string;
|
||||
};
|
||||
|
||||
const MIN_ROWS = 50;
|
||||
const MIN_COLS = 26;
|
||||
|
||||
const PureSpreadsheetEditor = ({
|
||||
content,
|
||||
saveContent,
|
||||
status,
|
||||
isCurrentVersion,
|
||||
}: SheetEditorProps) => {
|
||||
const PureSpreadsheetEditor = ({ content, saveContent }: SheetEditorProps) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const parseData = useMemo(() => {
|
||||
if (!content) return Array(MIN_ROWS).fill(Array(MIN_COLS).fill(''));
|
||||
if (!content) {
|
||||
return new Array(MIN_ROWS).fill(new Array(MIN_COLS).fill(""));
|
||||
}
|
||||
const result = parse<string[]>(content, { skipEmptyLines: true });
|
||||
|
||||
const paddedData = result.data.map((row) => {
|
||||
const paddedRow = [...row];
|
||||
while (paddedRow.length < MIN_COLS) {
|
||||
paddedRow.push('');
|
||||
paddedRow.push("");
|
||||
}
|
||||
return paddedRow;
|
||||
});
|
||||
|
||||
while (paddedData.length < MIN_ROWS) {
|
||||
paddedData.push(Array(MIN_COLS).fill(''));
|
||||
paddedData.push(new Array(MIN_COLS).fill(""));
|
||||
}
|
||||
|
||||
return paddedData;
|
||||
|
|
@ -48,13 +45,13 @@ const PureSpreadsheetEditor = ({
|
|||
|
||||
const columns = useMemo(() => {
|
||||
const rowNumberColumn = {
|
||||
key: 'rowNumber',
|
||||
name: '',
|
||||
key: "rowNumber",
|
||||
name: "",
|
||||
frozen: true,
|
||||
width: 50,
|
||||
renderCell: ({ rowIdx }: { rowIdx: number }) => rowIdx + 1,
|
||||
cellClass: 'border-t border-r dark:bg-zinc-950 dark:text-zinc-50',
|
||||
headerCellClass: 'border-t border-r dark:bg-zinc-900 dark:text-zinc-50',
|
||||
cellClass: "border-t border-r dark:bg-zinc-950 dark:text-zinc-50",
|
||||
headerCellClass: "border-t border-r dark:bg-zinc-900 dark:text-zinc-50",
|
||||
};
|
||||
|
||||
const dataColumns = Array.from({ length: MIN_COLS }, (_, i) => ({
|
||||
|
|
@ -62,11 +59,11 @@ const PureSpreadsheetEditor = ({
|
|||
name: String.fromCharCode(65 + i),
|
||||
renderEditCell: textEditor,
|
||||
width: 120,
|
||||
cellClass: cn(`border-t dark:bg-zinc-950 dark:text-zinc-50`, {
|
||||
'border-l': i !== 0,
|
||||
cellClass: cn("border-t dark:bg-zinc-950 dark:text-zinc-50", {
|
||||
"border-l": i !== 0,
|
||||
}),
|
||||
headerCellClass: cn(`border-t dark:bg-zinc-900 dark:text-zinc-50`, {
|
||||
'border-l': i !== 0,
|
||||
headerCellClass: cn("border-t dark:bg-zinc-900 dark:text-zinc-50", {
|
||||
"border-l": i !== 0,
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
@ -81,7 +78,7 @@ const PureSpreadsheetEditor = ({
|
|||
};
|
||||
|
||||
columns.slice(1).forEach((col, colIndex) => {
|
||||
rowData[col.key] = row[colIndex] || '';
|
||||
rowData[col.key] = row[colIndex] || "";
|
||||
});
|
||||
|
||||
return rowData;
|
||||
|
|
@ -102,7 +99,7 @@ const PureSpreadsheetEditor = ({
|
|||
setLocalRows(newRows);
|
||||
|
||||
const updatedData = newRows.map((row) => {
|
||||
return columns.slice(1).map((col) => row[col.key] || '');
|
||||
return columns.slice(1).map((col) => row[col.key] || "");
|
||||
});
|
||||
|
||||
const newCsvContent = generateCsv(updatedData);
|
||||
|
|
@ -111,21 +108,21 @@ const PureSpreadsheetEditor = ({
|
|||
|
||||
return (
|
||||
<DataGrid
|
||||
className={resolvedTheme === 'dark' ? 'rdg-dark' : 'rdg-light'}
|
||||
className={resolvedTheme === "dark" ? "rdg-dark" : "rdg-light"}
|
||||
columns={columns}
|
||||
rows={localRows}
|
||||
enableVirtualization
|
||||
onRowsChange={handleRowsChange}
|
||||
onCellClick={(args) => {
|
||||
if (args.column.key !== 'rowNumber') {
|
||||
args.selectCell(true);
|
||||
}
|
||||
}}
|
||||
style={{ height: '100%' }}
|
||||
defaultColumnOptions={{
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
}}
|
||||
enableVirtualization
|
||||
onCellClick={(args) => {
|
||||
if (args.column.key !== "rowNumber") {
|
||||
args.selectCell(true);
|
||||
}
|
||||
}}
|
||||
onRowsChange={handleRowsChange}
|
||||
rows={localRows}
|
||||
style={{ height: "100%" }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -134,7 +131,7 @@ function areEqual(prevProps: SheetEditorProps, nextProps: SheetEditorProps) {
|
|||
return (
|
||||
prevProps.currentVersionIndex === nextProps.currentVersionIndex &&
|
||||
prevProps.isCurrentVersion === nextProps.isCurrentVersion &&
|
||||
!(prevProps.status === 'streaming' && nextProps.status === 'streaming') &&
|
||||
!(prevProps.status === "streaming" && nextProps.status === "streaming") &&
|
||||
prevProps.content === nextProps.content &&
|
||||
prevProps.saveContent === nextProps.saveContent
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import type { Chat } from '@/lib/db/schema';
|
||||
import Link from "next/link";
|
||||
import { memo } from "react";
|
||||
import { useChatVisibility } from "@/hooks/use-chat-visibility";
|
||||
import type { Chat } from "@/lib/db/schema";
|
||||
import {
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from './ui/sidebar';
|
||||
import Link from 'next/link';
|
||||
CheckCircleFillIcon,
|
||||
GlobeIcon,
|
||||
LockIcon,
|
||||
MoreHorizontalIcon,
|
||||
ShareIcon,
|
||||
TrashIcon,
|
||||
} from "./icons";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -14,17 +19,12 @@ import {
|
|||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from './ui/dropdown-menu';
|
||||
} from "./ui/dropdown-menu";
|
||||
import {
|
||||
CheckCircleFillIcon,
|
||||
GlobeIcon,
|
||||
LockIcon,
|
||||
MoreHorizontalIcon,
|
||||
ShareIcon,
|
||||
TrashIcon,
|
||||
} from './icons';
|
||||
import { memo } from 'react';
|
||||
import { useChatVisibility } from '@/hooks/use-chat-visibility';
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "./ui/sidebar";
|
||||
|
||||
const PureChatItem = ({
|
||||
chat,
|
||||
|
|
@ -61,7 +61,7 @@ const PureChatItem = ({
|
|||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
<DropdownMenuContent align="end" side="bottom">
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="cursor-pointer">
|
||||
<ShareIcon />
|
||||
|
|
@ -72,28 +72,28 @@ const PureChatItem = ({
|
|||
<DropdownMenuItem
|
||||
className="cursor-pointer flex-row justify-between"
|
||||
onClick={() => {
|
||||
setVisibilityType('private');
|
||||
setVisibilityType("private");
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<LockIcon size={12} />
|
||||
<span>Private</span>
|
||||
</div>
|
||||
{visibilityType === 'private' ? (
|
||||
{visibilityType === "private" ? (
|
||||
<CheckCircleFillIcon />
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer flex-row justify-between"
|
||||
onClick={() => {
|
||||
setVisibilityType('public');
|
||||
setVisibilityType("public");
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<GlobeIcon />
|
||||
<span>Public</span>
|
||||
</div>
|
||||
{visibilityType === 'public' ? <CheckCircleFillIcon /> : null}
|
||||
{visibilityType === "public" ? <CheckCircleFillIcon /> : null}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
|
|
@ -113,6 +113,8 @@ const PureChatItem = ({
|
|||
};
|
||||
|
||||
export const ChatItem = memo(PureChatItem, (prevProps, nextProps) => {
|
||||
if (prevProps.isActive !== nextProps.isActive) return false;
|
||||
if (prevProps.isActive !== nextProps.isActive) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { isToday, isYesterday, subMonths, subWeeks } from 'date-fns';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import type { User } from 'next-auth';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { motion } from 'framer-motion';
|
||||
import { isToday, isYesterday, subMonths, subWeeks } from "date-fns";
|
||||
import { motion } from "framer-motion";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import type { User } from "next-auth";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import useSWRInfinite from "swr/infinite";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
|
@ -15,18 +16,17 @@ import {
|
|||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar';
|
||||
import type { Chat } from '@/lib/db/schema';
|
||||
import { fetcher } from '@/lib/utils';
|
||||
import { ChatItem } from './sidebar-history-item';
|
||||
import useSWRInfinite from 'swr/infinite';
|
||||
import { LoaderIcon } from './icons';
|
||||
} from "@/components/ui/sidebar";
|
||||
import type { Chat } from "@/lib/db/schema";
|
||||
import { fetcher } from "@/lib/utils";
|
||||
import { LoaderIcon } from "./icons";
|
||||
import { ChatItem } from "./sidebar-history-item";
|
||||
|
||||
type GroupedChats = {
|
||||
today: Chat[];
|
||||
|
|
@ -36,10 +36,10 @@ type GroupedChats = {
|
|||
older: Chat[];
|
||||
};
|
||||
|
||||
export interface ChatHistory {
|
||||
chats: Array<Chat>;
|
||||
export type ChatHistory = {
|
||||
chats: Chat[];
|
||||
hasMore: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
|
|
@ -72,23 +72,27 @@ const groupChatsByDate = (chats: Chat[]): GroupedChats => {
|
|||
lastWeek: [],
|
||||
lastMonth: [],
|
||||
older: [],
|
||||
} as GroupedChats,
|
||||
} as GroupedChats
|
||||
);
|
||||
};
|
||||
|
||||
export function getChatHistoryPaginationKey(
|
||||
pageIndex: number,
|
||||
previousPageData: ChatHistory,
|
||||
previousPageData: ChatHistory
|
||||
) {
|
||||
if (previousPageData && previousPageData.hasMore === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pageIndex === 0) return `/api/history?limit=${PAGE_SIZE}`;
|
||||
if (pageIndex === 0) {
|
||||
return `/api/history?limit=${PAGE_SIZE}`;
|
||||
}
|
||||
|
||||
const firstChatFromPage = previousPageData.chats.at(-1);
|
||||
|
||||
if (!firstChatFromPage) return null;
|
||||
if (!firstChatFromPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `/api/history?ending_before=${firstChatFromPage.id}&limit=${PAGE_SIZE}`;
|
||||
}
|
||||
|
|
@ -119,13 +123,13 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
|
|||
? paginatedChatHistories.every((page) => page.chats.length === 0)
|
||||
: false;
|
||||
|
||||
const handleDelete = async () => {
|
||||
const handleDelete = () => {
|
||||
const deletePromise = fetch(`/api/chat?id=${deleteId}`, {
|
||||
method: 'DELETE',
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
toast.promise(deletePromise, {
|
||||
loading: 'Deleting chat...',
|
||||
loading: "Deleting chat...",
|
||||
success: () => {
|
||||
mutate((chatHistories) => {
|
||||
if (chatHistories) {
|
||||
|
|
@ -136,15 +140,15 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
|
|||
}
|
||||
});
|
||||
|
||||
return 'Chat deleted successfully';
|
||||
return "Chat deleted successfully";
|
||||
},
|
||||
error: 'Failed to delete chat',
|
||||
error: "Failed to delete chat",
|
||||
});
|
||||
|
||||
setShowDeleteDialog(false);
|
||||
|
||||
if (deleteId === id) {
|
||||
router.push('/');
|
||||
router.push("/");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -170,14 +174,14 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
|
|||
<div className="flex flex-col">
|
||||
{[44, 32, 28, 64, 52].map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="flex h-8 items-center gap-2 rounded-md px-2"
|
||||
key={item}
|
||||
>
|
||||
<div
|
||||
className="h-4 max-w-(--skeleton-width) flex-1 rounded-md bg-sidebar-accent-foreground/10"
|
||||
style={
|
||||
{
|
||||
'--skeleton-width': `${item}%`,
|
||||
"--skeleton-width": `${item}%`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
|
|
@ -209,7 +213,7 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
|
|||
{paginatedChatHistories &&
|
||||
(() => {
|
||||
const chatsFromHistory = paginatedChatHistories.flatMap(
|
||||
(paginatedChatHistory) => paginatedChatHistory.chats,
|
||||
(paginatedChatHistory) => paginatedChatHistory.chats
|
||||
);
|
||||
|
||||
const groupedChats = groupChatsByDate(chatsFromHistory);
|
||||
|
|
@ -223,9 +227,9 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
|
|||
</div>
|
||||
{groupedChats.today.map((chat) => (
|
||||
<ChatItem
|
||||
key={chat.id}
|
||||
chat={chat}
|
||||
isActive={chat.id === id}
|
||||
key={chat.id}
|
||||
onDelete={(chatId) => {
|
||||
setDeleteId(chatId);
|
||||
setShowDeleteDialog(true);
|
||||
|
|
@ -243,9 +247,9 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
|
|||
</div>
|
||||
{groupedChats.yesterday.map((chat) => (
|
||||
<ChatItem
|
||||
key={chat.id}
|
||||
chat={chat}
|
||||
isActive={chat.id === id}
|
||||
key={chat.id}
|
||||
onDelete={(chatId) => {
|
||||
setDeleteId(chatId);
|
||||
setShowDeleteDialog(true);
|
||||
|
|
@ -263,9 +267,9 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
|
|||
</div>
|
||||
{groupedChats.lastWeek.map((chat) => (
|
||||
<ChatItem
|
||||
key={chat.id}
|
||||
chat={chat}
|
||||
isActive={chat.id === id}
|
||||
key={chat.id}
|
||||
onDelete={(chatId) => {
|
||||
setDeleteId(chatId);
|
||||
setShowDeleteDialog(true);
|
||||
|
|
@ -283,9 +287,9 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
|
|||
</div>
|
||||
{groupedChats.lastMonth.map((chat) => (
|
||||
<ChatItem
|
||||
key={chat.id}
|
||||
chat={chat}
|
||||
isActive={chat.id === id}
|
||||
key={chat.id}
|
||||
onDelete={(chatId) => {
|
||||
setDeleteId(chatId);
|
||||
setShowDeleteDialog(true);
|
||||
|
|
@ -303,9 +307,9 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
|
|||
</div>
|
||||
{groupedChats.older.map((chat) => (
|
||||
<ChatItem
|
||||
key={chat.id}
|
||||
chat={chat}
|
||||
isActive={chat.id === id}
|
||||
key={chat.id}
|
||||
onDelete={(chatId) => {
|
||||
setDeleteId(chatId);
|
||||
setShowDeleteDialog(true);
|
||||
|
|
@ -343,7 +347,7 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
|
|||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialog onOpenChange={setShowDeleteDialog} open={showDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import type { ComponentProps } from 'react';
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { type SidebarTrigger, useSidebar } from '@/components/ui/sidebar';
|
||||
import { type SidebarTrigger, useSidebar } from "@/components/ui/sidebar";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
import { SidebarLeftIcon } from './icons';
|
||||
import { Button } from './ui/button';
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SidebarLeftIcon } from "./icons";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export function SidebarToggle({
|
||||
className,
|
||||
|
|
@ -19,10 +19,10 @@ export function SidebarToggle({
|
|||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className={cn("h-8 px-2 md:h-fit md:px-2", className)}
|
||||
data-testid="sidebar-toggle-button"
|
||||
onClick={toggleSidebar}
|
||||
variant="outline"
|
||||
className="h-8 px-2 md:h-fit md:px-2"
|
||||
>
|
||||
<SidebarLeftIcon size={16} />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,41 +1,40 @@
|
|||
'use client';
|
||||
|
||||
import { ChevronUp } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import type { User } from 'next-auth';
|
||||
import { signOut, useSession } from 'next-auth/react';
|
||||
import { useTheme } from 'next-themes';
|
||||
"use client";
|
||||
|
||||
import { ChevronUp } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { User } from "next-auth";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useTheme } from "next-themes";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from '@/components/ui/sidebar';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from './toast';
|
||||
import { LoaderIcon } from './icons';
|
||||
import { guestRegex } from '@/lib/constants';
|
||||
} from "@/components/ui/sidebar";
|
||||
import { guestRegex } from "@/lib/constants";
|
||||
import { LoaderIcon } from "./icons";
|
||||
import { toast } from "./toast";
|
||||
|
||||
export function SidebarUserNav({ user }: { user: User }) {
|
||||
const router = useRouter();
|
||||
const { data, status } = useSession();
|
||||
const { setTheme, resolvedTheme } = useTheme();
|
||||
|
||||
const isGuest = guestRegex.test(data?.user?.email ?? '');
|
||||
const isGuest = guestRegex.test(data?.user?.email ?? "");
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{status === 'loading' ? (
|
||||
{status === "loading" ? (
|
||||
<SidebarMenuButton className="h-10 justify-between bg-background data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="size-6 animate-pulse rounded-full bg-zinc-500/30" />
|
||||
|
|
@ -49,63 +48,63 @@ export function SidebarUserNav({ user }: { user: User }) {
|
|||
</SidebarMenuButton>
|
||||
) : (
|
||||
<SidebarMenuButton
|
||||
data-testid="user-nav-button"
|
||||
className="h-10 bg-background data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
data-testid="user-nav-button"
|
||||
>
|
||||
<Image
|
||||
src={`https://avatar.vercel.sh/${user.email}`}
|
||||
alt={user.email ?? 'User Avatar'}
|
||||
width={24}
|
||||
height={24}
|
||||
alt={user.email ?? "User Avatar"}
|
||||
className="rounded-full"
|
||||
height={24}
|
||||
src={`https://avatar.vercel.sh/${user.email}`}
|
||||
width={24}
|
||||
/>
|
||||
<span data-testid="user-email" className="truncate">
|
||||
{isGuest ? 'Guest' : user?.email}
|
||||
<span className="truncate" data-testid="user-email">
|
||||
{isGuest ? "Guest" : user?.email}
|
||||
</span>
|
||||
<ChevronUp className="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-popper-anchor-width)"
|
||||
data-testid="user-nav-menu"
|
||||
side="top"
|
||||
className="w-(--radix-popper-anchor-width)"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
data-testid="user-nav-item-theme"
|
||||
className="cursor-pointer"
|
||||
data-testid="user-nav-item-theme"
|
||||
onSelect={() =>
|
||||
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}
|
||||
>
|
||||
{`Toggle ${resolvedTheme === 'light' ? 'dark' : 'light'} mode`}
|
||||
{`Toggle ${resolvedTheme === "light" ? "dark" : "light"} mode`}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild data-testid="user-nav-item-auth">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full cursor-pointer"
|
||||
onClick={() => {
|
||||
if (status === 'loading') {
|
||||
if (status === "loading") {
|
||||
toast({
|
||||
type: 'error',
|
||||
type: "error",
|
||||
description:
|
||||
'Checking authentication status, please try again!',
|
||||
"Checking authentication status, please try again!",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isGuest) {
|
||||
router.push('/login');
|
||||
router.push("/login");
|
||||
} else {
|
||||
signOut({
|
||||
redirectTo: '/',
|
||||
redirectTo: "/",
|
||||
});
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{isGuest ? 'Login to your account' : 'Sign out'}
|
||||
{isGuest ? "Login to your account" : "Sign out"}
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import Form from 'next/form';
|
||||
import Form from "next/form";
|
||||
|
||||
import { signOut } from '@/app/(auth)/auth';
|
||||
import { signOut } from "@/app/(auth)/auth";
|
||||
|
||||
export const SignOutForm = () => {
|
||||
return (
|
||||
<Form
|
||||
className="w-full"
|
||||
action={async () => {
|
||||
'use server';
|
||||
"use server";
|
||||
|
||||
await signOut({
|
||||
redirectTo: '/',
|
||||
redirectTo: "/",
|
||||
});
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full px-1 py-0.5 text-left text-red-500"
|
||||
type="submit"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useFormStatus } from 'react-dom';
|
||||
import { useFormStatus } from "react-dom";
|
||||
|
||||
import { LoaderIcon } from '@/components/icons';
|
||||
import { LoaderIcon } from "@/components/icons";
|
||||
|
||||
import { Button } from './ui/button';
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export function SubmitButton({
|
||||
children,
|
||||
|
|
@ -17,10 +17,10 @@ export function SubmitButton({
|
|||
|
||||
return (
|
||||
<Button
|
||||
type={pending ? 'button' : 'submit'}
|
||||
aria-disabled={pending || isSuccessful}
|
||||
disabled={pending || isSuccessful}
|
||||
className="relative"
|
||||
disabled={pending || isSuccessful}
|
||||
type={pending ? "button" : "submit"}
|
||||
>
|
||||
{children}
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ export function SubmitButton({
|
|||
)}
|
||||
|
||||
<output aria-live="polite" className="sr-only">
|
||||
{pending || isSuccessful ? 'Loading' : 'Submit form'}
|
||||
{pending || isSuccessful ? "Loading" : "Submit form"}
|
||||
</output>
|
||||
</Button>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,53 +1,49 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { memo } from 'react';
|
||||
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||
import type { VisibilityType } from './visibility-selector';
|
||||
import type { ChatMessage } from '@/lib/types';
|
||||
import { Suggestion } from './elements/suggestion';
|
||||
import type { UseChatHelpers } from "@ai-sdk/react";
|
||||
import { motion } from "framer-motion";
|
||||
import { memo } from "react";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import { Suggestion } from "./elements/suggestion";
|
||||
import type { VisibilityType } from "./visibility-selector";
|
||||
|
||||
interface SuggestedActionsProps {
|
||||
type SuggestedActionsProps = {
|
||||
chatId: string;
|
||||
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
|
||||
sendMessage: UseChatHelpers<ChatMessage>["sendMessage"];
|
||||
selectedVisibilityType: VisibilityType;
|
||||
}
|
||||
};
|
||||
|
||||
function PureSuggestedActions({
|
||||
chatId,
|
||||
sendMessage,
|
||||
selectedVisibilityType,
|
||||
}: SuggestedActionsProps) {
|
||||
function PureSuggestedActions({ chatId, sendMessage }: SuggestedActionsProps) {
|
||||
const suggestedActions = [
|
||||
'What are the advantages of using Next.js?',
|
||||
"What are the advantages of using Next.js?",
|
||||
"Write code to demonstrate Dijkstra's algorithm",
|
||||
'Help me write an essay about Silicon Valley',
|
||||
'What is the weather in San Francisco?',
|
||||
"Help me write an essay about Silicon Valley",
|
||||
"What is the weather in San Francisco?",
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="suggested-actions"
|
||||
className="grid w-full gap-2 sm:grid-cols-2"
|
||||
data-testid="suggested-actions"
|
||||
>
|
||||
{suggestedActions.map((suggestedAction, index) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
transition={{ delay: 0.05 * index }}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
key={suggestedAction}
|
||||
transition={{ delay: 0.05 * index }}
|
||||
>
|
||||
<Suggestion
|
||||
suggestion={suggestedAction}
|
||||
className="h-auto w-full whitespace-normal p-3 text-left"
|
||||
onClick={(suggestion) => {
|
||||
window.history.replaceState({}, '', `/chat/${chatId}`);
|
||||
window.history.replaceState({}, "", `/chat/${chatId}`);
|
||||
sendMessage({
|
||||
role: 'user',
|
||||
parts: [{ type: 'text', text: suggestion }],
|
||||
role: "user",
|
||||
parts: [{ type: "text", text: suggestion }],
|
||||
});
|
||||
}}
|
||||
className="h-auto w-full whitespace-normal p-3 text-left"
|
||||
suggestion={suggestedAction}
|
||||
>
|
||||
{suggestedAction}
|
||||
</Suggestion>
|
||||
|
|
@ -60,10 +56,13 @@ function PureSuggestedActions({
|
|||
export const SuggestedActions = memo(
|
||||
PureSuggestedActions,
|
||||
(prevProps, nextProps) => {
|
||||
if (prevProps.chatId !== nextProps.chatId) return false;
|
||||
if (prevProps.selectedVisibilityType !== nextProps.selectedVisibilityType)
|
||||
if (prevProps.chatId !== nextProps.chatId) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.selectedVisibilityType !== nextProps.selectedVisibilityType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { useWindowSize } from 'usehooks-ts';
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { useWindowSize } from "usehooks-ts";
|
||||
|
||||
import type { UISuggestion } from '@/lib/editor/suggestions';
|
||||
|
||||
import { CrossIcon, MessageIcon } from './icons';
|
||||
import { Button } from './ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ArtifactKind } from './artifact';
|
||||
import type { UISuggestion } from "@/lib/editor/suggestions";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ArtifactKind } from "./artifact";
|
||||
import { CrossIcon, MessageIcon } from "./icons";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export const Suggestion = ({
|
||||
suggestion,
|
||||
|
|
@ -25,27 +24,14 @@ export const Suggestion = ({
|
|||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{!isExpanded ? (
|
||||
{isExpanded ? (
|
||||
<motion.div
|
||||
className={cn('cursor-pointer p-1 text-muted-foreground', {
|
||||
'-right-8 absolute': artifactKind === 'text',
|
||||
'sticky top-0 right-4': artifactKind === 'code',
|
||||
})}
|
||||
onClick={() => {
|
||||
setIsExpanded(true);
|
||||
}}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
>
|
||||
<MessageIcon size={windowWidth && windowWidth < 768 ? 16 : 14} />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key={suggestion.id}
|
||||
className="-right-12 md:-right-16 absolute z-50 flex w-56 flex-col gap-3 rounded-2xl border bg-background p-3 font-sans text-sm shadow-xl"
|
||||
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: -20 }}
|
||||
className="-right-12 md:-right-16 absolute z-50 flex w-56 flex-col gap-3 rounded-2xl border bg-background p-3 font-sans text-sm shadow-xl"
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
key={suggestion.id}
|
||||
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
|
|
@ -54,24 +40,37 @@ export const Suggestion = ({
|
|||
<div className="font-medium">Assistant</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer text-gray-500 text-xs"
|
||||
onClick={() => {
|
||||
setIsExpanded(false);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<CrossIcon size={12} />
|
||||
</button>
|
||||
</div>
|
||||
<div>{suggestion.description}</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-fit rounded-full px-3 py-1.5"
|
||||
onClick={onApply}
|
||||
variant="outline"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
className={cn("cursor-pointer p-1 text-muted-foreground", {
|
||||
"-right-8 absolute": artifactKind === "text",
|
||||
"sticky top-0 right-4": artifactKind === "code",
|
||||
})}
|
||||
onClick={() => {
|
||||
setIsExpanded(true);
|
||||
}}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
>
|
||||
<MessageIcon size={windowWidth && windowWidth < 768 ? 16 : 14} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,35 +1,35 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { exampleSetup } from 'prosemirror-example-setup';
|
||||
import { inputRules } from 'prosemirror-inputrules';
|
||||
import { EditorState } from 'prosemirror-state';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import React, { memo, useEffect, useRef } from 'react';
|
||||
import { exampleSetup } from "prosemirror-example-setup";
|
||||
import { inputRules } from "prosemirror-inputrules";
|
||||
import { EditorState } from "prosemirror-state";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import { memo, useEffect, useRef } from "react";
|
||||
|
||||
import type { Suggestion } from '@/lib/db/schema';
|
||||
import type { Suggestion } from "@/lib/db/schema";
|
||||
import {
|
||||
documentSchema,
|
||||
handleTransaction,
|
||||
headingRule,
|
||||
} from '@/lib/editor/config';
|
||||
} from "@/lib/editor/config";
|
||||
import {
|
||||
buildContentFromDocument,
|
||||
buildDocumentFromContent,
|
||||
createDecorations,
|
||||
} from '@/lib/editor/functions';
|
||||
} from "@/lib/editor/functions";
|
||||
import {
|
||||
projectWithPositions,
|
||||
suggestionsPlugin,
|
||||
suggestionsPluginKey,
|
||||
} from '@/lib/editor/suggestions';
|
||||
} from "@/lib/editor/suggestions";
|
||||
|
||||
type EditorProps = {
|
||||
content: string;
|
||||
onSaveContent: (updatedContent: string, debounce: boolean) => void;
|
||||
status: 'streaming' | 'idle';
|
||||
status: "streaming" | "idle";
|
||||
isCurrentVersion: boolean;
|
||||
currentVersionIndex: number;
|
||||
suggestions: Array<Suggestion>;
|
||||
suggestions: Suggestion[];
|
||||
};
|
||||
|
||||
function PureEditor({
|
||||
|
|
@ -74,7 +74,7 @@ function PureEditor({
|
|||
};
|
||||
// NOTE: we only want to run this effect once
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
}, [content]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
|
|
@ -93,19 +93,19 @@ function PureEditor({
|
|||
useEffect(() => {
|
||||
if (editorRef.current && content) {
|
||||
const currentContent = buildContentFromDocument(
|
||||
editorRef.current.state.doc,
|
||||
editorRef.current.state.doc
|
||||
);
|
||||
|
||||
if (status === 'streaming') {
|
||||
if (status === "streaming") {
|
||||
const newDocument = buildDocumentFromContent(content);
|
||||
|
||||
const transaction = editorRef.current.state.tr.replaceWith(
|
||||
0,
|
||||
editorRef.current.state.doc.content.size,
|
||||
newDocument.content,
|
||||
newDocument.content
|
||||
);
|
||||
|
||||
transaction.setMeta('no-save', true);
|
||||
transaction.setMeta("no-save", true);
|
||||
editorRef.current.dispatch(transaction);
|
||||
return;
|
||||
}
|
||||
|
|
@ -116,10 +116,10 @@ function PureEditor({
|
|||
const transaction = editorRef.current.state.tr.replaceWith(
|
||||
0,
|
||||
editorRef.current.state.doc.content.size,
|
||||
newDocument.content,
|
||||
newDocument.content
|
||||
);
|
||||
|
||||
transaction.setMeta('no-save', true);
|
||||
transaction.setMeta("no-save", true);
|
||||
editorRef.current.dispatch(transaction);
|
||||
}
|
||||
}
|
||||
|
|
@ -129,14 +129,14 @@ function PureEditor({
|
|||
if (editorRef.current?.state.doc && content) {
|
||||
const projectedSuggestions = projectWithPositions(
|
||||
editorRef.current.state.doc,
|
||||
suggestions,
|
||||
suggestions
|
||||
).filter(
|
||||
(suggestion) => suggestion.selectionStart && suggestion.selectionEnd,
|
||||
(suggestion) => suggestion.selectionStart && suggestion.selectionEnd
|
||||
);
|
||||
|
||||
const decorations = createDecorations(
|
||||
projectedSuggestions,
|
||||
editorRef.current,
|
||||
editorRef.current
|
||||
);
|
||||
|
||||
const transaction = editorRef.current.state.tr;
|
||||
|
|
@ -155,7 +155,7 @@ function areEqual(prevProps: EditorProps, nextProps: EditorProps) {
|
|||
prevProps.suggestions === nextProps.suggestions &&
|
||||
prevProps.currentVersionIndex === nextProps.currentVersionIndex &&
|
||||
prevProps.isCurrentVersion === nextProps.isCurrentVersion &&
|
||||
!(prevProps.status === 'streaming' && nextProps.status === 'streaming') &&
|
||||
!(prevProps.status === "streaming" && nextProps.status === "streaming") &&
|
||||
prevProps.content === nextProps.content &&
|
||||
prevProps.onSaveContent === nextProps.onSaveContent
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||
import type { ThemeProviderProps } from 'next-themes/dist/types';
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import type { ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import { toast as sonnerToast } from 'sonner';
|
||||
import { CheckCircleFillIcon, WarningIcon } from './icons';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { toast as sonnerToast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckCircleFillIcon, WarningIcon } from "./icons";
|
||||
|
||||
const iconsByType: Record<'success' | 'error', ReactNode> = {
|
||||
const iconsByType: Record<"success" | "error", ReactNode> = {
|
||||
success: <CheckCircleFillIcon />,
|
||||
error: <WarningIcon />,
|
||||
};
|
||||
|
||||
export function toast(props: Omit<ToastProps, 'id'>) {
|
||||
export function toast(props: Omit<ToastProps, "id">) {
|
||||
return sonnerToast.custom((id) => (
|
||||
<Toast id={id} type={props.type} description={props.description} />
|
||||
<Toast description={props.description} id={id} type={props.type} />
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -24,7 +24,9 @@ function Toast(props: ToastProps) {
|
|||
|
||||
useEffect(() => {
|
||||
const el = descriptionRef.current;
|
||||
if (!el) return;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
const lineHeight = Number.parseFloat(getComputedStyle(el).lineHeight);
|
||||
|
|
@ -37,28 +39,28 @@ function Toast(props: ToastProps) {
|
|||
ro.observe(el);
|
||||
|
||||
return () => ro.disconnect();
|
||||
}, [description]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex toast-mobile:w-[356px] w-full justify-center">
|
||||
<div
|
||||
className={cn(
|
||||
"flex toast-mobile:w-fit w-full flex-row gap-3 rounded-lg bg-zinc-100 p-3",
|
||||
multiLine ? "items-start" : "items-center"
|
||||
)}
|
||||
data-testid="toast"
|
||||
key={id}
|
||||
className={cn(
|
||||
'flex toast-mobile:w-fit w-full flex-row gap-3 rounded-lg bg-zinc-100 p-3',
|
||||
multiLine ? 'items-start' : 'items-center',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-type={type}
|
||||
className={cn(
|
||||
'data-[type=error]:text-red-600 data-[type=success]:text-green-600',
|
||||
{ 'pt-1': multiLine },
|
||||
"data-[type=error]:text-red-600 data-[type=success]:text-green-600",
|
||||
{ "pt-1": multiLine }
|
||||
)}
|
||||
data-type={type}
|
||||
>
|
||||
{iconsByType[type]}
|
||||
</div>
|
||||
<div ref={descriptionRef} className="text-sm text-zinc-950">
|
||||
<div className="text-sm text-zinc-950" ref={descriptionRef}>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -66,8 +68,8 @@ function Toast(props: ToastProps) {
|
|||
);
|
||||
}
|
||||
|
||||
interface ToastProps {
|
||||
type ToastProps = {
|
||||
id: string | number;
|
||||
type: 'success' | 'error';
|
||||
type: "success" | "error";
|
||||
description: string;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
'use client';
|
||||
import cx from 'classnames';
|
||||
"use client";
|
||||
import type { UseChatHelpers } from "@ai-sdk/react";
|
||||
import cx from "classnames";
|
||||
import {
|
||||
AnimatePresence,
|
||||
motion,
|
||||
useMotionValue,
|
||||
useTransform,
|
||||
} from 'framer-motion';
|
||||
} from "framer-motion";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
type Dispatch,
|
||||
memo,
|
||||
|
|
@ -14,21 +16,18 @@ import {
|
|||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useOnClickOutside } from 'usehooks-ts';
|
||||
import { nanoid } from 'nanoid';
|
||||
} from "react";
|
||||
import { useOnClickOutside } from "usehooks-ts";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
import { ArrowUpIcon, StopIcon, SummarizeIcon } from './icons';
|
||||
import { artifactDefinitions, type ArtifactKind } from './artifact';
|
||||
import type { ArtifactToolbarItem } from './create-artifact';
|
||||
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||
import type { ChatMessage } from '@/lib/types';
|
||||
} from "@/components/ui/tooltip";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import { type ArtifactKind, artifactDefinitions } from "./artifact";
|
||||
import type { ArtifactToolbarItem } from "./create-artifact";
|
||||
import { ArrowUpIcon, StopIcon, SummarizeIcon } from "./icons";
|
||||
|
||||
type ToolProps = {
|
||||
description: string;
|
||||
|
|
@ -38,11 +37,11 @@ type ToolProps = {
|
|||
isToolbarVisible?: boolean;
|
||||
setIsToolbarVisible?: Dispatch<SetStateAction<boolean>>;
|
||||
isAnimating: boolean;
|
||||
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
|
||||
sendMessage: UseChatHelpers<ChatMessage>["sendMessage"];
|
||||
onClick: ({
|
||||
sendMessage,
|
||||
}: {
|
||||
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
|
||||
sendMessage: UseChatHelpers<ChatMessage>["sendMessage"];
|
||||
}) => void;
|
||||
};
|
||||
|
||||
|
|
@ -89,40 +88,42 @@ const Tool = ({
|
|||
<Tooltip open={isHovered && !isAnimating}>
|
||||
<TooltipTrigger asChild>
|
||||
<motion.div
|
||||
className={cx('rounded-full p-3', {
|
||||
'bg-primary text-primary-foreground!': selectedTool === description,
|
||||
})}
|
||||
onHoverStart={() => {
|
||||
setIsHovered(true);
|
||||
}}
|
||||
onHoverEnd={() => {
|
||||
if (selectedTool !== description) setIsHovered(false);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleSelect();
|
||||
}
|
||||
}}
|
||||
initial={{ scale: 1, opacity: 0 }}
|
||||
animate={{ opacity: 1, transition: { delay: 0.1 } }}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className={cx("rounded-full p-3", {
|
||||
"bg-primary text-primary-foreground!": selectedTool === description,
|
||||
})}
|
||||
exit={{
|
||||
scale: 0.9,
|
||||
opacity: 0,
|
||||
transition: { duration: 0.1 },
|
||||
}}
|
||||
initial={{ scale: 1, opacity: 0 }}
|
||||
onClick={() => {
|
||||
handleSelect();
|
||||
}}
|
||||
onHoverEnd={() => {
|
||||
if (selectedTool !== description) {
|
||||
setIsHovered(false);
|
||||
}
|
||||
}}
|
||||
onHoverStart={() => {
|
||||
setIsHovered(true);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
handleSelect();
|
||||
}
|
||||
}}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{selectedTool === description ? <ArrowUpIcon /> : icon}
|
||||
</motion.div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="rounded-2xl bg-foreground p-3 px-4 text-background"
|
||||
side="left"
|
||||
sideOffset={16}
|
||||
className="rounded-2xl bg-foreground p-3 px-4 text-background"
|
||||
>
|
||||
{description}
|
||||
</TooltipContent>
|
||||
|
|
@ -130,7 +131,7 @@ const Tool = ({
|
|||
);
|
||||
};
|
||||
|
||||
const randomArr = [...Array(6)].map((x) => nanoid(5));
|
||||
const randomArr = [...new Array(6)].map((_x) => nanoid(5));
|
||||
|
||||
const ReadingLevelSelector = ({
|
||||
setSelectedTool,
|
||||
|
|
@ -139,15 +140,15 @@ const ReadingLevelSelector = ({
|
|||
}: {
|
||||
setSelectedTool: Dispatch<SetStateAction<string | null>>;
|
||||
isAnimating: boolean;
|
||||
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
|
||||
sendMessage: UseChatHelpers<ChatMessage>["sendMessage"];
|
||||
}) => {
|
||||
const LEVELS = [
|
||||
'Elementary',
|
||||
'Middle School',
|
||||
'Keep current level',
|
||||
'High School',
|
||||
'College',
|
||||
'Graduate',
|
||||
"Elementary",
|
||||
"Middle School",
|
||||
"Keep current level",
|
||||
"High School",
|
||||
"College",
|
||||
"Graduate",
|
||||
];
|
||||
|
||||
const y = useMotionValue(-40 * 2);
|
||||
|
|
@ -159,7 +160,7 @@ const ReadingLevelSelector = ({
|
|||
useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = yToLevel.on('change', (latest) => {
|
||||
const unsubscribe = yToLevel.on("change", (latest) => {
|
||||
const level = Math.min(5, Math.max(0, Math.round(Math.abs(latest))));
|
||||
setCurrentLevel(level);
|
||||
});
|
||||
|
|
@ -171,11 +172,11 @@ const ReadingLevelSelector = ({
|
|||
<div className="relative flex flex-col items-center justify-end">
|
||||
{randomArr.map((id) => (
|
||||
<motion.div
|
||||
key={id}
|
||||
className="flex size-[40px] flex-row items-center justify-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex size-[40px] flex-row items-center justify-center"
|
||||
exit={{ opacity: 0 }}
|
||||
initial={{ opacity: 0 }}
|
||||
key={id}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<div className="size-2 rounded-full bg-muted-foreground/40" />
|
||||
|
|
@ -187,37 +188,23 @@ const ReadingLevelSelector = ({
|
|||
<TooltipTrigger asChild>
|
||||
<motion.div
|
||||
className={cx(
|
||||
'absolute flex flex-row items-center rounded-full border bg-background p-3',
|
||||
"absolute flex flex-row items-center rounded-full border bg-background p-3",
|
||||
{
|
||||
'bg-primary text-primary-foreground': currentLevel !== 2,
|
||||
'bg-background text-foreground': currentLevel === 2,
|
||||
},
|
||||
"bg-primary text-primary-foreground": currentLevel !== 2,
|
||||
"bg-background text-foreground": currentLevel === 2,
|
||||
}
|
||||
)}
|
||||
style={{ y }}
|
||||
drag="y"
|
||||
dragConstraints={{ top: -dragConstraints, bottom: 0 }}
|
||||
dragElastic={0}
|
||||
dragMomentum={false}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
dragConstraints={{ top: -dragConstraints, bottom: 0 }}
|
||||
onDragStart={() => {
|
||||
setHasUserSelectedLevel(false);
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
if (currentLevel === 2) {
|
||||
setSelectedTool(null);
|
||||
} else {
|
||||
setHasUserSelectedLevel(true);
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (currentLevel !== 2 && hasUserSelectedLevel) {
|
||||
sendMessage({
|
||||
role: 'user',
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: 'text',
|
||||
type: "text",
|
||||
text: `Please adjust the reading level to ${LEVELS[currentLevel]} level.`,
|
||||
},
|
||||
],
|
||||
|
|
@ -226,14 +213,28 @@ const ReadingLevelSelector = ({
|
|||
setSelectedTool(null);
|
||||
}
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
if (currentLevel === 2) {
|
||||
setSelectedTool(null);
|
||||
} else {
|
||||
setHasUserSelectedLevel(true);
|
||||
}
|
||||
}}
|
||||
onDragStart={() => {
|
||||
setHasUserSelectedLevel(false);
|
||||
}}
|
||||
style={{ y }}
|
||||
transition={{ duration: 0.1 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{currentLevel === 2 ? <SummarizeIcon /> : <ArrowUpIcon />}
|
||||
</motion.div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="rounded-2xl bg-foreground p-3 px-4 text-background text-sm"
|
||||
side="left"
|
||||
sideOffset={16}
|
||||
className="rounded-2xl bg-foreground p-3 px-4 text-background text-sm"
|
||||
>
|
||||
{LEVELS[currentLevel]}
|
||||
</TooltipContent>
|
||||
|
|
@ -255,32 +256,32 @@ export const Tools = ({
|
|||
isToolbarVisible: boolean;
|
||||
selectedTool: string | null;
|
||||
setSelectedTool: Dispatch<SetStateAction<string | null>>;
|
||||
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
|
||||
sendMessage: UseChatHelpers<ChatMessage>["sendMessage"];
|
||||
isAnimating: boolean;
|
||||
setIsToolbarVisible: Dispatch<SetStateAction<boolean>>;
|
||||
tools: Array<ArtifactToolbarItem>;
|
||||
tools: ArtifactToolbarItem[];
|
||||
}) => {
|
||||
const [primaryTool, ...secondaryTools] = tools;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="flex flex-col gap-1.5"
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="flex flex-col gap-1.5"
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{isToolbarVisible &&
|
||||
secondaryTools.map((secondaryTool) => (
|
||||
<Tool
|
||||
key={secondaryTool.description}
|
||||
description={secondaryTool.description}
|
||||
icon={secondaryTool.icon}
|
||||
selectedTool={selectedTool}
|
||||
setSelectedTool={setSelectedTool}
|
||||
sendMessage={sendMessage}
|
||||
isAnimating={isAnimating}
|
||||
key={secondaryTool.description}
|
||||
onClick={secondaryTool.onClick}
|
||||
selectedTool={selectedTool}
|
||||
sendMessage={sendMessage}
|
||||
setSelectedTool={setSelectedTool}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
|
@ -288,13 +289,13 @@ export const Tools = ({
|
|||
<Tool
|
||||
description={primaryTool.description}
|
||||
icon={primaryTool.icon}
|
||||
selectedTool={selectedTool}
|
||||
setSelectedTool={setSelectedTool}
|
||||
isToolbarVisible={isToolbarVisible}
|
||||
setIsToolbarVisible={setIsToolbarVisible}
|
||||
sendMessage={sendMessage}
|
||||
isAnimating={isAnimating}
|
||||
isToolbarVisible={isToolbarVisible}
|
||||
onClick={primaryTool.onClick}
|
||||
selectedTool={selectedTool}
|
||||
sendMessage={sendMessage}
|
||||
setIsToolbarVisible={setIsToolbarVisible}
|
||||
setSelectedTool={setSelectedTool}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
|
|
@ -311,10 +312,10 @@ const PureToolbar = ({
|
|||
}: {
|
||||
isToolbarVisible: boolean;
|
||||
setIsToolbarVisible: Dispatch<SetStateAction<boolean>>;
|
||||
status: UseChatHelpers<ChatMessage>['status'];
|
||||
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
|
||||
stop: UseChatHelpers<ChatMessage>['stop'];
|
||||
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
|
||||
status: UseChatHelpers<ChatMessage>["status"];
|
||||
sendMessage: UseChatHelpers<ChatMessage>["sendMessage"];
|
||||
stop: UseChatHelpers<ChatMessage>["stop"];
|
||||
setMessages: UseChatHelpers<ChatMessage>["setMessages"];
|
||||
artifactKind: ArtifactKind;
|
||||
}) => {
|
||||
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -354,17 +355,17 @@ const PureToolbar = ({
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'streaming') {
|
||||
if (status === "streaming") {
|
||||
setIsToolbarVisible(false);
|
||||
}
|
||||
}, [status, setIsToolbarVisible]);
|
||||
|
||||
const artifactDefinition = artifactDefinitions.find(
|
||||
(definition) => definition.kind === artifactKind,
|
||||
(definition) => definition.kind === artifactKind
|
||||
);
|
||||
|
||||
if (!artifactDefinition) {
|
||||
throw new Error('Artifact definition not found!');
|
||||
throw new Error("Artifact definition not found!");
|
||||
}
|
||||
|
||||
const toolsByArtifactKind = artifactDefinition.toolbar;
|
||||
|
|
@ -376,11 +377,9 @@ const PureToolbar = ({
|
|||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<motion.div
|
||||
className="absolute right-6 bottom-6 flex cursor-pointer flex-col justify-end rounded-full border bg-background p-1.5 shadow-lg"
|
||||
initial={{ opacity: 0, y: -20, scale: 1 }}
|
||||
animate={
|
||||
isToolbarVisible
|
||||
? selectedTool === 'adjust-reading-level'
|
||||
? selectedTool === "adjust-reading-level"
|
||||
? {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
|
|
@ -397,34 +396,40 @@ const PureToolbar = ({
|
|||
}
|
||||
: { opacity: 1, y: 0, height: 54, transition: { delay: 0 } }
|
||||
}
|
||||
className="absolute right-6 bottom-6 flex cursor-pointer flex-col justify-end rounded-full border bg-background p-1.5 shadow-lg"
|
||||
exit={{ opacity: 0, y: -20, transition: { duration: 0.1 } }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||
onHoverStart={() => {
|
||||
if (status === 'streaming') return;
|
||||
|
||||
cancelCloseTimer();
|
||||
setIsToolbarVisible(true);
|
||||
}}
|
||||
onHoverEnd={() => {
|
||||
if (status === 'streaming') return;
|
||||
|
||||
startCloseTimer();
|
||||
initial={{ opacity: 0, y: -20, scale: 1 }}
|
||||
onAnimationComplete={() => {
|
||||
setIsAnimating(false);
|
||||
}}
|
||||
onAnimationStart={() => {
|
||||
setIsAnimating(true);
|
||||
}}
|
||||
onAnimationComplete={() => {
|
||||
setIsAnimating(false);
|
||||
onHoverEnd={() => {
|
||||
if (status === "streaming") {
|
||||
return;
|
||||
}
|
||||
|
||||
startCloseTimer();
|
||||
}}
|
||||
onHoverStart={() => {
|
||||
if (status === "streaming") {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelCloseTimer();
|
||||
setIsToolbarVisible(true);
|
||||
}}
|
||||
ref={toolbarRef}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
||||
>
|
||||
{status === 'streaming' ? (
|
||||
{status === "streaming" ? (
|
||||
<motion.div
|
||||
key="stop-icon"
|
||||
initial={{ scale: 1 }}
|
||||
animate={{ scale: 1.4 }}
|
||||
exit={{ scale: 1 }}
|
||||
className="p-3"
|
||||
exit={{ scale: 1 }}
|
||||
initial={{ scale: 1 }}
|
||||
key="stop-icon"
|
||||
onClick={() => {
|
||||
stop();
|
||||
setMessages((messages) => messages);
|
||||
|
|
@ -432,20 +437,20 @@ const PureToolbar = ({
|
|||
>
|
||||
<StopIcon />
|
||||
</motion.div>
|
||||
) : selectedTool === 'adjust-reading-level' ? (
|
||||
) : selectedTool === "adjust-reading-level" ? (
|
||||
<ReadingLevelSelector
|
||||
isAnimating={isAnimating}
|
||||
key="reading-level-selector"
|
||||
sendMessage={sendMessage}
|
||||
setSelectedTool={setSelectedTool}
|
||||
isAnimating={isAnimating}
|
||||
/>
|
||||
) : (
|
||||
<Tools
|
||||
key="tools"
|
||||
sendMessage={sendMessage}
|
||||
isAnimating={isAnimating}
|
||||
isToolbarVisible={isToolbarVisible}
|
||||
key="tools"
|
||||
selectedTool={selectedTool}
|
||||
sendMessage={sendMessage}
|
||||
setIsToolbarVisible={setIsToolbarVisible}
|
||||
setSelectedTool={setSelectedTool}
|
||||
tools={toolsByArtifactKind}
|
||||
|
|
@ -457,9 +462,15 @@ const PureToolbar = ({
|
|||
};
|
||||
|
||||
export const Toolbar = memo(PureToolbar, (prevProps, nextProps) => {
|
||||
if (prevProps.status !== nextProps.status) return false;
|
||||
if (prevProps.isToolbarVisible !== nextProps.isToolbarVisible) return false;
|
||||
if (prevProps.artifactKind !== nextProps.artifactKind) return false;
|
||||
if (prevProps.status !== nextProps.status) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.isToolbarVisible !== nextProps.isToolbarVisible) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.artifactKind !== nextProps.artifactKind) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
'use client';
|
||||
"use client"
|
||||
|
||||
import * as React from 'react';
|
||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
||||
import * as React from "react"
|
||||
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root;
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
|
|
@ -18,14 +18,14 @@ const AlertDialogOverlay = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||
className,
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
|
|
@ -36,14 +36,14 @@ const AlertDialogContent = React.forwardRef<
|
|||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:rounded-lg',
|
||||
className,
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
));
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
|
|
@ -51,13 +51,13 @@ const AlertDialogHeader = ({
|
|||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-2 text-center sm:text-left',
|
||||
className,
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
AlertDialogHeader.displayName = 'AlertDialogHeader';
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
|
|
@ -65,13 +65,13 @@ const AlertDialogFooter = ({
|
|||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className,
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
AlertDialogFooter.displayName = 'AlertDialogFooter';
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
|
|
@ -79,11 +79,11 @@ const AlertDialogTitle = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('font-semibold text-lg', className)}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
|
|
@ -91,12 +91,12 @@ const AlertDialogDescription = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
))
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName;
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
|
|
@ -107,8 +107,8 @@ const AlertDialogAction = React.forwardRef<
|
|||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
|
|
@ -117,14 +117,14 @@ const AlertDialogCancel = React.forwardRef<
|
|||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'mt-2 sm:mt-0',
|
||||
className,
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
|
|
@ -138,4 +138,4 @@ export {
|
|||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
'use client';
|
||||
"use client"
|
||||
|
||||
import * as React from 'react';
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||
import * as React from "react"
|
||||
import { Avatar as AvatarPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
|
|
@ -12,13 +12,13 @@ const Avatar = React.forwardRef<
|
|||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
|
||||
className,
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
|
|
@ -26,11 +26,11 @@ const AvatarImage = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn('aspect-square h-full w-full', className)}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
|
|
@ -39,12 +39,12 @@ const AvatarFallback = React.forwardRef<
|
|||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full items-center justify-center rounded-full bg-muted',
|
||||
className,
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
|
|
@ -30,7 +30,7 @@ export interface BadgeProps
|
|||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
export { Badge, badgeVariants }
|
||||
|
|
|
|||
|
|
@ -1,56 +1,56 @@
|
|||
import * as React from 'react';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from "react"
|
||||
import { Slot as SlotPrimitive } from "radix-ui"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
const Comp = asChild ? SlotPrimitive.Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants };
|
||||
export { Button, buttonVariants }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
|
|
@ -9,13 +9,13 @@ const Card = React.forwardRef<
|
|||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-lg border bg-card text-card-foreground shadow-xs',
|
||||
className,
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = 'Card';
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
|
|
@ -23,11 +23,11 @@ const CardHeader = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
|
|
@ -36,13 +36,13 @@ const CardTitle = React.forwardRef<
|
|||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'font-semibold text-2xl leading-none tracking-tight',
|
||||
className,
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
|
|
@ -50,19 +50,19 @@ const CardDescription = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = 'CardDescription';
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = 'CardContent';
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
|
|
@ -70,17 +70,10 @@ const CardFooter = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center p-6 pt-0', className)}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
|
|
|
|||
|
|
@ -1,45 +1,45 @@
|
|||
'use client';
|
||||
"use client"
|
||||
|
||||
import * as React from 'react';
|
||||
import * as React from "react"
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from 'embla-carousel-react';
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
} from "embla-carousel-react"
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: "horizontal" | "vertical"
|
||||
setApi?: (api: CarouselApi) => void
|
||||
}
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext);
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useCarousel must be used within a <Carousel />');
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
}
|
||||
|
||||
return context;
|
||||
return context
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef<
|
||||
|
|
@ -48,7 +48,7 @@ const Carousel = React.forwardRef<
|
|||
>(
|
||||
(
|
||||
{
|
||||
orientation = 'horizontal',
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
|
|
@ -56,69 +56,69 @@ const Carousel = React.forwardRef<
|
|||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
ref
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === 'horizontal' ? 'x' : 'y',
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
plugins
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext],
|
||||
);
|
||||
[scrollPrev, scrollNext]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
onSelect(api);
|
||||
api.on('reInit', onSelect);
|
||||
api.on('select', onSelect);
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
|
||||
return () => {
|
||||
api?.off('select', onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
api?.off("select", onSelect)
|
||||
}
|
||||
}, [api, onSelect])
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
|
|
@ -127,7 +127,7 @@ const Carousel = React.forwardRef<
|
|||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
|
|
@ -137,7 +137,7 @@ const Carousel = React.forwardRef<
|
|||
<div
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn('relative', className)}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
|
|
@ -145,38 +145,38 @@ const Carousel = React.forwardRef<
|
|||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
Carousel.displayName = 'Carousel';
|
||||
)
|
||||
}
|
||||
)
|
||||
Carousel.displayName = "Carousel"
|
||||
|
||||
const CarouselContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className="overflow-hidden">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex',
|
||||
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||
className,
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CarouselContent.displayName = 'CarouselContent';
|
||||
)
|
||||
})
|
||||
CarouselContent.displayName = "CarouselContent"
|
||||
|
||||
const CarouselItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel();
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -184,21 +184,21 @@ const CarouselItem = React.forwardRef<
|
|||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn(
|
||||
'min-w-0 shrink-0 grow-0 basis-full',
|
||||
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||
className,
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
CarouselItem.displayName = 'CarouselItem';
|
||||
)
|
||||
})
|
||||
CarouselItem.displayName = "CarouselItem"
|
||||
|
||||
const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
|
@ -206,11 +206,11 @@ const CarouselPrevious = React.forwardRef<
|
|||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'absolute h-8 w-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? '-left-12 -translate-y-1/2 top-1/2'
|
||||
: '-top-12 -translate-x-1/2 left-1/2 rotate-90',
|
||||
className,
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-left-12 top-1/2 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
|
|
@ -219,15 +219,15 @@ const CarouselPrevious = React.forwardRef<
|
|||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CarouselPrevious.displayName = 'CarouselPrevious';
|
||||
)
|
||||
})
|
||||
CarouselPrevious.displayName = "CarouselPrevious"
|
||||
|
||||
const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
|
@ -235,11 +235,11 @@ const CarouselNext = React.forwardRef<
|
|||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'absolute h-8 w-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? '-right-12 -translate-y-1/2 top-1/2'
|
||||
: '-bottom-12 -translate-x-1/2 left-1/2 rotate-90',
|
||||
className,
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-right-12 top-1/2 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
|
|
@ -248,9 +248,9 @@ const CarouselNext = React.forwardRef<
|
|||
<ArrowRight className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CarouselNext.displayName = 'CarouselNext';
|
||||
)
|
||||
})
|
||||
CarouselNext.displayName = "CarouselNext"
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
|
|
@ -259,4 +259,4 @@ export {
|
|||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
'use client';
|
||||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
|
|
|
|||
|
|
@ -1,44 +1,44 @@
|
|||
'use client';
|
||||
"use client"
|
||||
|
||||
import * as React from 'react';
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { Check, ChevronRight, Circle } from 'lucide-react';
|
||||
import * as React from "react"
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
|
|
@ -47,14 +47,14 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||
className,
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
|
|
@ -65,32 +65,32 @@ const DropdownMenuContent = React.forwardRef<
|
|||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||
className,
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
|
|
@ -99,8 +99,8 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
className,
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
|
|
@ -112,9 +112,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
|
|
@ -123,8 +123,8 @@ const DropdownMenuRadioItem = React.forwardRef<
|
|||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
className,
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -135,26 +135,26 @@ const DropdownMenuRadioItem = React.forwardRef<
|
|||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'px-2 py-1.5 font-semibold text-sm',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
|
|
@ -162,11 +162,11 @@ const DropdownMenuSeparator = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
|
|
@ -174,12 +174,12 @@ const DropdownMenuShortcut = ({
|
|||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
|
|
@ -197,4 +197,4 @@ export {
|
|||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
'use client';
|
||||
"use client"
|
||||
|
||||
import * as React from 'react';
|
||||
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
|
||||
import * as React from "react"
|
||||
import { HoverCard as HoverCardPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root;
|
||||
const HoverCard = HoverCardPrimitive.Root
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||
className,
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
||||
))
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import * as React from 'react';
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className,
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input };
|
||||
export { Input }
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue