├── .eslintrc.json ├── .gitignore ├── .vscode └── settings.json ├── Embed ├── db.sql ├── entry.py ├── keys_to_delete.json └── yc_data.csv ├── README.md ├── _routes.json ├── app ├── about │ └── page.tsx ├── action.tsx ├── actions.ts ├── ai.ts ├── api │ ├── chat │ │ └── route.ts │ └── embed │ │ └── route.ts ├── bindings.ts ├── chat │ └── [id] │ │ └── page.tsx ├── favicon.ico ├── globals.css ├── layout.tsx ├── not-found.tsx ├── opengraph-image.tsx ├── page.tsx └── share │ └── chat │ └── [id] │ ├── opengraph-image.tsx │ └── page.tsx ├── components.json ├── components ├── ChartWrapper.tsx ├── Charts.tsx ├── ChatBar.tsx ├── ChatMessages.tsx ├── Footer.tsx ├── InitialMessages.tsx ├── Navbar.tsx ├── llm-charts │ ├── AreaChartComponent.tsx │ ├── AreaSkeleton.tsx │ ├── BarChartComponent.tsx │ ├── BarListChartComponent.tsx │ ├── DonutChartComponent.tsx │ ├── LineChartComponent.tsx │ ├── NumberChartComponent.tsx │ ├── NumberSkeleton.tsx │ ├── ProgressChartComponent.tsx │ ├── ScatterChartComponent.tsx │ ├── TableChartComponent.tsx │ ├── index.tsx │ └── markdown.tsx ├── message.tsx └── ui │ ├── button.tsx │ ├── card.tsx │ ├── icons.tsx │ ├── input.tsx │ ├── skeleton.tsx │ ├── sonner.tsx │ ├── spinner.tsx │ └── textarea.tsx ├── db.sql ├── documents └── schema.txt ├── env.d.ts ├── lib ├── assets │ ├── logo1.png │ ├── logobg.png │ └── logocf.png ├── context.ts ├── hooks │ ├── use-enter-submit.tsx │ ├── use-local-storage.ts │ └── use-streamable-text.ts ├── prompt.ts ├── tool-definition.ts ├── types.ts ├── utils.ts └── validation │ └── index.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── 11053969_x_logo_twitter_new_brand_icon.svg ├── logo1.png ├── logobg.png ├── next.svg ├── twitter-x-seeklogo-3.svg ├── vercel.svg └── x.svg ├── schema.sql ├── tailwind.config.ts ├── tsconfig.json └── wrangler.toml /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "plugin:eslint-plugin-next-on-pages/recommended" 5 | ], 6 | "plugins": [ 7 | "eslint-plugin-next-on-pages" 8 | ] 9 | } -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # wrangler files 39 | .wrangler 40 | .dev.vars 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.activeBackground": "#15c22a", 4 | "activityBar.background": "#15c22a", 5 | "activityBar.foreground": "#e7e7e7", 6 | "activityBar.inactiveForeground": "#e7e7e799", 7 | "activityBarBadge.background": "#6552ec", 8 | "activityBarBadge.foreground": "#e7e7e7", 9 | "commandCenter.border": "#e7e7e799", 10 | "sash.hoverBorder": "#15c22a", 11 | "statusBar.background": "#109420", 12 | "statusBar.foreground": "#e7e7e7", 13 | "statusBarItem.hoverBackground": "#15c22a", 14 | "statusBarItem.remoteBackground": "#109420", 15 | "statusBarItem.remoteForeground": "#e7e7e7", 16 | "titleBar.activeBackground": "#109420", 17 | "titleBar.activeForeground": "#e7e7e7", 18 | "titleBar.inactiveBackground": "#10942099", 19 | "titleBar.inactiveForeground": "#e7e7e799" 20 | }, 21 | "peacock.color": "#109420" 22 | } -------------------------------------------------------------------------------- /Embed/entry.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from io import StringIO 3 | from js import Response 4 | 5 | MAX_RECORDS = 2000 # Constant to control the number of records to process 6 | 7 | async def on_fetch(request, env): 8 | if request.method == "POST": 9 | csv_data = await request.text() 10 | if csv_data: 11 | reader = csv.DictReader(StringIO(csv_data)) 12 | 13 | # Prepare the INSERT statement 14 | insert_stmt = env.DB.prepare(""" 15 | INSERT INTO companies ( 16 | company_id, company_name, short_description, long_description, 17 | batch, status, tags, location, country, year_founded, 18 | num_founders, founders_names, team_size, website, cb_url, linkedin_url 19 | ) 20 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 21 | """) 22 | 23 | # Process the CSV data row by row 24 | count = 0 25 | for row in reader: 26 | if count >= MAX_RECORDS: 27 | break 28 | 29 | await insert_stmt.bind( 30 | row["company_id"], row["company_name"], row["short_description"], row["long_description"], 31 | row["batch"], row["status"], row["tags"], row["location"], row["country"], row["year_founded"], 32 | row["num_founders"], row["founders_names"], row["team_size"], row["website"], row["cb_url"], row["linkedin_url"] 33 | ).run() 34 | 35 | count += 1 36 | 37 | return Response.new(f"CSV data imported successfully. Processed {count} records.") 38 | else: 39 | return Response.new("No CSV data found", status=400) 40 | else: 41 | return Response.new("Invalid request method", status=405) -------------------------------------------------------------------------------- /Embed/keys_to_delete.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`c3`](https://developers.cloudflare.com/pages/get-started/c3). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | ## Cloudflare integration 20 | 21 | Besides the `dev` script mentioned above `c3` has added a few extra scripts that allow you to integrate the application with the [Cloudflare Pages](https://pages.cloudflare.com/) environment, these are: 22 | - `pages:build` to build the application for Pages using the [`@cloudflare/next-on-pages`](https://github.com/cloudflare/next-on-pages) CLI 23 | - `preview` to locally preview your Pages application using the [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI 24 | - `deploy` to deploy your Pages application using the [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI 25 | 26 | > __Note:__ while the `dev` script is optimal for local development you should preview your Pages application as well (periodically or before deployments) in order to make sure that it can properly work in the Pages environment (for more details see the [`@cloudflare/next-on-pages` recommended workflow](https://github.com/cloudflare/next-on-pages/blob/05b6256/internal-packages/next-dev/README.md#recommended-workflow)) 27 | 28 | ### Bindings 29 | 30 | Cloudflare [Bindings](https://developers.cloudflare.com/pages/functions/bindings/) are what allows you to interact with resources available in the Cloudflare Platform. 31 | 32 | You can use bindings during development, when previewing locally your application and of course in the deployed application: 33 | 34 | - To use bindings in dev mode you need to define them in the `next.config.js` file under `setupDevBindings`, this mode uses the `next-dev` `@cloudflare/next-on-pages` submodule. For more details see its [documentation](https://github.com/cloudflare/next-on-pages/blob/05b6256/internal-packages/next-dev/README.md). 35 | 36 | - To use bindings in the preview mode you need to add them to the `pages:preview` script accordingly to the `wrangler pages dev` command. For more details see its [documentation](https://developers.cloudflare.com/workers/wrangler/commands/#dev-1) or the [Pages Bindings documentation](https://developers.cloudflare.com/pages/functions/bindings/). 37 | 38 | - To use bindings in the deployed application you will need to configure them in the Cloudflare [dashboard](https://dash.cloudflare.com/). For more details see the [Pages Bindings documentation](https://developers.cloudflare.com/pages/functions/bindings/). 39 | 40 | #### KV Example 41 | 42 | `c3` has added for you an example showing how you can use a KV binding. 43 | 44 | In order to enable the example: 45 | - Search for javascript/typescript lines containing the following comment: 46 | ```ts 47 | // KV Example: 48 | ``` 49 | and uncomment the commented lines below it. 50 | - Do the same in the `wrangler.toml` file, where 51 | the comment is: 52 | ``` 53 | # KV Example: 54 | ``` 55 | - If you're using TypeScript run the `build-cf-types` script to update the `env.d.ts` file: 56 | ```bash 57 | npm run build-cf-types 58 | # or 59 | yarn build-cf-types 60 | # or 61 | pnpm build-cf-types 62 | # or 63 | bun build-cf-types 64 | ``` 65 | 66 | After doing this you can run the `dev` or `preview` script and visit the `/api/hello` route to see the example in action. 67 | 68 | Finally, if you also want to see the example work in the deployed application make sure to add a `MY_KV_NAMESPACE` binding to your Pages application in its [dashboard kv bindings settings section](https://dash.cloudflare.com/?to=/:account/pages/view/:pages-project/settings/functions#kv_namespace_bindings_section). After having configured it make sure to re-deploy your application. 69 | -------------------------------------------------------------------------------- /_routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "exclude": ["/api/embed/route.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /app/about/page.tsx: -------------------------------------------------------------------------------- 1 | export const runtime = "edge"; 2 | 3 | export default function AboutPage() { 4 | return ( 5 |
6 |
7 |

8 | Discover Di1 9 |

10 | 11 |
12 |
13 |

14 | Di1 15 | an AI-powered T2SQL Chatbot 16 | for Cloudflare D1.{`It's`} like having a{" "} 17 | conversation with your database! 18 |

19 |

20 | {`I've`} made Di1{" "} 21 | 25 | open-source 26 | 27 | , so feel free to tinker with it and make it even better. 28 |

29 |
30 | 31 |
32 |
33 |

34 | The Y Combinator Dataset 35 |

36 |

37 | Imagine having access to a{" "} 38 | treasure trove of startup 39 | data. {`That's`} exactly what the{" "} 40 | Y Combinator dataset offers. 41 | {`It's`} like a secret playbook{" "} 42 | for entrepreneurs and investors alike. 43 |

44 |

45 | This dataset is a snapshot of the Y Combinator directory as of{" "} 46 | July 13th, 2023. 47 | It contains detailed information about the companies that have 48 | been part of Y {`Combinator's`} accelerator program. 49 |

50 |

51 | From company names and descriptions to industry tags, locations, 52 | founding years, and team sizes, this dataset provides a{" "} 53 | comprehensive look at the 54 | startups that have been shaped by Y {`Combinator's`} expertise. 55 |

56 |

57 | Y Combinator has an impressive track record, with over{" "} 58 | 4,000 companies{" "} 59 | under its wing, collectively valued at a staggering{" "} 60 | $600 billion. 61 | {`That's`} a lot of zeros and a testament to the impact of this 62 | accelerator program. 63 |

64 |
65 | 66 |
67 |

68 | About Me 69 |

70 |

71 | Hi there! {`I'm`}{" "} 72 | 78 | Kaarthik 79 | 80 | , the mastermind behind Di1. I have a knack for building{" "} 81 | innovative projects that push the 82 | boundaries of {`what's`} possible. 83 |

84 |

85 | When {`I'm`} not immersed in code, you can find me sharing my 86 | thoughts and discoveries on{" "} 87 | 93 | X 94 | {" "} 95 | and showcasing my projects on{" "} 96 | 102 | GitHub 103 | 104 | / 105 | 111 | Linkedin 112 | 113 | . Feel free to connect with me and explore the{" "} 114 | 115 | exciting world of technology 116 | {" "} 117 | together! 118 |

119 |
120 |
121 |
122 |
123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /app/action.tsx: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { 4 | createAI, 5 | createStreamableUI, 6 | createStreamableValue, 7 | getAIState, 8 | getMutableAIState, 9 | } from "ai/rsc"; 10 | 11 | import { ChartWrapper } from "@/components/ChartWrapper"; 12 | import { BotCard, BotMessage, UserMessage } from "@/components/message"; 13 | import { spinner } from "@/components/ui/spinner"; 14 | import { getContext } from "@/lib/context"; 15 | import { nanoid } from "@/lib/utils"; 16 | import { querySchema } from "@/lib/validation"; 17 | import { OpenAI } from "@ai-sdk/openai"; 18 | import { Message, streamText } from "ai"; 19 | import { z } from "zod"; 20 | import { Chat, saveChat } from "./actions"; 21 | 22 | type WokersAIQueryResponse = z.infer; 23 | function isMessage(obj: any): obj is Message { 24 | return ( 25 | obj && 26 | typeof obj.id === "string" && 27 | (obj.role === "user" || obj.role === "assistant") && 28 | typeof obj.content === "string" 29 | ); 30 | } 31 | export interface QueryResult { 32 | columns: string[]; 33 | data: Array<{ [key: string]: any }>; 34 | } 35 | interface _Message { 36 | id: string; 37 | role: "user" | "assistant"; 38 | content: string; 39 | } 40 | const openai = new OpenAI({ 41 | baseUrl: `https://gateway.ai.cloudflare.com/v1/${process.env.CLOUDFLARE_ACCOUNT_ID}/cfd/openai`, 42 | apiKey: process.env.OPENAI_API_KEY, 43 | }); 44 | 45 | async function submitUserMessage(content: string) { 46 | "use server"; 47 | const aiState = getMutableAIState(); 48 | 49 | aiState.update({ 50 | ...aiState.get(), 51 | messages: [ 52 | ...aiState.get().messages, 53 | { 54 | id: nanoid(), 55 | role: "user", 56 | content: `${aiState.get().interactions.join("\n\n")}\n\n${content}`, 57 | }, 58 | ], 59 | }); 60 | 61 | const history = aiState.get().messages.map((message: any) => ({ 62 | role: message.role, 63 | content: message.content, 64 | })); 65 | 66 | const textStream = createStreamableValue(""); 67 | const spinnerStream = createStreamableUI({spinner}); 68 | // const skeletonStream = createStreamableUI(); 69 | const messageStream = createStreamableUI(null); 70 | const uiStream = createStreamableUI(); 71 | 72 | const getDDL = await getContext(content); 73 | 74 | (async () => { 75 | try { 76 | const result = await streamText({ 77 | model: openai.chat("gpt-4-turbo"), 78 | temperature: 0, 79 | tools: { 80 | query_data: { 81 | description: 82 | "Query the data from the sqlite database and return the results.", 83 | parameters: querySchema, 84 | }, 85 | }, 86 | system: ` 87 | You are a Sqlite data analytics assistant. You can help users with sql queries and you can help users query their data with only using Sqlite sql syntax. Based on the context provided about Sqlite DDL schema details, you can help users with their queries. 88 | You and the user can discuss their events and the user can request to create new queries or refine existing ones, in the UI. 89 | 90 | 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". 91 | 92 | Messages inside [] means that it's a UI element or a user event. For example: 93 | - "[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. 94 | 95 | Context: (DDL schema details) \n 96 | 97 | ${getDDL} 98 | 99 | \n 100 | 101 | If the user requests to fetch or query data, call \`query_data\` to query the data from the Sqlite database and return the results. 102 | 103 | Besides that, you can also chat with users and do some calculations if needed. 104 | `, 105 | messages: [...history], 106 | }); 107 | 108 | let textContent = ""; 109 | spinnerStream.done(null); 110 | 111 | for await (const delta of result.fullStream) { 112 | const { type } = delta; 113 | 114 | if (type === "text-delta") { 115 | const { textDelta } = delta; 116 | 117 | textContent += textDelta; 118 | 119 | messageStream.update(); 120 | 121 | // aiState.update({ 122 | // ...aiState.get(), 123 | // messages: [ 124 | // ...aiState.get().messages, 125 | // { 126 | // id: "1", 127 | // role: "assistant", 128 | // content: textContent, 129 | // }, 130 | // ], 131 | // }); 132 | 133 | aiState.done({ 134 | ...aiState.get(), 135 | interactions: [], 136 | messages: [ 137 | ...aiState.get().messages, 138 | { 139 | id: nanoid(), 140 | role: "assistant", 141 | content: textContent, 142 | }, 143 | ], 144 | }); 145 | } else if (type === "tool-call") { 146 | const { toolName, args } = delta; 147 | 148 | if (toolName === "query_data") { 149 | const { 150 | format, 151 | title, 152 | timeField, 153 | categories, 154 | index, 155 | yaxis, 156 | size, 157 | query, 158 | } = args; 159 | console.log("args", args); 160 | 161 | uiStream.update( 162 | 163 | 164 | 165 | ); 166 | 167 | aiState.done({ 168 | ...aiState.get(), 169 | interactions: [], 170 | messages: [ 171 | ...aiState.get().messages, 172 | { 173 | id: nanoid(), 174 | role: "assistant", 175 | content: `[Sqlite query results for code: ${query} and chart format: ${format} with categories: ${categories} and data index: ${index} and yaxis: ${yaxis} and size: ${size}]`, 176 | display: { 177 | name: "query_data", 178 | props: args, 179 | }, 180 | }, 181 | ], 182 | }); 183 | } 184 | } 185 | } 186 | const ms = aiState.get().messages; 187 | const id = aiState.get().chatId; 188 | // @ts-ignore 189 | const latestMessages: _Message[] = Array.from( 190 | new Map( 191 | ms.filter(isMessage).map((msg: Message) => [msg.role, msg]) 192 | ).values() 193 | ); 194 | 195 | let title = ""; 196 | if (ms.length > 0 && ms[0].content) { 197 | title = ms[0].content.substring(0, 100); 198 | } 199 | 200 | await saveChat({ 201 | id, 202 | title, 203 | messages: latestMessages, 204 | path: `/chat/${id}`, 205 | userId: "test", 206 | }); 207 | 208 | uiStream.done(); 209 | textStream.done(); 210 | messageStream.done(); 211 | } catch (e) { 212 | console.error(e); 213 | 214 | const error = new Error("The AI got into some problem."); 215 | uiStream.error(error); 216 | textStream.error(error); 217 | messageStream.error(error); 218 | // @ts-ignore 219 | aiState.done(); 220 | } 221 | })(); 222 | 223 | return { 224 | id: nanoid(), 225 | attachments: uiStream.value, 226 | spinner: spinnerStream.value, 227 | display: messageStream.value, 228 | }; 229 | } 230 | 231 | // export type Message = { 232 | // role: "user" | "assistant" | "system" | "function" | "data" | "tool"; 233 | // content: string; 234 | // id?: string; 235 | // name?: string; 236 | // display?: { 237 | // name: string; 238 | // props: Record; 239 | // }; 240 | // }; 241 | 242 | export type AIState = { 243 | chatId: string; 244 | interactions?: string[]; 245 | messages: Message[]; 246 | }; 247 | 248 | export type UIState = { 249 | id: string; 250 | display: React.ReactNode; 251 | spinner?: React.ReactNode; 252 | attachments?: React.ReactNode; 253 | }[]; 254 | 255 | export const AI = createAI({ 256 | actions: { 257 | submitUserMessage, 258 | }, 259 | initialUIState: [], 260 | initialAIState: { chatId: nanoid(), interactions: [], messages: [] }, 261 | onGetUIState: async () => { 262 | "use server"; 263 | 264 | const aiState = getAIState(); 265 | 266 | if (aiState) { 267 | //@ts-ignore 268 | const uiState = getUIStateFromAIState(aiState); 269 | 270 | return uiState; 271 | } 272 | }, 273 | }); 274 | 275 | export const getUIStateFromAIState = (aiState: Chat) => { 276 | return aiState.messages 277 | .filter((message) => message.role !== "system") 278 | .map((message: any, index) => ({ 279 | id: `${aiState.chatId}-${index}`, 280 | display: 281 | message.role === "assistant" /** @ts-ignore */ ? ( 282 | message.display?.name === "query_data" ? ( 283 | 284 | 285 | 286 | ) : ( 287 | 288 | ) 289 | ) : message.role === "user" ? ( 290 | {message.content} 291 | ) : ( 292 | 293 | ), 294 | })); 295 | }; 296 | -------------------------------------------------------------------------------- /app/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { Message } from "ai"; 4 | import { db } from "./bindings"; 5 | 6 | export interface Vector { 7 | id: string; 8 | metadata?: Record; 9 | values?: number[]; 10 | } 11 | 12 | interface Match { 13 | id: string; 14 | metadata: Record; 15 | score: number; 16 | values: number[]; 17 | } 18 | 19 | interface CloudflareQueryResponse { 20 | errors: any[]; 21 | messages: any[]; 22 | result: QueryResult; 23 | success: boolean; 24 | } 25 | 26 | interface QueryResult { 27 | count: number; 28 | matches: Match[]; 29 | } 30 | /** 31 | * Converts an array of vectors to NDJSON format. 32 | * @param vectors Array of vectors to convert. 33 | * @returns NDJSON string. 34 | */ 35 | function vectorsToNDJSON(vectors: VectorizeVector[]): string { 36 | return vectors.map((vector) => JSON.stringify(vector)).join("\n"); 37 | } 38 | 39 | interface CloudflareResponse { 40 | errors: any[]; 41 | messages: any[]; 42 | result: { 43 | count: number; 44 | ids: string[]; 45 | }; 46 | success: boolean; 47 | } 48 | 49 | /** 50 | * Sends vectors to Cloudflare for indexing. 51 | * @param vectors Array of vectors to send. 52 | * @returns The Cloudflare response or undefined if an error occurs. 53 | */ 54 | export async function sendToCloudflare( 55 | vectors: VectorizeVector[] 56 | ): Promise { 57 | const ndjson = vectorsToNDJSON(vectors); 58 | 59 | const endpoint = `https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_ACCOUNT_ID}/vectorize/indexes/ycindex/upsert`; 60 | 61 | try { 62 | const response = await fetch(endpoint, { 63 | method: "POST", 64 | headers: { 65 | "Content-Type": "application/x-ndjson", 66 | Authorization: `Bearer ${process.env.CLOUDFLARE_API_TOKEN}`, 67 | }, 68 | body: ndjson, 69 | }); 70 | if (!response.ok) { 71 | console.error("Error response:", await response.text()); // Log error response 72 | throw new Error(`Error in vectorize INSERT: ${response.statusText}`); 73 | } 74 | 75 | return await response.json(); 76 | } catch (error) { 77 | console.error("Error sending to Cloudflare:", error); 78 | return undefined; 79 | } 80 | } 81 | 82 | /** 83 | * Queries Cloudflare for vector matches. 84 | * @param embeddings The embeddings to query. 85 | * @param returnMetadata Whether to return metadata in the response. 86 | * @param returnValues Whether to return values in the response. 87 | * @param topK The number of top matches to return. 88 | * @returns Array of matches or undefined if an error occurs. 89 | */ 90 | export async function queryCloudflare( 91 | embeddings: number[], 92 | returnMetadata: boolean = false, 93 | returnValues: boolean = false, 94 | topK: number = 5 95 | ): Promise { 96 | const endpoint = `https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_ACCOUNT_ID}/vectorize/indexes/ycindex/query`; 97 | 98 | try { 99 | const response = await fetch(endpoint, { 100 | method: "POST", 101 | headers: { 102 | "Content-Type": "application/json", 103 | Authorization: `Bearer ${process.env.CLOUDFLARE_API_TOKEN}`, 104 | }, 105 | body: JSON.stringify({ 106 | vector: embeddings, 107 | returnMetadata, 108 | returnValues, 109 | topK, 110 | }), 111 | }); 112 | 113 | if (!response.ok) { 114 | throw new Error(`HTTP error! status: ${response.status}`); 115 | } 116 | 117 | const data: CloudflareQueryResponse = await response.json(); 118 | 119 | if (!data.success || !data.result || !data.result.matches) { 120 | console.error("Invalid response structure:", data); 121 | return undefined; 122 | } 123 | 124 | return data.result.matches; 125 | } catch (error) { 126 | console.error("Error querying Cloudflare:", error); 127 | return undefined; 128 | } 129 | } 130 | 131 | interface CloudflareEmbeddingResponse { 132 | result: { 133 | shape: [number, number]; 134 | data: number[][]; 135 | }; 136 | success: boolean; 137 | errors: any[]; 138 | messages: any[]; 139 | } 140 | 141 | /** 142 | * Creates embeddings for the given text using Cloudflare's AI API. 143 | * @param text The text string to create embeddings for. 144 | * @returns Array of embeddings or undefined if an error occurs. 145 | */ 146 | export async function createEmbeddings( 147 | text: string 148 | ): Promise { 149 | const endpoint = `https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_ACCOUNT_ID}/ai/run/@cf/baai/bge-large-en-v1.5`; 150 | 151 | try { 152 | const response = await fetch(endpoint, { 153 | method: "POST", 154 | headers: { 155 | "Content-Type": "application/json", 156 | Authorization: `Bearer ${process.env.CLOUDFLARE_API_TOKEN}`, 157 | }, 158 | body: JSON.stringify({ text: [text] }), 159 | }); 160 | 161 | if (!response.ok) { 162 | throw new Error(`HTTP error! status: ${response.status}`); 163 | } 164 | 165 | const data: CloudflareEmbeddingResponse = await response.json(); 166 | 167 | if ( 168 | !data.success || 169 | !data.result || 170 | !data.result.data || 171 | data.result.data.length === 0 172 | ) { 173 | console.error("Invalid response structure:", data); 174 | return undefined; 175 | } 176 | 177 | return data.result.data[0]; 178 | } catch (error) { 179 | console.error("Error creating embeddings:", error); 180 | return undefined; 181 | } 182 | } 183 | 184 | export async function executeD1(query: string) { 185 | const { results } = await db.prepare(query).all(); 186 | 187 | return results; 188 | } 189 | 190 | export interface Chat extends Record { 191 | id: string; 192 | title: string; 193 | createdAt?: Date; 194 | userId?: string; 195 | path: string; 196 | messages: Message[]; 197 | sharePath?: string; 198 | } 199 | 200 | export interface Chat extends Record { 201 | id: string; 202 | title: string; 203 | createdAt?: Date; 204 | userId?: string; 205 | path: string; 206 | messages: Message[]; 207 | sharePath?: string; 208 | } 209 | 210 | export async function saveChat(chat: Chat) { 211 | const { id, title, userId = "defaultUserId", path, messages } = chat; 212 | const sharePath = chat.sharePath ? chat.sharePath : null; 213 | 214 | const stmt = db.prepare( 215 | "SELECT json_extract(messages, '$') AS messages FROM chats WHERE id = ?" 216 | ); 217 | const response = await stmt.bind(id).first(); 218 | 219 | let existingMessages: Message[] = []; 220 | 221 | if (response && response.messages) { 222 | // @ts-ignore 223 | existingMessages = JSON.parse(response.messages); 224 | } 225 | 226 | const mergedMessages: Message[] = [...existingMessages, ...messages]; 227 | 228 | const messagesJson = JSON.stringify(mergedMessages); 229 | 230 | const updateQuery = ` 231 | INSERT INTO chats (id, title, userId, path, messages, sharePath) 232 | VALUES (?, ?, ?, ?, ?, ?) 233 | ON CONFLICT(id) DO UPDATE SET 234 | title = excluded.title, 235 | userId = excluded.userId, 236 | path = excluded.path, 237 | messages = excluded.messages, 238 | sharePath = excluded.sharePath; 239 | `; 240 | 241 | try { 242 | const info = await db 243 | .prepare(updateQuery) 244 | .bind(id, title, userId, path, messagesJson, sharePath) 245 | .run(); 246 | console.log(info); 247 | } catch (error) { 248 | console.error("Error saving chat:", error); 249 | } 250 | } 251 | 252 | export async function getChat(id: string): Promise { 253 | const query = `SELECT * FROM chats WHERE id = ?`; 254 | const { results } = await db.prepare(query).bind(id).all(); 255 | 256 | if (results.length > 0) { 257 | const { id, title, createdAt, userId, path, messages, sharePath } = 258 | results[0] as Record; 259 | 260 | if (typeof messages === "string") { 261 | const deserializedMessages: Message[] = JSON.parse(messages); 262 | return { 263 | id: id as string, 264 | title: title as string, 265 | createdAt: new Date(createdAt as string), 266 | userId: userId as string, 267 | path: path as string, 268 | messages: deserializedMessages, 269 | sharePath: sharePath as string, 270 | }; 271 | } 272 | } 273 | 274 | return null; 275 | } 276 | 277 | // { 278 | // "message": [ 279 | // "Error: D1_TYPE_ERROR: Type 'object' not supported for value 'Sat Apr 13 2024 21:41:03 GMT+0000 (Coordinated Universal Time)'" 280 | // ], 281 | // "level": "error", 282 | // "timestamp": 1713044463478 283 | // }, 284 | // { 285 | -------------------------------------------------------------------------------- /app/ai.ts: -------------------------------------------------------------------------------- 1 | import { Ai } from "@cloudflare/ai"; 2 | import { getRequestContext } from "@cloudflare/next-on-pages"; 3 | 4 | const accountId = process.env.CLOUDFLARE_ACCOUNT_ID!; 5 | const apiToken = process.env.CLOUDFLARE_API_TOKEN!; 6 | 7 | const devAI = { 8 | async run( 9 | modelId: string, 10 | options: { 11 | prompt?: string; 12 | messages?: Array; 13 | text?: string; 14 | stream?: boolean; 15 | } 16 | ): Promise { 17 | const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${modelId}`; 18 | const headers = { 19 | Authorization: `Bearer ${apiToken}`, 20 | "Content-Type": "application/json", 21 | }; 22 | const body = JSON.stringify(options); 23 | 24 | try { 25 | const response = await fetch(url, { 26 | method: "POST", 27 | headers: headers, 28 | body: body, 29 | }); 30 | 31 | if (!response.ok) { 32 | throw new Error(`HTTP error! status: ${response.status}`); 33 | } 34 | 35 | const data = await response.json(); 36 | 37 | console.log(data); 38 | return data; 39 | } catch (error) { 40 | console.error("Error:", error); 41 | throw error; 42 | } 43 | }, 44 | }; 45 | 46 | // const prodAI = () => { 47 | // const aiInstance = new Ai(getRequestContext().env.AI); 48 | // return aiInstance; 49 | // }; 50 | 51 | export const ai = devAI; 52 | 53 | // -- prompt 54 | 55 | // { 56 | // "result": { 57 | // "response": "Hello there! I'm glad you're interested in learning more about the origins of the phrase \"Hello World.\"\nThe term \"Hello World\" has its roots in the early days of computer programming. In the late 1960s and early 1970s, computer scientists and programmers were experimenting with different ways to write and test code. One of the most common programs they wrote was a simple \"hello world\" program, which would print the words \"Hello, World!\" on the screen.\nThe term \"Hello World\" became a sort of shorthand for this basic program, and it was often used as a way to test the functionality of a new computer or programming language. Over time, the phrase became a sort of inside joke among programmers, and it has since become a common greeting in the tech industry.\nSo, the next time you hear someone say \"Hello World,\" you'll know that they're not just being polite – they're actually referencing a piece of computer programming history! 😊" 58 | // }, 59 | // "success": true, 60 | // "errors": [], 61 | // "messages": [] 62 | // } 63 | 64 | // -- text 65 | 66 | // { 67 | // "result": { 68 | // "shape": [ 69 | // 1, 70 | // 768 71 | // ], 72 | // "data": [ 73 | // [ 74 | // 0.018773017451167107, 75 | // 0.0387987457215786, 76 | // 0.021945253014564514, 77 | 78 | // 0.0005791907897219062, 79 | 80 | // -0.03062792308628559 81 | // ] 82 | // ] 83 | // }, 84 | // "success": true, 85 | // "errors": [], 86 | // "messages": [] 87 | // } 88 | -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | interface RequestBody { 2 | modelId: string; 3 | options: { 4 | stream: boolean; 5 | messages: Array; 6 | }; 7 | } 8 | 9 | const accountId = process.env.CLOUDFLARE_ACCOUNT_ID!; 10 | const apiToken = process.env.CLOUDFLARE_API_TOKEN!; 11 | 12 | export const runtime = "edge"; 13 | 14 | export async function POST(req: Request): Promise { 15 | const { modelId, options } = (await req.json()) as RequestBody; 16 | const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${modelId}`; 17 | const headers = { 18 | Authorization: `Bearer ${apiToken}`, 19 | "Content-Type": "application/json", 20 | }; 21 | const body = JSON.stringify(options); 22 | 23 | const response = await fetch(url, { 24 | method: "POST", 25 | headers: headers, 26 | body: body, 27 | }); 28 | 29 | if (!response.ok) { 30 | return new Response(`HTTP error! status: ${response.status}`, { 31 | status: response.status, 32 | }); 33 | } 34 | 35 | // Create a TransformStream to process the response 36 | const { readable, writable } = new TransformStream(); 37 | const writer = writable.getWriter(); 38 | 39 | // Start streaming the response 40 | streamResponse(response, writer); 41 | 42 | return new Response(readable, { 43 | headers: { 44 | "content-type": "text/plain", 45 | }, 46 | }); 47 | } 48 | 49 | async function streamResponse( 50 | response: Response, 51 | writer: WritableStreamDefaultWriter 52 | ) { 53 | const reader = response.body!.getReader(); 54 | const decoder = new TextDecoder("utf-8"); 55 | let buffer = ""; // Buffer to accumulate chunks 56 | 57 | while (true) { 58 | const { done, value } = await reader.read(); 59 | if (done) { 60 | // Handle any remaining data in the buffer at the end of the stream 61 | if (buffer.trim() !== "") { 62 | try { 63 | const parsedData = JSON.parse(buffer); 64 | console.log(parsedData.response); 65 | await writer.write(parsedData.response); 66 | } catch (error) { 67 | console.error("Final buffer parsing error:", error); 68 | } 69 | } 70 | break; 71 | } 72 | 73 | buffer += decoder.decode(value, { stream: true }); 74 | 75 | // Process complete lines from the buffer 76 | let newlineIndex; 77 | while ((newlineIndex = buffer.indexOf("\n")) !== -1) { 78 | const line = buffer.substring(0, newlineIndex).trim(); 79 | buffer = buffer.substring(newlineIndex + 1); 80 | 81 | if (line.startsWith("data: ")) { 82 | const data = line.slice(6).trim(); 83 | 84 | if (data === "[DONE]") { 85 | // End of stream 86 | break; 87 | } else { 88 | try { 89 | const parsedData = JSON.parse(data); 90 | await writer.write(parsedData.response); 91 | } catch (error) { 92 | console.error("Error parsing JSON:", error); 93 | // Keep incomplete JSON in the buffer to try again after more data is received 94 | buffer = line + "\n" + buffer; 95 | break; 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | await writer.close(); 103 | } 104 | -------------------------------------------------------------------------------- /app/api/embed/route.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { db } from "@/app/bindings"; 3 | import { Document } from "@langchain/core/documents"; 4 | import { DirectoryLoader } from "langchain/document_loaders/fs/directory"; 5 | import { TextLoader } from "langchain/document_loaders/fs/text"; 6 | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; 7 | import md5 from "md5"; 8 | import { createEmbeddings, sendToCloudflare } from "../../actions"; 9 | // export const runtime = "edge"; 10 | 11 | async function embedDocument(doc: any) { 12 | try { 13 | const text = doc.pageContent; 14 | const hash = md5(text); 15 | 16 | // Check if the document already exists in the database 17 | const selectStmt = db.prepare("SELECT * FROM v_yc WHERE hash = ?"); 18 | const stmt = await selectStmt.bind(hash).all(); 19 | 20 | if (stmt.results.length > 0) { 21 | const existingRecord = stmt.results[0]; 22 | console.log("existingRecord", existingRecord); 23 | 24 | return { 25 | id: existingRecord.id, 26 | values: [], 27 | alreadyExists: true, 28 | }; 29 | } 30 | // Generate embeddings for the document 31 | const embeddings = await createEmbeddings(text); 32 | if (embeddings?.length !== 1024) { 33 | throw new Error( 34 | `Unexpected vector length: ${embeddings?.length}. Expected 1024.` 35 | ); 36 | } 37 | 38 | // Insert new document into the database 39 | const result = await db 40 | .prepare("INSERT INTO v_yc (hash, text) VALUES (?, ?) RETURNING id") 41 | .bind(hash, text) 42 | .all<{ id: number }>(); 43 | 44 | const { id } = result.results[0]; 45 | 46 | return { 47 | id: id.toString(), 48 | values: embeddings, 49 | alreadyExists: false, 50 | }; 51 | } catch (error) { 52 | console.log("error embedding document", error); 53 | throw error; 54 | } 55 | } 56 | async function prepareDocument(page: any) { 57 | let { pageContent, metadata } = page; 58 | pageContent = pageContent.replace(/\n/g, ""); 59 | // split the docs 60 | const splitter = new RecursiveCharacterTextSplitter({ 61 | chunkSize: 2000, 62 | chunkOverlap: 0, 63 | }); 64 | const docs = await splitter.splitDocuments([ 65 | new Document({ 66 | pageContent, 67 | }), 68 | ]); 69 | return docs; 70 | } 71 | 72 | export async function GET(req: Request) { 73 | // this should only run on developement 74 | 75 | if (process.env.NODE_ENV !== "development") { 76 | return new Response("Not Found", { status: 404 }); 77 | } 78 | 79 | try { 80 | // const data = await downloadFromR2("KAARTHIK_ai_engineer.pdf"); 81 | const data = new DirectoryLoader("documents/", { 82 | ".txt": (path: string) => new TextLoader(path), 83 | }); 84 | const docs = await data.load(); 85 | // console.log("docs", docs); 86 | const documents = await Promise.all(docs.map(prepareDocument)); 87 | const vectors = await Promise.all(documents.flat().map(embedDocument)); 88 | // console.log("vectors", vectors); 89 | if (vectors.length === 0) { 90 | console.log("No documents found."); 91 | return new Response(JSON.stringify("No documents found."), { 92 | headers: { "content-type": "application/json" }, 93 | }); 94 | } 95 | 96 | const newVectors = vectors 97 | .filter( 98 | (vector) => 99 | vector && !vector.alreadyExists && vector.values !== undefined 100 | ) 101 | .map((vector) => ({ 102 | id: vector.id as string, 103 | values: vector.values as number[], 104 | })); 105 | 106 | console.log(newVectors); 107 | 108 | if (newVectors.length > 0) { 109 | console.log("Sending new vectors to Cloudflare..."); 110 | // await sendToCloudflare(newVectors); 111 | return new Response(JSON.stringify("Sent new vectors to Cloudflare."), { 112 | headers: { "content-type": "application/json" }, 113 | }); 114 | } else { 115 | return new Response( 116 | JSON.stringify("No new vectors to send to Cloudflare."), 117 | { 118 | headers: { "content-type": "application/json" }, 119 | } 120 | ); 121 | } 122 | } catch (error) { 123 | return new Response(JSON.stringify(error), { 124 | headers: { "content-type": "application/json" }, 125 | }); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /app/bindings.ts: -------------------------------------------------------------------------------- 1 | import type { D1Database, KVNamespace } from "@cloudflare/workers-types"; 2 | import { binding } from "cf-bindings-proxy"; 3 | 4 | // export const kv = binding("kv"); 5 | export const db = process.env.DB; 6 | -------------------------------------------------------------------------------- /app/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { AI } from "@/app/action"; 2 | import { getChat } from "@/app/actions"; 3 | import { ChatBar } from "@/components/ChatBar"; 4 | import { redirect } from "next/navigation"; 5 | 6 | export const runtime = "edge"; 7 | 8 | export interface ChatPageProps { 9 | params: { 10 | id: string; 11 | }; 12 | } 13 | 14 | export default async function ChatPage({ params }: ChatPageProps) { 15 | const chat = await getChat(params.id); 16 | 17 | if (!chat) { 18 | redirect("/"); 19 | } 20 | 21 | return ( 22 | 29 |
30 | 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaarthik108/di1/2fb71603577f67b3eedf05a79aa863830d7a059c/app/favicon.ico -------------------------------------------------------------------------------- /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: 0 0% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 0 0% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 0 0% 3.9%; 15 | 16 | --primary: 0 0% 9%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | 22 | --muted: 0 0% 96.1%; 23 | --muted-foreground: 0 0% 45.1%; 24 | 25 | --accent: 0 0% 96.1%; 26 | --accent-foreground: 0 0% 9%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 0 0% 89.8%; 32 | --input: 0 0% 89.8%; 33 | --ring: 0 0% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 0 0% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 0 0% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 0 0% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 0 0% 9%; 50 | 51 | --secondary: 0 0% 14.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 0 0% 14.9%; 55 | --muted-foreground: 0 0% 63.9%; 56 | 57 | --accent: 0 0% 14.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 0 0% 14.9%; 64 | --input: 0 0% 14.9%; 65 | --ring: 0 0% 83.1%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Footer from "@/components/Footer"; 2 | import Navbar from "@/components/Navbar"; 3 | import { Toaster } from "@/components/ui/sonner"; 4 | import { cn } from "@/lib/utils"; 5 | import type { Metadata, Viewport } from "next"; 6 | import { Farro } from "next/font/google"; 7 | import { Suspense } from "react"; 8 | import "./globals.css"; 9 | 10 | const farro = Farro({ 11 | weight: ["300", "400", "500", "700"], 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | metadataBase: new URL("https://di1-iyr.pages.dev"), 17 | title: "Di1", 18 | description: "Di1 is an AI T2SQL Chatbot for Cloudflare D1", 19 | openGraph: { 20 | title: "Di1", 21 | description: "Di1 is an AI T2SQL Chatbot for Cloudflare D1", 22 | url: "https://di1-iyr.pages.dev", 23 | siteName: "Di1", 24 | locale: "en_US", 25 | type: "website", 26 | images: "/logobg.png", 27 | }, 28 | robots: { 29 | index: true, 30 | follow: true, 31 | googleBot: { 32 | index: true, 33 | follow: true, 34 | "max-video-preview": -1, 35 | "max-image-preview": "large", 36 | "max-snippet": -1, 37 | }, 38 | }, 39 | twitter: { 40 | title: "Di1", 41 | card: "summary_large_image", 42 | }, 43 | }; 44 | export const viewport: Viewport = { 45 | width: "device-width", 46 | initialScale: 1, 47 | minimumScale: 1, 48 | maximumScale: 1, 49 | }; 50 | 51 | export default function RootLayout({ 52 | children, 53 | }: Readonly<{ 54 | children: React.ReactNode; 55 | }>) { 56 | return ( 57 | 58 | 59 | 60 | ...}>{children} 61 | 62 |