NeoPy commited on
Commit
867b17d
·
verified ·
1 Parent(s): f42cf71

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +19 -0
  2. .eslintrc.json +23 -0
  3. .gitattributes +1 -0
  4. .gitignore +43 -0
  5. .vscode/extensions.json +3 -0
  6. .vscode/settings.json +17 -0
  7. Dockerfile +19 -0
  8. LICENSE +13 -0
  9. README.md +62 -10
  10. app/(auth)/actions.ts +84 -0
  11. app/(auth)/api/auth/[...nextauth]/route.ts +1 -0
  12. app/(auth)/api/auth/guest/route.ts +21 -0
  13. app/(auth)/auth.config.ts +13 -0
  14. app/(auth)/auth.ts +92 -0
  15. app/(auth)/login/page.tsx +77 -0
  16. app/(auth)/register/page.tsx +78 -0
  17. app/(chat)/actions.ts +53 -0
  18. app/(chat)/api/chat/[id]/stream/route.ts +112 -0
  19. app/(chat)/api/chat/route.ts +251 -0
  20. app/(chat)/api/chat/schema.ts +28 -0
  21. app/(chat)/api/document/route.ts +126 -0
  22. app/(chat)/api/files/upload/route.ts +68 -0
  23. app/(chat)/api/history/route.ts +34 -0
  24. app/(chat)/api/suggestions/route.ts +37 -0
  25. app/(chat)/api/vote/route.ts +75 -0
  26. app/(chat)/chat/[id]/page.tsx +76 -0
  27. app/(chat)/layout.tsx +33 -0
  28. app/(chat)/opengraph-image.png +3 -0
  29. app/(chat)/page.tsx +55 -0
  30. app/(chat)/twitter-image.png +0 -0
  31. app/favicon.ico +0 -0
  32. app/globals.css +164 -0
  33. app/layout.tsx +86 -0
  34. artifacts/actions.ts +8 -0
  35. artifacts/code/client.tsx +280 -0
  36. artifacts/code/server.ts +75 -0
  37. artifacts/image/client.tsx +76 -0
  38. artifacts/image/server.ts +45 -0
  39. artifacts/sheet/client.tsx +121 -0
  40. artifacts/sheet/server.ts +81 -0
  41. artifacts/text/client.tsx +181 -0
  42. artifacts/text/server.ts +73 -0
  43. biome.jsonc +135 -0
  44. components.json +20 -0
  45. components/app-sidebar.tsx +67 -0
  46. components/artifact-actions.tsx +100 -0
  47. components/artifact-close-button.tsx +30 -0
  48. components/artifact-messages.tsx +99 -0
  49. components/artifact.tsx +511 -0
  50. components/auth-form.tsx +60 -0
.env.example ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
2
+ AUTH_SECRET=****
3
+
4
+ # The following keys below are automatically created and
5
+ # added to your environment when you deploy on vercel
6
+
7
+ # Get your xAI API Key here for chat and image models: https://console.x.ai/
8
+ XAI_API_KEY=****
9
+
10
+ # Instructions to create a Vercel Blob Store here: https://vercel.com/docs/storage/vercel-blob
11
+ BLOB_READ_WRITE_TOKEN=****
12
+
13
+ # Instructions to create a PostgreSQL database here: https://vercel.com/docs/storage/vercel-postgres/quickstart
14
+ POSTGRES_URL=****
15
+
16
+
17
+ # Instructions to create a Redis store here:
18
+ # https://vercel.com/docs/redis
19
+ REDIS_URL=****
.eslintrc.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": [
3
+ "next/core-web-vitals",
4
+ "plugin:import/recommended",
5
+ "plugin:import/typescript",
6
+ "prettier",
7
+ "plugin:tailwindcss/recommended"
8
+ ],
9
+ "plugins": ["tailwindcss"],
10
+ "rules": {
11
+ "tailwindcss/no-custom-classname": "off",
12
+ "tailwindcss/classnames-order": "off"
13
+ },
14
+ "settings": {
15
+ "import/resolver": {
16
+ "typescript": {
17
+ "alwaysTryTypes": true,
18
+ "project": "./tsconfig.json"
19
+ }
20
+ }
21
+ },
22
+ "ignorePatterns": ["**/components/ui/**"]
23
+ }
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ app/(chat)/opengraph-image.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ node_modules
5
+ .pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ coverage
10
+
11
+ # next.js
12
+ .next/
13
+ out/
14
+ build
15
+
16
+ # misc
17
+ .DS_Store
18
+ *.pem
19
+
20
+ # debug
21
+ npm-debug.log*
22
+ yarn-debug.log*
23
+ yarn-error.log*
24
+ .pnpm-debug.log*
25
+
26
+ # local env files
27
+ .env.local
28
+ .env.development.local
29
+ .env.test.local
30
+ .env.production.local
31
+
32
+ # turbo
33
+ .turbo
34
+
35
+ .env
36
+ .vercel
37
+ .env*.local
38
+
39
+ # Playwright
40
+ /test-results/
41
+ /playwright-report/
42
+ /blob-report/
43
+ /playwright/*
.vscode/extensions.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "recommendations": ["biomejs.biome"]
3
+ }
.vscode/settings.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "editor.formatOnSave": true,
3
+ "[javascript]": {
4
+ "editor.defaultFormatter": "biomejs.biome"
5
+ },
6
+ "[typescript]": {
7
+ "editor.defaultFormatter": "biomejs.biome"
8
+ },
9
+ "[typescriptreact]": {
10
+ "editor.defaultFormatter": "biomejs.biome"
11
+ },
12
+ "typescript.tsdk": "node_modules/typescript/lib",
13
+ "eslint.workingDirectories": [
14
+ { "pattern": "app/*" },
15
+ { "pattern": "packages/*" }
16
+ ]
17
+ }
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine
2
+ USER root
3
+
4
+ USER 1000
5
+ WORKDIR /usr/src/app
6
+ # Copy package.json and package-lock.json to the container
7
+ COPY --chown=1000 package.json package-lock.json ./
8
+
9
+ # Copy the rest of the application files to the container
10
+ COPY --chown=1000 . .
11
+
12
+ RUN npm install
13
+ RUN npm run build
14
+
15
+ # Expose the application port (assuming your app runs on port 3000)
16
+ EXPOSE 3000
17
+
18
+ # Start the application
19
+ CMD ["npm", "start"]
LICENSE ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright 2024 Vercel, Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
README.md CHANGED
@@ -1,10 +1,62 @@
1
- ---
2
- title: Next Chat
3
- emoji: 👀
4
- colorFrom: yellow
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <a href="https://chat.vercel.ai/">
2
+ <img alt="Next.js 14 and App Router-ready AI chatbot." src="app/(chat)/opengraph-image.png">
3
+ <h1 align="center">Chat SDK</h1>
4
+ </a>
5
+
6
+ <p align="center">
7
+ Chat SDK is a free, open-source template built with Next.js and the AI SDK that helps you quickly build powerful chatbot applications.
8
+ </p>
9
+
10
+ <p align="center">
11
+ <a href="https://chat-sdk.dev"><strong>Read Docs</strong></a> ·
12
+ <a href="#features"><strong>Features</strong></a> ·
13
+ <a href="#model-providers"><strong>Model Providers</strong></a> ·
14
+ <a href="#deploy-your-own"><strong>Deploy Your Own</strong></a> ·
15
+ <a href="#running-locally"><strong>Running locally</strong></a>
16
+ </p>
17
+ <br/>
18
+
19
+ ## Features
20
+
21
+ - [Next.js](https://nextjs.org) App Router
22
+ - Advanced routing for seamless navigation and performance
23
+ - React Server Components (RSCs) and Server Actions for server-side rendering and increased performance
24
+ - [AI SDK](https://sdk.vercel.ai/docs)
25
+ - Unified API for generating text, structured objects, and tool calls with LLMs
26
+ - Hooks for building dynamic chat and generative user interfaces
27
+ - Supports xAI (default), OpenAI, Fireworks, and other model providers
28
+ - [shadcn/ui](https://ui.shadcn.com)
29
+ - Styling with [Tailwind CSS](https://tailwindcss.com)
30
+ - Component primitives from [Radix UI](https://radix-ui.com) for accessibility and flexibility
31
+ - Data Persistence
32
+ - [Neon Serverless Postgres](https://vercel.com/marketplace/neon) for saving chat history and user data
33
+ - [Vercel Blob](https://vercel.com/storage/blob) for efficient file storage
34
+ - [Auth.js](https://authjs.dev)
35
+ - Simple and secure authentication
36
+
37
+ ## Model Providers
38
+
39
+ This template ships with [xAI](https://x.ai) `grok-2-1212` as the default chat model. However, with the [AI SDK](https://sdk.vercel.ai/docs), you can switch LLM providers to [OpenAI](https://openai.com), [Anthropic](https://anthropic.com), [Cohere](https://cohere.com/), and [many more](https://sdk.vercel.ai/providers/ai-sdk-providers) with just a few lines of code.
40
+
41
+ ## Deploy Your Own
42
+
43
+ You can deploy your own version of the Next.js AI Chatbot to Vercel with one click:
44
+
45
+ [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fai-chatbot&env=AUTH_SECRET&envDescription=Learn+more+about+how+to+get+the+API+Keys+for+the+application&envLink=https%3A%2F%2Fgithub.com%2Fvercel%2Fai-chatbot%2Fblob%2Fmain%2F.env.example&demo-title=AI+Chatbot&demo-description=An+Open-Source+AI+Chatbot+Template+Built+With+Next.js+and+the+AI+SDK+by+Vercel.&demo-url=https%3A%2F%2Fchat.vercel.ai&products=%5B%7B%22type%22%3A%22integration%22%2C%22protocol%22%3A%22ai%22%2C%22productSlug%22%3A%22grok%22%2C%22integrationSlug%22%3A%22xai%22%7D%2C%7B%22type%22%3A%22integration%22%2C%22protocol%22%3A%22storage%22%2C%22productSlug%22%3A%22neon%22%2C%22integrationSlug%22%3A%22neon%22%7D%2C%7B%22type%22%3A%22integration%22%2C%22protocol%22%3A%22storage%22%2C%22productSlug%22%3A%22upstash-kv%22%2C%22integrationSlug%22%3A%22upstash%22%7D%2C%7B%22type%22%3A%22blob%22%7D%5D)
46
+
47
+ ## Running locally
48
+
49
+ You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js AI Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/projects/environment-variables) for this, but a `.env` file is all that is necessary.
50
+
51
+ > Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various AI and authentication provider accounts.
52
+
53
+ 1. Install Vercel CLI: `npm i -g vercel`
54
+ 2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`
55
+ 3. Download your environment variables: `vercel env pull`
56
+
57
+ ```bash
58
+ pnpm install
59
+ pnpm dev
60
+ ```
61
+
62
+ Your app template should now be running on [localhost:3000](http://localhost:3000).
app/(auth)/actions.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use server';
2
+
3
+ import { z } from 'zod';
4
+
5
+ import { createUser, getUser } from '@/lib/db/queries';
6
+
7
+ import { signIn } from './auth';
8
+
9
+ const authFormSchema = z.object({
10
+ email: z.string().email(),
11
+ password: z.string().min(6),
12
+ });
13
+
14
+ export interface LoginActionState {
15
+ status: 'idle' | 'in_progress' | 'success' | 'failed' | 'invalid_data';
16
+ }
17
+
18
+ export const login = async (
19
+ _: LoginActionState,
20
+ formData: FormData,
21
+ ): Promise<LoginActionState> => {
22
+ try {
23
+ const validatedData = authFormSchema.parse({
24
+ email: formData.get('email'),
25
+ password: formData.get('password'),
26
+ });
27
+
28
+ await signIn('credentials', {
29
+ email: validatedData.email,
30
+ password: validatedData.password,
31
+ redirect: false,
32
+ });
33
+
34
+ return { status: 'success' };
35
+ } catch (error) {
36
+ if (error instanceof z.ZodError) {
37
+ return { status: 'invalid_data' };
38
+ }
39
+
40
+ return { status: 'failed' };
41
+ }
42
+ };
43
+
44
+ export interface RegisterActionState {
45
+ status:
46
+ | 'idle'
47
+ | 'in_progress'
48
+ | 'success'
49
+ | 'failed'
50
+ | 'user_exists'
51
+ | 'invalid_data';
52
+ }
53
+
54
+ export const register = async (
55
+ _: RegisterActionState,
56
+ formData: FormData,
57
+ ): Promise<RegisterActionState> => {
58
+ try {
59
+ const validatedData = authFormSchema.parse({
60
+ email: formData.get('email'),
61
+ password: formData.get('password'),
62
+ });
63
+
64
+ const [user] = await getUser(validatedData.email);
65
+
66
+ if (user) {
67
+ return { status: 'user_exists' } as RegisterActionState;
68
+ }
69
+ await createUser(validatedData.email, validatedData.password);
70
+ await signIn('credentials', {
71
+ email: validatedData.email,
72
+ password: validatedData.password,
73
+ redirect: false,
74
+ });
75
+
76
+ return { status: 'success' };
77
+ } catch (error) {
78
+ if (error instanceof z.ZodError) {
79
+ return { status: 'invalid_data' };
80
+ }
81
+
82
+ return { status: 'failed' };
83
+ }
84
+ };
app/(auth)/api/auth/[...nextauth]/route.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ export { GET, POST } from '@/app/(auth)/auth';
app/(auth)/api/auth/guest/route.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { signIn } from '@/app/(auth)/auth';
2
+ import { isDevelopmentEnvironment } from '@/lib/constants';
3
+ import { getToken } from 'next-auth/jwt';
4
+ import { NextResponse } from 'next/server';
5
+
6
+ export async function GET(request: Request) {
7
+ const { searchParams } = new URL(request.url);
8
+ const redirectUrl = searchParams.get('redirectUrl') || '/';
9
+
10
+ const token = await getToken({
11
+ req: request,
12
+ secret: process.env.AUTH_SECRET,
13
+ secureCookie: !isDevelopmentEnvironment,
14
+ });
15
+
16
+ if (token) {
17
+ return NextResponse.redirect(new URL('/', request.url));
18
+ }
19
+
20
+ return signIn('guest', { redirect: true, redirectTo: redirectUrl });
21
+ }
app/(auth)/auth.config.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextAuthConfig } from 'next-auth';
2
+
3
+ export const authConfig = {
4
+ pages: {
5
+ signIn: '/login',
6
+ newUser: '/',
7
+ },
8
+ providers: [
9
+ // added later in auth.ts since it requires bcrypt which is only compatible with Node.js
10
+ // while this file is also used in non-Node.js environments
11
+ ],
12
+ callbacks: {},
13
+ } satisfies NextAuthConfig;
app/(auth)/auth.ts ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { compare } from 'bcrypt-ts';
2
+ import NextAuth, { type DefaultSession } from 'next-auth';
3
+ import Credentials from 'next-auth/providers/credentials';
4
+ import { createGuestUser, getUser } from '@/lib/db/queries';
5
+ import { authConfig } from './auth.config';
6
+ import { DUMMY_PASSWORD } from '@/lib/constants';
7
+ import type { DefaultJWT } from 'next-auth/jwt';
8
+
9
+ export type UserType = 'guest' | 'regular';
10
+
11
+ declare module 'next-auth' {
12
+ interface Session extends DefaultSession {
13
+ user: {
14
+ id: string;
15
+ type: UserType;
16
+ } & DefaultSession['user'];
17
+ }
18
+
19
+ interface User {
20
+ id?: string;
21
+ email?: string | null;
22
+ type: UserType;
23
+ }
24
+ }
25
+
26
+ declare module 'next-auth/jwt' {
27
+ interface JWT extends DefaultJWT {
28
+ id: string;
29
+ type: UserType;
30
+ }
31
+ }
32
+
33
+ export const {
34
+ handlers: { GET, POST },
35
+ auth,
36
+ signIn,
37
+ signOut,
38
+ } = NextAuth({
39
+ ...authConfig,
40
+ providers: [
41
+ Credentials({
42
+ credentials: {},
43
+ async authorize({ email, password }: any) {
44
+ const users = await getUser(email);
45
+
46
+ if (users.length === 0) {
47
+ await compare(password, DUMMY_PASSWORD);
48
+ return null;
49
+ }
50
+
51
+ const [user] = users;
52
+
53
+ if (!user.password) {
54
+ await compare(password, DUMMY_PASSWORD);
55
+ return null;
56
+ }
57
+
58
+ const passwordsMatch = await compare(password, user.password);
59
+
60
+ if (!passwordsMatch) return null;
61
+
62
+ return { ...user, type: 'regular' };
63
+ },
64
+ }),
65
+ Credentials({
66
+ id: 'guest',
67
+ credentials: {},
68
+ async authorize() {
69
+ const [guestUser] = await createGuestUser();
70
+ return { ...guestUser, type: 'guest' };
71
+ },
72
+ }),
73
+ ],
74
+ callbacks: {
75
+ async jwt({ token, user }) {
76
+ if (user) {
77
+ token.id = user.id as string;
78
+ token.type = user.type;
79
+ }
80
+
81
+ return token;
82
+ },
83
+ async session({ session, token }) {
84
+ if (session.user) {
85
+ session.user.id = token.id;
86
+ session.user.type = token.type;
87
+ }
88
+
89
+ return session;
90
+ },
91
+ },
92
+ });
app/(auth)/login/page.tsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useActionState, useEffect, useState } from 'react';
6
+ import { toast } from '@/components/toast';
7
+
8
+ import { AuthForm } from '@/components/auth-form';
9
+ import { SubmitButton } from '@/components/submit-button';
10
+
11
+ import { login, type LoginActionState } from '../actions';
12
+ import { useSession } from 'next-auth/react';
13
+
14
+ export default function Page() {
15
+ const router = useRouter();
16
+
17
+ const [email, setEmail] = useState('');
18
+ const [isSuccessful, setIsSuccessful] = useState(false);
19
+
20
+ const [state, formAction] = useActionState<LoginActionState, FormData>(
21
+ login,
22
+ {
23
+ status: 'idle',
24
+ },
25
+ );
26
+
27
+ const { update: updateSession } = useSession();
28
+
29
+ useEffect(() => {
30
+ if (state.status === 'failed') {
31
+ toast({
32
+ type: 'error',
33
+ description: 'Invalid credentials!',
34
+ });
35
+ } else if (state.status === 'invalid_data') {
36
+ toast({
37
+ type: 'error',
38
+ description: 'Failed validating your submission!',
39
+ });
40
+ } else if (state.status === 'success') {
41
+ setIsSuccessful(true);
42
+ updateSession();
43
+ router.refresh();
44
+ }
45
+ }, [state.status]);
46
+
47
+ const handleSubmit = (formData: FormData) => {
48
+ setEmail(formData.get('email') as string);
49
+ formAction(formData);
50
+ };
51
+
52
+ return (
53
+ <div className="flex h-dvh w-screen items-start pt-12 md:pt-0 md:items-center justify-center bg-background">
54
+ <div className="w-full max-w-md overflow-hidden rounded-2xl flex flex-col gap-12">
55
+ <div className="flex flex-col items-center justify-center gap-2 px-4 text-center sm:px-16">
56
+ <h3 className="text-xl font-semibold dark:text-zinc-50">Sign In</h3>
57
+ <p className="text-sm text-gray-500 dark:text-zinc-400">
58
+ Use your email and password to sign in
59
+ </p>
60
+ </div>
61
+ <AuthForm action={handleSubmit} defaultEmail={email}>
62
+ <SubmitButton isSuccessful={isSuccessful}>Sign in</SubmitButton>
63
+ <p className="text-center text-sm text-gray-600 mt-4 dark:text-zinc-400">
64
+ {"Don't have an account? "}
65
+ <Link
66
+ href="/register"
67
+ className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
68
+ >
69
+ Sign up
70
+ </Link>
71
+ {' for free.'}
72
+ </p>
73
+ </AuthForm>
74
+ </div>
75
+ </div>
76
+ );
77
+ }
app/(auth)/register/page.tsx ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useActionState, useEffect, useState } from 'react';
6
+
7
+ import { AuthForm } from '@/components/auth-form';
8
+ import { SubmitButton } from '@/components/submit-button';
9
+
10
+ import { register, type RegisterActionState } from '../actions';
11
+ import { toast } from '@/components/toast';
12
+ import { useSession } from 'next-auth/react';
13
+
14
+ export default function Page() {
15
+ const router = useRouter();
16
+
17
+ const [email, setEmail] = useState('');
18
+ const [isSuccessful, setIsSuccessful] = useState(false);
19
+
20
+ const [state, formAction] = useActionState<RegisterActionState, FormData>(
21
+ register,
22
+ {
23
+ status: 'idle',
24
+ },
25
+ );
26
+
27
+ const { update: updateSession } = useSession();
28
+
29
+ useEffect(() => {
30
+ if (state.status === 'user_exists') {
31
+ toast({ type: 'error', description: 'Account already exists!' });
32
+ } else if (state.status === 'failed') {
33
+ toast({ type: 'error', description: 'Failed to create account!' });
34
+ } else if (state.status === 'invalid_data') {
35
+ toast({
36
+ type: 'error',
37
+ description: 'Failed validating your submission!',
38
+ });
39
+ } else if (state.status === 'success') {
40
+ toast({ type: 'success', description: 'Account created successfully!' });
41
+
42
+ setIsSuccessful(true);
43
+ updateSession();
44
+ router.refresh();
45
+ }
46
+ }, [state]);
47
+
48
+ const handleSubmit = (formData: FormData) => {
49
+ setEmail(formData.get('email') as string);
50
+ formAction(formData);
51
+ };
52
+
53
+ return (
54
+ <div className="flex h-dvh w-screen items-start pt-12 md:pt-0 md:items-center justify-center bg-background">
55
+ <div className="w-full max-w-md overflow-hidden rounded-2xl gap-12 flex flex-col">
56
+ <div className="flex flex-col items-center justify-center gap-2 px-4 text-center sm:px-16">
57
+ <h3 className="text-xl font-semibold dark:text-zinc-50">Sign Up</h3>
58
+ <p className="text-sm text-gray-500 dark:text-zinc-400">
59
+ Create an account with your email and password
60
+ </p>
61
+ </div>
62
+ <AuthForm action={handleSubmit} defaultEmail={email}>
63
+ <SubmitButton isSuccessful={isSuccessful}>Sign Up</SubmitButton>
64
+ <p className="text-center text-sm text-gray-600 mt-4 dark:text-zinc-400">
65
+ {'Already have an account? '}
66
+ <Link
67
+ href="/login"
68
+ className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
69
+ >
70
+ Sign in
71
+ </Link>
72
+ {' instead.'}
73
+ </p>
74
+ </AuthForm>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
app/(chat)/actions.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use server';
2
+
3
+ import { generateText, type UIMessage } from 'ai';
4
+ import { cookies } from 'next/headers';
5
+ import {
6
+ deleteMessagesByChatIdAfterTimestamp,
7
+ getMessageById,
8
+ updateChatVisiblityById,
9
+ } from '@/lib/db/queries';
10
+ import type { VisibilityType } from '@/components/visibility-selector';
11
+ import { myProvider } from '@/lib/ai/providers';
12
+
13
+ export async function saveChatModelAsCookie(model: string) {
14
+ const cookieStore = await cookies();
15
+ cookieStore.set('chat-model', model);
16
+ }
17
+
18
+ export async function generateTitleFromUserMessage({
19
+ message,
20
+ }: {
21
+ message: UIMessage;
22
+ }) {
23
+ const { text: title } = await generateText({
24
+ model: myProvider.languageModel('title-model'),
25
+ system: `\n
26
+ - you will generate a short title based on the first message a user begins a conversation with
27
+ - ensure it is not more than 80 characters long
28
+ - the title should be a summary of the user's message
29
+ - do not use quotes or colons`,
30
+ prompt: JSON.stringify(message),
31
+ });
32
+
33
+ return title;
34
+ }
35
+
36
+ export async function deleteTrailingMessages({ id }: { id: string }) {
37
+ const [message] = await getMessageById({ id });
38
+
39
+ await deleteMessagesByChatIdAfterTimestamp({
40
+ chatId: message.chatId,
41
+ timestamp: message.createdAt,
42
+ });
43
+ }
44
+
45
+ export async function updateChatVisibility({
46
+ chatId,
47
+ visibility,
48
+ }: {
49
+ chatId: string;
50
+ visibility: VisibilityType;
51
+ }) {
52
+ await updateChatVisiblityById({ chatId, visibility });
53
+ }
app/(chat)/api/chat/[id]/stream/route.ts ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth } from '@/app/(auth)/auth';
2
+ import {
3
+ getChatById,
4
+ getMessagesByChatId,
5
+ getStreamIdsByChatId,
6
+ } from '@/lib/db/queries';
7
+ import type { Chat } from '@/lib/db/schema';
8
+ import { ChatSDKError } from '@/lib/errors';
9
+ import type { ChatMessage } from '@/lib/types';
10
+ import { createUIMessageStream, JsonToSseTransformStream } from 'ai';
11
+ import { getStreamContext } from '../../route';
12
+ import { differenceInSeconds } from 'date-fns';
13
+
14
+ export async function GET(
15
+ _: Request,
16
+ { params }: { params: Promise<{ id: string }> },
17
+ ) {
18
+ const { id: chatId } = await params;
19
+
20
+ const streamContext = getStreamContext();
21
+ const resumeRequestedAt = new Date();
22
+
23
+ if (!streamContext) {
24
+ return new Response(null, { status: 204 });
25
+ }
26
+
27
+ if (!chatId) {
28
+ return new ChatSDKError('bad_request:api').toResponse();
29
+ }
30
+
31
+ const session = await auth();
32
+
33
+ if (!session?.user) {
34
+ return new ChatSDKError('unauthorized:chat').toResponse();
35
+ }
36
+
37
+ let chat: Chat;
38
+
39
+ try {
40
+ chat = await getChatById({ id: chatId });
41
+ } catch {
42
+ return new ChatSDKError('not_found:chat').toResponse();
43
+ }
44
+
45
+ if (!chat) {
46
+ return new ChatSDKError('not_found:chat').toResponse();
47
+ }
48
+
49
+ if (chat.visibility === 'private' && chat.userId !== session.user.id) {
50
+ return new ChatSDKError('forbidden:chat').toResponse();
51
+ }
52
+
53
+ const streamIds = await getStreamIdsByChatId({ chatId });
54
+
55
+ if (!streamIds.length) {
56
+ return new ChatSDKError('not_found:stream').toResponse();
57
+ }
58
+
59
+ const recentStreamId = streamIds.at(-1);
60
+
61
+ if (!recentStreamId) {
62
+ return new ChatSDKError('not_found:stream').toResponse();
63
+ }
64
+
65
+ const emptyDataStream = createUIMessageStream<ChatMessage>({
66
+ execute: () => {},
67
+ });
68
+
69
+ const stream = await streamContext.resumableStream(recentStreamId, () =>
70
+ emptyDataStream.pipeThrough(new JsonToSseTransformStream()),
71
+ );
72
+
73
+ /*
74
+ * For when the generation is streaming during SSR
75
+ * but the resumable stream has concluded at this point.
76
+ */
77
+ if (!stream) {
78
+ const messages = await getMessagesByChatId({ id: chatId });
79
+ const mostRecentMessage = messages.at(-1);
80
+
81
+ if (!mostRecentMessage) {
82
+ return new Response(emptyDataStream, { status: 200 });
83
+ }
84
+
85
+ if (mostRecentMessage.role !== 'assistant') {
86
+ return new Response(emptyDataStream, { status: 200 });
87
+ }
88
+
89
+ const messageCreatedAt = new Date(mostRecentMessage.createdAt);
90
+
91
+ if (differenceInSeconds(resumeRequestedAt, messageCreatedAt) > 15) {
92
+ return new Response(emptyDataStream, { status: 200 });
93
+ }
94
+
95
+ const restoredStream = createUIMessageStream<ChatMessage>({
96
+ execute: ({ writer }) => {
97
+ writer.write({
98
+ type: 'data-appendMessage',
99
+ data: JSON.stringify(mostRecentMessage),
100
+ transient: true,
101
+ });
102
+ },
103
+ });
104
+
105
+ return new Response(
106
+ restoredStream.pipeThrough(new JsonToSseTransformStream()),
107
+ { status: 200 },
108
+ );
109
+ }
110
+
111
+ return new Response(stream, { status: 200 });
112
+ }
app/(chat)/api/chat/route.ts ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ convertToModelMessages,
3
+ createUIMessageStream,
4
+ JsonToSseTransformStream,
5
+ smoothStream,
6
+ stepCountIs,
7
+ streamText,
8
+ } from 'ai';
9
+ import { auth, type UserType } from '@/app/(auth)/auth';
10
+ import { type RequestHints, systemPrompt } from '@/lib/ai/prompts';
11
+ import {
12
+ createStreamId,
13
+ deleteChatById,
14
+ getChatById,
15
+ getMessageCountByUserId,
16
+ getMessagesByChatId,
17
+ saveChat,
18
+ saveMessages,
19
+ } from '@/lib/db/queries';
20
+ import { convertToUIMessages, generateUUID } from '@/lib/utils';
21
+ import { generateTitleFromUserMessage } from '../../actions';
22
+ import { createDocument } from '@/lib/ai/tools/create-document';
23
+ import { updateDocument } from '@/lib/ai/tools/update-document';
24
+ import { requestSuggestions } from '@/lib/ai/tools/request-suggestions';
25
+ import { getWeather } from '@/lib/ai/tools/get-weather';
26
+ import { isProductionEnvironment } from '@/lib/constants';
27
+ import { myProvider } from '@/lib/ai/providers';
28
+ import { entitlementsByUserType } from '@/lib/ai/entitlements';
29
+ import { postRequestBodySchema, type PostRequestBody } from './schema';
30
+ import { geolocation } from '@vercel/functions';
31
+ import {
32
+ createResumableStreamContext,
33
+ type ResumableStreamContext,
34
+ } from 'resumable-stream';
35
+ import { after } from 'next/server';
36
+ import { ChatSDKError } from '@/lib/errors';
37
+ import type { ChatMessage } from '@/lib/types';
38
+ import type { ChatModel } from '@/lib/ai/models';
39
+ import type { VisibilityType } from '@/components/visibility-selector';
40
+
41
+ export const maxDuration = 60;
42
+
43
+ let globalStreamContext: ResumableStreamContext | null = null;
44
+
45
+ export function getStreamContext() {
46
+ if (!globalStreamContext) {
47
+ try {
48
+ globalStreamContext = createResumableStreamContext({
49
+ waitUntil: after,
50
+ });
51
+ } catch (error: any) {
52
+ if (error.message.includes('REDIS_URL')) {
53
+ console.log(
54
+ ' > Resumable streams are disabled due to missing REDIS_URL',
55
+ );
56
+ } else {
57
+ console.error(error);
58
+ }
59
+ }
60
+ }
61
+
62
+ return globalStreamContext;
63
+ }
64
+
65
+ export async function POST(request: Request) {
66
+ let requestBody: PostRequestBody;
67
+
68
+ try {
69
+ const json = await request.json();
70
+ requestBody = postRequestBodySchema.parse(json);
71
+ } catch (_) {
72
+ return new ChatSDKError('bad_request:api').toResponse();
73
+ }
74
+
75
+ try {
76
+ const {
77
+ id,
78
+ message,
79
+ selectedChatModel,
80
+ selectedVisibilityType,
81
+ }: {
82
+ id: string;
83
+ message: ChatMessage;
84
+ selectedChatModel: ChatModel['id'];
85
+ selectedVisibilityType: VisibilityType;
86
+ } = requestBody;
87
+
88
+ const session = await auth();
89
+
90
+ if (!session?.user) {
91
+ return new ChatSDKError('unauthorized:chat').toResponse();
92
+ }
93
+
94
+ const userType: UserType = session.user.type;
95
+
96
+ const messageCount = await getMessageCountByUserId({
97
+ id: session.user.id,
98
+ differenceInHours: 24,
99
+ });
100
+
101
+ if (messageCount > entitlementsByUserType[userType].maxMessagesPerDay) {
102
+ return new ChatSDKError('rate_limit:chat').toResponse();
103
+ }
104
+
105
+ const chat = await getChatById({ id });
106
+
107
+ if (!chat) {
108
+ const title = await generateTitleFromUserMessage({
109
+ message,
110
+ });
111
+
112
+ await saveChat({
113
+ id,
114
+ userId: session.user.id,
115
+ title,
116
+ visibility: selectedVisibilityType,
117
+ });
118
+ } else {
119
+ if (chat.userId !== session.user.id) {
120
+ return new ChatSDKError('forbidden:chat').toResponse();
121
+ }
122
+ }
123
+
124
+ const messagesFromDb = await getMessagesByChatId({ id });
125
+ const uiMessages = [...convertToUIMessages(messagesFromDb), message];
126
+
127
+ const { longitude, latitude, city, country } = geolocation(request);
128
+
129
+ const requestHints: RequestHints = {
130
+ longitude,
131
+ latitude,
132
+ city,
133
+ country,
134
+ };
135
+
136
+ await saveMessages({
137
+ messages: [
138
+ {
139
+ chatId: id,
140
+ id: message.id,
141
+ role: 'user',
142
+ parts: message.parts,
143
+ attachments: [],
144
+ createdAt: new Date(),
145
+ },
146
+ ],
147
+ });
148
+
149
+ const streamId = generateUUID();
150
+ await createStreamId({ streamId, chatId: id });
151
+
152
+ const stream = createUIMessageStream({
153
+ execute: ({ writer: dataStream }) => {
154
+ const result = streamText({
155
+ model: myProvider.languageModel(selectedChatModel),
156
+ system: systemPrompt({ selectedChatModel, requestHints }),
157
+ messages: convertToModelMessages(uiMessages),
158
+ stopWhen: stepCountIs(5),
159
+ experimental_activeTools:
160
+ selectedChatModel === 'chat-model-reasoning'
161
+ ? []
162
+ : [
163
+ 'getWeather',
164
+ 'createDocument',
165
+ 'updateDocument',
166
+ 'requestSuggestions',
167
+ ],
168
+ experimental_transform: smoothStream({ chunking: 'word' }),
169
+ tools: {
170
+ getWeather,
171
+ createDocument: createDocument({ session, dataStream }),
172
+ updateDocument: updateDocument({ session, dataStream }),
173
+ requestSuggestions: requestSuggestions({
174
+ session,
175
+ dataStream,
176
+ }),
177
+ },
178
+ experimental_telemetry: {
179
+ isEnabled: isProductionEnvironment,
180
+ functionId: 'stream-text',
181
+ },
182
+ });
183
+
184
+ result.consumeStream();
185
+
186
+ dataStream.merge(
187
+ result.toUIMessageStream({
188
+ sendReasoning: true,
189
+ }),
190
+ );
191
+ },
192
+ generateId: generateUUID,
193
+ onFinish: async ({ messages }) => {
194
+ await saveMessages({
195
+ messages: messages.map((message) => ({
196
+ id: message.id,
197
+ role: message.role,
198
+ parts: message.parts,
199
+ createdAt: new Date(),
200
+ attachments: [],
201
+ chatId: id,
202
+ })),
203
+ });
204
+ },
205
+ onError: () => {
206
+ return 'Oops, an error occurred!';
207
+ },
208
+ });
209
+
210
+ const streamContext = getStreamContext();
211
+
212
+ if (streamContext) {
213
+ return new Response(
214
+ await streamContext.resumableStream(streamId, () =>
215
+ stream.pipeThrough(new JsonToSseTransformStream()),
216
+ ),
217
+ );
218
+ } else {
219
+ return new Response(stream.pipeThrough(new JsonToSseTransformStream()));
220
+ }
221
+ } catch (error) {
222
+ if (error instanceof ChatSDKError) {
223
+ return error.toResponse();
224
+ }
225
+ }
226
+ }
227
+
228
+ export async function DELETE(request: Request) {
229
+ const { searchParams } = new URL(request.url);
230
+ const id = searchParams.get('id');
231
+
232
+ if (!id) {
233
+ return new ChatSDKError('bad_request:api').toResponse();
234
+ }
235
+
236
+ const session = await auth();
237
+
238
+ if (!session?.user) {
239
+ return new ChatSDKError('unauthorized:chat').toResponse();
240
+ }
241
+
242
+ const chat = await getChatById({ id });
243
+
244
+ if (chat.userId !== session.user.id) {
245
+ return new ChatSDKError('forbidden:chat').toResponse();
246
+ }
247
+
248
+ const deletedChat = await deleteChatById({ id });
249
+
250
+ return Response.json(deletedChat, { status: 200 });
251
+ }
app/(chat)/api/chat/schema.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { z } from 'zod';
2
+
3
+ const textPartSchema = z.object({
4
+ type: z.enum(['text']),
5
+ text: z.string().min(1).max(2000),
6
+ });
7
+
8
+ const filePartSchema = z.object({
9
+ type: z.enum(['file']),
10
+ mediaType: z.enum(['image/jpeg', 'image/png']),
11
+ name: z.string().min(1).max(100),
12
+ url: z.string().url(),
13
+ });
14
+
15
+ const partSchema = z.union([textPartSchema, filePartSchema]);
16
+
17
+ export const postRequestBodySchema = z.object({
18
+ id: z.string().uuid(),
19
+ message: z.object({
20
+ id: z.string().uuid(),
21
+ role: z.enum(['user']),
22
+ parts: z.array(partSchema),
23
+ }),
24
+ selectedChatModel: z.enum(['chat-model', 'chat-model-reasoning']),
25
+ selectedVisibilityType: z.enum(['public', 'private']),
26
+ });
27
+
28
+ export type PostRequestBody = z.infer<typeof postRequestBodySchema>;
app/(chat)/api/document/route.ts ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth } from '@/app/(auth)/auth';
2
+ import type { ArtifactKind } from '@/components/artifact';
3
+ import {
4
+ deleteDocumentsByIdAfterTimestamp,
5
+ getDocumentsById,
6
+ saveDocument,
7
+ } from '@/lib/db/queries';
8
+ import { ChatSDKError } from '@/lib/errors';
9
+
10
+ export async function GET(request: Request) {
11
+ const { searchParams } = new URL(request.url);
12
+ const id = searchParams.get('id');
13
+
14
+ if (!id) {
15
+ return new ChatSDKError(
16
+ 'bad_request:api',
17
+ 'Parameter id is missing',
18
+ ).toResponse();
19
+ }
20
+
21
+ const session = await auth();
22
+
23
+ if (!session?.user) {
24
+ return new ChatSDKError('unauthorized:document').toResponse();
25
+ }
26
+
27
+ const documents = await getDocumentsById({ id });
28
+
29
+ const [document] = documents;
30
+
31
+ if (!document) {
32
+ return new ChatSDKError('not_found:document').toResponse();
33
+ }
34
+
35
+ if (document.userId !== session.user.id) {
36
+ return new ChatSDKError('forbidden:document').toResponse();
37
+ }
38
+
39
+ return Response.json(documents, { status: 200 });
40
+ }
41
+
42
+ export async function POST(request: Request) {
43
+ const { searchParams } = new URL(request.url);
44
+ const id = searchParams.get('id');
45
+
46
+ if (!id) {
47
+ return new ChatSDKError(
48
+ 'bad_request:api',
49
+ 'Parameter id is required.',
50
+ ).toResponse();
51
+ }
52
+
53
+ const session = await auth();
54
+
55
+ if (!session?.user) {
56
+ return new ChatSDKError('not_found:document').toResponse();
57
+ }
58
+
59
+ const {
60
+ content,
61
+ title,
62
+ kind,
63
+ }: { content: string; title: string; kind: ArtifactKind } =
64
+ await request.json();
65
+
66
+ const documents = await getDocumentsById({ id });
67
+
68
+ if (documents.length > 0) {
69
+ const [document] = documents;
70
+
71
+ if (document.userId !== session.user.id) {
72
+ return new ChatSDKError('forbidden:document').toResponse();
73
+ }
74
+ }
75
+
76
+ const document = await saveDocument({
77
+ id,
78
+ content,
79
+ title,
80
+ kind,
81
+ userId: session.user.id,
82
+ });
83
+
84
+ return Response.json(document, { status: 200 });
85
+ }
86
+
87
+ export async function DELETE(request: Request) {
88
+ const { searchParams } = new URL(request.url);
89
+ const id = searchParams.get('id');
90
+ const timestamp = searchParams.get('timestamp');
91
+
92
+ if (!id) {
93
+ return new ChatSDKError(
94
+ 'bad_request:api',
95
+ 'Parameter id is required.',
96
+ ).toResponse();
97
+ }
98
+
99
+ if (!timestamp) {
100
+ return new ChatSDKError(
101
+ 'bad_request:api',
102
+ 'Parameter timestamp is required.',
103
+ ).toResponse();
104
+ }
105
+
106
+ const session = await auth();
107
+
108
+ if (!session?.user) {
109
+ return new ChatSDKError('unauthorized:document').toResponse();
110
+ }
111
+
112
+ const documents = await getDocumentsById({ id });
113
+
114
+ const [document] = documents;
115
+
116
+ if (document.userId !== session.user.id) {
117
+ return new ChatSDKError('forbidden:document').toResponse();
118
+ }
119
+
120
+ const documentsDeleted = await deleteDocumentsByIdAfterTimestamp({
121
+ id,
122
+ timestamp: new Date(timestamp),
123
+ });
124
+
125
+ return Response.json(documentsDeleted, { status: 200 });
126
+ }
app/(chat)/api/files/upload/route.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { put } from '@vercel/blob';
2
+ import { NextResponse } from 'next/server';
3
+ import { z } from 'zod';
4
+
5
+ import { auth } from '@/app/(auth)/auth';
6
+
7
+ // Use Blob instead of File since File is not available in Node.js environment
8
+ const FileSchema = z.object({
9
+ file: z
10
+ .instanceof(Blob)
11
+ .refine((file) => file.size <= 5 * 1024 * 1024, {
12
+ message: 'File size should be less than 5MB',
13
+ })
14
+ // Update the file type based on the kind of files you want to accept
15
+ .refine((file) => ['image/jpeg', 'image/png'].includes(file.type), {
16
+ message: 'File type should be JPEG or PNG',
17
+ }),
18
+ });
19
+
20
+ export async function POST(request: Request) {
21
+ const session = await auth();
22
+
23
+ if (!session) {
24
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
25
+ }
26
+
27
+ if (request.body === null) {
28
+ return new Response('Request body is empty', { status: 400 });
29
+ }
30
+
31
+ try {
32
+ const formData = await request.formData();
33
+ const file = formData.get('file') as Blob;
34
+
35
+ if (!file) {
36
+ return NextResponse.json({ error: 'No file uploaded' }, { status: 400 });
37
+ }
38
+
39
+ const validatedFile = FileSchema.safeParse({ file });
40
+
41
+ if (!validatedFile.success) {
42
+ const errorMessage = validatedFile.error.errors
43
+ .map((error) => error.message)
44
+ .join(', ');
45
+
46
+ return NextResponse.json({ error: errorMessage }, { status: 400 });
47
+ }
48
+
49
+ // Get filename from formData since Blob doesn't have name property
50
+ const filename = (formData.get('file') as File).name;
51
+ const fileBuffer = await file.arrayBuffer();
52
+
53
+ try {
54
+ const data = await put(`${filename}`, fileBuffer, {
55
+ access: 'public',
56
+ });
57
+
58
+ return NextResponse.json(data);
59
+ } catch (error) {
60
+ return NextResponse.json({ error: 'Upload failed' }, { status: 500 });
61
+ }
62
+ } catch (error) {
63
+ return NextResponse.json(
64
+ { error: 'Failed to process request' },
65
+ { status: 500 },
66
+ );
67
+ }
68
+ }
app/(chat)/api/history/route.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth } from '@/app/(auth)/auth';
2
+ import type { NextRequest } from 'next/server';
3
+ import { getChatsByUserId } from '@/lib/db/queries';
4
+ import { ChatSDKError } from '@/lib/errors';
5
+
6
+ export async function GET(request: NextRequest) {
7
+ const { searchParams } = request.nextUrl;
8
+
9
+ const limit = Number.parseInt(searchParams.get('limit') || '10');
10
+ const startingAfter = searchParams.get('starting_after');
11
+ const endingBefore = searchParams.get('ending_before');
12
+
13
+ if (startingAfter && endingBefore) {
14
+ return new ChatSDKError(
15
+ 'bad_request:api',
16
+ 'Only one of starting_after or ending_before can be provided.',
17
+ ).toResponse();
18
+ }
19
+
20
+ const session = await auth();
21
+
22
+ if (!session?.user) {
23
+ return new ChatSDKError('unauthorized:chat').toResponse();
24
+ }
25
+
26
+ const chats = await getChatsByUserId({
27
+ id: session.user.id,
28
+ limit,
29
+ startingAfter,
30
+ endingBefore,
31
+ });
32
+
33
+ return Response.json(chats);
34
+ }
app/(chat)/api/suggestions/route.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth } from '@/app/(auth)/auth';
2
+ import { getSuggestionsByDocumentId } from '@/lib/db/queries';
3
+ import { ChatSDKError } from '@/lib/errors';
4
+
5
+ export async function GET(request: Request) {
6
+ const { searchParams } = new URL(request.url);
7
+ const documentId = searchParams.get('documentId');
8
+
9
+ if (!documentId) {
10
+ return new ChatSDKError(
11
+ 'bad_request:api',
12
+ 'Parameter documentId is required.',
13
+ ).toResponse();
14
+ }
15
+
16
+ const session = await auth();
17
+
18
+ if (!session?.user) {
19
+ return new ChatSDKError('unauthorized:suggestions').toResponse();
20
+ }
21
+
22
+ const suggestions = await getSuggestionsByDocumentId({
23
+ documentId,
24
+ });
25
+
26
+ const [suggestion] = suggestions;
27
+
28
+ if (!suggestion) {
29
+ return Response.json([], { status: 200 });
30
+ }
31
+
32
+ if (suggestion.userId !== session.user.id) {
33
+ return new ChatSDKError('forbidden:api').toResponse();
34
+ }
35
+
36
+ return Response.json(suggestions, { status: 200 });
37
+ }
app/(chat)/api/vote/route.ts ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth } from '@/app/(auth)/auth';
2
+ import { getChatById, getVotesByChatId, voteMessage } from '@/lib/db/queries';
3
+ import { ChatSDKError } from '@/lib/errors';
4
+
5
+ export async function GET(request: Request) {
6
+ const { searchParams } = new URL(request.url);
7
+ const chatId = searchParams.get('chatId');
8
+
9
+ if (!chatId) {
10
+ return new ChatSDKError(
11
+ 'bad_request:api',
12
+ 'Parameter chatId is required.',
13
+ ).toResponse();
14
+ }
15
+
16
+ const session = await auth();
17
+
18
+ if (!session?.user) {
19
+ return new ChatSDKError('unauthorized:vote').toResponse();
20
+ }
21
+
22
+ const chat = await getChatById({ id: chatId });
23
+
24
+ if (!chat) {
25
+ return new ChatSDKError('not_found:chat').toResponse();
26
+ }
27
+
28
+ if (chat.userId !== session.user.id) {
29
+ return new ChatSDKError('forbidden:vote').toResponse();
30
+ }
31
+
32
+ const votes = await getVotesByChatId({ id: chatId });
33
+
34
+ return Response.json(votes, { status: 200 });
35
+ }
36
+
37
+ export async function PATCH(request: Request) {
38
+ const {
39
+ chatId,
40
+ messageId,
41
+ type,
42
+ }: { chatId: string; messageId: string; type: 'up' | 'down' } =
43
+ await request.json();
44
+
45
+ if (!chatId || !messageId || !type) {
46
+ return new ChatSDKError(
47
+ 'bad_request:api',
48
+ 'Parameters chatId, messageId, and type are required.',
49
+ ).toResponse();
50
+ }
51
+
52
+ const session = await auth();
53
+
54
+ if (!session?.user) {
55
+ return new ChatSDKError('unauthorized:vote').toResponse();
56
+ }
57
+
58
+ const chat = await getChatById({ id: chatId });
59
+
60
+ if (!chat) {
61
+ return new ChatSDKError('not_found:vote').toResponse();
62
+ }
63
+
64
+ if (chat.userId !== session.user.id) {
65
+ return new ChatSDKError('forbidden:vote').toResponse();
66
+ }
67
+
68
+ await voteMessage({
69
+ chatId,
70
+ messageId,
71
+ type: type,
72
+ });
73
+
74
+ return new Response('Message voted', { status: 200 });
75
+ }
app/(chat)/chat/[id]/page.tsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cookies } from 'next/headers';
2
+ import { notFound, redirect } from 'next/navigation';
3
+
4
+ import { auth } from '@/app/(auth)/auth';
5
+ import { Chat } from '@/components/chat';
6
+ import { getChatById, getMessagesByChatId } from '@/lib/db/queries';
7
+ import { DataStreamHandler } from '@/components/data-stream-handler';
8
+ import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models';
9
+ import { convertToUIMessages } from '@/lib/utils';
10
+
11
+ export default async function Page(props: { params: Promise<{ id: string }> }) {
12
+ const params = await props.params;
13
+ const { id } = params;
14
+ const chat = await getChatById({ id });
15
+
16
+ if (!chat) {
17
+ notFound();
18
+ }
19
+
20
+ const session = await auth();
21
+
22
+ if (!session) {
23
+ redirect('/api/auth/guest');
24
+ }
25
+
26
+ if (chat.visibility === 'private') {
27
+ if (!session.user) {
28
+ return notFound();
29
+ }
30
+
31
+ if (session.user.id !== chat.userId) {
32
+ return notFound();
33
+ }
34
+ }
35
+
36
+ const messagesFromDb = await getMessagesByChatId({
37
+ id,
38
+ });
39
+
40
+ const uiMessages = convertToUIMessages(messagesFromDb);
41
+
42
+ const cookieStore = await cookies();
43
+ const chatModelFromCookie = cookieStore.get('chat-model');
44
+
45
+ if (!chatModelFromCookie) {
46
+ return (
47
+ <>
48
+ <Chat
49
+ id={chat.id}
50
+ initialMessages={uiMessages}
51
+ initialChatModel={DEFAULT_CHAT_MODEL}
52
+ initialVisibilityType={chat.visibility}
53
+ isReadonly={session?.user?.id !== chat.userId}
54
+ session={session}
55
+ autoResume={true}
56
+ />
57
+ <DataStreamHandler />
58
+ </>
59
+ );
60
+ }
61
+
62
+ return (
63
+ <>
64
+ <Chat
65
+ id={chat.id}
66
+ initialMessages={uiMessages}
67
+ initialChatModel={chatModelFromCookie.value}
68
+ initialVisibilityType={chat.visibility}
69
+ isReadonly={session?.user?.id !== chat.userId}
70
+ session={session}
71
+ autoResume={true}
72
+ />
73
+ <DataStreamHandler />
74
+ </>
75
+ );
76
+ }
app/(chat)/layout.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cookies } from 'next/headers';
2
+
3
+ import { AppSidebar } from '@/components/app-sidebar';
4
+ import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
5
+ import { auth } from '../(auth)/auth';
6
+ import Script from 'next/script';
7
+ import { DataStreamProvider } from '@/components/data-stream-provider';
8
+
9
+ export const experimental_ppr = true;
10
+
11
+ export default async function Layout({
12
+ children,
13
+ }: {
14
+ children: React.ReactNode;
15
+ }) {
16
+ const [session, cookieStore] = await Promise.all([auth(), cookies()]);
17
+ const isCollapsed = cookieStore.get('sidebar:state')?.value !== 'true';
18
+
19
+ return (
20
+ <>
21
+ <Script
22
+ src="https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"
23
+ strategy="beforeInteractive"
24
+ />
25
+ <DataStreamProvider>
26
+ <SidebarProvider defaultOpen={!isCollapsed}>
27
+ <AppSidebar user={session?.user} />
28
+ <SidebarInset>{children}</SidebarInset>
29
+ </SidebarProvider>
30
+ </DataStreamProvider>
31
+ </>
32
+ );
33
+ }
app/(chat)/opengraph-image.png ADDED

Git LFS Details

  • SHA256: 2debd7baddd1f018254740064e2f5aee84c62937083c707d577bd4f99fc3be36
  • Pointer size: 131 Bytes
  • Size of remote file: 166 kB
app/(chat)/page.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cookies } from 'next/headers';
2
+
3
+ import { Chat } from '@/components/chat';
4
+ import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models';
5
+ import { generateUUID } from '@/lib/utils';
6
+ import { DataStreamHandler } from '@/components/data-stream-handler';
7
+ import { auth } from '../(auth)/auth';
8
+ import { redirect } from 'next/navigation';
9
+
10
+ export default async function Page() {
11
+ const session = await auth();
12
+
13
+ if (!session) {
14
+ redirect('/api/auth/guest');
15
+ }
16
+
17
+ const id = generateUUID();
18
+
19
+ const cookieStore = await cookies();
20
+ const modelIdFromCookie = cookieStore.get('chat-model');
21
+
22
+ if (!modelIdFromCookie) {
23
+ return (
24
+ <>
25
+ <Chat
26
+ key={id}
27
+ id={id}
28
+ initialMessages={[]}
29
+ initialChatModel={DEFAULT_CHAT_MODEL}
30
+ initialVisibilityType="private"
31
+ isReadonly={false}
32
+ session={session}
33
+ autoResume={false}
34
+ />
35
+ <DataStreamHandler />
36
+ </>
37
+ );
38
+ }
39
+
40
+ return (
41
+ <>
42
+ <Chat
43
+ key={id}
44
+ id={id}
45
+ initialMessages={[]}
46
+ initialChatModel={modelIdFromCookie.value}
47
+ initialVisibilityType="private"
48
+ isReadonly={false}
49
+ session={session}
50
+ autoResume={false}
51
+ />
52
+ <DataStreamHandler />
53
+ </>
54
+ );
55
+ }
app/(chat)/twitter-image.png ADDED
app/favicon.ico ADDED
app/globals.css ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ --foreground-rgb: 0, 0, 0;
7
+ --background-start-rgb: 214, 219, 220;
8
+ --background-end-rgb: 255, 255, 255;
9
+ }
10
+
11
+ @media (prefers-color-scheme: dark) {
12
+ :root {
13
+ --foreground-rgb: 255, 255, 255;
14
+ --background-start-rgb: 0, 0, 0;
15
+ --background-end-rgb: 0, 0, 0;
16
+ }
17
+ }
18
+
19
+ @layer utilities {
20
+ .text-balance {
21
+ text-wrap: balance;
22
+ }
23
+ }
24
+
25
+ @layer base {
26
+ :root {
27
+ --background: 0 0% 100%;
28
+ --foreground: 240 10% 3.9%;
29
+ --card: 0 0% 100%;
30
+ --card-foreground: 240 10% 3.9%;
31
+ --popover: 0 0% 100%;
32
+ --popover-foreground: 240 10% 3.9%;
33
+ --primary: 240 5.9% 10%;
34
+ --primary-foreground: 0 0% 98%;
35
+ --secondary: 240 4.8% 95.9%;
36
+ --secondary-foreground: 240 5.9% 10%;
37
+ --muted: 240 4.8% 95.9%;
38
+ --muted-foreground: 240 3.8% 46.1%;
39
+ --accent: 240 4.8% 95.9%;
40
+ --accent-foreground: 240 5.9% 10%;
41
+ --destructive: 0 84.2% 60.2%;
42
+ --destructive-foreground: 0 0% 98%;
43
+ --border: 240 5.9% 90%;
44
+ --input: 240 5.9% 90%;
45
+ --ring: 240 10% 3.9%;
46
+ --chart-1: 12 76% 61%;
47
+ --chart-2: 173 58% 39%;
48
+ --chart-3: 197 37% 24%;
49
+ --chart-4: 43 74% 66%;
50
+ --chart-5: 27 87% 67%;
51
+ --radius: 0.5rem;
52
+ --sidebar-background: 0 0% 98%;
53
+ --sidebar-foreground: 240 5.3% 26.1%;
54
+ --sidebar-primary: 240 5.9% 10%;
55
+ --sidebar-primary-foreground: 0 0% 98%;
56
+ --sidebar-accent: 240 4.8% 95.9%;
57
+ --sidebar-accent-foreground: 240 5.9% 10%;
58
+ --sidebar-border: 220 13% 91%;
59
+ --sidebar-ring: 217.2 91.2% 59.8%;
60
+ }
61
+ .dark {
62
+ --background: 240 10% 3.9%;
63
+ --foreground: 0 0% 98%;
64
+ --card: 240 10% 3.9%;
65
+ --card-foreground: 0 0% 98%;
66
+ --popover: 240 10% 3.9%;
67
+ --popover-foreground: 0 0% 98%;
68
+ --primary: 0 0% 98%;
69
+ --primary-foreground: 240 5.9% 10%;
70
+ --secondary: 240 3.7% 15.9%;
71
+ --secondary-foreground: 0 0% 98%;
72
+ --muted: 240 3.7% 15.9%;
73
+ --muted-foreground: 240 5% 64.9%;
74
+ --accent: 240 3.7% 15.9%;
75
+ --accent-foreground: 0 0% 98%;
76
+ --destructive: 0 62.8% 30.6%;
77
+ --destructive-foreground: 0 0% 98%;
78
+ --border: 240 3.7% 15.9%;
79
+ --input: 240 3.7% 15.9%;
80
+ --ring: 240 4.9% 83.9%;
81
+ --chart-1: 220 70% 50%;
82
+ --chart-2: 160 60% 45%;
83
+ --chart-3: 30 80% 55%;
84
+ --chart-4: 280 65% 60%;
85
+ --chart-5: 340 75% 55%;
86
+ --sidebar-background: 240 5.9% 10%;
87
+ --sidebar-foreground: 240 4.8% 95.9%;
88
+ --sidebar-primary: 224.3 76.3% 48%;
89
+ --sidebar-primary-foreground: 0 0% 100%;
90
+ --sidebar-accent: 240 3.7% 15.9%;
91
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
92
+ --sidebar-border: 240 3.7% 15.9%;
93
+ --sidebar-ring: 217.2 91.2% 59.8%;
94
+ }
95
+ }
96
+
97
+ @layer base {
98
+ * {
99
+ @apply border-border;
100
+ }
101
+
102
+ body {
103
+ @apply bg-background text-foreground;
104
+ }
105
+ }
106
+
107
+ .skeleton {
108
+ * {
109
+ pointer-events: none !important;
110
+ }
111
+
112
+ *[class^="text-"] {
113
+ color: transparent;
114
+ @apply rounded-md bg-foreground/20 select-none animate-pulse;
115
+ }
116
+
117
+ .skeleton-bg {
118
+ @apply bg-foreground/10;
119
+ }
120
+
121
+ .skeleton-div {
122
+ @apply bg-foreground/20 animate-pulse;
123
+ }
124
+ }
125
+
126
+ .ProseMirror {
127
+ outline: none;
128
+ }
129
+
130
+ .cm-editor,
131
+ .cm-gutters {
132
+ @apply bg-background dark:bg-zinc-800 outline-none selection:bg-zinc-900 !important;
133
+ }
134
+
135
+ .ͼo.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground,
136
+ .ͼo.cm-selectionBackground,
137
+ .ͼo.cm-content::selection {
138
+ @apply bg-zinc-200 dark:bg-zinc-900 !important;
139
+ }
140
+
141
+ .cm-activeLine,
142
+ .cm-activeLineGutter {
143
+ @apply bg-transparent !important;
144
+ }
145
+
146
+ .cm-activeLine {
147
+ @apply rounded-r-sm !important;
148
+ }
149
+
150
+ .cm-lineNumbers {
151
+ @apply min-w-7;
152
+ }
153
+
154
+ .cm-foldGutter {
155
+ @apply min-w-3;
156
+ }
157
+
158
+ .cm-lineNumbers .cm-activeLineGutter {
159
+ @apply rounded-l-sm !important;
160
+ }
161
+
162
+ .suggestion-highlight {
163
+ @apply bg-blue-200 hover:bg-blue-300 dark:hover:bg-blue-400/50 dark:text-blue-50 dark:bg-blue-500/40;
164
+ }
app/layout.tsx ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Toaster } from 'sonner';
2
+ import type { Metadata } from 'next';
3
+ import { Geist, Geist_Mono } from 'next/font/google';
4
+ import { ThemeProvider } from '@/components/theme-provider';
5
+
6
+ import './globals.css';
7
+ import { SessionProvider } from 'next-auth/react';
8
+
9
+ export const metadata: Metadata = {
10
+ metadataBase: new URL('https://chat.vercel.ai'),
11
+ title: 'Next.js Chatbot Template',
12
+ description: 'Next.js chatbot template using the AI SDK.',
13
+ };
14
+
15
+ export const viewport = {
16
+ maximumScale: 1, // Disable auto-zoom on mobile Safari
17
+ };
18
+
19
+ const geist = Geist({
20
+ subsets: ['latin'],
21
+ display: 'swap',
22
+ variable: '--font-geist',
23
+ });
24
+
25
+ const geistMono = Geist_Mono({
26
+ subsets: ['latin'],
27
+ display: 'swap',
28
+ variable: '--font-geist-mono',
29
+ });
30
+
31
+ const LIGHT_THEME_COLOR = 'hsl(0 0% 100%)';
32
+ const DARK_THEME_COLOR = 'hsl(240deg 10% 3.92%)';
33
+ const THEME_COLOR_SCRIPT = `\
34
+ (function() {
35
+ var html = document.documentElement;
36
+ var meta = document.querySelector('meta[name="theme-color"]');
37
+ if (!meta) {
38
+ meta = document.createElement('meta');
39
+ meta.setAttribute('name', 'theme-color');
40
+ document.head.appendChild(meta);
41
+ }
42
+ function updateThemeColor() {
43
+ var isDark = html.classList.contains('dark');
44
+ meta.setAttribute('content', isDark ? '${DARK_THEME_COLOR}' : '${LIGHT_THEME_COLOR}');
45
+ }
46
+ var observer = new MutationObserver(updateThemeColor);
47
+ observer.observe(html, { attributes: true, attributeFilter: ['class'] });
48
+ updateThemeColor();
49
+ })();`;
50
+
51
+ export default async function RootLayout({
52
+ children,
53
+ }: Readonly<{
54
+ children: React.ReactNode;
55
+ }>) {
56
+ return (
57
+ <html
58
+ lang="en"
59
+ // `next-themes` injects an extra classname to the body element to avoid
60
+ // visual flicker before hydration. Hence the `suppressHydrationWarning`
61
+ // prop is necessary to avoid the React hydration mismatch warning.
62
+ // https://github.com/pacocoursey/next-themes?tab=readme-ov-file#with-app
63
+ suppressHydrationWarning
64
+ className={`${geist.variable} ${geistMono.variable}`}
65
+ >
66
+ <head>
67
+ <script
68
+ dangerouslySetInnerHTML={{
69
+ __html: THEME_COLOR_SCRIPT,
70
+ }}
71
+ />
72
+ </head>
73
+ <body className="antialiased">
74
+ <ThemeProvider
75
+ attribute="class"
76
+ defaultTheme="system"
77
+ enableSystem
78
+ disableTransitionOnChange
79
+ >
80
+ <Toaster position="top-center" />
81
+ <SessionProvider>{children}</SessionProvider>
82
+ </ThemeProvider>
83
+ </body>
84
+ </html>
85
+ );
86
+ }
artifacts/actions.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ 'use server';
2
+
3
+ import { getSuggestionsByDocumentId } from '@/lib/db/queries';
4
+
5
+ export async function getSuggestions({ documentId }: { documentId: string }) {
6
+ const suggestions = await getSuggestionsByDocumentId({ documentId });
7
+ return suggestions ?? [];
8
+ }
artifacts/code/client.tsx ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Artifact } from '@/components/create-artifact';
2
+ import { CodeEditor } from '@/components/code-editor';
3
+ import {
4
+ CopyIcon,
5
+ LogsIcon,
6
+ MessageIcon,
7
+ PlayIcon,
8
+ RedoIcon,
9
+ UndoIcon,
10
+ } from '@/components/icons';
11
+ import { toast } from 'sonner';
12
+ import { generateUUID } from '@/lib/utils';
13
+ import {
14
+ Console,
15
+ type ConsoleOutput,
16
+ type ConsoleOutputContent,
17
+ } from '@/components/console';
18
+
19
+ const OUTPUT_HANDLERS = {
20
+ matplotlib: `
21
+ import io
22
+ import base64
23
+ from matplotlib import pyplot as plt
24
+
25
+ # Clear any existing plots
26
+ plt.clf()
27
+ plt.close('all')
28
+
29
+ # Switch to agg backend
30
+ plt.switch_backend('agg')
31
+
32
+ def setup_matplotlib_output():
33
+ def custom_show():
34
+ if plt.gcf().get_size_inches().prod() * plt.gcf().dpi ** 2 > 25_000_000:
35
+ print("Warning: Plot size too large, reducing quality")
36
+ plt.gcf().set_dpi(100)
37
+
38
+ png_buf = io.BytesIO()
39
+ plt.savefig(png_buf, format='png')
40
+ png_buf.seek(0)
41
+ png_base64 = base64.b64encode(png_buf.read()).decode('utf-8')
42
+ print(f'data:image/png;base64,{png_base64}')
43
+ png_buf.close()
44
+
45
+ plt.clf()
46
+ plt.close('all')
47
+
48
+ plt.show = custom_show
49
+ `,
50
+ basic: `
51
+ # Basic output capture setup
52
+ `,
53
+ };
54
+
55
+ function detectRequiredHandlers(code: string): string[] {
56
+ const handlers: string[] = ['basic'];
57
+
58
+ if (code.includes('matplotlib') || code.includes('plt.')) {
59
+ handlers.push('matplotlib');
60
+ }
61
+
62
+ return handlers;
63
+ }
64
+
65
+ interface Metadata {
66
+ outputs: Array<ConsoleOutput>;
67
+ }
68
+
69
+ export const codeArtifact = new Artifact<'code', Metadata>({
70
+ kind: 'code',
71
+ description:
72
+ 'Useful for code generation; Code execution is only available for python code.',
73
+ initialize: async ({ setMetadata }) => {
74
+ setMetadata({
75
+ outputs: [],
76
+ });
77
+ },
78
+ onStreamPart: ({ streamPart, setArtifact }) => {
79
+ if (streamPart.type === 'data-codeDelta') {
80
+ setArtifact((draftArtifact) => ({
81
+ ...draftArtifact,
82
+ content: streamPart.data,
83
+ isVisible:
84
+ draftArtifact.status === 'streaming' &&
85
+ draftArtifact.content.length > 300 &&
86
+ draftArtifact.content.length < 310
87
+ ? true
88
+ : draftArtifact.isVisible,
89
+ status: 'streaming',
90
+ }));
91
+ }
92
+ },
93
+ content: ({ metadata, setMetadata, ...props }) => {
94
+ return (
95
+ <>
96
+ <div className="px-1">
97
+ <CodeEditor {...props} />
98
+ </div>
99
+
100
+ {metadata?.outputs && (
101
+ <Console
102
+ consoleOutputs={metadata.outputs}
103
+ setConsoleOutputs={() => {
104
+ setMetadata({
105
+ ...metadata,
106
+ outputs: [],
107
+ });
108
+ }}
109
+ />
110
+ )}
111
+ </>
112
+ );
113
+ },
114
+ actions: [
115
+ {
116
+ icon: <PlayIcon size={18} />,
117
+ label: 'Run',
118
+ description: 'Execute code',
119
+ onClick: async ({ content, setMetadata }) => {
120
+ const runId = generateUUID();
121
+ const outputContent: Array<ConsoleOutputContent> = [];
122
+
123
+ setMetadata((metadata) => ({
124
+ ...metadata,
125
+ outputs: [
126
+ ...metadata.outputs,
127
+ {
128
+ id: runId,
129
+ contents: [],
130
+ status: 'in_progress',
131
+ },
132
+ ],
133
+ }));
134
+
135
+ try {
136
+ // @ts-expect-error - loadPyodide is not defined
137
+ const currentPyodideInstance = await globalThis.loadPyodide({
138
+ indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.23.4/full/',
139
+ });
140
+
141
+ currentPyodideInstance.setStdout({
142
+ batched: (output: string) => {
143
+ outputContent.push({
144
+ type: output.startsWith('data:image/png;base64')
145
+ ? 'image'
146
+ : 'text',
147
+ value: output,
148
+ });
149
+ },
150
+ });
151
+
152
+ await currentPyodideInstance.loadPackagesFromImports(content, {
153
+ messageCallback: (message: string) => {
154
+ setMetadata((metadata) => ({
155
+ ...metadata,
156
+ outputs: [
157
+ ...metadata.outputs.filter((output) => output.id !== runId),
158
+ {
159
+ id: runId,
160
+ contents: [{ type: 'text', value: message }],
161
+ status: 'loading_packages',
162
+ },
163
+ ],
164
+ }));
165
+ },
166
+ });
167
+
168
+ const requiredHandlers = detectRequiredHandlers(content);
169
+ for (const handler of requiredHandlers) {
170
+ if (OUTPUT_HANDLERS[handler as keyof typeof OUTPUT_HANDLERS]) {
171
+ await currentPyodideInstance.runPythonAsync(
172
+ OUTPUT_HANDLERS[handler as keyof typeof OUTPUT_HANDLERS],
173
+ );
174
+
175
+ if (handler === 'matplotlib') {
176
+ await currentPyodideInstance.runPythonAsync(
177
+ 'setup_matplotlib_output()',
178
+ );
179
+ }
180
+ }
181
+ }
182
+
183
+ await currentPyodideInstance.runPythonAsync(content);
184
+
185
+ setMetadata((metadata) => ({
186
+ ...metadata,
187
+ outputs: [
188
+ ...metadata.outputs.filter((output) => output.id !== runId),
189
+ {
190
+ id: runId,
191
+ contents: outputContent,
192
+ status: 'completed',
193
+ },
194
+ ],
195
+ }));
196
+ } catch (error: any) {
197
+ setMetadata((metadata) => ({
198
+ ...metadata,
199
+ outputs: [
200
+ ...metadata.outputs.filter((output) => output.id !== runId),
201
+ {
202
+ id: runId,
203
+ contents: [{ type: 'text', value: error.message }],
204
+ status: 'failed',
205
+ },
206
+ ],
207
+ }));
208
+ }
209
+ },
210
+ },
211
+ {
212
+ icon: <UndoIcon size={18} />,
213
+ description: 'View Previous version',
214
+ onClick: ({ handleVersionChange }) => {
215
+ handleVersionChange('prev');
216
+ },
217
+ isDisabled: ({ currentVersionIndex }) => {
218
+ if (currentVersionIndex === 0) {
219
+ return true;
220
+ }
221
+
222
+ return false;
223
+ },
224
+ },
225
+ {
226
+ icon: <RedoIcon size={18} />,
227
+ description: 'View Next version',
228
+ onClick: ({ handleVersionChange }) => {
229
+ handleVersionChange('next');
230
+ },
231
+ isDisabled: ({ isCurrentVersion }) => {
232
+ if (isCurrentVersion) {
233
+ return true;
234
+ }
235
+
236
+ return false;
237
+ },
238
+ },
239
+ {
240
+ icon: <CopyIcon size={18} />,
241
+ description: 'Copy code to clipboard',
242
+ onClick: ({ content }) => {
243
+ navigator.clipboard.writeText(content);
244
+ toast.success('Copied to clipboard!');
245
+ },
246
+ },
247
+ ],
248
+ toolbar: [
249
+ {
250
+ icon: <MessageIcon />,
251
+ description: 'Add comments',
252
+ onClick: ({ sendMessage }) => {
253
+ sendMessage({
254
+ role: 'user',
255
+ parts: [
256
+ {
257
+ type: 'text',
258
+ text: 'Add comments to the code snippet for understanding',
259
+ },
260
+ ],
261
+ });
262
+ },
263
+ },
264
+ {
265
+ icon: <LogsIcon />,
266
+ description: 'Add logs',
267
+ onClick: ({ sendMessage }) => {
268
+ sendMessage({
269
+ role: 'user',
270
+ parts: [
271
+ {
272
+ type: 'text',
273
+ text: 'Add logs to the code snippet for debugging',
274
+ },
275
+ ],
276
+ });
277
+ },
278
+ },
279
+ ],
280
+ });
artifacts/code/server.ts ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { z } from 'zod';
2
+ import { streamObject } from 'ai';
3
+ import { myProvider } from '@/lib/ai/providers';
4
+ import { codePrompt, updateDocumentPrompt } from '@/lib/ai/prompts';
5
+ import { createDocumentHandler } from '@/lib/artifacts/server';
6
+
7
+ export const codeDocumentHandler = createDocumentHandler<'code'>({
8
+ kind: 'code',
9
+ onCreateDocument: async ({ title, dataStream }) => {
10
+ let draftContent = '';
11
+
12
+ const { fullStream } = streamObject({
13
+ model: myProvider.languageModel('artifact-model'),
14
+ system: codePrompt,
15
+ prompt: title,
16
+ schema: z.object({
17
+ code: z.string(),
18
+ }),
19
+ });
20
+
21
+ for await (const delta of fullStream) {
22
+ const { type } = delta;
23
+
24
+ if (type === 'object') {
25
+ const { object } = delta;
26
+ const { code } = object;
27
+
28
+ if (code) {
29
+ dataStream.write({
30
+ type: 'data-codeDelta',
31
+ data: code ?? '',
32
+ transient: true,
33
+ });
34
+
35
+ draftContent = code;
36
+ }
37
+ }
38
+ }
39
+
40
+ return draftContent;
41
+ },
42
+ onUpdateDocument: async ({ document, description, dataStream }) => {
43
+ let draftContent = '';
44
+
45
+ const { fullStream } = streamObject({
46
+ model: myProvider.languageModel('artifact-model'),
47
+ system: updateDocumentPrompt(document.content, 'code'),
48
+ prompt: description,
49
+ schema: z.object({
50
+ code: z.string(),
51
+ }),
52
+ });
53
+
54
+ for await (const delta of fullStream) {
55
+ const { type } = delta;
56
+
57
+ if (type === 'object') {
58
+ const { object } = delta;
59
+ const { code } = object;
60
+
61
+ if (code) {
62
+ dataStream.write({
63
+ type: 'data-codeDelta',
64
+ data: code ?? '',
65
+ transient: true,
66
+ });
67
+
68
+ draftContent = code;
69
+ }
70
+ }
71
+ }
72
+
73
+ return draftContent;
74
+ },
75
+ });
artifacts/image/client.tsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Artifact } from '@/components/create-artifact';
2
+ import { CopyIcon, RedoIcon, UndoIcon } from '@/components/icons';
3
+ import { ImageEditor } from '@/components/image-editor';
4
+ import { toast } from 'sonner';
5
+
6
+ export const imageArtifact = new Artifact({
7
+ kind: 'image',
8
+ description: 'Useful for image generation',
9
+ onStreamPart: ({ streamPart, setArtifact }) => {
10
+ if (streamPart.type === 'data-imageDelta') {
11
+ setArtifact((draftArtifact) => ({
12
+ ...draftArtifact,
13
+ content: streamPart.data,
14
+ isVisible: true,
15
+ status: 'streaming',
16
+ }));
17
+ }
18
+ },
19
+ content: ImageEditor,
20
+ actions: [
21
+ {
22
+ icon: <UndoIcon size={18} />,
23
+ description: 'View Previous version',
24
+ onClick: ({ handleVersionChange }) => {
25
+ handleVersionChange('prev');
26
+ },
27
+ isDisabled: ({ currentVersionIndex }) => {
28
+ if (currentVersionIndex === 0) {
29
+ return true;
30
+ }
31
+
32
+ return false;
33
+ },
34
+ },
35
+ {
36
+ icon: <RedoIcon size={18} />,
37
+ description: 'View Next version',
38
+ onClick: ({ handleVersionChange }) => {
39
+ handleVersionChange('next');
40
+ },
41
+ isDisabled: ({ isCurrentVersion }) => {
42
+ if (isCurrentVersion) {
43
+ return true;
44
+ }
45
+
46
+ return false;
47
+ },
48
+ },
49
+ {
50
+ icon: <CopyIcon size={18} />,
51
+ description: 'Copy image to clipboard',
52
+ onClick: ({ content }) => {
53
+ const img = new Image();
54
+ img.src = `data:image/png;base64,${content}`;
55
+
56
+ img.onload = () => {
57
+ const canvas = document.createElement('canvas');
58
+ canvas.width = img.width;
59
+ canvas.height = img.height;
60
+ const ctx = canvas.getContext('2d');
61
+ ctx?.drawImage(img, 0, 0);
62
+ canvas.toBlob((blob) => {
63
+ if (blob) {
64
+ navigator.clipboard.write([
65
+ new ClipboardItem({ 'image/png': blob }),
66
+ ]);
67
+ }
68
+ }, 'image/png');
69
+ };
70
+
71
+ toast.success('Copied image to clipboard!');
72
+ },
73
+ },
74
+ ],
75
+ toolbar: [],
76
+ });
artifacts/image/server.ts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { myProvider } from '@/lib/ai/providers';
2
+ import { createDocumentHandler } from '@/lib/artifacts/server';
3
+ import { experimental_generateImage } from 'ai';
4
+
5
+ export const imageDocumentHandler = createDocumentHandler<'image'>({
6
+ kind: 'image',
7
+ onCreateDocument: async ({ title, dataStream }) => {
8
+ let draftContent = '';
9
+
10
+ const { image } = await experimental_generateImage({
11
+ model: myProvider.imageModel('small-model'),
12
+ prompt: title,
13
+ n: 1,
14
+ });
15
+
16
+ draftContent = image.base64;
17
+
18
+ dataStream.write({
19
+ type: 'data-imageDelta',
20
+ data: image.base64,
21
+ transient: true,
22
+ });
23
+
24
+ return draftContent;
25
+ },
26
+ onUpdateDocument: async ({ description, dataStream }) => {
27
+ let draftContent = '';
28
+
29
+ const { image } = await experimental_generateImage({
30
+ model: myProvider.imageModel('small-model'),
31
+ prompt: description,
32
+ n: 1,
33
+ });
34
+
35
+ draftContent = image.base64;
36
+
37
+ dataStream.write({
38
+ type: 'data-imageDelta',
39
+ data: image.base64,
40
+ transient: true,
41
+ });
42
+
43
+ return draftContent;
44
+ },
45
+ });
artifacts/sheet/client.tsx ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Artifact } from '@/components/create-artifact';
2
+ import {
3
+ CopyIcon,
4
+ LineChartIcon,
5
+ RedoIcon,
6
+ SparklesIcon,
7
+ UndoIcon,
8
+ } from '@/components/icons';
9
+ import { SpreadsheetEditor } from '@/components/sheet-editor';
10
+ import { parse, unparse } from 'papaparse';
11
+ import { toast } from 'sonner';
12
+
13
+ type Metadata = any;
14
+
15
+ export const sheetArtifact = new Artifact<'sheet', Metadata>({
16
+ kind: 'sheet',
17
+ description: 'Useful for working with spreadsheets',
18
+ initialize: async () => {},
19
+ onStreamPart: ({ setArtifact, streamPart }) => {
20
+ if (streamPart.type === 'data-sheetDelta') {
21
+ setArtifact((draftArtifact) => ({
22
+ ...draftArtifact,
23
+ content: streamPart.data,
24
+ isVisible: true,
25
+ status: 'streaming',
26
+ }));
27
+ }
28
+ },
29
+ content: ({
30
+ content,
31
+ currentVersionIndex,
32
+ isCurrentVersion,
33
+ onSaveContent,
34
+ status,
35
+ }) => {
36
+ return (
37
+ <SpreadsheetEditor
38
+ content={content}
39
+ currentVersionIndex={currentVersionIndex}
40
+ isCurrentVersion={isCurrentVersion}
41
+ saveContent={onSaveContent}
42
+ status={status}
43
+ />
44
+ );
45
+ },
46
+ actions: [
47
+ {
48
+ icon: <UndoIcon size={18} />,
49
+ description: 'View Previous version',
50
+ onClick: ({ handleVersionChange }) => {
51
+ handleVersionChange('prev');
52
+ },
53
+ isDisabled: ({ currentVersionIndex }) => {
54
+ if (currentVersionIndex === 0) {
55
+ return true;
56
+ }
57
+
58
+ return false;
59
+ },
60
+ },
61
+ {
62
+ icon: <RedoIcon size={18} />,
63
+ description: 'View Next version',
64
+ onClick: ({ handleVersionChange }) => {
65
+ handleVersionChange('next');
66
+ },
67
+ isDisabled: ({ isCurrentVersion }) => {
68
+ if (isCurrentVersion) {
69
+ return true;
70
+ }
71
+
72
+ return false;
73
+ },
74
+ },
75
+ {
76
+ icon: <CopyIcon />,
77
+ description: 'Copy as .csv',
78
+ onClick: ({ content }) => {
79
+ const parsed = parse<string[]>(content, { skipEmptyLines: true });
80
+
81
+ const nonEmptyRows = parsed.data.filter((row) =>
82
+ row.some((cell) => cell.trim() !== ''),
83
+ );
84
+
85
+ const cleanedCsv = unparse(nonEmptyRows);
86
+
87
+ navigator.clipboard.writeText(cleanedCsv);
88
+ toast.success('Copied csv to clipboard!');
89
+ },
90
+ },
91
+ ],
92
+ toolbar: [
93
+ {
94
+ description: 'Format and clean data',
95
+ icon: <SparklesIcon />,
96
+ onClick: ({ sendMessage }) => {
97
+ sendMessage({
98
+ role: 'user',
99
+ parts: [
100
+ { type: 'text', text: 'Can you please format and clean the data?' },
101
+ ],
102
+ });
103
+ },
104
+ },
105
+ {
106
+ description: 'Analyze and visualize data',
107
+ icon: <LineChartIcon />,
108
+ onClick: ({ sendMessage }) => {
109
+ sendMessage({
110
+ role: 'user',
111
+ parts: [
112
+ {
113
+ type: 'text',
114
+ text: 'Can you please analyze and visualize the data by creating a new code artifact in python?',
115
+ },
116
+ ],
117
+ });
118
+ },
119
+ },
120
+ ],
121
+ });
artifacts/sheet/server.ts ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { myProvider } from '@/lib/ai/providers';
2
+ import { sheetPrompt, updateDocumentPrompt } from '@/lib/ai/prompts';
3
+ import { createDocumentHandler } from '@/lib/artifacts/server';
4
+ import { streamObject } from 'ai';
5
+ import { z } from 'zod';
6
+
7
+ export const sheetDocumentHandler = createDocumentHandler<'sheet'>({
8
+ kind: 'sheet',
9
+ onCreateDocument: async ({ title, dataStream }) => {
10
+ let draftContent = '';
11
+
12
+ const { fullStream } = streamObject({
13
+ model: myProvider.languageModel('artifact-model'),
14
+ system: sheetPrompt,
15
+ prompt: title,
16
+ schema: z.object({
17
+ csv: z.string().describe('CSV data'),
18
+ }),
19
+ });
20
+
21
+ for await (const delta of fullStream) {
22
+ const { type } = delta;
23
+
24
+ if (type === 'object') {
25
+ const { object } = delta;
26
+ const { csv } = object;
27
+
28
+ if (csv) {
29
+ dataStream.write({
30
+ type: 'data-sheetDelta',
31
+ data: csv,
32
+ transient: true,
33
+ });
34
+
35
+ draftContent = csv;
36
+ }
37
+ }
38
+ }
39
+
40
+ dataStream.write({
41
+ type: 'data-sheetDelta',
42
+ data: draftContent,
43
+ transient: true,
44
+ });
45
+
46
+ return draftContent;
47
+ },
48
+ onUpdateDocument: async ({ document, description, dataStream }) => {
49
+ let draftContent = '';
50
+
51
+ const { fullStream } = streamObject({
52
+ model: myProvider.languageModel('artifact-model'),
53
+ system: updateDocumentPrompt(document.content, 'sheet'),
54
+ prompt: description,
55
+ schema: z.object({
56
+ csv: z.string(),
57
+ }),
58
+ });
59
+
60
+ for await (const delta of fullStream) {
61
+ const { type } = delta;
62
+
63
+ if (type === 'object') {
64
+ const { object } = delta;
65
+ const { csv } = object;
66
+
67
+ if (csv) {
68
+ dataStream.write({
69
+ type: 'data-sheetDelta',
70
+ data: csv,
71
+ transient: true,
72
+ });
73
+
74
+ draftContent = csv;
75
+ }
76
+ }
77
+ }
78
+
79
+ return draftContent;
80
+ },
81
+ });
artifacts/text/client.tsx ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Artifact } from '@/components/create-artifact';
2
+ import { DiffView } from '@/components/diffview';
3
+ import { DocumentSkeleton } from '@/components/document-skeleton';
4
+ import { Editor } from '@/components/text-editor';
5
+ import {
6
+ ClockRewind,
7
+ CopyIcon,
8
+ MessageIcon,
9
+ PenIcon,
10
+ RedoIcon,
11
+ UndoIcon,
12
+ } from '@/components/icons';
13
+ import type { Suggestion } from '@/lib/db/schema';
14
+ import { toast } from 'sonner';
15
+ import { getSuggestions } from '../actions';
16
+
17
+ interface TextArtifactMetadata {
18
+ suggestions: Array<Suggestion>;
19
+ }
20
+
21
+ export const textArtifact = new Artifact<'text', TextArtifactMetadata>({
22
+ kind: 'text',
23
+ description: 'Useful for text content, like drafting essays and emails.',
24
+ initialize: async ({ documentId, setMetadata }) => {
25
+ const suggestions = await getSuggestions({ documentId });
26
+
27
+ setMetadata({
28
+ suggestions,
29
+ });
30
+ },
31
+ onStreamPart: ({ streamPart, setMetadata, setArtifact }) => {
32
+ if (streamPart.type === 'data-suggestion') {
33
+ setMetadata((metadata) => {
34
+ return {
35
+ suggestions: [...metadata.suggestions, streamPart.data],
36
+ };
37
+ });
38
+ }
39
+
40
+ if (streamPart.type === 'data-textDelta') {
41
+ setArtifact((draftArtifact) => {
42
+ return {
43
+ ...draftArtifact,
44
+ content: draftArtifact.content + streamPart.data,
45
+ isVisible:
46
+ draftArtifact.status === 'streaming' &&
47
+ draftArtifact.content.length > 400 &&
48
+ draftArtifact.content.length < 450
49
+ ? true
50
+ : draftArtifact.isVisible,
51
+ status: 'streaming',
52
+ };
53
+ });
54
+ }
55
+ },
56
+ content: ({
57
+ mode,
58
+ status,
59
+ content,
60
+ isCurrentVersion,
61
+ currentVersionIndex,
62
+ onSaveContent,
63
+ getDocumentContentById,
64
+ isLoading,
65
+ metadata,
66
+ }) => {
67
+ if (isLoading) {
68
+ return <DocumentSkeleton artifactKind="text" />;
69
+ }
70
+
71
+ if (mode === 'diff') {
72
+ const oldContent = getDocumentContentById(currentVersionIndex - 1);
73
+ const newContent = getDocumentContentById(currentVersionIndex);
74
+
75
+ return <DiffView oldContent={oldContent} newContent={newContent} />;
76
+ }
77
+
78
+ return (
79
+ <>
80
+ <div className="flex flex-row py-8 md:p-20 px-4">
81
+ <Editor
82
+ content={content}
83
+ suggestions={metadata ? metadata.suggestions : []}
84
+ isCurrentVersion={isCurrentVersion}
85
+ currentVersionIndex={currentVersionIndex}
86
+ status={status}
87
+ onSaveContent={onSaveContent}
88
+ />
89
+
90
+ {metadata?.suggestions && metadata.suggestions.length > 0 ? (
91
+ <div className="md:hidden h-dvh w-12 shrink-0" />
92
+ ) : null}
93
+ </div>
94
+ </>
95
+ );
96
+ },
97
+ actions: [
98
+ {
99
+ icon: <ClockRewind size={18} />,
100
+ description: 'View changes',
101
+ onClick: ({ handleVersionChange }) => {
102
+ handleVersionChange('toggle');
103
+ },
104
+ isDisabled: ({ currentVersionIndex, setMetadata }) => {
105
+ if (currentVersionIndex === 0) {
106
+ return true;
107
+ }
108
+
109
+ return false;
110
+ },
111
+ },
112
+ {
113
+ icon: <UndoIcon size={18} />,
114
+ description: 'View Previous version',
115
+ onClick: ({ handleVersionChange }) => {
116
+ handleVersionChange('prev');
117
+ },
118
+ isDisabled: ({ currentVersionIndex }) => {
119
+ if (currentVersionIndex === 0) {
120
+ return true;
121
+ }
122
+
123
+ return false;
124
+ },
125
+ },
126
+ {
127
+ icon: <RedoIcon size={18} />,
128
+ description: 'View Next version',
129
+ onClick: ({ handleVersionChange }) => {
130
+ handleVersionChange('next');
131
+ },
132
+ isDisabled: ({ isCurrentVersion }) => {
133
+ if (isCurrentVersion) {
134
+ return true;
135
+ }
136
+
137
+ return false;
138
+ },
139
+ },
140
+ {
141
+ icon: <CopyIcon size={18} />,
142
+ description: 'Copy to clipboard',
143
+ onClick: ({ content }) => {
144
+ navigator.clipboard.writeText(content);
145
+ toast.success('Copied to clipboard!');
146
+ },
147
+ },
148
+ ],
149
+ toolbar: [
150
+ {
151
+ icon: <PenIcon />,
152
+ description: 'Add final polish',
153
+ onClick: ({ sendMessage }) => {
154
+ sendMessage({
155
+ role: 'user',
156
+ parts: [
157
+ {
158
+ type: 'text',
159
+ text: 'Please add final polish and check for grammar, add section titles for better structure, and ensure everything reads smoothly.',
160
+ },
161
+ ],
162
+ });
163
+ },
164
+ },
165
+ {
166
+ icon: <MessageIcon />,
167
+ description: 'Request suggestions',
168
+ onClick: ({ sendMessage }) => {
169
+ sendMessage({
170
+ role: 'user',
171
+ parts: [
172
+ {
173
+ type: 'text',
174
+ text: 'Please add suggestions you have that could improve the writing.',
175
+ },
176
+ ],
177
+ });
178
+ },
179
+ },
180
+ ],
181
+ });
artifacts/text/server.ts ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { smoothStream, streamText } from 'ai';
2
+ import { myProvider } from '@/lib/ai/providers';
3
+ import { createDocumentHandler } from '@/lib/artifacts/server';
4
+ import { updateDocumentPrompt } from '@/lib/ai/prompts';
5
+
6
+ export const textDocumentHandler = createDocumentHandler<'text'>({
7
+ kind: 'text',
8
+ onCreateDocument: async ({ title, dataStream }) => {
9
+ let draftContent = '';
10
+
11
+ const { fullStream } = streamText({
12
+ model: myProvider.languageModel('artifact-model'),
13
+ system:
14
+ 'Write about the given topic. Markdown is supported. Use headings wherever appropriate.',
15
+ experimental_transform: smoothStream({ chunking: 'word' }),
16
+ prompt: title,
17
+ });
18
+
19
+ for await (const delta of fullStream) {
20
+ const { type } = delta;
21
+
22
+ if (type === 'text-delta') {
23
+ const { text } = delta;
24
+
25
+ draftContent += text;
26
+
27
+ dataStream.write({
28
+ type: 'data-textDelta',
29
+ data: text,
30
+ transient: true,
31
+ });
32
+ }
33
+ }
34
+
35
+ return draftContent;
36
+ },
37
+ onUpdateDocument: async ({ document, description, dataStream }) => {
38
+ let draftContent = '';
39
+
40
+ const { fullStream } = streamText({
41
+ model: myProvider.languageModel('artifact-model'),
42
+ system: updateDocumentPrompt(document.content, 'text'),
43
+ experimental_transform: smoothStream({ chunking: 'word' }),
44
+ prompt: description,
45
+ providerOptions: {
46
+ openai: {
47
+ prediction: {
48
+ type: 'content',
49
+ content: document.content,
50
+ },
51
+ },
52
+ },
53
+ });
54
+
55
+ for await (const delta of fullStream) {
56
+ const { type } = delta;
57
+
58
+ if (type === 'text-delta') {
59
+ const { text } = delta;
60
+
61
+ draftContent += text;
62
+
63
+ dataStream.write({
64
+ type: 'data-textDelta',
65
+ data: text,
66
+ transient: true,
67
+ });
68
+ }
69
+ }
70
+
71
+ return draftContent;
72
+ },
73
+ });
biome.jsonc ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3
+ "files": {
4
+ "ignoreUnknown": false,
5
+ "ignore": [
6
+ "**/pnpm-lock.yaml",
7
+ "lib/db/migrations",
8
+ "lib/editor/react-renderer.tsx",
9
+ "node_modules",
10
+ ".next",
11
+ "public",
12
+ ".vercel"
13
+ ]
14
+ },
15
+ "vcs": {
16
+ "enabled": true,
17
+ "clientKind": "git",
18
+ "defaultBranch": "main",
19
+ "useIgnoreFile": true
20
+ },
21
+ "formatter": {
22
+ "enabled": true,
23
+ "formatWithErrors": false,
24
+ "indentStyle": "space",
25
+ "indentWidth": 2,
26
+ "lineEnding": "lf",
27
+ "lineWidth": 80,
28
+ "attributePosition": "auto"
29
+ },
30
+ "linter": {
31
+ "enabled": true,
32
+ "rules": {
33
+ "recommended": true,
34
+ "a11y": {
35
+ "useHtmlLang": "warn", // Not in recommended ruleset, turning on manually
36
+ "noHeaderScope": "warn", // Not in recommended ruleset, turning on manually
37
+ "useValidAriaRole": {
38
+ "level": "warn",
39
+ "options": {
40
+ "ignoreNonDom": false,
41
+ "allowInvalidRoles": ["none", "text"]
42
+ }
43
+ },
44
+ "useSemanticElements": "off", // Rule is buggy, revisit later
45
+ "noSvgWithoutTitle": "off", // We do not intend to adhere to this rule
46
+ "useMediaCaption": "off", // We would need a cultural change to turn this on
47
+ "noAutofocus": "off", // We're highly intentional about when we use autofocus
48
+ "noBlankTarget": "off", // Covered by Conformance
49
+ "useFocusableInteractive": "off", // Disable focusable interactive element requirement
50
+ "useAriaPropsForRole": "off", // Disable required ARIA attributes check
51
+ "useKeyWithClickEvents": "off" // Disable keyboard event requirement with click events
52
+ },
53
+ "complexity": {
54
+ "noUselessStringConcat": "warn", // Not in recommended ruleset, turning on manually
55
+ "noForEach": "off", // forEach is too familiar to ban
56
+ "noUselessSwitchCase": "off", // Turned off due to developer preferences
57
+ "noUselessThisAlias": "off", // Turned off due to developer preferences
58
+ "noBannedTypes": "off"
59
+ },
60
+ "correctness": {
61
+ "noUnusedImports": "warn", // Not in recommended ruleset, turning on manually
62
+ "useArrayLiterals": "warn", // Not in recommended ruleset, turning on manually
63
+ "noNewSymbol": "warn", // Not in recommended ruleset, turning on manually
64
+ "useJsxKeyInIterable": "off", // Rule is buggy, revisit later
65
+ "useExhaustiveDependencies": "off", // Community feedback on this rule has been poor, we will continue with ESLint
66
+ "noUnnecessaryContinue": "off" // Turned off due to developer preferences
67
+ },
68
+ "security": {
69
+ "noDangerouslySetInnerHtml": "off" // Covered by Conformance
70
+ },
71
+ "style": {
72
+ "useFragmentSyntax": "warn", // Not in recommended ruleset, turning on manually
73
+ "noYodaExpression": "warn", // Not in recommended ruleset, turning on manually
74
+ "useDefaultParameterLast": "warn", // Not in recommended ruleset, turning on manually
75
+ "useExponentiationOperator": "off", // Obscure and arguably not easily readable
76
+ "noUnusedTemplateLiteral": "off", // Stylistic opinion
77
+ "noUselessElse": "off" // Stylistic opinion
78
+ },
79
+ "suspicious": {
80
+ "noExplicitAny": "off" // We trust Vercelians to use any only when necessary
81
+ },
82
+ "nursery": {
83
+ "noStaticElementInteractions": "warn",
84
+ "noHeadImportInDocument": "warn",
85
+ "noDocumentImportInPage": "warn",
86
+ "noDuplicateElseIf": "warn",
87
+ "noIrregularWhitespace": "warn",
88
+ "useValidAutocomplete": "warn"
89
+ }
90
+ }
91
+ },
92
+ "javascript": {
93
+ "jsxRuntime": "reactClassic",
94
+ "formatter": {
95
+ "jsxQuoteStyle": "double",
96
+ "quoteProperties": "asNeeded",
97
+ "trailingCommas": "all",
98
+ "semicolons": "always",
99
+ "arrowParentheses": "always",
100
+ "bracketSpacing": true,
101
+ "bracketSameLine": false,
102
+ "quoteStyle": "single",
103
+ "attributePosition": "auto"
104
+ }
105
+ },
106
+ "json": {
107
+ "formatter": {
108
+ "enabled": true,
109
+ "trailingCommas": "none"
110
+ },
111
+ "parser": {
112
+ "allowComments": true,
113
+ "allowTrailingCommas": false
114
+ }
115
+ },
116
+ "css": {
117
+ "formatter": { "enabled": false },
118
+ "linter": { "enabled": false }
119
+ },
120
+ "organizeImports": { "enabled": false },
121
+ "overrides": [
122
+ // Playwright requires an object destructure, even if empty
123
+ // https://github.com/microsoft/playwright/issues/30007
124
+ {
125
+ "include": ["playwright/**"],
126
+ "linter": {
127
+ "rules": {
128
+ "correctness": {
129
+ "noEmptyPattern": "off"
130
+ }
131
+ }
132
+ }
133
+ }
134
+ ]
135
+ }
components.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "default",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "app/globals.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ }
20
+ }
components/app-sidebar.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import type { User } from 'next-auth';
4
+ import { useRouter } from 'next/navigation';
5
+
6
+ import { PlusIcon } from '@/components/icons';
7
+ import { SidebarHistory } from '@/components/sidebar-history';
8
+ import { SidebarUserNav } from '@/components/sidebar-user-nav';
9
+ import { Button } from '@/components/ui/button';
10
+ import {
11
+ Sidebar,
12
+ SidebarContent,
13
+ SidebarFooter,
14
+ SidebarHeader,
15
+ SidebarMenu,
16
+ useSidebar,
17
+ } from '@/components/ui/sidebar';
18
+ import Link from 'next/link';
19
+ import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
20
+
21
+ export function AppSidebar({ user }: { user: User | undefined }) {
22
+ const router = useRouter();
23
+ const { setOpenMobile } = useSidebar();
24
+
25
+ return (
26
+ <Sidebar className="group-data-[side=left]:border-r-0">
27
+ <SidebarHeader>
28
+ <SidebarMenu>
29
+ <div className="flex flex-row justify-between items-center">
30
+ <Link
31
+ href="/"
32
+ onClick={() => {
33
+ setOpenMobile(false);
34
+ }}
35
+ className="flex flex-row gap-3 items-center"
36
+ >
37
+ <span className="text-lg font-semibold px-2 hover:bg-muted rounded-md cursor-pointer">
38
+ Chatbot
39
+ </span>
40
+ </Link>
41
+ <Tooltip>
42
+ <TooltipTrigger asChild>
43
+ <Button
44
+ variant="ghost"
45
+ type="button"
46
+ className="p-2 h-fit"
47
+ onClick={() => {
48
+ setOpenMobile(false);
49
+ router.push('/');
50
+ router.refresh();
51
+ }}
52
+ >
53
+ <PlusIcon />
54
+ </Button>
55
+ </TooltipTrigger>
56
+ <TooltipContent align="end">New Chat</TooltipContent>
57
+ </Tooltip>
58
+ </div>
59
+ </SidebarMenu>
60
+ </SidebarHeader>
61
+ <SidebarContent>
62
+ <SidebarHistory user={user} />
63
+ </SidebarContent>
64
+ <SidebarFooter>{user && <SidebarUserNav user={user} />}</SidebarFooter>
65
+ </Sidebar>
66
+ );
67
+ }
components/artifact-actions.tsx ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from './ui/button';
2
+ import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
3
+ import { artifactDefinitions, UIArtifact } from './artifact';
4
+ import { Dispatch, memo, SetStateAction, useState } from 'react';
5
+ import { ArtifactActionContext } from './create-artifact';
6
+ import { cn } from '@/lib/utils';
7
+ import { toast } from 'sonner';
8
+
9
+ interface ArtifactActionsProps {
10
+ artifact: UIArtifact;
11
+ handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void;
12
+ currentVersionIndex: number;
13
+ isCurrentVersion: boolean;
14
+ mode: 'edit' | 'diff';
15
+ metadata: any;
16
+ setMetadata: Dispatch<SetStateAction<any>>;
17
+ }
18
+
19
+ function PureArtifactActions({
20
+ artifact,
21
+ handleVersionChange,
22
+ currentVersionIndex,
23
+ isCurrentVersion,
24
+ mode,
25
+ metadata,
26
+ setMetadata,
27
+ }: ArtifactActionsProps) {
28
+ const [isLoading, setIsLoading] = useState(false);
29
+
30
+ const artifactDefinition = artifactDefinitions.find(
31
+ (definition) => definition.kind === artifact.kind,
32
+ );
33
+
34
+ if (!artifactDefinition) {
35
+ throw new Error('Artifact definition not found!');
36
+ }
37
+
38
+ const actionContext: ArtifactActionContext = {
39
+ content: artifact.content,
40
+ handleVersionChange,
41
+ currentVersionIndex,
42
+ isCurrentVersion,
43
+ mode,
44
+ metadata,
45
+ setMetadata,
46
+ };
47
+
48
+ return (
49
+ <div className="flex flex-row gap-1">
50
+ {artifactDefinition.actions.map((action) => (
51
+ <Tooltip key={action.description}>
52
+ <TooltipTrigger asChild>
53
+ <Button
54
+ variant="outline"
55
+ className={cn('h-fit dark:hover:bg-zinc-700', {
56
+ 'p-2': !action.label,
57
+ 'py-1.5 px-2': action.label,
58
+ })}
59
+ onClick={async () => {
60
+ setIsLoading(true);
61
+
62
+ try {
63
+ await Promise.resolve(action.onClick(actionContext));
64
+ } catch (error) {
65
+ toast.error('Failed to execute action');
66
+ } finally {
67
+ setIsLoading(false);
68
+ }
69
+ }}
70
+ disabled={
71
+ isLoading || artifact.status === 'streaming'
72
+ ? true
73
+ : action.isDisabled
74
+ ? action.isDisabled(actionContext)
75
+ : false
76
+ }
77
+ >
78
+ {action.icon}
79
+ {action.label}
80
+ </Button>
81
+ </TooltipTrigger>
82
+ <TooltipContent>{action.description}</TooltipContent>
83
+ </Tooltip>
84
+ ))}
85
+ </div>
86
+ );
87
+ }
88
+
89
+ export const ArtifactActions = memo(
90
+ PureArtifactActions,
91
+ (prevProps, nextProps) => {
92
+ if (prevProps.artifact.status !== nextProps.artifact.status) return false;
93
+ if (prevProps.currentVersionIndex !== nextProps.currentVersionIndex)
94
+ return false;
95
+ if (prevProps.isCurrentVersion !== nextProps.isCurrentVersion) return false;
96
+ if (prevProps.artifact.content !== nextProps.artifact.content) return false;
97
+
98
+ return true;
99
+ },
100
+ );
components/artifact-close-button.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { memo } from 'react';
2
+ import { CrossIcon } from './icons';
3
+ import { Button } from './ui/button';
4
+ import { initialArtifactData, useArtifact } from '@/hooks/use-artifact';
5
+
6
+ function PureArtifactCloseButton() {
7
+ const { setArtifact } = useArtifact();
8
+
9
+ return (
10
+ <Button
11
+ data-testid="artifact-close-button"
12
+ variant="outline"
13
+ className="h-fit p-2 dark:hover:bg-zinc-700"
14
+ onClick={() => {
15
+ setArtifact((currentArtifact) =>
16
+ currentArtifact.status === 'streaming'
17
+ ? {
18
+ ...currentArtifact,
19
+ isVisible: false,
20
+ }
21
+ : { ...initialArtifactData, status: 'idle' },
22
+ );
23
+ }}
24
+ >
25
+ <CrossIcon size={18} />
26
+ </Button>
27
+ );
28
+ }
29
+
30
+ export const ArtifactCloseButton = memo(PureArtifactCloseButton, () => true);
components/artifact-messages.tsx ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PreviewMessage, ThinkingMessage } from './message';
2
+ import type { Vote } from '@/lib/db/schema';
3
+ import { memo } from 'react';
4
+ import equal from 'fast-deep-equal';
5
+ import type { UIArtifact } from './artifact';
6
+ import type { UseChatHelpers } from '@ai-sdk/react';
7
+ import { motion } from 'framer-motion';
8
+ import { useMessages } from '@/hooks/use-messages';
9
+ import type { ChatMessage } from '@/lib/types';
10
+
11
+ interface ArtifactMessagesProps {
12
+ chatId: string;
13
+ status: UseChatHelpers<ChatMessage>['status'];
14
+ votes: Array<Vote> | undefined;
15
+ messages: ChatMessage[];
16
+ setMessages: UseChatHelpers<ChatMessage>['setMessages'];
17
+ regenerate: UseChatHelpers<ChatMessage>['regenerate'];
18
+ isReadonly: boolean;
19
+ artifactStatus: UIArtifact['status'];
20
+ }
21
+
22
+ function PureArtifactMessages({
23
+ chatId,
24
+ status,
25
+ votes,
26
+ messages,
27
+ setMessages,
28
+ regenerate,
29
+ isReadonly,
30
+ }: ArtifactMessagesProps) {
31
+ const {
32
+ containerRef: messagesContainerRef,
33
+ endRef: messagesEndRef,
34
+ onViewportEnter,
35
+ onViewportLeave,
36
+ hasSentMessage,
37
+ } = useMessages({
38
+ chatId,
39
+ status,
40
+ });
41
+
42
+ return (
43
+ <div
44
+ ref={messagesContainerRef}
45
+ className="flex flex-col gap-4 h-full items-center overflow-y-scroll px-4 pt-20"
46
+ >
47
+ {messages.map((message, index) => (
48
+ <PreviewMessage
49
+ chatId={chatId}
50
+ key={message.id}
51
+ message={message}
52
+ isLoading={status === 'streaming' && index === messages.length - 1}
53
+ vote={
54
+ votes
55
+ ? votes.find((vote) => vote.messageId === message.id)
56
+ : undefined
57
+ }
58
+ setMessages={setMessages}
59
+ regenerate={regenerate}
60
+ isReadonly={isReadonly}
61
+ requiresScrollPadding={
62
+ hasSentMessage && index === messages.length - 1
63
+ }
64
+ />
65
+ ))}
66
+
67
+ {status === 'submitted' &&
68
+ messages.length > 0 &&
69
+ messages[messages.length - 1].role === 'user' && <ThinkingMessage />}
70
+
71
+ <motion.div
72
+ ref={messagesEndRef}
73
+ className="shrink-0 min-w-[24px] min-h-[24px]"
74
+ onViewportLeave={onViewportLeave}
75
+ onViewportEnter={onViewportEnter}
76
+ />
77
+ </div>
78
+ );
79
+ }
80
+
81
+ function areEqual(
82
+ prevProps: ArtifactMessagesProps,
83
+ nextProps: ArtifactMessagesProps,
84
+ ) {
85
+ if (
86
+ prevProps.artifactStatus === 'streaming' &&
87
+ nextProps.artifactStatus === 'streaming'
88
+ )
89
+ return true;
90
+
91
+ if (prevProps.status !== nextProps.status) return false;
92
+ if (prevProps.status && nextProps.status) return false;
93
+ if (prevProps.messages.length !== nextProps.messages.length) return false;
94
+ if (!equal(prevProps.votes, nextProps.votes)) return false;
95
+
96
+ return true;
97
+ }
98
+
99
+ export const ArtifactMessages = memo(PureArtifactMessages, areEqual);
components/artifact.tsx ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { formatDistance } from 'date-fns';
2
+ import { AnimatePresence, motion } from 'framer-motion';
3
+ import {
4
+ type Dispatch,
5
+ memo,
6
+ type SetStateAction,
7
+ useCallback,
8
+ useEffect,
9
+ useState,
10
+ } from 'react';
11
+ import useSWR, { useSWRConfig } from 'swr';
12
+ import { useDebounceCallback, useWindowSize } from 'usehooks-ts';
13
+ import type { Document, Vote } from '@/lib/db/schema';
14
+ import { fetcher } from '@/lib/utils';
15
+ import { MultimodalInput } from './multimodal-input';
16
+ import { Toolbar } from './toolbar';
17
+ import { VersionFooter } from './version-footer';
18
+ import { ArtifactActions } from './artifact-actions';
19
+ import { ArtifactCloseButton } from './artifact-close-button';
20
+ import { ArtifactMessages } from './artifact-messages';
21
+ import { useSidebar } from './ui/sidebar';
22
+ import { useArtifact } from '@/hooks/use-artifact';
23
+ import { imageArtifact } from '@/artifacts/image/client';
24
+ import { codeArtifact } from '@/artifacts/code/client';
25
+ import { sheetArtifact } from '@/artifacts/sheet/client';
26
+ import { textArtifact } from '@/artifacts/text/client';
27
+ import equal from 'fast-deep-equal';
28
+ import type { UseChatHelpers } from '@ai-sdk/react';
29
+ import type { VisibilityType } from './visibility-selector';
30
+ import type { Attachment, ChatMessage } from '@/lib/types';
31
+
32
+ export const artifactDefinitions = [
33
+ textArtifact,
34
+ codeArtifact,
35
+ imageArtifact,
36
+ sheetArtifact,
37
+ ];
38
+ export type ArtifactKind = (typeof artifactDefinitions)[number]['kind'];
39
+
40
+ export interface UIArtifact {
41
+ title: string;
42
+ documentId: string;
43
+ kind: ArtifactKind;
44
+ content: string;
45
+ isVisible: boolean;
46
+ status: 'streaming' | 'idle';
47
+ boundingBox: {
48
+ top: number;
49
+ left: number;
50
+ width: number;
51
+ height: number;
52
+ };
53
+ }
54
+
55
+ function PureArtifact({
56
+ chatId,
57
+ input,
58
+ setInput,
59
+ status,
60
+ stop,
61
+ attachments,
62
+ setAttachments,
63
+ sendMessage,
64
+ messages,
65
+ setMessages,
66
+ regenerate,
67
+ votes,
68
+ isReadonly,
69
+ selectedVisibilityType,
70
+ }: {
71
+ chatId: string;
72
+ input: string;
73
+ setInput: Dispatch<SetStateAction<string>>;
74
+ status: UseChatHelpers<ChatMessage>['status'];
75
+ stop: UseChatHelpers<ChatMessage>['stop'];
76
+ attachments: Attachment[];
77
+ setAttachments: Dispatch<SetStateAction<Attachment[]>>;
78
+ messages: ChatMessage[];
79
+ setMessages: UseChatHelpers<ChatMessage>['setMessages'];
80
+ votes: Array<Vote> | undefined;
81
+ sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
82
+ regenerate: UseChatHelpers<ChatMessage>['regenerate'];
83
+ isReadonly: boolean;
84
+ selectedVisibilityType: VisibilityType;
85
+ }) {
86
+ const { artifact, setArtifact, metadata, setMetadata } = useArtifact();
87
+
88
+ const {
89
+ data: documents,
90
+ isLoading: isDocumentsFetching,
91
+ mutate: mutateDocuments,
92
+ } = useSWR<Array<Document>>(
93
+ artifact.documentId !== 'init' && artifact.status !== 'streaming'
94
+ ? `/api/document?id=${artifact.documentId}`
95
+ : null,
96
+ fetcher,
97
+ );
98
+
99
+ const [mode, setMode] = useState<'edit' | 'diff'>('edit');
100
+ const [document, setDocument] = useState<Document | null>(null);
101
+ const [currentVersionIndex, setCurrentVersionIndex] = useState(-1);
102
+
103
+ const { open: isSidebarOpen } = useSidebar();
104
+
105
+ useEffect(() => {
106
+ if (documents && documents.length > 0) {
107
+ const mostRecentDocument = documents.at(-1);
108
+
109
+ if (mostRecentDocument) {
110
+ setDocument(mostRecentDocument);
111
+ setCurrentVersionIndex(documents.length - 1);
112
+ setArtifact((currentArtifact) => ({
113
+ ...currentArtifact,
114
+ content: mostRecentDocument.content ?? '',
115
+ }));
116
+ }
117
+ }
118
+ }, [documents, setArtifact]);
119
+
120
+ useEffect(() => {
121
+ mutateDocuments();
122
+ }, [artifact.status, mutateDocuments]);
123
+
124
+ const { mutate } = useSWRConfig();
125
+ const [isContentDirty, setIsContentDirty] = useState(false);
126
+
127
+ const handleContentChange = useCallback(
128
+ (updatedContent: string) => {
129
+ if (!artifact) return;
130
+
131
+ mutate<Array<Document>>(
132
+ `/api/document?id=${artifact.documentId}`,
133
+ async (currentDocuments) => {
134
+ if (!currentDocuments) return undefined;
135
+
136
+ const currentDocument = currentDocuments.at(-1);
137
+
138
+ if (!currentDocument || !currentDocument.content) {
139
+ setIsContentDirty(false);
140
+ return currentDocuments;
141
+ }
142
+
143
+ if (currentDocument.content !== updatedContent) {
144
+ await fetch(`/api/document?id=${artifact.documentId}`, {
145
+ method: 'POST',
146
+ body: JSON.stringify({
147
+ title: artifact.title,
148
+ content: updatedContent,
149
+ kind: artifact.kind,
150
+ }),
151
+ });
152
+
153
+ setIsContentDirty(false);
154
+
155
+ const newDocument = {
156
+ ...currentDocument,
157
+ content: updatedContent,
158
+ createdAt: new Date(),
159
+ };
160
+
161
+ return [...currentDocuments, newDocument];
162
+ }
163
+ return currentDocuments;
164
+ },
165
+ { revalidate: false },
166
+ );
167
+ },
168
+ [artifact, mutate],
169
+ );
170
+
171
+ const debouncedHandleContentChange = useDebounceCallback(
172
+ handleContentChange,
173
+ 2000,
174
+ );
175
+
176
+ const saveContent = useCallback(
177
+ (updatedContent: string, debounce: boolean) => {
178
+ if (document && updatedContent !== document.content) {
179
+ setIsContentDirty(true);
180
+
181
+ if (debounce) {
182
+ debouncedHandleContentChange(updatedContent);
183
+ } else {
184
+ handleContentChange(updatedContent);
185
+ }
186
+ }
187
+ },
188
+ [document, debouncedHandleContentChange, handleContentChange],
189
+ );
190
+
191
+ function getDocumentContentById(index: number) {
192
+ if (!documents) return '';
193
+ if (!documents[index]) return '';
194
+ return documents[index].content ?? '';
195
+ }
196
+
197
+ const handleVersionChange = (type: 'next' | 'prev' | 'toggle' | 'latest') => {
198
+ if (!documents) return;
199
+
200
+ if (type === 'latest') {
201
+ setCurrentVersionIndex(documents.length - 1);
202
+ setMode('edit');
203
+ }
204
+
205
+ if (type === 'toggle') {
206
+ setMode((mode) => (mode === 'edit' ? 'diff' : 'edit'));
207
+ }
208
+
209
+ if (type === 'prev') {
210
+ if (currentVersionIndex > 0) {
211
+ setCurrentVersionIndex((index) => index - 1);
212
+ }
213
+ } else if (type === 'next') {
214
+ if (currentVersionIndex < documents.length - 1) {
215
+ setCurrentVersionIndex((index) => index + 1);
216
+ }
217
+ }
218
+ };
219
+
220
+ const [isToolbarVisible, setIsToolbarVisible] = useState(false);
221
+
222
+ /*
223
+ * NOTE: if there are no documents, or if
224
+ * the documents are being fetched, then
225
+ * we mark it as the current version.
226
+ */
227
+
228
+ const isCurrentVersion =
229
+ documents && documents.length > 0
230
+ ? currentVersionIndex === documents.length - 1
231
+ : true;
232
+
233
+ const { width: windowWidth, height: windowHeight } = useWindowSize();
234
+ const isMobile = windowWidth ? windowWidth < 768 : false;
235
+
236
+ const artifactDefinition = artifactDefinitions.find(
237
+ (definition) => definition.kind === artifact.kind,
238
+ );
239
+
240
+ if (!artifactDefinition) {
241
+ throw new Error('Artifact definition not found!');
242
+ }
243
+
244
+ useEffect(() => {
245
+ if (artifact.documentId !== 'init') {
246
+ if (artifactDefinition.initialize) {
247
+ artifactDefinition.initialize({
248
+ documentId: artifact.documentId,
249
+ setMetadata,
250
+ });
251
+ }
252
+ }
253
+ }, [artifact.documentId, artifactDefinition, setMetadata]);
254
+
255
+ return (
256
+ <AnimatePresence>
257
+ {artifact.isVisible && (
258
+ <motion.div
259
+ data-testid="artifact"
260
+ className="flex flex-row h-dvh w-dvw fixed top-0 left-0 z-50 bg-transparent"
261
+ initial={{ opacity: 1 }}
262
+ animate={{ opacity: 1 }}
263
+ exit={{ opacity: 0, transition: { delay: 0.4 } }}
264
+ >
265
+ {!isMobile && (
266
+ <motion.div
267
+ className="fixed bg-background h-dvh"
268
+ initial={{
269
+ width: isSidebarOpen ? windowWidth - 256 : windowWidth,
270
+ right: 0,
271
+ }}
272
+ animate={{ width: windowWidth, right: 0 }}
273
+ exit={{
274
+ width: isSidebarOpen ? windowWidth - 256 : windowWidth,
275
+ right: 0,
276
+ }}
277
+ />
278
+ )}
279
+
280
+ {!isMobile && (
281
+ <motion.div
282
+ className="relative w-[400px] bg-muted dark:bg-background h-dvh shrink-0"
283
+ initial={{ opacity: 0, x: 10, scale: 1 }}
284
+ animate={{
285
+ opacity: 1,
286
+ x: 0,
287
+ scale: 1,
288
+ transition: {
289
+ delay: 0.2,
290
+ type: 'spring',
291
+ stiffness: 200,
292
+ damping: 30,
293
+ },
294
+ }}
295
+ exit={{
296
+ opacity: 0,
297
+ x: 0,
298
+ scale: 1,
299
+ transition: { duration: 0 },
300
+ }}
301
+ >
302
+ <AnimatePresence>
303
+ {!isCurrentVersion && (
304
+ <motion.div
305
+ className="left-0 absolute h-dvh w-[400px] top-0 bg-zinc-900/50 z-50"
306
+ initial={{ opacity: 0 }}
307
+ animate={{ opacity: 1 }}
308
+ exit={{ opacity: 0 }}
309
+ />
310
+ )}
311
+ </AnimatePresence>
312
+
313
+ <div className="flex flex-col h-full justify-between items-center">
314
+ <ArtifactMessages
315
+ chatId={chatId}
316
+ status={status}
317
+ votes={votes}
318
+ messages={messages}
319
+ setMessages={setMessages}
320
+ regenerate={regenerate}
321
+ isReadonly={isReadonly}
322
+ artifactStatus={artifact.status}
323
+ />
324
+
325
+ <div className="flex flex-row gap-2 relative items-end w-full px-4 pb-4">
326
+ <MultimodalInput
327
+ chatId={chatId}
328
+ input={input}
329
+ setInput={setInput}
330
+ status={status}
331
+ stop={stop}
332
+ attachments={attachments}
333
+ setAttachments={setAttachments}
334
+ messages={messages}
335
+ sendMessage={sendMessage}
336
+ className="bg-background dark:bg-muted"
337
+ setMessages={setMessages}
338
+ selectedVisibilityType={selectedVisibilityType}
339
+ />
340
+ </div>
341
+ </div>
342
+ </motion.div>
343
+ )}
344
+
345
+ <motion.div
346
+ className="fixed dark:bg-muted bg-background h-dvh flex flex-col overflow-y-scroll md:border-l dark:border-zinc-700 border-zinc-200"
347
+ initial={
348
+ isMobile
349
+ ? {
350
+ opacity: 1,
351
+ x: artifact.boundingBox.left,
352
+ y: artifact.boundingBox.top,
353
+ height: artifact.boundingBox.height,
354
+ width: artifact.boundingBox.width,
355
+ borderRadius: 50,
356
+ }
357
+ : {
358
+ opacity: 1,
359
+ x: artifact.boundingBox.left,
360
+ y: artifact.boundingBox.top,
361
+ height: artifact.boundingBox.height,
362
+ width: artifact.boundingBox.width,
363
+ borderRadius: 50,
364
+ }
365
+ }
366
+ animate={
367
+ isMobile
368
+ ? {
369
+ opacity: 1,
370
+ x: 0,
371
+ y: 0,
372
+ height: windowHeight,
373
+ width: windowWidth ? windowWidth : 'calc(100dvw)',
374
+ borderRadius: 0,
375
+ transition: {
376
+ delay: 0,
377
+ type: 'spring',
378
+ stiffness: 200,
379
+ damping: 30,
380
+ duration: 5000,
381
+ },
382
+ }
383
+ : {
384
+ opacity: 1,
385
+ x: 400,
386
+ y: 0,
387
+ height: windowHeight,
388
+ width: windowWidth
389
+ ? windowWidth - 400
390
+ : 'calc(100dvw-400px)',
391
+ borderRadius: 0,
392
+ transition: {
393
+ delay: 0,
394
+ type: 'spring',
395
+ stiffness: 200,
396
+ damping: 30,
397
+ duration: 5000,
398
+ },
399
+ }
400
+ }
401
+ exit={{
402
+ opacity: 0,
403
+ scale: 0.5,
404
+ transition: {
405
+ delay: 0.1,
406
+ type: 'spring',
407
+ stiffness: 600,
408
+ damping: 30,
409
+ },
410
+ }}
411
+ >
412
+ <div className="p-2 flex flex-row justify-between items-start">
413
+ <div className="flex flex-row gap-4 items-start">
414
+ <ArtifactCloseButton />
415
+
416
+ <div className="flex flex-col">
417
+ <div className="font-medium">{artifact.title}</div>
418
+
419
+ {isContentDirty ? (
420
+ <div className="text-sm text-muted-foreground">
421
+ Saving changes...
422
+ </div>
423
+ ) : document ? (
424
+ <div className="text-sm text-muted-foreground">
425
+ {`Updated ${formatDistance(
426
+ new Date(document.createdAt),
427
+ new Date(),
428
+ {
429
+ addSuffix: true,
430
+ },
431
+ )}`}
432
+ </div>
433
+ ) : (
434
+ <div className="w-32 h-3 mt-2 bg-muted-foreground/20 rounded-md animate-pulse" />
435
+ )}
436
+ </div>
437
+ </div>
438
+
439
+ <ArtifactActions
440
+ artifact={artifact}
441
+ currentVersionIndex={currentVersionIndex}
442
+ handleVersionChange={handleVersionChange}
443
+ isCurrentVersion={isCurrentVersion}
444
+ mode={mode}
445
+ metadata={metadata}
446
+ setMetadata={setMetadata}
447
+ />
448
+ </div>
449
+
450
+ <div className="dark:bg-muted bg-background h-full overflow-y-scroll !max-w-full items-center">
451
+ <artifactDefinition.content
452
+ title={artifact.title}
453
+ content={
454
+ isCurrentVersion
455
+ ? artifact.content
456
+ : getDocumentContentById(currentVersionIndex)
457
+ }
458
+ mode={mode}
459
+ status={artifact.status}
460
+ currentVersionIndex={currentVersionIndex}
461
+ suggestions={[]}
462
+ onSaveContent={saveContent}
463
+ isInline={false}
464
+ isCurrentVersion={isCurrentVersion}
465
+ getDocumentContentById={getDocumentContentById}
466
+ isLoading={isDocumentsFetching && !artifact.content}
467
+ metadata={metadata}
468
+ setMetadata={setMetadata}
469
+ />
470
+
471
+ <AnimatePresence>
472
+ {isCurrentVersion && (
473
+ <Toolbar
474
+ isToolbarVisible={isToolbarVisible}
475
+ setIsToolbarVisible={setIsToolbarVisible}
476
+ sendMessage={sendMessage}
477
+ status={status}
478
+ stop={stop}
479
+ setMessages={setMessages}
480
+ artifactKind={artifact.kind}
481
+ />
482
+ )}
483
+ </AnimatePresence>
484
+ </div>
485
+
486
+ <AnimatePresence>
487
+ {!isCurrentVersion && (
488
+ <VersionFooter
489
+ currentVersionIndex={currentVersionIndex}
490
+ documents={documents}
491
+ handleVersionChange={handleVersionChange}
492
+ />
493
+ )}
494
+ </AnimatePresence>
495
+ </motion.div>
496
+ </motion.div>
497
+ )}
498
+ </AnimatePresence>
499
+ );
500
+ }
501
+
502
+ export const Artifact = memo(PureArtifact, (prevProps, nextProps) => {
503
+ if (prevProps.status !== nextProps.status) return false;
504
+ if (!equal(prevProps.votes, nextProps.votes)) return false;
505
+ if (prevProps.input !== nextProps.input) return false;
506
+ if (!equal(prevProps.messages, nextProps.messages.length)) return false;
507
+ if (prevProps.selectedVisibilityType !== nextProps.selectedVisibilityType)
508
+ return false;
509
+
510
+ return true;
511
+ });
components/auth-form.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Form from 'next/form';
2
+
3
+ import { Input } from './ui/input';
4
+ import { Label } from './ui/label';
5
+
6
+ export function AuthForm({
7
+ action,
8
+ children,
9
+ defaultEmail = '',
10
+ }: {
11
+ action: NonNullable<
12
+ string | ((formData: FormData) => void | Promise<void>) | undefined
13
+ >;
14
+ children: React.ReactNode;
15
+ defaultEmail?: string;
16
+ }) {
17
+ return (
18
+ <Form action={action} className="flex flex-col gap-4 px-4 sm:px-16">
19
+ <div className="flex flex-col gap-2">
20
+ <Label
21
+ htmlFor="email"
22
+ className="text-zinc-600 font-normal dark:text-zinc-400"
23
+ >
24
+ Email Address
25
+ </Label>
26
+
27
+ <Input
28
+ id="email"
29
+ name="email"
30
+ className="bg-muted text-md md:text-sm"
31
+ type="email"
32
+ placeholder="user@acme.com"
33
+ autoComplete="email"
34
+ required
35
+ autoFocus
36
+ defaultValue={defaultEmail}
37
+ />
38
+ </div>
39
+
40
+ <div className="flex flex-col gap-2">
41
+ <Label
42
+ htmlFor="password"
43
+ className="text-zinc-600 font-normal dark:text-zinc-400"
44
+ >
45
+ Password
46
+ </Label>
47
+
48
+ <Input
49
+ id="password"
50
+ name="password"
51
+ className="bg-muted text-md md:text-sm"
52
+ type="password"
53
+ required
54
+ />
55
+ </div>
56
+
57
+ {children}
58
+ </Form>
59
+ );
60
+ }