├── .dockerignore ├── .env.example ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── (auth) │ ├── actions.ts │ ├── api │ │ └── auth │ │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── auth.config.ts │ ├── auth.ts │ ├── login │ │ └── page.tsx │ └── register │ │ └── page.tsx ├── (chat) │ ├── actions.ts │ ├── api │ │ ├── chat │ │ │ └── route.ts │ │ ├── document │ │ │ └── route.ts │ │ ├── files │ │ │ └── upload │ │ │ │ └── route.ts │ │ ├── history │ │ │ └── route.ts │ │ ├── suggestions │ │ │ └── route.ts │ │ └── vote │ │ │ └── route.ts │ ├── chat │ │ └── [id] │ │ │ └── page.tsx │ ├── layout.tsx │ ├── opengraph-image.png │ ├── page.tsx │ └── twitter-image.png ├── favicon.ico ├── globals.css └── layout.tsx ├── biome.jsonc ├── components.json ├── components ├── app-sidebar.tsx ├── auth-form.tsx ├── block-actions.tsx ├── block-close-button.tsx ├── block-messages.tsx ├── block.tsx ├── chat-header.tsx ├── chat.tsx ├── code-block.tsx ├── code-editor.tsx ├── console.tsx ├── data-stream-handler.tsx ├── deep-research.tsx ├── diffview.tsx ├── document-preview.tsx ├── document-skeleton.tsx ├── document.tsx ├── editor.tsx ├── extract-results.tsx ├── icons.tsx ├── markdown.tsx ├── message-actions.tsx ├── message-editor.tsx ├── message.tsx ├── messages.tsx ├── model-selector.tsx ├── multimodal-input.tsx ├── overview.tsx ├── preview-attachment.tsx ├── run-code-button.tsx ├── scrape-results.tsx ├── search-results.tsx ├── sidebar-history.tsx ├── sidebar-toggle.tsx ├── sidebar-user-nav.tsx ├── sign-out-form.tsx ├── spreadsheet-editor.tsx ├── submit-button.tsx ├── suggested-actions.tsx ├── suggestion.tsx ├── theme-provider.tsx ├── toolbar.tsx ├── ui │ ├── alert-dialog.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── progress.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── sidebar.tsx │ ├── skeleton.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ └── tooltip.tsx ├── use-scroll-to-bottom.ts ├── version-footer.tsx ├── visibility-selector.tsx └── weather.tsx ├── docker-compose.yml ├── drizzle.config.ts ├── hooks ├── use-block.ts ├── use-chat-visibility.ts ├── use-mobile.tsx └── use-user-message-id.ts ├── lib ├── ai │ ├── custom-middleware.ts │ ├── index.ts │ ├── models.ts │ └── prompts.ts ├── db │ ├── migrate.ts │ ├── migrations │ │ ├── 0000_keen_devos.sql │ │ ├── 0001_sparkling_blue_marvel.sql │ │ ├── 0002_wandering_riptide.sql │ │ ├── 0003_cloudy_glorian.sql │ │ ├── 0004_odd_slayback.sql │ │ ├── 0005_kind_sheet.sql │ │ └── meta │ │ │ ├── 0000_snapshot.json │ │ │ ├── 0001_snapshot.json │ │ │ ├── 0002_snapshot.json │ │ │ ├── 0003_snapshot.json │ │ │ ├── 0004_snapshot.json │ │ │ └── _journal.json │ ├── queries.ts │ └── schema.ts ├── deep-research-context.tsx ├── editor │ ├── config.ts │ ├── diff.js │ ├── functions.tsx │ ├── react-renderer.tsx │ └── suggestions.tsx ├── rate-limit.ts ├── spreadsheet.ts ├── types.ts └── utils.ts ├── middleware.ts ├── next-env.d.ts ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── fonts │ ├── geist-mono.woff2 │ └── geist.woff2 ├── images │ ├── demo-thumbnail.png │ ├── hero.png │ ├── open-deep-research.png │ └── readme.png ├── open-deep-researched-pic.png └── open-hero.png ├── start.sh ├── tailwind.config.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | .next 4 | .env* 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | .DS_Store 9 | *.pem 10 | coverage 11 | .vscode 12 | .idea 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Get your OpenAI API Key here: https://platform.openai.com/account/api-keys 2 | OPENAI_API_KEY=**** 3 | 4 | # Get your OpenRouter API Key here: https://openrouter.ai/settings/keys 5 | OPENROUTER_API_KEY=**** 6 | 7 | # Get your Firecrawl API Key here: https://www.firecrawl.dev/ 8 | FIRECRAWL_API_KEY=**** 9 | 10 | # Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32` 11 | AUTH_SECRET=**** 12 | 13 | # Maximum duration in seconds for serverless functions (Vercel Edge has a lower limit on lower plans) 14 | MAX_DURATION=60 15 | 16 | # The following keys below are automatically created and 17 | # added to your environment when you deploy on vercel 18 | 19 | # Instructions to create a Vercel Blob Store here: https://vercel.com/docs/storage/vercel-blob 20 | BLOB_READ_WRITE_TOKEN=**** 21 | 22 | # Instructions to create a database here: https://vercel.com/docs/storage/vercel-postgres/quickstart 23 | POSTGRES_URL=**** 24 | 25 | # Get your Upstash Redis URL and Token here: https://console.upstash.com/ 26 | UPSTASH_REDIS_REST_URL=**** 27 | 28 | # Get your Upstash Redis Token here: https://console.upstash.com/ 29 | UPSTASH_REDIS_REST_TOKEN=**** 30 | 31 | # Specify which model to use for reasoning (o1, o1-mini, o3-mini, etc..) 32 | # REASONING_MODEL=deepseek-reasoner 33 | 34 | # Bypass JSON schema validation for models that don't support it 35 | BYPASS_JSON_VALIDATION=false 36 | 37 | # Get your TogetherAI API Key here: https://together.ai/ (optional) 38 | TOGETHER_API_KEY=**** 39 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 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 | } 19 | } 20 | }, 21 | "ignorePatterns": ["**/components/ui/**"] 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 | .vscode 38 | .env*.local 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | 5 | # Install pnpm 6 | RUN npm install -g pnpm 7 | 8 | # Install dependencies first for better caching 9 | COPY package.json pnpm-lock.yaml ./ 10 | RUN pnpm install 11 | 12 | # Copy the rest of the application 13 | COPY . . 14 | 15 | # Build the application without running migrations 16 | RUN pnpm build 17 | 18 | # Expose the port the app runs on 19 | EXPOSE 3000 20 | 21 | # Create a startup script 22 | COPY start.sh /start.sh 23 | RUN chmod +x /start.sh 24 | 25 | # Start the application using the startup script 26 | CMD ["/start.sh"] 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. -------------------------------------------------------------------------------- /app/(auth)/actions.ts: -------------------------------------------------------------------------------- 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 => { 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 => { 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 | }; 85 | -------------------------------------------------------------------------------- /app/(auth)/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from '@/app/(auth)/auth'; 2 | -------------------------------------------------------------------------------- /app/(auth)/auth.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextAuthConfig } from 'next-auth'; 2 | 3 | export const authConfig = { 4 | pages: { 5 | newUser: '/', 6 | }, 7 | providers: [ 8 | // added later in auth.ts since it requires bcrypt which is only compatible with Node.js 9 | // while this file is also used in non-Node.js environments 10 | ], 11 | callbacks: { 12 | authorized({ auth, request: { nextUrl } }) { 13 | const isLoggedIn = !!auth?.user; 14 | const isOnRegister = nextUrl.pathname.startsWith('/register'); 15 | const isOnLogin = nextUrl.pathname.startsWith('/login'); 16 | 17 | // Redirect authenticated users away from auth pages 18 | if (isLoggedIn && (isOnLogin || isOnRegister)) { 19 | return Response.redirect(new URL('/', nextUrl as unknown as URL)); 20 | } 21 | 22 | // Allow access to everything 23 | return true; 24 | }, 25 | }, 26 | } satisfies NextAuthConfig; 27 | -------------------------------------------------------------------------------- /app/(auth)/auth.ts: -------------------------------------------------------------------------------- 1 | import { compare } from 'bcrypt-ts'; 2 | import NextAuth, { type User, type Session } from 'next-auth'; 3 | import Credentials from 'next-auth/providers/credentials'; 4 | 5 | import { getUser, createUser } from '@/lib/db/queries'; 6 | import { authConfig } from './auth.config'; 7 | 8 | interface ExtendedSession extends Session { 9 | user: User; 10 | } 11 | 12 | async function createAnonymousUser() { 13 | const anonymousEmail = `anon_${Date.now()}@anonymous.user`; 14 | const anonymousPassword = `anon_${Date.now()}_${Math.random().toString(36).slice(2)}`; 15 | 16 | try { 17 | // First create the user 18 | await createUser(anonymousEmail, anonymousPassword); 19 | 20 | // Then verify the user was created by fetching it 21 | const [user] = await getUser(anonymousEmail); 22 | return user; 23 | 24 | } catch (error) { 25 | console.error('Failed to create anonymous user:', error); 26 | // Instead of returning null, throw an error to prevent auth from proceeding 27 | throw new Error('Anonymous user creation failed'); 28 | } 29 | } 30 | 31 | export const { 32 | handlers: { GET, POST }, 33 | auth, 34 | signIn, 35 | signOut, 36 | } = NextAuth({ 37 | ...authConfig, 38 | providers: [ 39 | Credentials({ 40 | credentials: {}, 41 | async authorize({ email, password }: any) { 42 | try { 43 | // Handle anonymous access 44 | if (!email && !password) { 45 | return await createAnonymousUser(); 46 | } 47 | 48 | // Handle regular authentication 49 | const users = await getUser(email); 50 | if (users.length === 0) return null; 51 | 52 | // biome-ignore lint: Forbidden non-null assertion. 53 | const passwordsMatch = await compare(password, users[0].password!); 54 | if (!passwordsMatch) return null; 55 | 56 | return users[0] as any; 57 | } catch (error) { 58 | console.error('Authentication failed:', error); 59 | return null; 60 | } 61 | }, 62 | }), 63 | ], 64 | // Add proxy configuration for Docker environment 65 | cookies: { 66 | sessionToken: { 67 | name: `next-auth.session-token`, 68 | options: { 69 | httpOnly: true, 70 | sameSite: 'lax', 71 | path: '/', 72 | secure: process.env.NODE_ENV === 'production' 73 | }, 74 | }, 75 | }, 76 | trustHost: true, 77 | callbacks: { 78 | async jwt({ token, user }) { 79 | if (user) { 80 | token.id = user.id; 81 | } else if (!token.id) { 82 | // Create anonymous user if no user exists 83 | const anonymousUser = await createAnonymousUser(); 84 | if (anonymousUser) { 85 | token.id = anonymousUser.id; 86 | token.email = anonymousUser.email; 87 | } 88 | } 89 | 90 | return token; 91 | }, 92 | async session({ 93 | session, 94 | token, 95 | }: { 96 | session: ExtendedSession; 97 | token: any; 98 | }) { 99 | if (session.user) { 100 | session.user.id = token.id as string; 101 | session.user.email = token.email as string; 102 | } 103 | 104 | return session; 105 | }, 106 | }, 107 | }); 108 | -------------------------------------------------------------------------------- /app/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 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 'sonner'; 7 | 8 | import { AuthForm } from '@/components/auth-form'; 9 | import { SubmitButton } from '@/components/submit-button'; 10 | import { Button } from '@/components/ui/button'; 11 | 12 | import { login, type LoginActionState } from '../actions'; 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( 21 | login, 22 | { 23 | status: 'idle', 24 | }, 25 | ); 26 | 27 | useEffect(() => { 28 | if (state.status === 'failed') { 29 | toast.error('Invalid credentials!'); 30 | } else if (state.status === 'invalid_data') { 31 | toast.error('Failed validating your submission!'); 32 | } else if (state.status === 'success') { 33 | setIsSuccessful(true); 34 | router.refresh(); 35 | } 36 | }, [state.status, router]); 37 | 38 | const handleSubmit = (formData: FormData) => { 39 | setEmail(formData.get('email') as string); 40 | formAction(formData); 41 | }; 42 | 43 | // const handleAnonymousLogin = async () => { 44 | // try { 45 | // await signIn('credentials', { 46 | // redirect: false, 47 | // }); 48 | // router.refresh(); 49 | // } catch (error) { 50 | // toast.error('Failed to continue as guest'); 51 | // } 52 | // }; 53 | 54 | return ( 55 |
56 |
57 |
58 |

Sign In

59 |

60 | Use your email and password to sign in 61 |

62 |
63 | 64 | Sign in 65 |
66 |
67 |
68 | 69 |
70 | {/*
71 | 72 | Or 73 | 74 |
*/} 75 |
76 | {/* */} 83 |
84 |

85 | {"Don't have an account? "} 86 | 90 | Sign up 91 | 92 | {' for free.'} 93 |

94 |
95 |
96 |
97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /app/(auth)/register/page.tsx: -------------------------------------------------------------------------------- 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 'sonner'; 7 | 8 | import { AuthForm } from '@/components/auth-form'; 9 | import { SubmitButton } from '@/components/submit-button'; 10 | 11 | import { register, type RegisterActionState } from '../actions'; 12 | 13 | export default function Page() { 14 | const router = useRouter(); 15 | 16 | const [email, setEmail] = useState(''); 17 | const [isSuccessful, setIsSuccessful] = useState(false); 18 | 19 | const [state, formAction] = useActionState( 20 | register, 21 | { 22 | status: 'idle', 23 | }, 24 | ); 25 | 26 | useEffect(() => { 27 | if (state.status === 'user_exists') { 28 | toast.error('Account already exists'); 29 | } else if (state.status === 'failed') { 30 | toast.error('Failed to create account'); 31 | } else if (state.status === 'invalid_data') { 32 | toast.error('Failed validating your submission!'); 33 | } else if (state.status === 'success') { 34 | toast.success('Account created successfully'); 35 | setIsSuccessful(true); 36 | router.refresh(); 37 | } 38 | }, [state, router]); 39 | 40 | const handleSubmit = (formData: FormData) => { 41 | setEmail(formData.get('email') as string); 42 | formAction(formData); 43 | }; 44 | 45 | return ( 46 |
47 |
48 |
49 |

Sign Up

50 |

51 | Create an account with your email and password 52 |

53 |
54 | 55 | Sign Up 56 |

57 | {'Already have an account? '} 58 | 62 | Sign in 63 | 64 | {' instead.'} 65 |

66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /app/(chat)/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { type CoreUserMessage, generateText } from 'ai'; 4 | import { cookies } from 'next/headers'; 5 | import { models, reasoningModels } from '@/lib/ai/models'; 6 | 7 | import { customModel } from '@/lib/ai'; 8 | import { 9 | deleteMessagesByChatIdAfterTimestamp, 10 | getMessageById, 11 | updateChatVisiblityById, 12 | } from '@/lib/db/queries'; 13 | import { VisibilityType } from '@/components/visibility-selector'; 14 | 15 | export async function saveModelId(model: string) { 16 | const cookieStore = await cookies(); 17 | if (models.some((m) => m.id === model)) { 18 | cookieStore.set('model-id', model); 19 | } 20 | if (reasoningModels.some((m) => m.id === model)) { 21 | cookieStore.set('reasoning-model-id', model); 22 | } 23 | } 24 | 25 | export async function generateTitleFromUserMessage({ 26 | message, 27 | }: { 28 | message: CoreUserMessage; 29 | }) { 30 | const { text: title } = await generateText({ 31 | model: customModel('gpt-4o'), 32 | system: `\n 33 | - you will generate a short title based on the first message a user begins a conversation with 34 | - ensure it is not more than 80 characters long 35 | - the title should be a summary of the user's message 36 | - do not use quotes or colons`, 37 | prompt: JSON.stringify(message), 38 | }); 39 | 40 | return title; 41 | } 42 | 43 | export async function deleteTrailingMessages({ id }: { id: string }) { 44 | const [message] = await getMessageById({ id }); 45 | 46 | await deleteMessagesByChatIdAfterTimestamp({ 47 | chatId: message.chatId, 48 | timestamp: message.createdAt, 49 | }); 50 | } 51 | 52 | export async function updateChatVisibility({ 53 | chatId, 54 | visibility, 55 | }: { 56 | chatId: string; 57 | visibility: VisibilityType; 58 | }) { 59 | await updateChatVisiblityById({ chatId, visibility }); 60 | } 61 | -------------------------------------------------------------------------------- /app/(chat)/api/document/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/app/(auth)/auth'; 2 | import { BlockKind } from '@/components/block'; 3 | import { 4 | deleteDocumentsByIdAfterTimestamp, 5 | getDocumentsById, 6 | saveDocument, 7 | } from '@/lib/db/queries'; 8 | 9 | export async function GET(request: Request) { 10 | const { searchParams } = new URL(request.url); 11 | const id = searchParams.get('id'); 12 | 13 | if (!id) { 14 | return new Response('Missing id', { status: 400 }); 15 | } 16 | 17 | const session = await auth(); 18 | 19 | if (!session || !session.user) { 20 | return new Response('Unauthorized', { status: 401 }); 21 | } 22 | 23 | const documents = await getDocumentsById({ id }); 24 | 25 | const [document] = documents; 26 | 27 | if (!document) { 28 | return new Response('Not Found', { status: 404 }); 29 | } 30 | 31 | if (document.userId !== session.user.id) { 32 | return new Response('Unauthorized', { status: 401 }); 33 | } 34 | 35 | return Response.json(documents, { status: 200 }); 36 | } 37 | 38 | export async function POST(request: Request) { 39 | const { searchParams } = new URL(request.url); 40 | const id = searchParams.get('id'); 41 | 42 | if (!id) { 43 | return new Response('Missing id', { status: 400 }); 44 | } 45 | 46 | const session = await auth(); 47 | 48 | if (!session) { 49 | return new Response('Unauthorized', { status: 401 }); 50 | } 51 | 52 | const { 53 | content, 54 | title, 55 | kind, 56 | }: { content: string; title: string; kind: BlockKind } = await request.json(); 57 | 58 | if (session.user?.id) { 59 | const document = await saveDocument({ 60 | id, 61 | content, 62 | title, 63 | kind, 64 | userId: session.user.id, 65 | }); 66 | 67 | return Response.json(document, { status: 200 }); 68 | } 69 | return new Response('Unauthorized', { status: 401 }); 70 | } 71 | 72 | export async function PATCH(request: Request) { 73 | const { searchParams } = new URL(request.url); 74 | const id = searchParams.get('id'); 75 | 76 | const { timestamp }: { timestamp: string } = await request.json(); 77 | 78 | if (!id) { 79 | return new Response('Missing id', { status: 400 }); 80 | } 81 | 82 | const session = await auth(); 83 | 84 | if (!session || !session.user) { 85 | return new Response('Unauthorized', { status: 401 }); 86 | } 87 | 88 | const documents = await getDocumentsById({ id }); 89 | 90 | const [document] = documents; 91 | 92 | if (document.userId !== session.user.id) { 93 | return new Response('Unauthorized', { status: 401 }); 94 | } 95 | 96 | await deleteDocumentsByIdAfterTimestamp({ 97 | id, 98 | timestamp: new Date(timestamp), 99 | }); 100 | 101 | return new Response('Deleted', { status: 200 }); 102 | } 103 | -------------------------------------------------------------------------------- /app/(chat)/api/files/upload/route.ts: -------------------------------------------------------------------------------- 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 | } 69 | -------------------------------------------------------------------------------- /app/(chat)/api/history/route.ts: -------------------------------------------------------------------------------- 1 | import { auth, signIn } from '@/app/(auth)/auth'; 2 | import { getChatsByUserId } from '@/lib/db/queries'; 3 | 4 | export async function GET() { 5 | let session = await auth(); 6 | 7 | // If no session exists, create an anonymous session 8 | if (!session?.user) { 9 | try { 10 | const result = await signIn('credentials', { 11 | redirect: false, 12 | }); 13 | 14 | if (result?.error) { 15 | console.error('Failed to create anonymous session:', result.error); 16 | return Response.json('Failed to create anonymous session', { 17 | status: 500, 18 | }); 19 | } 20 | 21 | session = await auth(); 22 | 23 | if (!session?.user) { 24 | console.error('Failed to get session after creation'); 25 | return Response.json('Failed to create session', { status: 500 }); 26 | } 27 | } catch (error) { 28 | console.error('Error creating anonymous session:', error); 29 | return Response.json('Failed to create anonymous session', { 30 | status: 500, 31 | }); 32 | } 33 | } 34 | 35 | // biome-ignore lint: Forbidden non-null assertion. 36 | const chats = await getChatsByUserId({ id: session.user.id! }); 37 | return Response.json(chats); 38 | } 39 | -------------------------------------------------------------------------------- /app/(chat)/api/suggestions/route.ts: -------------------------------------------------------------------------------- 1 | import { auth, signIn } from '@/app/(auth)/auth'; 2 | import { getSuggestionsByDocumentId } from '@/lib/db/queries'; 3 | 4 | export async function GET(request: Request) { 5 | const { searchParams } = new URL(request.url); 6 | const documentId = searchParams.get('documentId'); 7 | 8 | if (!documentId) { 9 | return new Response('Not Found', { status: 404 }); 10 | } 11 | 12 | let session = await auth(); 13 | 14 | // If no session exists, create an anonymous session 15 | if (!session?.user) { 16 | try { 17 | const result = await signIn('credentials', { 18 | redirect: false, 19 | }); 20 | 21 | if (result?.error) { 22 | console.error('Failed to create anonymous session:', result.error); 23 | return new Response('Failed to create anonymous session', { 24 | status: 500, 25 | }); 26 | } 27 | 28 | session = await auth(); 29 | 30 | if (!session?.user) { 31 | console.error('Failed to get session after creation'); 32 | return new Response('Failed to create session', { status: 500 }); 33 | } 34 | } catch (error) { 35 | console.error('Error creating anonymous session:', error); 36 | return new Response('Failed to create anonymous session', { 37 | status: 500, 38 | }); 39 | } 40 | } 41 | 42 | const suggestions = await getSuggestionsByDocumentId({ 43 | documentId, 44 | }); 45 | 46 | const [suggestion] = suggestions; 47 | 48 | if (!suggestion) { 49 | return Response.json([], { status: 200 }); 50 | } 51 | 52 | if (suggestion.userId !== session.user.id) { 53 | return new Response('Unauthorized', { status: 401 }); 54 | } 55 | 56 | return Response.json(suggestions, { status: 200 }); 57 | } 58 | -------------------------------------------------------------------------------- /app/(chat)/api/vote/route.ts: -------------------------------------------------------------------------------- 1 | import { auth, signIn } from '@/app/(auth)/auth'; 2 | import { getVotesByChatId, voteMessage } from '@/lib/db/queries'; 3 | 4 | async function getOrCreateSession() { 5 | let session = await auth(); 6 | 7 | // If no session exists, create an anonymous session 8 | if (!session?.user) { 9 | try { 10 | const result = await signIn('credentials', { 11 | redirect: false, 12 | }); 13 | 14 | if (result?.error) { 15 | console.error('Failed to create anonymous session:', result.error); 16 | return null; 17 | } 18 | 19 | session = await auth(); 20 | 21 | if (!session?.user) { 22 | console.error('Failed to get session after creation'); 23 | return null; 24 | } 25 | } catch (error) { 26 | console.error('Error creating anonymous session:', error); 27 | return null; 28 | } 29 | } 30 | 31 | return session; 32 | } 33 | 34 | export async function GET(request: Request) { 35 | const { searchParams } = new URL(request.url); 36 | const chatId = searchParams.get('chatId'); 37 | 38 | if (!chatId) { 39 | return new Response('chatId is required', { status: 400 }); 40 | } 41 | 42 | const session = await getOrCreateSession(); 43 | 44 | if (!session?.user) { 45 | return new Response('Failed to create session', { status: 500 }); 46 | } 47 | 48 | const votes = await getVotesByChatId({ id: chatId }); 49 | 50 | return Response.json(votes, { status: 200 }); 51 | } 52 | 53 | export async function PATCH(request: Request) { 54 | const { 55 | chatId, 56 | messageId, 57 | type, 58 | }: { chatId: string; messageId: string; type: 'up' | 'down' } = 59 | await request.json(); 60 | 61 | if (!chatId || !messageId || !type) { 62 | return new Response('messageId and type are required', { status: 400 }); 63 | } 64 | 65 | const session = await getOrCreateSession(); 66 | 67 | if (!session?.user) { 68 | return new Response('Failed to create session', { status: 500 }); 69 | } 70 | 71 | await voteMessage({ 72 | chatId, 73 | messageId, 74 | type: type, 75 | }); 76 | 77 | return new Response('Message voted', { status: 200 }); 78 | } 79 | -------------------------------------------------------------------------------- /app/(chat)/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { cookies } from 'next/headers'; 2 | import { notFound } from 'next/navigation'; 3 | 4 | import { auth } from '@/app/(auth)/auth'; 5 | import { Chat } from '@/components/chat'; 6 | import { DEFAULT_MODEL_NAME, DEFAULT_REASONING_MODEL_NAME, models, reasoningModels } from '@/lib/ai/models'; 7 | import { getChatById, getMessagesByChatId } from '@/lib/db/queries'; 8 | import { convertToUIMessages } from '@/lib/utils'; 9 | import { DataStreamHandler } from '@/components/data-stream-handler'; 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 (chat.visibility === 'private') { 23 | if (!session || !session.user) { 24 | return notFound(); 25 | } 26 | 27 | if (session.user.id !== chat.userId) { 28 | return notFound(); 29 | } 30 | } 31 | 32 | const messagesFromDb = await getMessagesByChatId({ 33 | id, 34 | }); 35 | 36 | const cookieStore = await cookies(); 37 | const modelIdFromCookie = cookieStore.get('model-id')?.value; 38 | const selectedModelId = 39 | models.find((model) => model.id === modelIdFromCookie)?.id || 40 | DEFAULT_MODEL_NAME; 41 | 42 | const reasoningModelIdFromCookie = cookieStore.get('reasoning-model-id')?.value; 43 | const reasoningModelId = 44 | reasoningModels.find((model) => model.id === reasoningModelIdFromCookie)?.id || 45 | DEFAULT_REASONING_MODEL_NAME; 46 | 47 | return ( 48 | <> 49 | 57 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /app/(chat)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { cookies } from 'next/headers'; 2 | 3 | import { AppSidebar } from '@/components/app-sidebar'; 4 | import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; 5 | 6 | import { auth } from '../(auth)/auth'; 7 | import Script from 'next/script'; 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 |