├── .env.example ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ └── deploy-modal.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── actions.ts ├── api │ ├── modal │ │ └── route.ts │ ├── py │ │ └── route.ts │ ├── snow │ │ └── route.ts │ └── sql │ │ └── route.ts ├── chat │ └── [id] │ │ └── page.tsx ├── globals.css ├── icon.tsx ├── layout.tsx ├── opengraph-image.png ├── page.tsx ├── share │ └── [id] │ │ └── page.tsx └── sign-in │ └── [[...sign-in]] │ └── page.tsx ├── bun.lockb ├── code-plugin ├── common.py ├── main.py ├── output.png ├── poetry.lock ├── pyproject.toml ├── requirements.txt └── temp.py ├── components ├── button-scroll-to-bottom.tsx ├── chat-list.tsx ├── chat-message-actions.tsx ├── chat-message.tsx ├── chat-panel.tsx ├── chat-scroll-anchor.tsx ├── chat.tsx ├── clear-history.tsx ├── external-link.tsx ├── footer.tsx ├── header.tsx ├── login-button.tsx ├── markdown.tsx ├── prompt-form.tsx ├── providers.tsx ├── schema.tsx ├── sidebar-actions.tsx ├── sidebar-footer.tsx ├── sidebar-item.tsx ├── sidebar-list.tsx ├── sidebar.tsx ├── tailwind-indicator.tsx ├── theme-toggle.tsx ├── toaster.tsx ├── ui │ ├── LogoIcon.tsx │ ├── alert-dialog.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── codeblock.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── icons.tsx │ ├── input.tsx │ ├── label.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── switch.tsx │ ├── textarea.tsx │ ├── toast.tsx │ └── tooltip.tsx └── user-menu.tsx ├── embed ├── checksums.json ├── docs │ ├── customer_details.md │ ├── order_details.md │ ├── payments.md │ ├── products.md │ └── transactions.md ├── embed.py ├── requirements.txt └── snowflake_ddl_fetcher.py ├── lib ├── fonts.ts ├── hooks │ ├── use-at-bottom.tsx │ ├── use-copy-to-clipboard.tsx │ ├── use-enter-submit.tsx │ ├── use-local-storage.ts │ └── use-toast.ts ├── rate-limiter.ts ├── redis.ts ├── types.ts ├── uploadToCloudinary.ts └── utils.ts ├── middleware.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── prettier.config.cjs ├── supabase ├── .gitignore ├── config.toml ├── migrations │ └── 20230707053030_init.sql └── seed.sql ├── tailwind.config.js ├── tsconfig.json └── utils ├── fetchHelpers.ts ├── initialChat.ts ├── pinecone-client.ts ├── prompts.ts ├── supabaseClient.ts └── tables.ts /.env.example: -------------------------------------------------------------------------------- 1 | 2 | OPENAI_API_KEY= 3 | 4 | NEXT_PUBLIC_SUPABASE_URL= 5 | NEXT_PUBLIC_SUPABASE_ANON_KEY= 6 | 7 | AUTH_GITHUB_ID= 8 | AUTH_GITHUB_SECRET= 9 | 10 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 11 | CLERK_SECRET_KEY= 12 | NEXT_PUBLIC_CLERK_FRONTEND_API= 13 | 14 | PINECONE_API_KEY= 15 | PINECONE_ENVIRONMENT= 16 | PINECONE_INDEX_NAME=snowbrain-v1 17 | PINECONE_NAME_SPACE=snowbrain 18 | 19 | ACCOUNT= 20 | USER_NAME= 21 | PASSWORD= 22 | ROLE= 23 | DATABASE= 24 | SCHEMA= 25 | WAREHOUSE= 26 | 27 | NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME= 28 | NEXT_PUBLIC_CLOUDINARY_API_KEY= 29 | CLOUDINARY_API_SECRET= 30 | NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET= 31 | 32 | UPSTASH_REDIS_REST_URL= 33 | UPSTASH_REDIS_REST_TOKEN= 34 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc", 3 | "root": true, 4 | "extends": [ 5 | "next/core-web-vitals", 6 | "prettier", 7 | "plugin:tailwindcss/recommended" 8 | ], 9 | "plugins": ["tailwindcss"], 10 | "rules": { 11 | "tailwindcss/no-custom-classname": "off" 12 | }, 13 | "settings": { 14 | "tailwindcss": { 15 | "callees": ["cn", "cva"], 16 | "config": "tailwind.config.js" 17 | } 18 | }, 19 | "overrides": [ 20 | { 21 | "files": ["*.ts", "*.tsx"], 22 | "parser": "@typescript-eslint/parser" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [kaarthik108] 4 | -------------------------------------------------------------------------------- /.github/workflows/deploy-modal.yml: -------------------------------------------------------------------------------- 1 | name: Deploy fastapi 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - "code-plugin/main.py" 8 | 9 | jobs: 10 | deploy: 11 | name: Deploy example app 12 | if: github.ref == 'refs/heads/main' 13 | runs-on: ubuntu-20.04 14 | env: 15 | MODAL_TOKEN_ID: ${{ secrets.MODAL_MODAL_LABS_TOKEN_ID }} 16 | MODAL_TOKEN_SECRET: ${{ secrets.MODAL_MODAL_LABS_TOKEN_SECRET }} 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - uses: actions/setup-python@v3 22 | with: 23 | python-version: "3.10" 24 | 25 | - name: Install Poetry 26 | run: | 27 | curl -sSL https://install.python-poetry.org | python3 - 28 | 29 | - name: Cache Poetry 30 | uses: actions/cache@v3 31 | with: 32 | path: ~/.cache/pypoetry 33 | key: ${{ runner.os }}-poetry-${{ hashFiles('**/pyproject.toml') }} 34 | restore-keys: | 35 | ${{ runner.os }}-poetry- 36 | 37 | - name: Install dependencies 38 | run: | 39 | cd code-plugin 40 | poetry install 41 | 42 | - name: Deploy FastAPI app to Modal 43 | run: | 44 | cd code-plugin 45 | poetry run modal deploy main.py 46 | -------------------------------------------------------------------------------- /.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 | __pycache__/ 33 | 34 | code-plugin/main-test.py 35 | code-plugin/__pycache__/ 36 | .venv 37 | 38 | # turbo 39 | .turbo 40 | 41 | .contentlayer 42 | .env 43 | 44 | 45 | .vercel 46 | .vscode 47 | 48 | test.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snowBrain 2 | 3 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/kaarthik108/snowbrain&project-name=snowbrain&repo-name=snowbrain) 4 | 5 | 6 | 7 | https://github.com/kaarthik108/snowBrain/assets/53030784/582eee20-bcf6-4db7-9343-2941937374f9 8 | 9 | 10 | 11 | 12 | SnowBrain is an open-source prototype that serves as your personal data analyst. It converses in SQL, remembers previous discussions, and even draws data visualizations for you. 13 | 14 | This project is a unique blend of Snowflake, Langchain, OpenAI, Pinecone, NEXTjs, and FastAPI, among other technologies. It's all about reimagining the simplicity of SQL querying. Dive in and discover a new way of interacting with your data. 15 | 16 | ## Tech Stack 17 | - [Snowflake](https://www.snowflake.com/) - Data Cloud 18 | - [Next.js](https://nextjs.org/) - Frontend & backend 19 | - [Supabase](https://supabase.com/) - DB - Persist chat messages 20 | - [Tailwindcss](https://tailwindcss.com/) - Styling 21 | - [Pinecone](https://www.pinecone.io/) - Vector database 22 | - [OpenAI](https://www.openai.com/) - LLM 23 | - [Langchain](https://js.langchain.com/docs/) - LLM wrapper 24 | - [Cloudinary](https://cloudinary.com/) - Image data 25 | - [Clerk.dev](https://clerk.dev/) - Auth 26 | - [Upstash Redis](https://upstash.com/) - Rate limiting 27 | - [Fast API](https://fastapi.tiangolo.com/) - Backend python 28 | - [Modal Labs](https://modal.com/) - Host backend fastapi 29 | - [Vercel](https://vercel.com/) - Hosting 30 | - [umami](https://umami.is/) - Web analytics 31 | 32 | ## Features 33 | 34 | - **Snowflake to Vector Database**: Automatic conversion of all Snowflake DDL to a vector database. 35 | - **Conversational Memory**: Maintain context and improve the quality of interactions. 36 | - **Snowflake Integration**: Integrate with Snowflake schema for automatic SQL generation and visualization. 37 | - **Pinecone Vector Database**: Leverage Pinecone's vector database management system for efficient searching capabilities. 38 | - **Secure Authentication**: Employ Clerk.dev for secure and hassle-free user authentication. 39 | - **Rate Limit Handling**: Utilize Upstash Redis for managing rate limits. 40 | - **Fast API**: High-performance Python web framework for building APIs. 41 | 42 | ## Example Queries 43 | 44 | snowBrain is designed to make complex data querying simple. Here are some example queries you can try: 45 | 46 | - **Total revenue per product category**: "Show me the total revenue for each product category." 47 | - **Top customers by sales**: "Who are the top 10 customers by sales?" 48 | - **Average order value per region**: "What is the average order value for each region?" 49 | - **Order volume**: "How many orders were placed last week?" 50 | - **Product price listing**: "Display the list of products with their prices." 51 | 52 | ## Installation 53 | 54 | Follow these steps to get **snowBrain** up and running in your local environment. 55 | 56 | 1. **Update Environment Variables** 57 | 58 | Make sure to update the environment variables as necessary. Refer to the example provided: 59 | 60 | ```bash 61 | .env.example 62 | ``` 63 | 64 | 2. **Auto fetch All Schema DDL** 65 | 66 | You can do this by running the following command: 67 | 68 | ```bash 69 | python3 embed/snowflake_ddl_fetcher.py 70 | ``` 71 | Make sure to install requirements using 72 | ```bash 73 | pip3 install -r embed/requirements.txt 74 | ``` 75 | 76 | 3. **Convert DDL Documents to Vector & Upload to Pinecone** 77 | 78 | Use the following command to do this: 79 | 80 | ```bash 81 | python3 embed/embed.py 82 | ``` 83 | 84 | 4. **Install Dependencies for the Code Plugin** 85 | 86 | Navigate to the code plugin directory and install the necessary dependencies using Poetry: 87 | 88 | ```bash 89 | cd code-plugin && poetry install 90 | ``` 91 | 92 | 5. **Deploy FastAPI to Modal Labs** 93 | 94 | Run the following command to deploy your FastAPI (make sure to add a secrets file in modal labs): 95 | 96 | ```bash 97 | modal deploy main.py 98 | ``` 99 | 100 | After deploying, make sure to store the endpoint in your environment variables: 101 | 102 | ```bash 103 | MODAL_API_ENDPOINT= 104 | MODAL_AUTH_TOKEN=random_secret 105 | ``` 106 | 107 | 6. **Install packages** 108 | 109 | Install packages using the following command: 110 | 111 | ```bash 112 | bun install 113 | ``` 114 | 115 | 7. **Run Locally** 116 | 117 | Test the setup locally using the following command: 118 | 119 | ```bash 120 | bun run dev 121 | ``` 122 | Test the build 123 | ```bash 124 | bun run build 125 | ``` 126 | 127 | 8. **Deploy to Vercel** 128 | 129 | Finally, when you're ready, deploy the project to Vercel. 130 | 131 |
132 | 133 | Note: Vercel build is automatically blocked on folders code-plugin, embed and readme.md. You can additionally add a build block command in vercel's dashboard. 134 | 135 |
136 | 137 | 138 | ## One-Click Deploy 139 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/kaarthik108/snowbrain&project-name=snowbrain&repo-name=snowbrain) 140 | 141 |
142 | 143 | ## Contributing 144 | 145 | Here's how you can contribute: 146 | 147 | - [Open an issue](https://github.com/kaarthik108/snowbrain/issues) if you believe you've encountered a bug. 148 | - Make a [pull request](https://github.com/kaarthik108/snowbrain/pulls) to add new features/make improvements/fix bugs. 149 | 150 |
151 | 152 | ## Credits 153 | 154 | Thanks to @jaredpalmer, @shuding_, @shadcn, @thorwebdev 155 | -------------------------------------------------------------------------------- /app/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { revalidatePath } from 'next/cache' 4 | 5 | import { type Chat } from '@/lib/types' 6 | import { supabaseClient } from '@/utils/supabaseClient' 7 | import { auth } from '@clerk/nextjs' 8 | import { redirect } from 'next/navigation' 9 | 10 | export async function getChats(userId?: string | null) { 11 | if (!userId) { 12 | return [] 13 | } 14 | const { getToken } = auth() 15 | const supabaseAccessToken = await getToken({ 16 | template: 'supabase' 17 | }) 18 | const supabase = await supabaseClient(supabaseAccessToken as string) 19 | 20 | try { 21 | const { data } = await supabase 22 | .from('chats') 23 | .select('payload') 24 | .order('payload->createdAt', { ascending: false }) 25 | .throwOnError() 26 | return (data?.map(entry => entry.payload) as Chat[]) ?? [] 27 | } catch (error) { 28 | return [] 29 | } 30 | } 31 | 32 | export async function getChat(id: string) { 33 | const { getToken } = auth() 34 | const supabaseAccessToken = await getToken({ 35 | template: 'supabase' 36 | }) 37 | const supabase = await supabaseClient(supabaseAccessToken as string) 38 | const { data } = await supabase 39 | .from('chats') 40 | .select('payload') 41 | .eq('id', id) 42 | .maybeSingle() 43 | return (data?.payload as Chat) ?? null 44 | } 45 | 46 | export async function removeChat({ id, path }: { id: string; path: string }) { 47 | const { getToken } = auth() 48 | const supabaseAccessToken = await getToken({ 49 | template: 'supabase' 50 | }) 51 | const supabase = await supabaseClient(supabaseAccessToken as string) 52 | 53 | try { 54 | await supabase.from('chats').delete().eq('id', id).throwOnError() 55 | 56 | revalidatePath('/') 57 | return revalidatePath(path) 58 | } catch (error) { 59 | return { 60 | error: 'Unauthorized' 61 | } 62 | } 63 | } 64 | 65 | export async function clearChats() { 66 | const { userId, getToken } = auth() 67 | const supabaseAccessToken = await getToken({ 68 | template: 'supabase' 69 | }) 70 | const supabase = await supabaseClient(supabaseAccessToken as string) 71 | 72 | try { 73 | await supabase.from('chats').delete().eq('user_id', userId).throwOnError() 74 | revalidatePath('/') 75 | return redirect('/') 76 | } catch (error) { 77 | console.log('error', error) 78 | return 79 | } 80 | } 81 | 82 | export async function getSharedChat(id: string) { 83 | const { getToken } = auth() 84 | const supabaseAccessToken = await getToken({ 85 | template: 'supabase' 86 | }) 87 | const supabase = await supabaseClient(supabaseAccessToken as string) 88 | const { data } = await supabase 89 | .from('chats') 90 | .select('payload') 91 | .eq('id', id) 92 | .not('payload->sharePath', 'is', null) 93 | .maybeSingle() 94 | return (data?.payload as Chat) ?? null 95 | } 96 | 97 | export async function shareChat(chat: Chat) { 98 | const { getToken } = auth() 99 | const supabaseAccessToken = await getToken({ 100 | template: 'supabase' 101 | }) 102 | 103 | const supabase = await supabaseClient(supabaseAccessToken as string) 104 | 105 | const payload = { 106 | ...chat, 107 | sharePath: `/share/${chat.id}` 108 | } 109 | 110 | await supabase 111 | .from('chats') 112 | .update({ payload: payload as any }) 113 | .eq('id', chat.id) 114 | .throwOnError() 115 | 116 | return payload 117 | } 118 | -------------------------------------------------------------------------------- /app/api/modal/route.ts: -------------------------------------------------------------------------------- 1 | import uploadToCloudinary from '@/lib/uploadToCloudinary' 2 | import { nanoid } from '@/lib/utils' 3 | import { supabaseClient } from '@/utils/supabaseClient' 4 | import { auth } from '@clerk/nextjs' 5 | import { NextRequest, NextResponse } from 'next/server' 6 | 7 | const MODAL_API = 8 | process.env.NODE_ENV === 'development' 9 | ? 'http://127.0.0.1:8000/execute' 10 | : process.env.MODAL_API_ENDPOINT! 11 | 12 | export const runtime = 'edge' 13 | 14 | export async function POST(req: NextRequest): Promise { 15 | const { pythonCode, sqlCode, messages } = await req.json() 16 | const { getToken, userId } = auth() 17 | const supabaseAccessToken = await getToken({ 18 | template: 'supabase' 19 | }) 20 | const supabase = await supabaseClient(supabaseAccessToken as string) 21 | 22 | if (!userId) { 23 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 24 | } 25 | 26 | const response = await fetch(MODAL_API, { 27 | method: 'POST', 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | Authorization: `Bearer ${process.env.MODAL_AUTH_TOKEN}` 31 | }, 32 | body: JSON.stringify({ script: pythonCode, sql: sqlCode }) 33 | }) 34 | console.log('response ---- ', response.ok) 35 | if (!response.ok) { 36 | return NextResponse.json({ error: `Script Error ${response.status}` }) 37 | } 38 | 39 | const imageData = await response.blob() 40 | 41 | const imageUrl = await uploadToCloudinary(imageData) 42 | 43 | if (!imageUrl) { 44 | return NextResponse.json({ error: 'Upload Error' }, { status: 500 }) 45 | } 46 | 47 | const title = 'Modal cloudinary' 48 | const id = messages[0].id ?? nanoid() 49 | const createdAt = Date.now() 50 | const path = `/chat/${id}` 51 | const payload = { 52 | id, 53 | title, 54 | userId, 55 | createdAt, 56 | path, 57 | messages: [ 58 | ...messages, 59 | { 60 | content: imageUrl, 61 | role: 'assistant' 62 | } 63 | ] 64 | } 65 | 66 | await supabase 67 | .from('chats') 68 | .upsert({ id, user_id: userId, payload }) 69 | .throwOnError() 70 | 71 | return NextResponse.json({ imageUrl }, { status: 200 }) 72 | } 73 | -------------------------------------------------------------------------------- /app/api/py/route.ts: -------------------------------------------------------------------------------- 1 | import { RetrievalQAChain } from 'langchain/chains' 2 | import { ChatOpenAI } from 'langchain/chat_models/openai' 3 | import { PromptTemplate } from 'langchain/prompts' 4 | import { initPinecone } from 'utils/pinecone-client' 5 | import { CODE_PROMPT } from 'utils/prompts' 6 | 7 | export const runtime = 'edge' 8 | 9 | const q_prompt = PromptTemplate.fromTemplate(CODE_PROMPT) 10 | 11 | export async function POST(req: Request) { 12 | const { prompt } = await req.json() 13 | const vectorStore = await initPinecone() 14 | 15 | const model = new ChatOpenAI({ 16 | temperature: 0.6, 17 | modelName: 'gpt-4', 18 | maxTokens: 800, 19 | openAIApiKey: process.env.OPENAI_API_KEY, 20 | streaming: false 21 | }) 22 | 23 | const chain = RetrievalQAChain.fromLLM(model, vectorStore.asRetriever(), { 24 | prompt: q_prompt 25 | }) 26 | 27 | const res = await chain.call({ 28 | query: prompt 29 | }) 30 | 31 | return new Response(JSON.stringify(res), { 32 | headers: { 33 | 'content-type': 'application/json;charset=UTF-8' 34 | } 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /app/api/snow/route.ts: -------------------------------------------------------------------------------- 1 | import { nanoid, toMarkdownTable } from '@/lib/utils' 2 | import { supabaseClient } from '@/utils/supabaseClient' 3 | import { auth } from '@clerk/nextjs' 4 | import { NextRequest, NextResponse } from 'next/server' 5 | import snowflake from 'snowflake-sdk' 6 | 7 | const connectionPool = snowflake.createPool( 8 | { 9 | account: process.env.ACCOUNT as string, 10 | username: process.env.USER_NAME as string, 11 | password: process.env.PASSWORD, 12 | role: process.env.ROLE, 13 | warehouse: process.env.WAREHOUSE, 14 | database: process.env.DATABASE, 15 | schema: process.env.SCHEMA 16 | }, 17 | { 18 | max: 10, 19 | min: 0 20 | } 21 | ) 22 | 23 | export async function POST(request: NextRequest): Promise { // specify the return type 24 | const { getToken, userId } = auth() 25 | const supabaseAccessToken = await getToken({ 26 | template: 'supabase' 27 | }) 28 | const supabase = await supabaseClient(supabaseAccessToken as string) 29 | if (!userId) { 30 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 31 | } 32 | const requestBody = await request.json() 33 | const query = requestBody.query 34 | const messages = requestBody.messages 35 | 36 | let result 37 | try { 38 | const clientConnection = await connectionPool.acquire(); 39 | const result: NextResponse = await new Promise((resolve, reject) => { // specify the type of result 40 | clientConnection.execute({ 41 | sqlText: query, 42 | complete: async (err, stmt, rows) => { 43 | if (err) { 44 | reject( 45 | NextResponse.json({ 46 | error: 'Failed to execute statement in Snowflake.', 47 | }) 48 | ); 49 | } else { 50 | const markdownTable = toMarkdownTable(rows as any[]) 51 | const title = 'snowflake-results' 52 | const id = messages[0].id ?? nanoid() 53 | const createdAt = Date.now() 54 | const path = `/chat/${id}` 55 | const payload = { 56 | id, 57 | title, 58 | userId, 59 | createdAt, 60 | path, 61 | messages: [ 62 | ...messages, 63 | { 64 | content: markdownTable, 65 | role: 'assistant' 66 | } 67 | ] 68 | } 69 | await supabase 70 | .from('chats') 71 | .upsert({ id, user_id: userId, payload }) 72 | .throwOnError() 73 | resolve(NextResponse.json(markdownTable)) 74 | } 75 | } 76 | }) 77 | }) 78 | connectionPool.release(clientConnection) 79 | return result; 80 | } catch (error: any) { 81 | return NextResponse.json({ error: error.message }, { status: 500 }); 82 | } 83 | } -------------------------------------------------------------------------------- /app/api/sql/route.ts: -------------------------------------------------------------------------------- 1 | import { rateLimiter } from '@/lib/rate-limiter' 2 | import { nanoid } from '@/lib/utils' 3 | import { supabaseClient } from '@/utils/supabaseClient' 4 | import { auth } from '@clerk/nextjs' 5 | import { ChatOpenAI } from '@langchain/openai' 6 | import { LangChainStream, Message, StreamingTextResponse } from 'ai' 7 | import { CallbackManager } from 'langchain/callbacks' 8 | import { ConversationalRetrievalQAChain } from 'langchain/chains' 9 | import { OpenAI } from 'langchain/llms/openai' 10 | import { BufferMemory, ChatMessageHistory } from 'langchain/memory' 11 | import { AIMessage, HumanMessage, SystemMessage } from 'langchain/schema' 12 | import { NextResponse } from 'next/server' 13 | import { initPinecone } from 'utils/pinecone-client' 14 | import { CONDENSE_QUESTION_PROMPT, QA_PROMPT } from 'utils/prompts' 15 | 16 | export const runtime = 'edge' 17 | 18 | export async function POST(req: Request): Promise { 19 | if (process.env.NODE_ENV != 'development') { 20 | const ip = req.headers.get('x-forwarded-for') 21 | const { success, limit, reset, remaining } = await rateLimiter.limit( 22 | `snowbrain_ratelimit_${ip}` 23 | ) 24 | 25 | if (!success) { 26 | return new Response('You have reached your request limit for the day.', { 27 | status: 429, 28 | headers: { 29 | 'X-RateLimit-Limit': limit.toString(), 30 | 'X-RateLimit-Remaining': remaining.toString(), 31 | 'X-RateLimit-Reset': reset.toString() 32 | } 33 | }) 34 | } 35 | } 36 | 37 | const { getToken, userId } = auth() 38 | const supabaseAccessToken = await getToken({ 39 | template: 'supabase' 40 | }) 41 | const supabase = await supabaseClient(supabaseAccessToken as string) 42 | 43 | if (!userId) { 44 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 45 | } 46 | try { 47 | const json = await req.json() 48 | 49 | const { messages } = json 50 | const vectorStore = await initPinecone() 51 | const id = json.id ?? nanoid() 52 | const { stream, handlers } = LangChainStream() 53 | 54 | if (messages.length > 25) { 55 | return NextResponse.json({ 56 | error: 'Too many messages, please use new chat..' 57 | }) 58 | } 59 | 60 | const model = new ChatOpenAI({ 61 | temperature: 1, 62 | modelName: 'gpt-3.5-turbo-1106', 63 | openAIApiKey: process.env.OPENAI_API_KEY, 64 | streaming: true, 65 | maxTokens: 2000, 66 | callbacks: CallbackManager.fromHandlers(handlers) as any 67 | }) 68 | const qamodel = new OpenAI({ 69 | modelName: 'gpt-3.5-turbo', 70 | temperature: 0.1, 71 | maxTokens: 2000 72 | }) 73 | const history = new ChatMessageHistory( 74 | messages.map((m: Message) => { 75 | if (m.role === 'user') { 76 | return new HumanMessage(m.content) 77 | } 78 | if (m.role === 'system') { 79 | return new SystemMessage(m.content) 80 | } 81 | return new AIMessage(m.content) 82 | }) 83 | ) 84 | const chain = ConversationalRetrievalQAChain.fromLLM( 85 | model as any, 86 | vectorStore.asRetriever(), 87 | { 88 | qaTemplate: QA_PROMPT, 89 | questionGeneratorChainOptions: { 90 | llm: qamodel, 91 | template: CONDENSE_QUESTION_PROMPT 92 | }, 93 | memory: new BufferMemory({ 94 | memoryKey: 'chat_history', 95 | humanPrefix: 96 | "You are a good assistant that answers question based on the document info you have. If you don't have any information just say I don't know.", 97 | inputKey: 'question', 98 | outputKey: 'text', 99 | returnMessages: true, 100 | chatHistory: history 101 | }) 102 | } 103 | ) 104 | const question = messages[messages.length - 1].content 105 | 106 | let completion: any 107 | chain 108 | .call({ 109 | question: question, 110 | chat_history: history 111 | }) 112 | .then(result => { 113 | completion = result 114 | }) 115 | .catch(console.error) 116 | .finally(async () => { 117 | handlers.handleChainEnd() 118 | 119 | const title = messages[0].content.substring(0, 100) 120 | const createdAt = Date.now() 121 | const path = `/chat/${id}` 122 | const payload = { 123 | id, 124 | title, 125 | userId, 126 | createdAt, 127 | path, 128 | messages: [ 129 | ...messages, 130 | { 131 | content: completion.text, 132 | role: 'assistant' 133 | } 134 | ] 135 | } 136 | 137 | await supabase 138 | .from('chats') 139 | .upsert({ id, user_id: userId, payload }) 140 | .throwOnError() 141 | }) 142 | 143 | return new StreamingTextResponse(stream) 144 | } catch (error) { 145 | console.error(error) 146 | return NextResponse.json( 147 | { error: 'Something went wrong, please try again..' }, 148 | { status: 500 } 149 | ) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /app/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from 'next'; 2 | import { notFound, redirect } from 'next/navigation'; 3 | 4 | import { getChat } from '@/app/actions'; 5 | import { Chat } from '@/components/chat'; 6 | import { auth } from '@clerk/nextjs'; 7 | 8 | // export const runtime = 'edge' 9 | export const preferredRegion = 'home' 10 | 11 | export interface ChatPageProps { 12 | params: { 13 | id: string 14 | } 15 | } 16 | 17 | export async function generateMetadata({ 18 | params 19 | }: ChatPageProps): Promise { 20 | const { userId } = auth(); 21 | 22 | 23 | if (!userId) { 24 | return {} 25 | } 26 | 27 | const chat = await getChat(params.id) 28 | return { 29 | title: chat?.title.toString().slice(0, 50) ?? 'Chat' 30 | } 31 | } 32 | 33 | export default async function ChatPage({ params }: ChatPageProps) { 34 | const { userId } = auth(); 35 | if (!userId) { 36 | redirect(`/sign-in?next=/chat/${params.id}`) 37 | } 38 | 39 | const chat = await getChat(params.id) 40 | 41 | if (!chat) { 42 | notFound() 43 | } 44 | 45 | if (chat?.userId !== userId) { 46 | notFound() 47 | } 48 | 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --muted: 240 4.8% 95.9%; 11 | --muted-foreground: 240 3.8% 46.1%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 240 10% 3.9%; 18 | 19 | --border: 240 5.9% 90%; 20 | --input: 240 5.9% 90%; 21 | 22 | --primary: 240 5.9% 10%; 23 | --primary-foreground: 0 0% 98%; 24 | 25 | --secondary: 240 4.8% 95.9%; 26 | --secondary-foreground: 240 5.9% 10%; 27 | 28 | --accent: 240 4.8% 95.9%; 29 | --accent-foreground: ; 30 | 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | 34 | --ring: 240 5% 64.9%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 240 10% 3.9%; 41 | --foreground: 0 0% 98%; 42 | 43 | --muted: 240 3.7% 15.9%; 44 | --muted-foreground: 240 5% 64.9%; 45 | 46 | --popover: 240 10% 3.9%; 47 | --popover-foreground: 0 0% 98%; 48 | 49 | --card: 240 10% 3.9%; 50 | --card-foreground: 0 0% 98%; 51 | 52 | --border: 240 3.7% 15.9%; 53 | --input: 240 3.7% 15.9%; 54 | 55 | --primary: 0 0% 98%; 56 | --primary-foreground: 240 5.9% 10%; 57 | 58 | --secondary: 240 3.7% 15.9%; 59 | --secondary-foreground: 0 0% 98%; 60 | 61 | --accent: 240 3.7% 15.9%; 62 | --accent-foreground: ; 63 | 64 | --destructive: 0 62.8% 30.6%; 65 | --destructive-foreground: 0 85.7% 97.3%; 66 | 67 | --ring: 240 3.7% 15.9%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/icon.tsx: -------------------------------------------------------------------------------- 1 | import LogoIcon from '@/components/ui/LogoIcon' 2 | import { ImageResponse } from 'next/og' 3 | 4 | export const runtime = 'edge' 5 | 6 | export const size = { 7 | width: 32, 8 | height: 32 9 | } 10 | // export const contentType = 'image/png' 11 | 12 | export default function Icon() { 13 | return new ImageResponse( 14 | ( 15 | // ImageResponse JSX element 16 | 17 | ), 18 | // ImageResponse options 19 | { 20 | ...size 21 | } 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | 3 | import '@/app/globals.css' 4 | import { Header } from '@/components/header' 5 | import { Providers } from '@/components/providers' 6 | import { Toaster } from '@/components/toaster' 7 | import { fontMono, fontSans } from '@/lib/fonts' 8 | import { cn } from '@/lib/utils' 9 | import { ClerkProvider } from "@clerk/nextjs" 10 | import Script from 'next/script' 11 | 12 | const title = "snowBrain"; 13 | const description = `snowBrain - AI Driven snowflake data insights`; 14 | 15 | export const metadata: Metadata = { 16 | title, 17 | description, 18 | openGraph: { 19 | title, 20 | description, 21 | }, 22 | twitter: { 23 | title, 24 | description, 25 | card: "summary_large_image", 26 | creator: "@kaarthikcodes", 27 | }, 28 | metadataBase: new URL(`https://${process.env.NEXT_PUBLIC_VERCEL_URL}`), 29 | } 30 | 31 | interface RootLayoutProps { 32 | children: React.ReactNode 33 | } 34 | 35 | export default function RootLayout({ children }: RootLayoutProps) { 36 | return ( 37 | 38 | 39 | 40 | 41 | 48 | 49 | 50 |
51 | {/* @ts-ignore */} 52 |
53 |
{children}
54 |
55 |
56 | 57 |
58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaarthik108/snowBrain/5f7412e245510c04c471b2e797a650963e87db7c/app/opengraph-image.png -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Chat } from '@/components/chat' 2 | import { nanoid } from '@/lib/utils' 3 | 4 | // export const runtime = 'edge' 5 | 6 | export default function IndexPage() { 7 | const id = nanoid() 8 | 9 | return 10 | } 11 | -------------------------------------------------------------------------------- /app/share/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from 'next' 2 | import { notFound } from 'next/navigation' 3 | 4 | import { getSharedChat } from '@/app/actions' 5 | import { ChatList } from '@/components/chat-list' 6 | import { FooterText } from '@/components/footer' 7 | import { formatDate } from '@/lib/utils' 8 | 9 | // export const runtime = 'edge' 10 | export const preferredRegion = 'home' 11 | 12 | interface SharePageProps { 13 | params: { 14 | id: string 15 | } 16 | } 17 | 18 | export async function generateMetadata({ 19 | params 20 | }: SharePageProps): Promise { 21 | const chat = await getSharedChat(params.id) 22 | 23 | return { 24 | title: chat?.title.slice(0, 50) ?? 'Chat' 25 | } 26 | } 27 | 28 | export default async function SharePage({ params }: SharePageProps) { 29 | const chat = await getSharedChat(params.id) 30 | 31 | if (!chat || !chat?.sharePath) { 32 | notFound() 33 | } 34 | 35 | return ( 36 | <> 37 |
38 |
39 |
40 |
41 |

{chat.title}

42 |
43 | {formatDate(chat.createdAt)} · {chat.messages.length} messages 44 |
45 |
46 |
47 |
48 | 49 |
50 | 51 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /app/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 | ; 7 |
8 | ); 9 | } -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaarthik108/snowBrain/5f7412e245510c04c471b2e797a650963e87db7c/bun.lockb -------------------------------------------------------------------------------- /code-plugin/common.py: -------------------------------------------------------------------------------- 1 | from modal import Stub 2 | 3 | stub = Stub("snowbrain_modal") 4 | -------------------------------------------------------------------------------- /code-plugin/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from common import stub 4 | 5 | import subprocess 6 | import sys 7 | import modal 8 | 9 | from fastapi.responses import FileResponse 10 | from fastapi.middleware.cors import CORSMiddleware 11 | from fastapi import Depends, status, Request 12 | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials 13 | 14 | from pydantic import BaseModel 15 | 16 | 17 | image = modal.Image.debian_slim(python_version="3.10").pip_install( 18 | "modal-client", 19 | "fastapi==0.95.1", 20 | "uvicorn==0.22.0", 21 | "numpy==1.24.3", 22 | "matplotlib==3.7.1", 23 | "pillow==9.5.0", 24 | "seaborn==0.12.2", 25 | "snowflake-connector-python==3.0.0", 26 | ) 27 | stub.sb_image = image 28 | 29 | 30 | class Script(BaseModel): 31 | script: str 32 | sql: str 33 | 34 | 35 | auth_scheme = HTTPBearer() 36 | 37 | 38 | @stub.function(image=image, secret=modal.Secret.from_name("snowbrain"), cpu=2) 39 | @modal.asgi_app() 40 | def fastapi_app(): 41 | from fastapi import FastAPI, HTTPException 42 | 43 | app = FastAPI() 44 | app.add_middleware( 45 | CORSMiddleware, 46 | allow_origins=["*"], 47 | allow_credentials=True, 48 | allow_methods=["*"], 49 | allow_headers=["*"], 50 | ) 51 | 52 | @app.post("/execute") 53 | async def execute( 54 | script: Script, 55 | request: Request, 56 | token: HTTPAuthorizationCredentials = Depends(auth_scheme), 57 | ): 58 | import os 59 | 60 | if token.credentials != os.environ["AUTH_TOKEN"]: 61 | raise HTTPException( 62 | status_code=status.HTTP_401_UNAUTHORIZED, 63 | detail="Incorrect bearer token", 64 | headers={"WWW-Authenticate": "Bearer"}, 65 | ) 66 | try: 67 | # Install the packages 68 | # for package in script.packages: 69 | # subprocess.check_call([sys.executable, "-m", "pip", "install", package]) 70 | 71 | # Prepare the script for getting data from Snowflake 72 | snowflake_script = f""" 73 | import os 74 | import snowflake.connector 75 | import pandas as pd 76 | import matplotlib 77 | matplotlib.use('Agg') 78 | import matplotlib.pyplot as plt 79 | import seaborn as sns 80 | 81 | conn = snowflake.connector.connect( 82 | user=os.environ["USER_NAME"], 83 | password=os.environ["PASSWORD"], 84 | account=os.environ["ACCOUNT"], 85 | warehouse=os.environ["WAREHOUSE"], 86 | role=os.environ["ROLE"], 87 | database=os.environ["DATABASE"], 88 | schema=os.environ["SCHEMA"], 89 | ) 90 | 91 | cur = conn.cursor() 92 | cur.execute('USE DATABASE ' + os.environ["DATABASE"]) 93 | cur.execute('USE SCHEMA ' + os.environ["SCHEMA"]) 94 | cur.execute(f\"\"\" 95 | {script.sql} 96 | \"\"\") 97 | all_rows = cur.fetchall() 98 | field_names = [i[0] for i in cur.description] 99 | df = pd.DataFrame(all_rows) 100 | df.columns = field_names 101 | """ 102 | 103 | combined_script = ( 104 | snowflake_script 105 | + "\n" 106 | + script.script 107 | + '\nplt.tight_layout()\n' 108 | + '\nplt.savefig("output.png", dpi=100)' 109 | ) 110 | 111 | with open("temp.py", "w") as file: 112 | file.write(combined_script) 113 | 114 | proc = subprocess.run( 115 | [sys.executable, "temp.py"], capture_output=True, text=True 116 | ) 117 | 118 | if proc.returncode == 0: 119 | try: 120 | return FileResponse("output.png") 121 | except Exception as e: 122 | raise HTTPException( 123 | status_code=500, detail=f"Failed to encode image: {str(e)}" 124 | ) 125 | 126 | else: 127 | raise HTTPException(status_code=400, detail=proc.stderr) 128 | 129 | except Exception as e: 130 | # print(e, file=sys.stderr) 131 | raise HTTPException(status_code=500, detail=str(e)) 132 | 133 | return app 134 | -------------------------------------------------------------------------------- /code-plugin/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaarthik108/snowBrain/5f7412e245510c04c471b2e797a650963e87db7c/code-plugin/output.png -------------------------------------------------------------------------------- /code-plugin/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "snowbrain" 3 | version = "0.1.0" 4 | description = "" 5 | authors = [""] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | modal-client = "^0.55.3910" 11 | fastapi = "^0.95.1" 12 | uvicorn = "^0.22.0" 13 | numpy = "^1.24.3" 14 | matplotlib = "^3.7.1" 15 | pillow = "^9.5.0" 16 | seaborn = "^0.12.2" 17 | snowflake-connector-python = "^3.0.0" 18 | 19 | [build-system] 20 | requires = ["poetry-core"] 21 | build-backend = "poetry.core.masonry.api" 22 | 23 | -------------------------------------------------------------------------------- /code-plugin/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.97.0 2 | uvicorn -------------------------------------------------------------------------------- /code-plugin/temp.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaarthik108/snowBrain/5f7412e245510c04c471b2e797a650963e87db7c/code-plugin/temp.py -------------------------------------------------------------------------------- /components/button-scroll-to-bottom.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { cn } from '@/lib/utils' 6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom' 7 | import { Button, type ButtonProps } from '@/components/ui/button' 8 | import { IconArrowDown } from '@/components/ui/icons' 9 | 10 | export function ButtonScrollToBottom({ className, ...props }: ButtonProps) { 11 | const isAtBottom = useAtBottom() 12 | 13 | return ( 14 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import { type Message } from 'ai' 2 | 3 | import { ChatMessage } from '@/components/chat-message' 4 | import { Separator } from '@/components/ui/separator' 5 | import { IconOpenAI } from './ui/icons' 6 | 7 | export interface ChatList { 8 | messages: Message[] 9 | isSnowLoading?: boolean 10 | } 11 | 12 | export function ChatList({ messages, isSnowLoading }: ChatList) { 13 | if (!messages.length) { 14 | return null 15 | } 16 | return ( 17 | <> 18 |
19 | {messages.map((message, index) => ( 20 |
21 | 22 | {index < messages.length - 1 && ( 23 | 24 | )} 25 | 26 |
27 | ))} 28 | {isSnowLoading && ( 29 |
30 |
31 | 32 |
33 |
34 |
35 | thinking... 36 |
37 |
38 | )} 39 |
40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /components/chat-message-actions.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { type Message } from 'ai' 4 | 5 | import { Button } from '@/components/ui/button' 6 | import { IconCheck, IconCopy } from '@/components/ui/icons' 7 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' 8 | import { cn } from '@/lib/utils' 9 | 10 | interface ChatMessageActionsProps extends React.ComponentProps<'div'> { 11 | message: Message 12 | } 13 | 14 | export function ChatMessageActions({ 15 | message, 16 | className, 17 | ...props 18 | }: ChatMessageActionsProps) { 19 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) 20 | 21 | const onCopy = () => { 22 | if (isCopied) return 23 | copyToClipboard(message.content) 24 | } 25 | 26 | return ( 27 |
34 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /components/chat-message.tsx: -------------------------------------------------------------------------------- 1 | import { Message } from 'ai' 2 | import remarkGfm from 'remark-gfm' 3 | import remarkMath from 'remark-math' 4 | 5 | import { ChatMessageActions } from '@/components/chat-message-actions' 6 | import { MemoizedReactMarkdown } from '@/components/markdown' 7 | import { CodeBlock } from '@/components/ui/codeblock' 8 | import { IconOpenAI, IconSnow } from '@/components/ui/icons' 9 | import { cn } from '@/lib/utils' 10 | import Image from 'next/image' 11 | 12 | export interface ChatMessageProps { 13 | message: Message 14 | isLoading?: boolean 15 | } 16 | 17 | export function ChatMessage({ 18 | message, 19 | isLoading, 20 | ...props 21 | }: ChatMessageProps) { 22 | const isImage = message && typeof message.content === 'string' 23 | ? message.content.startsWith('https://res.cloudinary.com/') 24 | : false; 25 | 26 | const isError = message && typeof message.content === 'string' 27 | ? message.content.startsWith('Error:') 28 | : false; 29 | 30 | return ( 31 | <> 32 |
36 |
44 | {message.role === 'user' ? : } 45 |
46 | 47 |
48 | {isImage ? ( 49 | Matplot python chart 50 | ) : isError ? ( 51 |
{message.content}
52 | ) : ( 53 | {children}

59 | }, 60 | code({ node, inline, className, children, ...props }) { 61 | if (children.length) { 62 | if (children[0] == '▍') { 63 | return ( 64 | 65 | ▍ 66 | 67 | ) 68 | } 69 | children[0] = (children[0] as string).replace('`▍`', '▍') 70 | } 71 | const match = /language-(\w+)/.exec(className || '') 72 | if (inline) { 73 | return ( 74 | 75 | {children} 76 | 77 | ) 78 | } 79 | return ( 80 | 86 | ) 87 | } 88 | }} 89 | > 90 | {message.content} 91 |
92 | )} 93 | 94 | 95 |
96 |
97 | 98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /components/chat-panel.tsx: -------------------------------------------------------------------------------- 1 | import { type UseChatHelpers } from 'ai/react' 2 | 3 | import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom' 4 | import { FooterText } from '@/components/footer' 5 | import { PromptForm } from '@/components/prompt-form' 6 | import { Button } from '@/components/ui/button' 7 | import { IconRefresh, IconShare, IconStop } from '@/components/ui/icons' 8 | 9 | export interface ChatPanelProps 10 | extends Pick< 11 | UseChatHelpers, 12 | | 'append' 13 | | 'isLoading' 14 | | 'reload' 15 | | 'messages' 16 | | 'stop' 17 | | 'input' 18 | | 'setInput' 19 | > { 20 | id?: string 21 | } 22 | 23 | export function ChatPanel({ 24 | id, 25 | isLoading, 26 | stop, 27 | append, 28 | reload, 29 | input, 30 | setInput, 31 | messages 32 | }: ChatPanelProps) { 33 | return ( 34 |
35 | 36 |
37 |
38 | {isLoading ? ( 39 | 47 | ) : ( 48 | messages?.length > 0 && ( 49 | 57 | ) 58 | )} 59 |
60 |
61 | { 63 | await append({ 64 | id, 65 | content: value, 66 | role: 'user' 67 | }) 68 | }} 69 | input={input} 70 | setInput={setInput} 71 | isLoading={isLoading} 72 | /> 73 | 74 |
75 |
76 |
77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /components/chat-scroll-anchor.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useInView } from 'react-intersection-observer' 5 | 6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom' 7 | 8 | interface ChatScrollAnchorProps { 9 | trackVisibility?: boolean 10 | } 11 | 12 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) { 13 | const isAtBottom = useAtBottom() 14 | const { ref, entry, inView } = useInView({ 15 | trackVisibility, 16 | delay: 100, 17 | rootMargin: '0px 0px -150px 0px' 18 | }) 19 | 20 | React.useEffect(() => { 21 | if (isAtBottom && trackVisibility && !inView) { 22 | entry?.target.scrollIntoView({ 23 | block: 'start' 24 | }) 25 | } 26 | }, [inView, entry, isAtBottom, trackVisibility]) 27 | 28 | return
29 | } 30 | -------------------------------------------------------------------------------- /components/chat.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useChat, type Message } from 'ai/react' 4 | 5 | import { ChatList } from '@/components/chat-list' 6 | import { ChatPanel } from '@/components/chat-panel' 7 | import { ChatScrollAnchor } from '@/components/chat-scroll-anchor' 8 | import { useToast } from '@/lib/hooks/use-toast' 9 | import { cn } from '@/lib/utils' 10 | import { extractPythonCode, extractSqlCode, snow } from '@/utils/fetchHelpers' 11 | import { _defaultpayload } from '@/utils/initialChat' 12 | import { nanoid } from 'nanoid' 13 | import { useEffect, useState } from 'react' 14 | 15 | export interface ChatProps extends React.ComponentProps<'div'> { 16 | initialMessages?: Message[] 17 | id?: string 18 | } 19 | 20 | export function Chat({ id, initialMessages, className }: ChatProps) { 21 | const { toast } = useToast() 22 | const [pythonCode, setPythonCode] = useState('') 23 | const [sqlCode, setSqlCode] = useState('') 24 | const [isSnowLoading, setIsSnowLoading] = useState(false) 25 | const [shouldExecuteSnow, setShouldExecuteSnow] = useState(false) 26 | const { 27 | messages, 28 | append, 29 | reload, 30 | stop, 31 | isLoading, 32 | input, 33 | setInput, 34 | setMessages 35 | } = useChat({ 36 | api: '/api/sql', 37 | initialMessages, 38 | id, 39 | body: { 40 | id 41 | }, 42 | onResponse(response) { 43 | if (response.status === 401) { 44 | console.log(response) 45 | toast({ 46 | title: 'Unauthorized', 47 | description: 48 | 'You do not have the necessary permissions to access this resource. Please log in and try again.', 49 | variant: 'destructive' 50 | }) 51 | } 52 | }, 53 | onFinish(response) { 54 | let extractedPythonCode = extractPythonCode(response.content) 55 | let extractedSqlCode = extractSqlCode(response.content) 56 | setPythonCode(extractedPythonCode) 57 | setSqlCode(extractedSqlCode) 58 | if (extractedPythonCode || extractedSqlCode) setShouldExecuteSnow(true) 59 | } 60 | }) 61 | 62 | useEffect(() => { 63 | if (shouldExecuteSnow) { 64 | setIsSnowLoading(true) 65 | snow(pythonCode, sqlCode, messages).then(newContent => { 66 | if (typeof newContent === 'string') { 67 | let newMessage: Message = { 68 | id: messages.length ? messages[messages.length - 1].id : nanoid(), 69 | content: newContent, 70 | role: 'assistant' 71 | } 72 | let newMessages = [...messages, newMessage] 73 | setIsSnowLoading(false) 74 | setMessages(newMessages) 75 | } 76 | }) 77 | setShouldExecuteSnow(false) 78 | } 79 | }, [shouldExecuteSnow, pythonCode, sqlCode]) 80 | 81 | return ( 82 | <> 83 |
84 | {messages.length ? ( 85 | <> 86 | 87 | 88 | 89 | ) : ( 90 | 91 | )} 92 |
93 | 103 | 104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /components/clear-history.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useRouter } from 'next/navigation' 4 | import * as React from 'react' 5 | 6 | import { 7 | AlertDialog, 8 | AlertDialogAction, 9 | AlertDialogCancel, 10 | AlertDialogContent, 11 | AlertDialogDescription, 12 | AlertDialogFooter, 13 | AlertDialogHeader, 14 | AlertDialogTitle, 15 | AlertDialogTrigger 16 | } from '@/components/ui/alert-dialog' 17 | import { Button } from '@/components/ui/button' 18 | import { IconSpinner } from '@/components/ui/icons' 19 | import { useToast } from '@/lib/hooks/use-toast' 20 | import { ServerActionResult } from '@/lib/types' 21 | 22 | interface ClearHistoryProps { 23 | clearChats: () => ServerActionResult 24 | } 25 | 26 | export function ClearHistory({ clearChats }: ClearHistoryProps) { 27 | const [open, setOpen] = React.useState(false) 28 | const [isPending, startTransition] = React.useTransition() 29 | const router = useRouter() 30 | const { toast } = useToast() 31 | return ( 32 | 33 | 34 | 38 | 39 | 40 | 41 | Are you absolutely sure? 42 | 43 | This will permanently delete your chat history and remove your data 44 | from our servers. 45 | 46 | 47 | 48 | Cancel 49 | { 52 | event.preventDefault() 53 | startTransition(async () => { 54 | const result = await clearChats() 55 | 56 | if (result && 'error' in result) { 57 | toast({ 58 | title: 'Error', 59 | description: result.error, 60 | variant: 'destructive' 61 | }) 62 | return 63 | } 64 | 65 | setOpen(false) 66 | router.push('/') 67 | }) 68 | }} 69 | > 70 | {isPending && } 71 | Delete 72 | 73 | 74 | 75 | 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /components/external-link.tsx: -------------------------------------------------------------------------------- 1 | export function ExternalLink({ 2 | href, 3 | children 4 | }: { 5 | href: string 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | 14 | {children} 15 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ExternalLink } from '@/components/external-link' 4 | import { cn } from '@/lib/utils' 5 | 6 | export function FooterText({ className, ...props }: React.ComponentProps<'p'>) { 7 | return ( 8 |

15 | AI Driven Insights with Snowflake 16 |

17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /components/header.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import * as React from 'react' 3 | 4 | import { clearChats } from '@/app/actions' 5 | import { ClearHistory } from '@/components/clear-history' 6 | import { Sidebar } from '@/components/sidebar' 7 | import { SidebarFooter } from '@/components/sidebar-footer' 8 | import { SidebarList } from '@/components/sidebar-list' 9 | import { ThemeToggle } from '@/components/theme-toggle' 10 | import { Button, buttonVariants } from '@/components/ui/button' 11 | import { 12 | IconGitHub, 13 | IconSeparator, 14 | IconTwitter, 15 | } from '@/components/ui/icons' 16 | import { UserMenu } from '@/components/user-menu' 17 | import { cn } from '@/lib/utils' 18 | import { SignInButton, currentUser } from '@clerk/nextjs' 19 | import LogoIcon from './ui/LogoIcon' 20 | 21 | export async function Header() { 22 | 23 | const user = await currentUser(); 24 | 25 | const serializableUser = { 26 | id: user?.id, 27 | name: user?.firstName, 28 | email: user?.emailAddresses[0].emailAddress, 29 | avatar_url: user?.imageUrl, 30 | }; 31 | 32 | return ( 33 |
34 |
35 | {user ? ( 36 | 37 | }> 38 | {/* @ts-ignore */} 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ) : ( 47 | 48 | 49 | 50 | )} 51 |
52 | 53 | {user ? ( 54 | 55 | ) : ( 56 | 59 | )} 60 |
61 |
62 |
63 | 64 | s n o w B r a i n 65 | 66 |
67 |
68 |
69 | 75 | 76 | 77 |
78 |
79 | 85 | 86 | 87 |
88 |
89 |
90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /components/login-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { Button, type ButtonProps } from '@/components/ui/button' 6 | import { IconGitHub, IconSpinner } from '@/components/ui/icons' 7 | import { cn } from '@/lib/utils' 8 | import { SignInButton, UserButton, useAuth } from "@clerk/nextjs" 9 | 10 | interface LoginButtonProps extends ButtonProps { 11 | showGithubIcon?: boolean 12 | text?: string 13 | } 14 | 15 | export function LoginButton({ 16 | text = 'Login with GitHub', 17 | showGithubIcon = true, 18 | className, 19 | ...props 20 | }: LoginButtonProps) { 21 | const [isLoading, setIsLoading] = React.useState(false) 22 | const [showSignInButton, setShowSignInButton] = React.useState(false); 23 | 24 | // Create a Supabase client configured to use cookies 25 | return ( 26 | <> 27 | 44 | {showSignInButton && } 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /components/markdown.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from 'react' 2 | import ReactMarkdown, { Options } from 'react-markdown' 3 | 4 | export const MemoizedReactMarkdown: FC = memo( 5 | ReactMarkdown, 6 | (prevProps, nextProps) => 7 | prevProps.children === nextProps.children && 8 | prevProps.className === nextProps.className 9 | ) 10 | -------------------------------------------------------------------------------- /components/prompt-form.tsx: -------------------------------------------------------------------------------- 1 | import { UseChatHelpers } from 'ai/react' 2 | import Link from 'next/link' 3 | import * as React from 'react' 4 | import Textarea from 'react-textarea-autosize' 5 | 6 | import { Button, buttonVariants } from '@/components/ui/button' 7 | import { IconArrowElbow, IconPlus } from '@/components/ui/icons' 8 | import { 9 | Tooltip, 10 | TooltipContent, 11 | TooltipTrigger 12 | } from '@/components/ui/tooltip' 13 | import { useEnterSubmit } from '@/lib/hooks/use-enter-submit' 14 | import { cn } from '@/lib/utils' 15 | 16 | export interface PromptProps 17 | extends Pick { 18 | onSubmit: (value: string) => Promise 19 | isLoading: boolean 20 | id?: string 21 | } 22 | 23 | export function PromptForm({ 24 | onSubmit, 25 | input, 26 | setInput, 27 | isLoading, 28 | id 29 | }: PromptProps) { 30 | const { formRef, onKeyDown } = useEnterSubmit() 31 | const inputRef = React.useRef(null) 32 | 33 | React.useEffect(() => { 34 | if (inputRef.current) { 35 | inputRef.current.focus() 36 | } 37 | }, []) 38 | 39 | return ( 40 |
{ 42 | e.preventDefault() 43 | if (!input?.trim()) { 44 | return 45 | } 46 | setInput('') 47 | await onSubmit(input) 48 | }} 49 | ref={formRef} 50 | > 51 |
52 | 53 | 54 | 61 | 62 | New Chat 63 | 64 | 65 | New Chat 66 | 67 |