├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── api │ └── chat │ │ └── route.ts ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── license ├── nba_cba_2023.pdf ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── schema.sql ├── scripts └── index.ts ├── tailwind.config.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=sk-XXXXXX 2 | SUPABASE_PRIVATE_KEY= 3 | SUPABASE_URL= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NBA CBA Chat 2 | 3 | Use AI to ask questions about the new 676-page NBA CBA. 4 | 5 | Built with OpenAI, Next.js, Vercel AI SDK, Supabase, and LangChain. 6 | -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js"; 2 | import { OpenAIStream, StreamingTextResponse } from "ai"; 3 | import endent from "endent"; 4 | import { OpenAIEmbeddings } from "langchain/embeddings/openai"; 5 | import { SupabaseVectorStore } from "langchain/vectorstores/supabase"; 6 | import { Configuration, OpenAIApi } from "openai-edge"; 7 | 8 | export const runtime = "edge"; 9 | 10 | export async function POST(req: Request) { 11 | const { messages } = await req.json(); 12 | 13 | const config = new Configuration({ 14 | apiKey: process.env.OPENAI_API_KEY 15 | }); 16 | 17 | const openai = new OpenAIApi(config); 18 | 19 | let finalMessages: any = []; 20 | 21 | if (messages.length === 1) { 22 | const client = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_PRIVATE_KEY!, { auth: { persistSession: false } }); 23 | 24 | const vectorstore = await SupabaseVectorStore.fromExistingIndex(new OpenAIEmbeddings(), { client, tableName: "nba", queryName: "match_documents_nba" }); 25 | 26 | const retriever = vectorstore.asRetriever(4); 27 | 28 | const pages = await retriever.getRelevantDocuments(messages[messages.length - 1].content); 29 | 30 | const systemMessage = { 31 | role: "system", 32 | content: endent`You are an expert lawyer and an NBA general manager. 33 | 34 | You are studying the new 2023 NBA Collective Bargaining Agreement. 35 | 36 | You are able to answer any question about the CBA in a way that is both accurate and easy to understand. 37 | 38 | You cite the relevant sections of the CBA in your answer. 39 | 40 | You will be given a question about the CBA and you will answer it based on the following pages of the CBA: 41 | 42 | ${pages.map((page) => page.pageContent).join("\n\n")}` 43 | }; 44 | 45 | finalMessages = [systemMessage, ...messages]; 46 | } else { 47 | finalMessages = messages; 48 | } 49 | 50 | const response = await openai.createChatCompletion({ 51 | model: "gpt-3.5-turbo", 52 | temperature: 0.2, 53 | stream: true, 54 | messages: finalMessages 55 | }); 56 | 57 | const stream = OpenAIStream(response); 58 | 59 | return new StreamingTextResponse(stream); 60 | } 61 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mckaywrigley/nba-cba-ai-chat/aafebf3060252108ac45997ed2b1a4d9bde49fb0/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from "next/font/google"; 2 | import "./globals.css"; 3 | 4 | const inter = Inter({ subsets: ["latin"] }); 5 | 6 | export const metadata = { 7 | title: "NBA CBA AI", 8 | description: "Use AI to ask questions about the new 676-page NBA CBA." 9 | }; 10 | 11 | export default function RootLayout({ children }: { children: React.ReactNode }) { 12 | return ( 13 | 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useChat } from "ai/react"; 4 | 5 | export default function Home() { 6 | const { messages, input, handleInputChange, handleSubmit } = useChat(); 7 | 8 | return ( 9 |
10 |
11 |
🏀
12 |
NBA CBA Chat
13 |
Use AI to ask questions about the new 676-page NBA CBA.
14 |
Please donate to help us keep the tool online for everyone :)
15 | 21 |
22 | 23 | {messages.length > 0 ? ( 24 |
25 | 31 | 32 | {messages.map((m) => ( 33 |
37 |
{m.role === "user" ? "You" : "AI"}:
38 |
{m.content}
39 |
40 | ))} 41 |
42 | ) : null} 43 | 44 |
48 | 55 | 56 | 62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mckay Wrigley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /nba_cba_2023.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mckaywrigley/nba-cba-ai-chat/aafebf3060252108ac45997ed2b1a4d9bde49fb0/nba_cba_2023.pdf -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "embed": "tsx scripts/index.ts" 11 | }, 12 | "dependencies": { 13 | "@nanostores/react": "github:ai/react", 14 | "@supabase/supabase-js": "^2.26.0", 15 | "@types/node": "20.3.2", 16 | "@types/react": "18.2.14", 17 | "@types/react-dom": "18.2.6", 18 | "ai": "^2.1.10", 19 | "autoprefixer": "10.4.14", 20 | "endent": "^2.1.0", 21 | "eslint": "8.43.0", 22 | "eslint-config-next": "^12.0.4", 23 | "next": "13.4.7", 24 | "openai": "^3.3.0", 25 | "openai-edge": "^1.1.0", 26 | "pdf-parse": "^1.1.1", 27 | "postcss": "8.4.24", 28 | "react": "18.2.0", 29 | "react-dom": "18.2.0", 30 | "tailwindcss": "3.3.2", 31 | "typescript": "5.1.6" 32 | }, 33 | "devDependencies": { 34 | "langchain": "^0.0.98", 35 | "tsx": "^3.12.7" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | -- Enable the pgvector extension to work with embedding vectors 2 | create extension vector; 3 | 4 | -- Create a table to store your documents 5 | create table nba ( 6 | id bigserial primary key, 7 | content text, -- corresponds to Document.pageContent 8 | metadata jsonb, -- corresponds to Document.metadata 9 | embedding vector(1536) -- 1536 works for OpenAI embeddings, change if needed 10 | ); 11 | 12 | -- Create a function to search for documents 13 | create function match_documents_nba ( 14 | query_embedding vector(1536), 15 | match_count int DEFAULT null, 16 | filter jsonb DEFAULT '{}' 17 | ) returns table ( 18 | id bigint, 19 | content text, 20 | metadata jsonb, 21 | similarity float 22 | ) 23 | language plpgsql 24 | as $$ 25 | #variable_conflict use_column 26 | begin 27 | return query 28 | select 29 | id, 30 | content, 31 | metadata, 32 | 1 - (nba.embedding <=> query_embedding) as similarity 33 | from nba 34 | where metadata @> filter 35 | order by nba.embedding <=> query_embedding 36 | limit match_count; 37 | end; 38 | $$; -------------------------------------------------------------------------------- /scripts/index.ts: -------------------------------------------------------------------------------- 1 | import { loadEnvConfig } from "@next/env"; 2 | import { createClient } from "@supabase/supabase-js"; 3 | import { PDFLoader } from "langchain/document_loaders/fs/pdf"; 4 | import { OpenAIEmbeddings } from "langchain/embeddings/openai"; 5 | import { SupabaseVectorStore } from "langchain/vectorstores/supabase"; 6 | 7 | loadEnvConfig(""); 8 | 9 | const privateKey = process.env.SUPABASE_PRIVATE_KEY; 10 | if (!privateKey) throw new Error(`Expected env var SUPABASE_PRIVATE_KEY`); 11 | 12 | const url = process.env.SUPABASE_URL; 13 | if (!url) throw new Error(`Expected env var SUPABASE_URL`); 14 | 15 | (async () => { 16 | const client = createClient(url, privateKey); 17 | 18 | const loader = new PDFLoader("nba_cba_2023.pdf"); 19 | 20 | const docs = await loader.load(); 21 | 22 | const pages = docs.map((doc) => doc.pageContent); 23 | const metadata = docs.map((doc) => doc.metadata); 24 | 25 | await SupabaseVectorStore.fromTexts(pages, metadata, new OpenAIEmbeddings(), { 26 | client, 27 | tableName: "nba", 28 | queryName: "match_documents_nba" 29 | }); 30 | })(); 31 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | --------------------------------------------------------------------------------