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