├── .env.example ├── .github └── FUNDING.yml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── app ├── action.tsx ├── api │ ├── snow │ │ └── route.ts │ └── snowai │ │ └── route.ts ├── error.tsx ├── globals.css ├── layout.tsx ├── opengraph-image.png └── page.tsx ├── bun.lockb ├── components.json ├── components ├── Charts.tsx ├── chat-list.tsx ├── empty-screen.tsx ├── external-link.tsx ├── footer.tsx ├── header.tsx ├── llm-charts │ ├── AreaChartComponent.tsx │ ├── AreaSkeleton.tsx │ ├── BarChartComponent.tsx │ ├── DonutChartComponent.tsx │ ├── LineChartComponent.tsx │ ├── NumberChartComponent.tsx │ ├── NumberSkeleton.tsx │ ├── ScatterChartComponent.tsx │ ├── TableChartComponent.tsx │ ├── index.tsx │ ├── markdown.tsx │ ├── message.tsx │ └── spinner.tsx ├── providers.tsx ├── schema.tsx └── ui │ ├── LogoIcon.tsx │ ├── button.tsx │ ├── icons.tsx │ ├── input.tsx │ ├── label.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── skeleton.tsx │ ├── sonner.tsx │ ├── table.tsx │ ├── textarea.tsx │ ├── toast.tsx │ └── tooltip.tsx ├── lib ├── context.ts ├── hooks │ ├── chat-scroll-anchor.tsx │ ├── use-at-bottom.tsx │ ├── use-copy-to-clipboard.tsx │ ├── use-enter-submit.tsx │ └── useDownloadChart.ts ├── openai.ts ├── rate-limiter.ts ├── redis.ts ├── snowCache.ts ├── snowflake.ts ├── supabase.ts ├── utils │ ├── ddl.ts │ ├── index.tsx │ └── tool-definition.ts └── validation │ └── index.ts ├── modal ├── main.py └── requirements.txt ├── next-env.d.ts ├── next.config.mjs ├── package.json ├── postcss.config.js ├── public └── favicon.ico ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # ai 2 | OPENAI_API_KEY= 3 | 4 | # snowflake 5 | ACCOUNT= 6 | USER_NAME= 7 | PASSWORD= 8 | ROLE= 9 | DATABASE= 10 | SCHEMA= 11 | WAREHOUSE= 12 | 13 | # supabase - RAG 14 | SUPABASE_URL= 15 | SUPABASE_SERVICE_KEY= 16 | 17 | # upstash - cache 18 | UPSTASH_REDIS_URL= 19 | UPSTASH_REDIS_TOKEN= 20 | 21 | # deployed main.py to modal labs to fetch data from snowflake 22 | MODAL_URL= 23 | AUTH_TOKEN=random 24 | 25 | # cloudflare gateway (optional) 26 | CLOUDFLARE_ACCOUNT_TAG= -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [kaarthik108] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .turbo 4 | *.log 5 | .next 6 | dist 7 | dist-ssr 8 | *.local 9 | .env 10 | .cache 11 | server/dist 12 | public/dist 13 | .turbo 14 | 15 | __pycache__ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.activeBackground": "#8e3418", 4 | "activityBar.background": "#8e3418", 5 | "activityBar.foreground": "#e7e7e7", 6 | "activityBar.inactiveForeground": "#e7e7e799", 7 | "activityBarBadge.background": "#051b0a", 8 | "activityBarBadge.foreground": "#e7e7e7", 9 | "commandCenter.border": "#e7e7e799", 10 | "sash.hoverBorder": "#8e3418", 11 | "statusBar.background": "#632411", 12 | "statusBar.foreground": "#e7e7e7", 13 | "statusBarItem.hoverBackground": "#8e3418", 14 | "statusBarItem.remoteBackground": "#632411", 15 | "statusBarItem.remoteForeground": "#e7e7e7", 16 | "titleBar.activeBackground": "#632411", 17 | "titleBar.activeForeground": "#e7e7e7", 18 | "titleBar.inactiveBackground": "#63241199", 19 | "titleBar.inactiveForeground": "#e7e7e799" 20 | }, 21 | "peacock.color": "#632411", 22 | "typescript.tsdk": "node_modules/typescript/lib" 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

snowBrain Generative UI Demo

3 |
4 | 5 |

6 | SnowBrain is an open-source prototype that serves as your personal data analyst. 7 |

8 | 9 | ## Features 10 | 11 | - [Next.js](https://nextjs.org) App Router + React Server Components 12 | - [Vercel AI SDK 3.0](https://sdk.vercel.ai/docs) for Generative UI 13 | - OpenAI Tools/Function Calling 14 | - [shadcn/ui](https://ui.shadcn.com) 15 | - RAG - Retrieval Augmented Generation [Supabase](https://supabase.com/) 16 | - Charts using [Tremor](https://tremor.so) 17 | - Real time data retrieval using [Snowflake](https://www.snowflake.com/) deployed on [Modal](https://modal.com) 18 | 19 | ## Deploy Your Own 20 | 21 | You can deploy your own version of the demo to Vercel with one click: 22 | 23 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/kaarthik108/snowbrain-AGUI&project-name=snowbrain&repo-name=snowbrain-agui) 24 | 25 | ## Running locally 26 | 27 | You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js AI Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/projects/environment-variables) for this, but a `.env` file is all that is necessary. 28 | 29 | > Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various OpenAI and authentication provider accounts. 30 | 31 | 1. Install Vercel CLI: `npm i -g vercel` 32 | 2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link` 33 | 3. Download your environment variables: `vercel env pull` 34 | 4. Deploy the modal code on modal/main.py using the following command: `modal deploy` 35 | 36 | ```bash 37 | bun install 38 | bun run dev 39 | ``` 40 | 41 | Your app should now be running on [localhost:3000](http://localhost:3000/). 42 | 43 | ## Example Queries 44 | 45 | snowBrain is designed to make complex data querying simple. Here are some example queries you can try: 46 | 47 | - **Total revenue per product category**: "Show me the total revenue for each product category." 48 | - **Top customers by sales**: "Who are the top 10 customers by sales?" 49 | - **Average order value per region**: "What is the average order value for each region?" 50 | - **Order volume**: "How many orders were placed last week?" 51 | - **Product price listing**: "Display the list of products with their prices." 52 | -------------------------------------------------------------------------------- /app/action.tsx: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { createAI, createStreamableUI, getMutableAIState } from "ai/rsc"; 4 | import OpenAI from "openai"; 5 | 6 | import { 7 | BotCard, 8 | BotMessage, 9 | Chart, 10 | SystemMessage, 11 | spinner, 12 | } from "@/components/llm-charts"; 13 | 14 | import { getContext } from "@/lib/context"; 15 | import { runOpenAICompletion } from "@/lib/utils"; 16 | import { FQueryResponse } from "@/lib/validation"; 17 | import { Code } from "bright"; 18 | import { z } from "zod"; 19 | 20 | import AreaSkeleton from "@/components/llm-charts/AreaSkeleton"; 21 | import { MemoizedReactMarkdown } from "@/components/llm-charts/markdown"; 22 | import { executeQueryWithCache } from "@/lib/snowCache"; 23 | import remarkGfm from "remark-gfm"; 24 | import remarkMath from "remark-math"; 25 | import { format as sql_format } from "sql-formatter"; 26 | 27 | if (!process.env.OPENAI_API_KEY) { 28 | throw new Error("Please set the OPENAI_API_KEY environment variable"); 29 | } 30 | 31 | const openai = new OpenAI({ 32 | apiKey: process.env.OPENAI_API_KEY, 33 | baseURL: `https://gateway.ai.cloudflare.com/v1/${process.env.CLOUDFLARE_ACCOUNT_TAG}/snowbrain/openai`, 34 | }); 35 | type OpenAIQueryResponse = z.infer; 36 | 37 | export interface QueryResult { 38 | columns: string[]; 39 | data: Array<{ [key: string]: any }>; 40 | } 41 | 42 | async function submitUserMessage(content: string) { 43 | "use server"; 44 | const getDDL = await getContext(content); 45 | const aiState = getMutableAIState(); 46 | aiState.update([ 47 | ...aiState.get(), 48 | { 49 | role: "user", 50 | content, 51 | }, 52 | ]); 53 | 54 | const reply = createStreamableUI( 55 | {spinner} 56 | ); 57 | const completion = runOpenAICompletion(openai, { 58 | model: "gpt-4o-mini", 59 | stream: true, 60 | messages: [ 61 | { 62 | role: "system", 63 | content: `\ 64 | You are a snowflake data analytics assistant. You can help users with sql queries and you can help users query their data with only using snowflake sql syntax. Based on the context provided about snowflake DDL schema details, you can help users with their queries. 65 | You and the user can discuss their events and the user can request to create new queries or refine existing ones, in the UI. 66 | 67 | Always use proper aliases for the columns and tables in the queries. For example, instead of using "select * from table_name", use "select column_name as alias_name from table_name as alias_name". 68 | 69 | Messages inside [] means that it's a UI element or a user event. For example: 70 | - "[Results for query: query with format: format and title: title and description: description. with data" means that a chart/table/number card is shown to that user. 71 | 72 | Context: (DDL schema details) \n 73 | 74 | ${getDDL} 75 | 76 | \n 77 | 78 | If the user requests to fetch or query data, call \`query_data\` to query the data from the snowflake database and return the results. 79 | 80 | Besides that, you can also chat with users and do some calculations if needed.`, 81 | }, 82 | ...aiState.get().map((info: any) => ({ 83 | role: info.role, 84 | content: info.content, 85 | name: info.name, 86 | })), 87 | ], 88 | functions: [ 89 | { 90 | name: "query_data", 91 | description: 92 | "Query the data from the snowflake database and return the results.", 93 | parameters: FQueryResponse, 94 | }, 95 | ], 96 | temperature: 0, 97 | }); 98 | 99 | completion.onTextContent((content: string, isFinal: boolean) => { 100 | reply.update( 101 | 102 | {children}

; 108 | }, 109 | }} 110 | > 111 | {content} 112 |
113 |
114 | ); 115 | if (isFinal) { 116 | reply.done(); 117 | aiState.done([...aiState.get(), { role: "assistant", content }]); 118 | } 119 | }); 120 | 121 | completion.onFunctionCall( 122 | "query_data", 123 | async (input: OpenAIQueryResponse) => { 124 | reply.update( 125 | 126 | 127 | 128 | 129 | 130 | ); 131 | const { format, title, timeField, categories, index, yaxis, size } = 132 | input; 133 | // console.log("Received timeField:", timeField); 134 | // console.log("Received format:", format); 135 | // console.log("Received title:", title); 136 | // console.log("Received categories:", categories); 137 | // console.log("Received index:", index); 138 | // console.log("Received yaxis:", yaxis); 139 | // console.log("Received size:", size); 140 | let query = input.query; 141 | 142 | const format_query = sql_format(query, { language: "sql" }); 143 | 144 | const res = await executeQueryWithCache(format_query); 145 | // console.log("Query results:", res); 146 | // const res = testquery; 147 | const compatibleQueryResult: QueryResult = { 148 | columns: res.columns, 149 | data: res.data, 150 | }; 151 | 152 | reply.done( 153 | 154 | 155 |
156 | 166 |
167 | {format_query} 168 |
169 |
170 |
171 |
172 | ); 173 | 174 | aiState.done([ 175 | ...aiState.get(), 176 | { 177 | role: "function", 178 | name: "query_data", 179 | content: `[Snowflake query results for code: ${query} and chart format: ${format} with categories: ${categories} and data ${res.columns} ${res.data}]`, 180 | }, 181 | ]); 182 | } 183 | ); 184 | 185 | return { 186 | id: Date.now(), 187 | display: reply.value, 188 | }; 189 | } 190 | 191 | const initialAIState: { 192 | role: "user" | "assistant" | "system" | "function"; 193 | content: string; 194 | id?: string; 195 | name?: string; 196 | }[] = []; 197 | 198 | const initialUIState: { 199 | id: number; 200 | display: React.ReactNode; 201 | }[] = []; 202 | 203 | export const AI = createAI({ 204 | actions: { 205 | submitUserMessage, 206 | }, 207 | initialUIState, 208 | initialAIState, 209 | }); 210 | 211 | export const testquery = { 212 | columns: ["ORDER_ID", "CUSTOMER_ID", "ORDER_DATE", "TOTAL_AMOUNT"], 213 | data: [ 214 | { 215 | ORDER_ID: 1, 216 | CUSTOMER_ID: 1, 217 | ORDER_DATE: "2023-04-01", 218 | TOTAL_AMOUNT: 120.99, 219 | }, 220 | { 221 | ORDER_ID: 2, 222 | CUSTOMER_ID: 2, 223 | ORDER_DATE: "2023-04-02", 224 | TOTAL_AMOUNT: 75.5, 225 | }, 226 | { 227 | ORDER_ID: 3, 228 | CUSTOMER_ID: 3, 229 | ORDER_DATE: "2023-04-03", 230 | TOTAL_AMOUNT: 140.25, 231 | }, 232 | { 233 | ORDER_ID: 4, 234 | CUSTOMER_ID: 4, 235 | ORDER_DATE: "2023-04-04", 236 | TOTAL_AMOUNT: 89.99, 237 | }, 238 | { 239 | ORDER_ID: 5, 240 | CUSTOMER_ID: 5, 241 | ORDER_DATE: "2023-04-05", 242 | TOTAL_AMOUNT: 210.45, 243 | }, 244 | { 245 | ORDER_ID: 6, 246 | CUSTOMER_ID: 6, 247 | ORDER_DATE: "2023-04-06", 248 | TOTAL_AMOUNT: 55, 249 | }, 250 | { 251 | ORDER_ID: 7, 252 | CUSTOMER_ID: 7, 253 | ORDER_DATE: "2023-04-07", 254 | TOTAL_AMOUNT: 123.75, 255 | }, 256 | { 257 | ORDER_ID: 8, 258 | CUSTOMER_ID: 8, 259 | ORDER_DATE: "2023-04-08", 260 | TOTAL_AMOUNT: 79.3, 261 | }, 262 | { 263 | ORDER_ID: 9, 264 | CUSTOMER_ID: 9, 265 | ORDER_DATE: "2023-04-09", 266 | TOTAL_AMOUNT: 45.9, 267 | }, 268 | { 269 | ORDER_ID: 10, 270 | CUSTOMER_ID: 10, 271 | ORDER_DATE: "2023-04-10", 272 | TOTAL_AMOUNT: 99.99, 273 | }, 274 | ], 275 | }; 276 | -------------------------------------------------------------------------------- /app/api/snow/route.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | import * as snowflake from "snowflake-sdk"; 4 | 5 | dotenv.config({ path: ".env.local" }); 6 | 7 | export const runtime = "nodejs"; 8 | 9 | export async function POST(request: NextRequest): Promise { 10 | const apiKey = request.headers.get("x-api-key"); 11 | if (!apiKey || apiKey !== process.env.X_API_KEY) { 12 | return new NextResponse("Unauthorized", { status: 401 }); 13 | } 14 | const snowConnect = snowflake.createConnection({ 15 | account: process.env.ACCOUNT as string, 16 | username: process.env.USER_NAME as string, 17 | password: process.env.PASSWORD, 18 | role: process.env.ROLE, 19 | warehouse: process.env.WAREHOUSE, 20 | database: process.env.DATABASE, 21 | schema: process.env.SCHEMA, 22 | }); 23 | 24 | snowflake.configure({ ocspFailOpen: false }); 25 | 26 | const requestBody = await request.json(); 27 | const query = requestBody.query; 28 | 29 | try { 30 | const result = await new Promise((resolve, reject) => { 31 | snowConnect.connect((err, conn) => { 32 | if (err) { 33 | console.error("Unable to connect: " + err.message); 34 | reject(err); 35 | } else { 36 | snowConnect.execute({ 37 | sqlText: query, 38 | complete: (err, stmt, rows) => { 39 | if (err) { 40 | console.error( 41 | "Failed to execute statement due to the following error: " + 42 | err.message 43 | ); 44 | reject(err); 45 | } else { 46 | resolve(rows || []); 47 | } 48 | }, 49 | }); 50 | } 51 | }); 52 | }); 53 | 54 | const columns = result.length > 0 ? Object.keys(result[0]) : []; 55 | const formattedResult = { 56 | columns, 57 | data: result, 58 | }; 59 | 60 | return NextResponse.json(formattedResult); 61 | } catch (error) { 62 | return NextResponse.json({ error: error }, { status: 500 }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/api/snowai/route.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | import * as snowflake from "snowflake-sdk"; 4 | 5 | dotenv.config({ path: ".env.local" }); 6 | 7 | export const runtime = "nodejs"; 8 | 9 | export async function POST(request: NextRequest): Promise { 10 | const apiKey = request.headers.get("x-api-key"); 11 | if (!apiKey || apiKey !== process.env.X_API_KEY) { 12 | return new NextResponse("Unauthorized", { status: 401 }); 13 | } 14 | const snowConnect = snowflake.createConnection({ 15 | account: process.env.ACCOUNT as string, 16 | username: process.env.USER_NAME as string, 17 | password: process.env.PASSWORD, 18 | role: process.env.ROLE, 19 | warehouse: process.env.WAREHOUSE, 20 | database: process.env.DATABASE, 21 | schema: process.env.SCHEMA, 22 | }); 23 | 24 | snowflake.configure({ ocspFailOpen: false }); 25 | 26 | const requestBody = await request.json(); 27 | const query = requestBody.query; 28 | 29 | try { 30 | const result = await new Promise((resolve, reject) => { 31 | snowConnect.connect((err, conn) => { 32 | if (err) { 33 | console.error("Unable to connect: " + err.message); 34 | reject(err); 35 | } else { 36 | snowConnect.execute({ 37 | sqlText: query, 38 | complete: (err, stmt, rows) => { 39 | if (err) { 40 | console.error( 41 | "Failed to execute statement due to the following error: " + 42 | err.message 43 | ); 44 | reject(err); 45 | } else { 46 | resolve(rows || []); 47 | } 48 | }, 49 | }); 50 | } 51 | }); 52 | }); 53 | 54 | return new Response(JSON.stringify(result), { 55 | status: 200, 56 | headers: { "Content-Type": "application/json" }, 57 | }); 58 | } catch (error) { 59 | return new Response(JSON.stringify({ error: error }), { 60 | status: 500, 61 | headers: { "Content-Type": "application/json" }, 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export default function Error({ 4 | error, 5 | reset, 6 | }: { 7 | error: Error & { digest?: string }; 8 | reset: () => void; 9 | }) { 10 | return ( 11 |
12 |

Something went wrong!

13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /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 | --muted: 240 4.8% 95.9%; 10 | --muted-foreground: 240 3.8% 46.1%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 240 10% 3.9%; 15 | --border: 240 5.9% 90%; 16 | --input: 240 5.9% 90%; 17 | --primary: 240 5.9% 10%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | --accent: 240 4.8% 95.9%; 22 | --accent-foreground: ; 23 | --destructive: 0 84.2% 60.2%; 24 | --destructive-foreground: 0 0% 98%; 25 | --ring: 240 5% 64.9%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 240 10% 3.9%; 31 | --foreground: 0 0% 98%; 32 | --muted: 240 3.7% 15.9%; 33 | --muted-foreground: 240 5% 64.9%; 34 | --popover: 240 10% 3.9%; 35 | --popover-foreground: 0 0% 98%; 36 | --card: 240 10% 3.9%; 37 | --card-foreground: 0 0% 98%; 38 | --border: 240 3.7% 15.9%; 39 | --input: 240 3.7% 15.9%; 40 | --primary: 0 0% 98%; 41 | --primary-foreground: 240 5.9% 10%; 42 | --secondary: 240 3.7% 15.9%; 43 | --secondary-foreground: 0 0% 98%; 44 | --accent: 240 3.7% 15.9%; 45 | --accent-foreground: ; 46 | --destructive: 0 62.8% 30.6%; 47 | --destructive-foreground: 0 85.7% 97.3%; 48 | --ring: 240 3.7% 15.9%; 49 | } 50 | } 51 | 52 | @layer base { 53 | * { 54 | @apply border-border; 55 | } 56 | body { 57 | @apply bg-background text-foreground; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from "@/components/ui/sonner"; 2 | import { Analytics } from "@vercel/analytics/react"; 3 | import { GeistMono } from "geist/font/mono"; 4 | import { GeistSans } from "geist/font/sans"; 5 | import type { Metadata } from "next"; 6 | import "./globals.css"; 7 | 8 | import { Header } from "@/components/header"; 9 | import { Providers } from "@/components/providers"; 10 | import { AI } from "./action"; 11 | 12 | const meta = { 13 | title: "snowBrain - AI RSC Demo", 14 | description: "snowBrain - AI Driven snowflake data insights", 15 | }; 16 | export const metadata: Metadata = { 17 | metadataBase: new URL("https://snowbrain.dev"), 18 | ...meta, 19 | title: { 20 | default: "snowBrain - AI", 21 | template: `%s - AI`, 22 | }, 23 | icons: { 24 | icon: "/favicon.ico", 25 | }, 26 | twitter: { 27 | ...meta, 28 | card: "summary_large_image", 29 | site: "@kaarthikcodes", 30 | images: "./opengraph-image.png", 31 | }, 32 | openGraph: { 33 | ...meta, 34 | images: "./opengraph-image.png", 35 | locale: "en-US", 36 | type: "website", 37 | }, 38 | }; 39 | 40 | export const viewport = { 41 | themeColor: [ 42 | { media: "(prefers-color-scheme: light)", color: "white" }, 43 | { media: "(prefers-color-scheme: dark)", color: "black" }, 44 | ], 45 | }; 46 | 47 | export const runtime = "edge"; 48 | 49 | export default function RootLayout({ 50 | children, 51 | }: Readonly<{ 52 | children: React.ReactNode; 53 | }>) { 54 | return ( 55 | 56 | 59 | 60 | 61 | 67 |
68 |
69 |
70 | {children} 71 |
72 |
73 |
74 |
75 | 76 | 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaarthik108/snowbrain-AGUI/7aaa925816493d2bd017fb75f48b1be5daf40894/app/opengraph-image.png -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useRef, useState } from "react"; 4 | 5 | import { UserMessage } from "@/components/llm-charts/message"; 6 | import { useActions, useUIState } from "ai/rsc"; 7 | 8 | import { ChatList } from "@/components/chat-list"; 9 | import { EmptyScreen } from "@/components/empty-screen"; 10 | import { FooterText } from "@/components/footer"; 11 | import { Button } from "@/components/ui/button"; 12 | import { IconArrowElbow } from "@/components/ui/icons"; 13 | import { ChatScrollAnchor } from "@/lib/hooks/chat-scroll-anchor"; 14 | import { useEnterSubmit } from "@/lib/hooks/use-enter-submit"; 15 | import Textarea from "react-textarea-autosize"; 16 | import { toast } from "sonner"; 17 | import { type AI } from "./action"; 18 | 19 | export default function Page() { 20 | const [messages, setMessages] = useUIState(); 21 | const { submitUserMessage } = useActions(); 22 | const [inputValue, setInputValue] = useState(""); 23 | const { formRef, onKeyDown } = useEnterSubmit(); 24 | const inputRef = useRef(null); 25 | 26 | useEffect(() => { 27 | const handleKeyDown = (e: KeyboardEvent) => { 28 | if (e.key === "/") { 29 | if ( 30 | e.target && 31 | ["INPUT", "TEXTAREA"].includes((e.target as any).nodeName) 32 | ) { 33 | return; 34 | } 35 | e.preventDefault(); 36 | e.stopPropagation(); 37 | if (inputRef?.current) { 38 | inputRef.current.focus(); 39 | } 40 | } 41 | }; 42 | 43 | document.addEventListener("keydown", handleKeyDown); 44 | 45 | return () => { 46 | document.removeEventListener("keydown", handleKeyDown); 47 | }; 48 | }, [inputRef]); 49 | 50 | return ( 51 |
52 |
53 | {messages.length ? ( 54 | <> 55 | 56 | 57 | ) : ( 58 | { 60 | // Add user message UI 61 | setMessages((currentMessages) => [ 62 | ...currentMessages, 63 | { 64 | id: Date.now(), 65 | display: {message}, 66 | }, 67 | ]); 68 | 69 | // Submit and get response message 70 | const responseMessage = await submitUserMessage(message); 71 | setMessages((currentMessages) => [ 72 | ...currentMessages, 73 | responseMessage, 74 | ]); 75 | }} 76 | /> 77 | )} 78 | 79 |
80 |
81 |
82 |
83 |
{ 86 | e.preventDefault(); 87 | 88 | // Blur focus on mobile 89 | if (window.innerWidth < 600) { 90 | e.target["message"]?.blur(); 91 | } 92 | 93 | const value = inputValue.trim(); 94 | setInputValue(""); 95 | if (!value) return; 96 | 97 | // Add user message UI 98 | setMessages((currentMessages) => [ 99 | ...currentMessages, 100 | { 101 | id: Date.now(), 102 | display: {value}, 103 | }, 104 | ]); 105 | 106 | try { 107 | // Submit and get response message 108 | const responseMessage = await submitUserMessage(value); 109 | setMessages((currentMessages) => [ 110 | ...currentMessages, 111 | responseMessage, 112 | ]); 113 | } catch (error) { 114 | toast("Something went wrong", { 115 | description: "Please try again later", 116 | duration: 5000, 117 | }); 118 | } 119 | }} 120 | > 121 |
122 |