├── .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 |
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 |
--------------------------------------------------------------------------------