├── .vscode ├── extensions.json └── settings.json ├── app ├── (auth) │ ├── api │ │ └── auth │ │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── auth.config.ts │ ├── auth.ts │ ├── actions.ts │ ├── login │ │ └── page.tsx │ └── register │ │ └── page.tsx ├── favicon.ico ├── (chat) │ ├── twitter-image.png │ ├── opengraph-image.png │ ├── api │ │ ├── suggestions │ │ │ └── route.ts │ │ ├── history │ │ │ └── route.ts │ │ ├── vote │ │ │ └── route.ts │ │ ├── files │ │ │ └── upload │ │ │ │ └── route.ts │ │ └── document │ │ │ └── route.ts │ ├── layout.tsx │ ├── page.tsx │ ├── actions.ts │ └── chat │ │ └── [id] │ │ └── page.tsx └── layout.tsx ├── lib ├── db │ ├── migrations │ │ ├── 0004_odd_slayback.sql │ │ ├── 0003_cloudy_glorian.sql │ │ ├── 0000_keen_devos.sql │ │ ├── meta │ │ │ ├── _journal.json │ │ │ └── 0000_snapshot.json │ │ ├── 0005_wooden_whistler.sql │ │ ├── 0002_wandering_riptide.sql │ │ └── 0001_sparkling_blue_marvel.sql │ └── migrate.ts ├── constants.ts ├── editor │ ├── react-renderer.tsx │ ├── config.ts │ ├── functions.tsx │ └── suggestions.tsx ├── ai │ ├── models.ts │ ├── tools │ │ ├── get-weather.ts │ │ ├── update-document.ts │ │ ├── create-document.ts │ │ └── request-suggestions.ts │ ├── providers.ts │ ├── models.test.ts │ └── prompts.ts ├── rag │ ├── types.ts │ └── clients.ts └── artifacts │ └── server.ts ├── public └── images │ ├── demo-thumbnail.png │ └── mouth of the seine, monet.jpg ├── postcss.config.mjs ├── next-env.d.ts ├── middleware.ts ├── artifacts ├── actions.ts ├── image │ ├── server.ts │ └── client.tsx ├── code │ └── server.ts ├── text │ └── server.ts └── sheet │ ├── server.ts │ └── client.tsx ├── next.config.ts ├── components ├── theme-provider.tsx ├── ui │ ├── skeleton.tsx │ ├── textarea.tsx │ ├── label.tsx │ ├── input.tsx │ ├── separator.tsx │ ├── tooltip.tsx │ ├── button.tsx │ └── card.tsx ├── sign-out-form.tsx ├── use-scroll-to-bottom.ts ├── greeting.tsx ├── sidebar-toggle.tsx ├── artifact-close-button.tsx ├── submit-button.tsx ├── code-block.tsx ├── ThemeColorUpdater.tsx ├── image-editor.tsx ├── toast.tsx ├── preview-attachment.tsx ├── auth-form.tsx ├── document-skeleton.tsx ├── app-sidebar.tsx ├── message-reasoning.tsx ├── sidebar-user-nav.tsx ├── artifact-messages.tsx ├── suggested-actions.tsx ├── messages.tsx ├── suggestion.tsx ├── data-stream-handler.tsx ├── model-selector.tsx ├── markdown.tsx ├── create-artifact.tsx ├── diffview.tsx ├── message-editor.tsx ├── artifact-actions.tsx ├── visibility-selector.tsx ├── chat-header.tsx ├── version-footer.tsx ├── code-editor.tsx ├── chat.tsx ├── sidebar-history-item.tsx └── sheet-editor.tsx ├── drizzle.config.ts ├── components.json ├── .eslintrc.json ├── LICENSE ├── .gitignore ├── hooks ├── use-mobile.tsx ├── use-chat-visibility.ts └── use-artifact.ts ├── tests ├── reasoning.setup.ts ├── auth.setup.ts ├── auth-helper.ts ├── reasoning.test.ts ├── artifacts.test.ts ├── auth.test.ts ├── pages │ └── artifact.ts └── prompts │ └── basic.ts ├── tsconfig.json ├── .env.example ├── tailwind.config.ts ├── package.json └── playwright.config.ts /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome"] 3 | } 4 | -------------------------------------------------------------------------------- /app/(auth)/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from '@/app/(auth)/auth'; 2 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/ai-chatbot-with-rag/master/app/favicon.ico -------------------------------------------------------------------------------- /lib/db/migrations/0004_odd_slayback.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "Document" ADD COLUMN "text" varchar DEFAULT 'text' NOT NULL; -------------------------------------------------------------------------------- /app/(chat)/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/ai-chatbot-with-rag/master/app/(chat)/twitter-image.png -------------------------------------------------------------------------------- /lib/db/migrations/0003_cloudy_glorian.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "Chat" ADD COLUMN "visibility" varchar DEFAULT 'private' NOT NULL; -------------------------------------------------------------------------------- /app/(chat)/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/ai-chatbot-with-rag/master/app/(chat)/opengraph-image.png -------------------------------------------------------------------------------- /public/images/demo-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/ai-chatbot-with-rag/master/public/images/demo-thumbnail.png -------------------------------------------------------------------------------- /public/images/mouth of the seine, monet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/ai-chatbot-with-rag/master/public/images/mouth of the seine, monet.jpg -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | 'tailwindcss/nesting': {}, 6 | }, 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const isProductionEnvironment = process.env.NODE_ENV === 'production'; 2 | 3 | export const isTestEnvironment = Boolean( 4 | process.env.PLAYWRIGHT_TEST_BASE_URL || 5 | process.env.PLAYWRIGHT || 6 | process.env.CI_PLAYWRIGHT, 7 | ); 8 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | 3 | import { authConfig } from '@/app/(auth)/auth.config'; 4 | 5 | export default NextAuth(authConfig).auth; 6 | 7 | export const config = { 8 | matcher: ['/', '/:id', '/api/:path*', '/login', '/register'], 9 | }; 10 | -------------------------------------------------------------------------------- /artifacts/actions.ts: -------------------------------------------------------------------------------- 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 | } 9 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | experimental: { 5 | ppr: true, 6 | }, 7 | images: { 8 | remotePatterns: [ 9 | { 10 | hostname: 'avatar.vercel.sh', 11 | }, 12 | ], 13 | }, 14 | }; 15 | 16 | export default nextConfig; 17 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ThemeProvider as NextThemesProvider } from 'next-themes'; 4 | import type { ThemeProviderProps } from 'next-themes/dist/types'; 5 | 6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 7 | return {children}; 8 | } 9 | -------------------------------------------------------------------------------- /lib/editor/react-renderer.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | 3 | export class ReactRenderer { 4 | static render(component: React.ReactElement, dom: HTMLElement) { 5 | const root = createRoot(dom); 6 | root.render(component); 7 | 8 | return { 9 | destroy: () => root.unmount(), 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import { defineConfig } from 'drizzle-kit'; 3 | 4 | config({ 5 | path: '.env.local', 6 | }); 7 | 8 | export default defineConfig({ 9 | schema: './lib/db/schema.ts', 10 | out: './lib/db/migrations', 11 | dialect: 'postgresql', 12 | dbCredentials: { 13 | // biome-ignore lint: Forbidden non-null assertion. 14 | url: process.env.POSTGRES_URL!, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /lib/ai/models.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_CHAT_MODEL: string = 'chat-model'; 2 | 3 | interface ChatModel { 4 | id: string; 5 | name: string; 6 | description: string; 7 | } 8 | 9 | export const chatModels: Array = [ 10 | { 11 | id: 'chat-model', 12 | name: 'Chat model', 13 | description: 'Primary model for all-purpose chat', 14 | }, 15 | { 16 | id: 'chat-model-reasoning', 17 | name: 'Reasoning model', 18 | description: 'Uses advanced reasoning', 19 | }, 20 | ]; 21 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 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 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[javascript]": { 4 | "editor.defaultFormatter": "biomejs.biome" 5 | }, 6 | "[typescript]": { 7 | "editor.defaultFormatter": "vscode.typescript-language-features" 8 | }, 9 | "[typescriptreact]": { 10 | "editor.defaultFormatter": "biomejs.biome" 11 | }, 12 | "typescript.tsdk": "node_modules/typescript/lib", 13 | "eslint.workingDirectories": [ 14 | { 15 | "pattern": "app/*" 16 | }, 17 | { 18 | "pattern": "packages/*" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /components/sign-out-form.tsx: -------------------------------------------------------------------------------- 1 | import Form from 'next/form'; 2 | 3 | import { signOut } from '@/app/(auth)/auth'; 4 | 5 | export const SignOutForm = () => { 6 | return ( 7 |
{ 10 | 'use server'; 11 | 12 | await signOut({ 13 | redirectTo: '/', 14 | }); 15 | }} 16 | > 17 | 23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /lib/ai/tools/get-weather.ts: -------------------------------------------------------------------------------- 1 | import { tool } from 'ai'; 2 | import { z } from 'zod'; 3 | 4 | export const getWeather = tool({ 5 | description: 'Get the current weather at a location', 6 | parameters: z.object({ 7 | latitude: z.number(), 8 | longitude: z.number(), 9 | }), 10 | execute: async ({ latitude, longitude }) => { 11 | const response = await fetch( 12 | `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m&hourly=temperature_2m&daily=sunrise,sunset&timezone=auto`, 13 | ); 14 | 15 | const weatherData = await response.json(); 16 | return weatherData; 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /.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 | .env*.local 38 | 39 | # Playwright 40 | /test-results/ 41 | /playwright-report/ 42 | /blob-report/ 43 | /playwright/* 44 | -------------------------------------------------------------------------------- /hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState( 7 | undefined, 8 | ); 9 | 10 | React.useEffect(() => { 11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 12 | const onChange = () => { 13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 14 | }; 15 | mql.addEventListener('change', onChange); 16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 17 | return () => mql.removeEventListener('change', onChange); 18 | }, []); 19 | 20 | return !!isMobile; 21 | } 22 | -------------------------------------------------------------------------------- /tests/reasoning.setup.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { expect, test as setup } from '@playwright/test'; 3 | import { ChatPage } from './pages/chat'; 4 | 5 | const reasoningFile = path.join( 6 | __dirname, 7 | '../playwright/.reasoning/session.json', 8 | ); 9 | 10 | setup('switch to reasoning model', async ({ page }) => { 11 | const chatPage = new ChatPage(page); 12 | await chatPage.createNewChat(); 13 | 14 | await chatPage.chooseModelFromSelector('chat-model-reasoning'); 15 | 16 | await expect(chatPage.getSelectedModel()).resolves.toEqual('Reasoning model'); 17 | 18 | await page.waitForTimeout(1000); 19 | await page.context().storageState({ path: reasoningFile }); 20 | }); 21 | -------------------------------------------------------------------------------- /lib/db/migrations/0000_keen_devos.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "Chat" ( 2 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 3 | "createdAt" timestamp NOT NULL, 4 | "messages" json NOT NULL, 5 | "userId" uuid NOT NULL 6 | ); 7 | --> statement-breakpoint 8 | CREATE TABLE IF NOT EXISTS "User" ( 9 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 10 | "email" varchar(64) NOT NULL, 11 | "password" varchar(64) 12 | ); 13 | --> statement-breakpoint 14 | DO $$ BEGIN 15 | ALTER TABLE "Chat" ADD CONSTRAINT "Chat_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action; 16 | EXCEPTION 17 | WHEN duplicate_object THEN null; 18 | END $$; 19 | -------------------------------------------------------------------------------- /lib/rag/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type definitions for the RAG system 3 | */ 4 | 5 | // Results returned by vector search 6 | export interface VectorSearchResult { 7 | id: string | number; // Upstash Vector can return numeric IDs 8 | score: number; 9 | data?: string; 10 | vector?: number[]; 11 | metadata?: Record; 12 | } 13 | 14 | // Options for configuring RAG 15 | export interface RAGOptions { 16 | topK?: number; 17 | includeData?: boolean; 18 | useReranking?: boolean; 19 | cohereModel?: string; 20 | relevanceThreshold?: number; 21 | } 22 | 23 | // Results from RAG process, to be used in prompt 24 | export interface RAGResponse { 25 | contextText: string; 26 | systemPrompt: string; 27 | results: VectorSearchResult[]; 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx", 29 | ".next/types/**/*.ts", 30 | "next.config.js" 31 | ], 32 | "exclude": ["node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.ComponentProps<'textarea'> 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |