├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── api │ └── chat │ │ └── route.ts ├── assets │ ├── swiftiegpt.png │ ├── taylors_version.png │ └── ts-chatbot-bg.jpg ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── components ├── Bubble.tsx ├── Footer.tsx ├── LoadingBubble.tsx └── PromptSuggestions │ ├── PromptSuggestionButton.tsx │ └── PromptSuggestionsRow.tsx ├── gh-hero.png ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── scripts └── loadDb.ts ├── tailwind.config.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | ASTRA_DB_APPLICATION_TOKEN=REPLACE_ME 2 | ASTRA_DB_API_ENDPOINT=REPLACE_ME 3 | ASTRA_DB_COLLECTION=swiftipedia 4 | OPENAI_API_KEY=REPLACE_ME 5 | 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "prettier"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | share/python-wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | *.py,cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | cover/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | .pybuilder/ 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | # For a library or package, you might want to ignore these files since the code is 86 | # intended to run in multiple environments; otherwise, check them in: 87 | # .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # poetry 97 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 98 | # This is especially recommended for binary packages to ensure reproducibility, and is more 99 | # commonly ignored for libraries. 100 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 101 | #poetry.lock 102 | 103 | # pdm 104 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 105 | #pdm.lock 106 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 107 | # in version control. 108 | # https://pdm.fming.dev/#use-with-ide 109 | .pdm.toml 110 | 111 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 112 | __pypackages__/ 113 | 114 | # Celery stuff 115 | celerybeat-schedule 116 | celerybeat.pid 117 | 118 | # SageMath parsed files 119 | *.sage.py 120 | 121 | # Environments 122 | .env 123 | .venv 124 | env/ 125 | venv/ 126 | ENV/ 127 | env.bak/ 128 | venv.bak/ 129 | 130 | # Spyder project settings 131 | .spyderproject 132 | .spyproject 133 | 134 | # Rope project settings 135 | .ropeproject 136 | 137 | # mkdocs documentation 138 | /site 139 | 140 | # mypy 141 | .mypy_cache/ 142 | .dmypy.json 143 | dmypy.json 144 | 145 | # Pyre type checker 146 | .pyre/ 147 | 148 | # pytype static type analyzer 149 | .pytype/ 150 | 151 | # Cython debug symbols 152 | cython_debug/ 153 | 154 | # PyCharm 155 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 156 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 157 | # and can be added to the global gitignore or merged into this file. For a more nuclear 158 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 159 | .idea/ 160 | 161 | node_modules 162 | .next -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftieGPT 2 | 3 | ![SwiftieGPT](gh-hero.png) 4 | 5 | SwiftieGPT knows Taylor all too well. Ask questions in a clear and concise manner and get back responses based on details from publicly available data on Taylor. From tour dates to song lyrics, learn everything there is to know about the songstress here. 6 | 7 | ## Prerequisites 8 | 9 | - An Astra DB account. You can [create one here](https://astra.datastax.com/register) 10 | - An OpenAI account and api key [create one here](https://platform.openai.com/) 11 | 12 | ## Setup 13 | 14 | Clone this repository to your local machine. 15 | 16 | Install Puppeteer: `npm i puppeteer`. If you're on a newer Macbook, you'll need to run `PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM=1 npm i puppeteer`. 17 | 18 | Install the rest of the dependencies by running `npm install`. 19 | 20 | [Create a Vector Database](https://docs.datastax.com/en/astra/astra-db-vector/get-started/quickstart.html#create-a-serverless-vector-database) in Astra and generate and Application Token. 21 | 22 | Copy to supplied `.env.example` to `.env` and enter your credentials for OpenAI and AstraDB: 23 | 24 | - `OPENAI_API_KEY`: API key for OPENAI 25 | - `ASTRA_DB_API_ENDPOINT`: Your Astra DB vector database endpoint 26 | - `ASTRA_DB_APPLICATION_TOKEN`: The generated app token for your Astra database 27 | 28 | 29 | ## Load the data 30 | 31 | The first thing you need to to run this chatbot is to load the data. This may take awhile, so grab some coffee! ☕️ 32 | 33 | `npx ts-node scripts/loadDb.ts` 34 | 35 | ## Running the Project 36 | 37 | Once the data is loaded, run `npm run dev` in your terminal. Open [http://localhost:3000](http://localhost:3000) to view the chatbot in your browser. 38 | 39 | ## More Resources 40 | 41 | - Astra DB [docs](https://docs.datastax.com/en/astra/astra-db-vector/index.html) and [quickstart](https://docs.datastax.com/en/astra/astra-db-vector/get-started/quickstart.html). 42 | - Astra DB TypeScript client [on Github](https://github.com/datastax/astra-db-ts) 43 | - SwiftieGPT how-to [blog post](https://www.datastax.com/blog/using-astradb-vector-to-build-taylor-swift-chatbot) 44 | 45 | -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { OpenAIStream, StreamingTextResponse } from "ai"; 3 | import { AstraDB } from "@datastax/astra-db-ts"; 4 | 5 | const { 6 | ASTRA_DB_API_ENDPOINT, 7 | ASTRA_DB_APPLICATION_TOKEN, 8 | ASTRA_DB_COLLECTION, 9 | OPENAI_API_KEY, 10 | } = process.env; 11 | 12 | const openai = new OpenAI({ 13 | apiKey: OPENAI_API_KEY, 14 | }); 15 | 16 | const astraDb = new AstraDB(ASTRA_DB_APPLICATION_TOKEN, ASTRA_DB_API_ENDPOINT); 17 | 18 | export async function POST(req: Request) { 19 | try { 20 | const {messages} = await req.json(); 21 | const latestMessage = messages[messages?.length - 1]?.content; 22 | 23 | let docContext = ''; 24 | 25 | const embedding = await openai.embeddings.create({ 26 | model: "text-embedding-3-small", 27 | input: latestMessage, 28 | encoding_format: "float", 29 | }); 30 | 31 | try { 32 | const collection = await astraDb.collection(ASTRA_DB_COLLECTION); 33 | const cursor = collection.find(null, { 34 | sort: { 35 | $vector: embedding.data[0].embedding, 36 | }, 37 | limit: 10, 38 | }); 39 | 40 | const documents = await cursor.toArray(); 41 | 42 | const docsMap = documents?.map(doc => doc.text); 43 | 44 | docContext = JSON.stringify(docsMap); 45 | } catch (e) { 46 | console.log("Error querying db..."); 47 | docContext = ""; 48 | } 49 | 50 | const Template = { 51 | role: 'system', 52 | content: `You are an AI assistant who is a Taylor Swift super fan. Use the below context to augement what you know about Taylor Swift and her music. 53 | The context will provide you with the most recent page data from her wikipedia, tour website and others. 54 | If the context doesn't include the information you need answer based on your existing knowledge and don't mention the source of your information or what the context does or doesn't include. 55 | Format responses using markdown where applicable and don't return images. 56 | ---------------- 57 | START CONTEXT 58 | ${docContext} 59 | END CONTEXT 60 | ---------------- 61 | QUESTION: ${latestMessage} 62 | ---------------- 63 | ` 64 | }; 65 | 66 | const response = await openai.chat.completions.create( 67 | { 68 | model: 'gpt-4', 69 | stream: true, 70 | messages: [Template, ...messages], 71 | } 72 | ); 73 | const stream = OpenAIStream(response); 74 | 75 | return new StreamingTextResponse(stream); 76 | } catch (e) { 77 | throw e; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/assets/swiftiegpt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datastax/SwiftieGPT/7ca8f26fad1509213a27ddf050ec7d795d01b28c/app/assets/swiftiegpt.png -------------------------------------------------------------------------------- /app/assets/taylors_version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datastax/SwiftieGPT/7ca8f26fad1509213a27ddf050ec7d795d01b28c/app/assets/taylors_version.png -------------------------------------------------------------------------------- /app/assets/ts-chatbot-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datastax/SwiftieGPT/7ca8f26fad1509213a27ddf050ec7d795d01b28c/app/assets/ts-chatbot-bg.jpg -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datastax/SwiftieGPT/7ca8f26fad1509213a27ddf050ec7d795d01b28c/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --text-primary: #FFFFFF; 7 | --text-primary-inverse: #090909; 8 | --text-primary-main: #7724AA; 9 | --text-secondary: #A6AAAE; 10 | --text-secondary-inverse: #494A4D; 11 | --text-bubble-primary: #EB4039; 12 | --background-bubble-primary: #FFDEE0; 13 | --background-bubble-secondary: #F0F0F0; 14 | --border-primary: #CED0D2; 15 | --background-soft: #F3E5F5; 16 | --background-start-rgb: 214, 219, 220; 17 | --background-end-rgb: 255, 255, 255; 18 | --text-tertiary: #820DEB; 19 | --shadow-primary: 0px 6px 16px 0px #0A0A0A1F, 0px 4px 14px 0px #0A0A0A14, 0px 8px 10px 0px #0A0A0A14; 20 | --processing-dot-size: 4px; 21 | --header-footer-background: #DA92FC; 22 | --button-background: #FDFFAA; 23 | } 24 | 25 | body { 26 | background-image: url('assets/ts-chatbot-bg.jpg'); 27 | background-size: cover; 28 | background-position: center; 29 | background-repeat: no-repeat; 30 | } 31 | 32 | .chatbot-section { 33 | background-color: var(--text-primary); 34 | box-shadow: var(--shadow-primary); 35 | /* border: 1px solid var(--border-primary); */ 36 | } 37 | 38 | .chatbot-header { 39 | border-bottom: 1px solid var(--border-primary);; 40 | } 41 | 42 | .chatbot-header svg { 43 | fill: var(--text-primary-inverse); 44 | } 45 | 46 | .chatbot-text-primary { 47 | color: var(--text-primary-inverse); 48 | } 49 | 50 | .chatbot-text-secondary { 51 | color: var(--text-secondary); 52 | } 53 | 54 | .chatbot-text-secondary-inverse { 55 | color: var(--text-secondary-inverse); 56 | } 57 | 58 | .chatbot-text-tertiary { 59 | color: var(--text-tertiary); 60 | } 61 | 62 | .chatbot-input { 63 | color: var(--text-primary-inverse); 64 | background-color: var(--text-primary); 65 | /* border: 1px solid var(--border-primary); */ 66 | } 67 | 68 | .chatbot-input::placeholder { 69 | color: var(--text-secondary); 70 | } 71 | 72 | .chatbot-send-button { 73 | background-color: var(--button-background); 74 | color: var(--text-primary-inverse); 75 | } 76 | 77 | .chatbot-send-button svg { 78 | fill: var(--text-primary); 79 | } 80 | 81 | .chatbot-button-primary { 82 | background-color: var(--text-primary-inverse); 83 | color: var(--text-primary); 84 | } 85 | 86 | .chatbot-button-secondary { 87 | border: 1px solid var(--border-primary); 88 | color: var(--text-primary-inverse); 89 | } 90 | 91 | .chatbot-faq-link { 92 | border: 1px solid var(--border-primary); 93 | border-radius: 24px; 94 | color: var(--background-bubble-primary); 95 | } 96 | 97 | .chatbot-faq-link svg { 98 | fill: var(--text-tertiary); 99 | } 100 | 101 | .talk-bubble { 102 | text-align: left; 103 | display: inline-block; 104 | position: relative; 105 | color: var(--text-secondary-inverse); 106 | background-color: var(--background-bubble-secondary); 107 | border-radius: 10px; 108 | border-bottom-left-radius: 0px; 109 | } 110 | 111 | .talk-bubble.user { 112 | text-align: right; 113 | color: var(--text-bubble-primary); 114 | background-color: var(--background-bubble-primary); 115 | border-radius: 10px; 116 | border-bottom-right-radius: 0px; 117 | } 118 | 119 | .talk-bubble svg { 120 | position: absolute; 121 | left: -1px; 122 | bottom: 2px; 123 | transform: translateY(100%) rotateY(180deg); 124 | fill: var(--background-bubble-secondary); 125 | } 126 | 127 | .talk-bubble.user svg { 128 | right: -1px; 129 | left: auto; 130 | bottom: 2px; 131 | transform: translateY(100%); 132 | fill: var(--background-bubble-primary); 133 | } 134 | 135 | .dot-flashing { 136 | position: relative; 137 | /* padding-left: 8px; */ 138 | /* padding-right: 8px; */ 139 | width: var(--processing-dot-size); 140 | height: var(--processing-dot-size); 141 | border-radius: 100%; 142 | background-color: var(--text-primary-inverse); 143 | color: var(--text-primary-inverse); 144 | animation: dot-flashing 1s infinite linear alternate; 145 | animation-delay: 0.5s; 146 | } 147 | 148 | .dot-flashing::before, 149 | .dot-flashing::after { 150 | content: ""; 151 | display: inline-block; 152 | position: absolute; 153 | top: 0; 154 | } 155 | 156 | .dot-flashing::before { 157 | left: -6px; 158 | width: var(--processing-dot-size); 159 | height: var(--processing-dot-size); 160 | border-radius: 100%; 161 | background-color: var(--text-primary-inverse); 162 | color: var(--text-primary-inverse); 163 | animation: dot-flashing 1s infinite alternate; 164 | animation-delay: 0s; 165 | } 166 | 167 | .dot-flashing::after { 168 | left: 6px; 169 | width: var(--processing-dot-size); 170 | height: var(--processing-dot-size); 171 | border-radius: 100%; 172 | background-color: var(--text-primary-inverse); 173 | color: var(--text-primary-inverse); 174 | animation: dot-flashing 1s infinite alternate; 175 | animation-delay: 1s; 176 | } 177 | 178 | @keyframes dot-flashing { 179 | 0% { 180 | background-color: var(--text-primary-inverse); 181 | } 182 | 183 | 50%, 184 | 100% { 185 | background-color: rgba(152, 128, 255, 0.2); 186 | } 187 | } 188 | 189 | .prompt-button { 190 | background-color: var(--background-bubble-primary); 191 | color: var(--text-bubble-primary); 192 | } 193 | 194 | /* Toggle Styles */ 195 | .toggle-background { 196 | background-color: var(--background-bubble-primary); 197 | } 198 | 199 | .toggle-boarder { 200 | border: 1px solid var(--background-bubble-primary); 201 | } 202 | 203 | .vercel-link { 204 | color: var(--text-primary-inverse); 205 | background-color: var(--text-primary); 206 | border-color: var(--border-primary); 207 | } 208 | 209 | .vercel-link hr { 210 | border-color: var(--border-primary); 211 | } 212 | 213 | .source-link { 214 | position: absolute; 215 | bottom: -2rem; 216 | right: 0; 217 | display: flex; 218 | align-items: center; 219 | } 220 | 221 | .source-link svg { 222 | position: static; 223 | transform: none; 224 | margin-right: 8px; 225 | } 226 | 227 | .source-link a { 228 | display: flex; 229 | align-items: center; 230 | color: var(--text-primary-main); 231 | border: 1px solid var(--border-primary); 232 | border-radius: 24px; 233 | padding: 0 8px; 234 | margin-left: 8px; 235 | } 236 | 237 | .link { 238 | color: var(--text-primary-main); 239 | } 240 | 241 | .active-chat-header { 242 | font-family: 'Brush Script MT', cursive; 243 | font-size: 24px; 244 | font-weight: 400; 245 | line-height: 36px; 246 | letter-spacing: 0em; 247 | text-align: right; 248 | } 249 | 250 | .header { 251 | overflow: hidden; 252 | margin: 0 -0.5rem 0 -0.5rem; 253 | } 254 | 255 | @media (min-width: 768px) { 256 | .header { 257 | overflow: hidden; 258 | margin: -1rem -1.5rem 0 -1.5rem; 259 | } 260 | } 261 | 262 | .clip-path-header { 263 | position: absolute; 264 | top: 0; 265 | right: 0; 266 | bottom: 0; 267 | left: 0; 268 | background: var(--header-footer-background); 269 | clip-path: polygon(0 0, 100% 0, 100% 100%, 0 70%); 270 | z-index: 0; 271 | } 272 | 273 | .footer { 274 | overflow: hidden; 275 | margin: 0 -0.5rem 0 -0.5rem; 276 | } 277 | 278 | @media (min-width: 768px) { 279 | .footer { 280 | overflow: hidden; 281 | margin: 0 -1.5rem -1rem -1.5rem; 282 | } 283 | } 284 | 285 | .clip-path-footer { 286 | position: absolute; 287 | top: 0; 288 | right: 0; 289 | bottom: 0; 290 | left: 0; 291 | background: var(--header-footer-background); 292 | clip-path: polygon(0 0, 100% 30%, 100% 100%, 0 100%); 293 | z-index: 0; 294 | } 295 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { GeistSans } from "geist/font/sans"; 2 | import Script from "next/script"; 3 | import "./globals.css"; 4 | 5 | export const metadata = { 6 | title: `SwiftieGPT`, 7 | description: `Welcome to SwiftieGPT, your go-to chatbot for all things Taylor Swift! Whether you're curious about her latest songs, looking for fun facts, or just want to chat about Tay's amazing journey, I'm here to keep you in the Swift loop. Let's talk Taylor!`, 8 | }; 9 | 10 | export default function RootLayout({ children }) { 11 | return ( 12 | 13 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import {useEffect, useRef} from 'react'; 3 | import Image from 'next/image'; 4 | import tswiftImg from './assets/swiftiegpt.png'; 5 | import Bubble from '../components/Bubble' 6 | import { useChat } from 'ai/react'; 7 | import Footer from '../components/Footer'; 8 | import PromptSuggestionRow from '../components/PromptSuggestions/PromptSuggestionsRow'; 9 | import { Message } from 'ai'; 10 | import LoadingBubble from '../components/LoadingBubble'; 11 | 12 | export default function Home() { 13 | const { append, isLoading, messages, input, handleInputChange, handleSubmit } = useChat(); 14 | 15 | const messagesEndRef = useRef(null); 16 | 17 | const scrollToBottom = () => { 18 | messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); 19 | }; 20 | 21 | useEffect(() => { 22 | scrollToBottom(); 23 | }, [messages]); 24 | 25 | 26 | const handlePrompt = (promptText) => { 27 | const msg: Message = { id: crypto.randomUUID(), content: promptText, role: 'user' }; 28 | append(msg); 29 | }; 30 | 31 | return ( 32 |
33 |
34 | {!messages || messages.length === 0 ? ( 35 |
36 | Swiftie GPT logo 37 |
38 |

39 | SwiftieGPT knows Taylor all too well. Ask questions in a clear and concise manner and get back responses based on details from publicly available data on Taylor. From tour dates to song lyrics, learn everything there is to know about the songstress here. 40 |

41 | For Swifties, by Swifties. 42 |

43 | 44 |
45 |
46 | ): ( 47 | <> 48 |
49 | Swiftie GPT logo 50 |
51 |
52 |
53 | SwiftieGPT logo 54 |
55 |
56 |
57 | {messages.map((message, index) => )} 58 | {isLoading && messages?.length % 2 !== 0 && } 59 |
60 |
61 | 62 | )} 63 |
64 |
65 |
66 |
67 | 68 | 71 |
72 |
74 |
75 |
76 |
77 | ) 78 | } -------------------------------------------------------------------------------- /components/Bubble.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Link from "next/link"; 3 | import {forwardRef, JSXElementConstructor, RefObject} from "react"; 4 | import Markdown from "react-markdown"; 5 | import remarkGfm from "remark-gfm"; 6 | 7 | const Bubble:JSXElementConstructor = forwardRef(function Bubble({ content }, ref) { 8 | const { role } = content; 9 | const isUser = role === "user" 10 | const [hasSource, setHasSource] = useState(false); 11 | 12 | return ( 13 |
} className={`block mt-4 md:mt-6 pb-[7px] clear-both ${isUser ? 'float-right' : 'float-left'}`}> 14 |
15 |
16 | 23 | Source: 24 | 29 | 30 | 31 | 32 | {children} 33 | 34 | 35 | ) 36 | }, 37 | code({ node, children, ...props }) { 38 | return ( 39 | 40 | {children} 41 | 42 | ) 43 | } 44 | }} 45 | > 46 | {content?.content} 47 | 48 | 49 | 50 | 51 |
52 |
53 |
54 | ) 55 | }) 56 | 57 | export default Bubble; -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | 2 | const Footer = () => { 3 | const fillColor = "#820DEB"; 4 | 5 | return ( 6 | 39 | ); 40 | }; 41 | 42 | export default Footer; 43 | -------------------------------------------------------------------------------- /components/LoadingBubble.tsx: -------------------------------------------------------------------------------- 1 | import {forwardRef, JSXElementConstructor, RefObject} from "react"; 2 | 3 | const LoadingBubble:JSXElementConstructor = forwardRef(function LoadingBubble({}, ref) { 4 | 5 | return ( 6 |
} className={'block mt-4 md:mt-6 pb-[7px] clear-both float-left'}> 7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ) 16 | }); 17 | 18 | export default LoadingBubble; 19 | -------------------------------------------------------------------------------- /components/PromptSuggestions/PromptSuggestionButton.tsx: -------------------------------------------------------------------------------- 1 | 2 | const PromptSuggestionButton = ({ text, onClick }) => { 3 | return ( 4 | 10 | ); 11 | }; 12 | 13 | export default PromptSuggestionButton; 14 | -------------------------------------------------------------------------------- /components/PromptSuggestions/PromptSuggestionsRow.tsx: -------------------------------------------------------------------------------- 1 | import PromptSuggestionButton from "./PromptSuggestionButton"; 2 | 3 | const PromptSuggestionRow = ({ onPromptClick }) => { 4 | const prompts = [ 5 | "How did Taylor become Time's Person of the Year? 🎤", 6 | "What is Taylor's net worth? 💰", 7 | "What were the surprise songs in Chicago? 🤯", 8 | "How many Grammy Awards has Taylor won? 🏆", 9 | ]; 10 | 11 | return ( 12 |
13 | {prompts.map((prompt, index) => ( 14 | onPromptClick(prompt)} /> 15 | ))} 16 |
17 | ); 18 | }; 19 | 20 | export default PromptSuggestionRow; 21 | -------------------------------------------------------------------------------- /gh-hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datastax/SwiftieGPT/7ca8f26fad1509213a27ddf050ec7d795d01b28c/gh-hero.png -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swiftiegpt", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "seed": "ts-node ./scripts/loadDb.ts", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@datastax/astra-db-ts": "^0.0.27", 14 | "@types/node": "^20.8.10", 15 | "ai": "^2.2.25", 16 | "geist": "^1.1.0", 17 | "next": "14.0.1", 18 | "openai": "^4.20.0", 19 | "react": "^18", 20 | "react-dom": "^18", 21 | "react-markdown": "^9.0.0", 22 | "remark-gfm": "^4.0.0" 23 | }, 24 | "devDependencies": { 25 | "@types/react": "^18.2.37", 26 | "autoprefixer": "^10", 27 | "dotenv": "^16.3.1", 28 | "eslint": "^8", 29 | "eslint-config-next": "13.5.5", 30 | "eslint-config-prettier": "^9.0.0", 31 | "langchain": "^0.1.34", 32 | "postcss": "^8", 33 | "puppeteer": "^22.6.5", 34 | "tailwindcss": "^3", 35 | "ts-node": "^10.9.1", 36 | "typescript": "5.2.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /scripts/loadDb.ts: -------------------------------------------------------------------------------- 1 | import { AstraDB } from "@datastax/astra-db-ts"; 2 | import { PuppeteerWebBaseLoader } from "langchain/document_loaders/web/puppeteer"; 3 | import OpenAI from "openai"; 4 | 5 | 6 | import {RecursiveCharacterTextSplitter} from "langchain/text_splitter"; 7 | import 'dotenv/config' 8 | 9 | type SimilarityMetric = "dot_product" | "cosine" | "euclidean"; 10 | 11 | const {ASTRA_DB_COLLECTION, ASTRA_DB_APPLICATION_TOKEN, ASTRA_DB_API_ENDPOINT, OPENAI_API_KEY} = process.env; 12 | 13 | const openai = new OpenAI(); 14 | 15 | const taylorData = [ 16 | 'https://time.com/6342806/person-of-the-year-2023-taylor-swift/', 17 | 'https://en.wikipedia.org/wiki/Taylor_Swift', 18 | 'https://en.wikipedia.org/wiki/Taylor_Swift_albums_discography', 19 | 'https://www.taylorswift.com/tour/', 20 | 'https://taylorswift.tumblr.com/', 21 | 'https://www.forbes.com/profile/taylor-swift/?sh=242c42f818e2', 22 | 'https://taylorswiftstyle.com/', 23 | 'https://www.tstheerastourfilm.com/participating-territories/', 24 | 'https://www.cosmopolitan.com/entertainment/celebs/a29684699/taylor-swift-dating-boyfriend-history/E', 25 | 'https://www.tstheerastourfilm.com/participating-territories/', 26 | 'https://en.wikipedia.org/wiki/The_Eras_Tour', 27 | 'https://taylorswift.fandom.com/wiki/The_Eras_Tour', 28 | ]; 29 | 30 | const astraDb = new AstraDB(ASTRA_DB_APPLICATION_TOKEN, ASTRA_DB_API_ENDPOINT); 31 | 32 | const splitter = new RecursiveCharacterTextSplitter({ 33 | chunkSize: 512, 34 | chunkOverlap: 100, 35 | }); 36 | 37 | const createCollection = async (similarityMetric: SimilarityMetric = 'dot_product') => { 38 | const res = await astraDb.createCollection(ASTRA_DB_COLLECTION, { 39 | vector: { 40 | dimension: 1536, 41 | metric: similarityMetric, 42 | } 43 | }); 44 | console.log(res); 45 | }; 46 | 47 | const loadSampleData = async (similarityMetric: SimilarityMetric = 'dot_product') => { 48 | const collection = await astraDb.collection(ASTRA_DB_COLLECTION); 49 | for await (const url of taylorData) { 50 | console.log(`Processing url ${url}`); 51 | const content = await scrapePage(url); 52 | const chunks = await splitter.splitText(content); 53 | for await (const chunk of chunks) { 54 | const embedding = await openai.embeddings.create({ 55 | model: "text-embedding-3-small", 56 | input: chunk, 57 | encoding_format: "float", 58 | }); 59 | 60 | const vector = embedding.data[0].embedding; 61 | 62 | const res = await collection.insertOne({ 63 | $vector: vector, 64 | text: chunk 65 | }); 66 | console.log(res) 67 | } 68 | } 69 | }; 70 | 71 | const scrapePage = async (url: string) => { 72 | const loader = new PuppeteerWebBaseLoader(url, { 73 | launchOptions: { 74 | headless: true 75 | }, 76 | gotoOptions: { 77 | waitUntil: "domcontentloaded", 78 | }, 79 | evaluate: async (page, browser) => { 80 | const result = await page.evaluate(() => document.body.innerHTML); 81 | await browser.close(); 82 | return result; 83 | }, 84 | }); 85 | return (await loader.scrape())?.replace(/<[^>]*>?/gm, ''); 86 | }; 87 | 88 | createCollection().then(() => loadSampleData()); -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 6 | ], 7 | future: { 8 | hoverOnlyWhenSupported: true, 9 | }, 10 | theme: { 11 | extend: { 12 | fontFamily: { 13 | sans: ["var(--font-geist-sans)"], 14 | }, 15 | screens: { 16 | origin: "800px", 17 | }, 18 | }, 19 | }, 20 | plugins: [], 21 | }; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": false, 11 | "noEmit": true, 12 | "incremental": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ] 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | ".next/types/**/*.ts", 28 | "**/*.ts", 29 | "**/*.tsx" 30 | ], 31 | "ts-node": { 32 | // these options are overrides used only by ts-node 33 | // same as the --compilerOptions flag and the TS_NODE_COMPILER_OPTIONS environment variable 34 | "compilerOptions": { 35 | "module": "commonjs" 36 | } 37 | }, 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | --------------------------------------------------------------------------------