87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className,
93 | )}
94 | {...props}
95 | />
96 | ));
97 | TableCell.displayName = "TableCell";
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | TableCaption.displayName = "TableCaption";
110 |
111 | export {
112 | Table,
113 | TableBody,
114 | TableCaption,
115 | TableCell,
116 | TableFooter,
117 | TableHead,
118 | TableHeader,
119 | TableRow,
120 | };
121 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | },
21 | );
22 | Textarea.displayName = "Textarea";
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import { Cross2Icon } from "@radix-ui/react-icons";
2 | import * as ToastPrimitives from "@radix-ui/react-toast";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const ToastProvider = ToastPrimitives.Provider;
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-background text-foreground",
31 | destructive:
32 | "destructive group border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | },
39 | );
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | );
53 | });
54 | Toast.displayName = ToastPrimitives.Root.displayName;
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ));
69 | ToastAction.displayName = ToastPrimitives.Action.displayName;
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ));
87 | ToastClose.displayName = ToastPrimitives.Close.displayName;
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ));
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName;
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ));
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName;
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef;
114 |
115 | type ToastActionElement = React.ReactElement;
116 |
117 | export {
118 | Toast,
119 | ToastAction,
120 | ToastClose,
121 | ToastDescription,
122 | ToastProvider,
123 | ToastTitle,
124 | ToastViewport,
125 | type ToastActionElement,
126 | type ToastProps,
127 | };
128 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
31 |
--------------------------------------------------------------------------------
/lib/context.ts:
--------------------------------------------------------------------------------
1 | import { getEmbeddings } from "./openai";
2 | import { supabase } from "./supabase";
3 |
4 | export async function getMatchesFromEmbeddings(embeddings: number[]) {
5 | try {
6 | const result = await supabase
7 | .rpc("v_match_documents", {
8 | query_embedding: embeddings,
9 | })
10 | .select("content, similarity")
11 | .limit(5);
12 | return result;
13 | } catch (error) {
14 | console.log("error querying embeddings", error);
15 | throw error;
16 | }
17 | }
18 |
19 | export async function getContext(query: string): Promise {
20 | const queryEmbeddings = await getEmbeddings(query);
21 | const matchesResponse = await getMatchesFromEmbeddings(queryEmbeddings);
22 | console.log("matchesResponse:", matchesResponse);
23 | if (matchesResponse.data && Array.isArray(matchesResponse.data)) {
24 | const highScoreMatches = matchesResponse.data.filter(
25 | (match) => match.similarity > 0.7,
26 | );
27 | const contextMessage =
28 | highScoreMatches.length > 0
29 | ? "\n" +
30 | highScoreMatches.map((match) => `- ${match.content}`).join("\n")
31 | : "";
32 | return contextMessage;
33 | } else {
34 | return null;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/hooks/chat-scroll-anchor.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { useInView } from "react-intersection-observer";
5 | import { useAtBottom } from "./use-at-bottom";
6 |
7 | interface ChatScrollAnchorProps {
8 | trackVisibility?: boolean;
9 | }
10 |
11 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) {
12 | const isAtBottom = useAtBottom();
13 | const { ref, entry, inView } = useInView({
14 | trackVisibility,
15 | delay: 100,
16 | rootMargin: "0px 0px -50px 0px",
17 | });
18 |
19 | React.useEffect(() => {
20 | if (isAtBottom && trackVisibility && !inView) {
21 | entry?.target.scrollIntoView({
22 | block: "start",
23 | });
24 | }
25 | }, [inView, entry, isAtBottom, trackVisibility]);
26 |
27 | return ;
28 | }
29 |
--------------------------------------------------------------------------------
/lib/hooks/use-at-bottom.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export function useAtBottom(offset = 0) {
4 | const [isAtBottom, setIsAtBottom] = React.useState(false);
5 |
6 | React.useEffect(() => {
7 | const handleScroll = () => {
8 | setIsAtBottom(
9 | window.innerHeight + window.scrollY >=
10 | document.body.offsetHeight - offset,
11 | );
12 | };
13 |
14 | window.addEventListener("scroll", handleScroll, { passive: true });
15 | handleScroll();
16 |
17 | return () => {
18 | window.removeEventListener("scroll", handleScroll);
19 | };
20 | }, [offset]);
21 |
22 | return isAtBottom;
23 | }
24 |
--------------------------------------------------------------------------------
/lib/hooks/use-copy-to-clipboard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | export interface useCopyToClipboardProps {
6 | timeout?: number;
7 | }
8 |
9 | export function useCopyToClipboard({
10 | timeout = 2000,
11 | }: useCopyToClipboardProps) {
12 | const [isCopied, setIsCopied] = React.useState(false);
13 |
14 | const copyToClipboard = (value: string) => {
15 | if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
16 | return;
17 | }
18 |
19 | if (!value) {
20 | return;
21 | }
22 |
23 | navigator.clipboard.writeText(value).then(() => {
24 | setIsCopied(true);
25 |
26 | setTimeout(() => {
27 | setIsCopied(false);
28 | }, timeout);
29 | });
30 | };
31 |
32 | return { isCopied, copyToClipboard };
33 | }
34 |
--------------------------------------------------------------------------------
/lib/hooks/use-enter-submit.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, type RefObject } from "react";
2 |
3 | export function useEnterSubmit(): {
4 | formRef: RefObject;
5 | onKeyDown: (event: React.KeyboardEvent) => void;
6 | } {
7 | const formRef = useRef(null);
8 |
9 | const handleKeyDown = (
10 | event: React.KeyboardEvent,
11 | ): void => {
12 | if (
13 | event.key === "Enter" &&
14 | !event.shiftKey &&
15 | !event.nativeEvent.isComposing
16 | ) {
17 | formRef.current?.requestSubmit();
18 | event.preventDefault();
19 | }
20 | };
21 |
22 | return { formRef, onKeyDown: handleKeyDown };
23 | }
24 |
--------------------------------------------------------------------------------
/lib/hooks/useDownloadChart.ts:
--------------------------------------------------------------------------------
1 | import domtoimage from "dom-to-image";
2 | import fileDownload from "js-file-download";
3 | import { useTheme } from "next-themes";
4 | import React, { useCallback } from "react";
5 |
6 | export const useDownloadChart = (chartRef: React.RefObject) => {
7 | const { theme, systemTheme } = useTheme();
8 |
9 | const downloadChart = useCallback(() => {
10 | const executeDownload = async () => {
11 | const currentTheme = theme === "system" ? systemTheme : theme;
12 | const backgroundColor = currentTheme === "dark" ? "#1f2937" : "#ffffff";
13 | const element = chartRef.current;
14 |
15 | if (element) {
16 | const originalBackground = element.style.backgroundColor;
17 | element.style.backgroundColor = backgroundColor;
18 |
19 | try {
20 | const blob = await domtoimage.toBlob(element, {
21 | height: element.offsetHeight,
22 | width: element.offsetWidth,
23 | style: {
24 | backgroundColor,
25 | },
26 | });
27 | fileDownload(blob, "chart-image.png");
28 | } catch (error) {
29 | console.error("Error downloading the chart:", error);
30 | } finally {
31 | if (chartRef.current) {
32 | chartRef.current.style.backgroundColor = originalBackground;
33 | }
34 | }
35 | }
36 | };
37 |
38 | executeDownload();
39 | }, [chartRef, theme, systemTheme]);
40 |
41 | return downloadChart;
42 | };
43 |
--------------------------------------------------------------------------------
/lib/openai.ts:
--------------------------------------------------------------------------------
1 | // import { Configuration, OpenAIApi } from "openai-edge";
2 |
3 | // const config = new Configuration({
4 | // apiKey: process.env.OPENAI_API_KEY as string,
5 | // basePath: `https://gateway.ai.cloudflare.com/v1/${process.env.ACCOUNT_TAG}/k-1-gpt/openai`,
6 | // });
7 | // const openai = new OpenAIApi(config);
8 |
9 | import OpenAI from "openai";
10 |
11 | const apiKey = process.env.OPENAI_API_KEY;
12 | if (!apiKey) {
13 | throw new Error("OPENAI_API_KEY is not set");
14 | }
15 | const openai = new OpenAI({
16 | apiKey,
17 | // baseURL: `https://gateway.ai.cloudflare.com/v1/${process.env.ACCOUNT_TAG}/k-1-gpt/openai`,
18 | });
19 |
20 | export async function getEmbeddings(text: string) {
21 | try {
22 | const response = await openai.embeddings.create({
23 | model: "text-embedding-ada-002",
24 | input: text.replace(/\n/g, " "),
25 | });
26 | const result = response.data[0].embedding as number[];
27 | return result;
28 | } catch (error) {
29 | console.log("error calling openai embeddings api", error);
30 | throw error;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/rate-limiter.ts:
--------------------------------------------------------------------------------
1 | import { Ratelimit } from "@upstash/ratelimit";
2 | import { redis } from "./redis";
3 |
4 | export const rateLimiter = new Ratelimit({
5 | redis,
6 | limiter: Ratelimit.slidingWindow(100, "1 d"),
7 | prefix: "snowbrain:limiter",
8 | });
9 |
--------------------------------------------------------------------------------
/lib/redis.ts:
--------------------------------------------------------------------------------
1 | import { Redis } from "@upstash/redis";
2 |
3 | if (!process.env.UPSTASH_REDIS_URL || !process.env.UPSTASH_REDIS_TOKEN) {
4 | throw new Error(
5 | "Please link a KV instance or populate `UPSTASH_REDIS_URL` and `UPSTASH_REDIS_TOKEN`",
6 | );
7 | }
8 |
9 | export const redis = new Redis({
10 | url: process.env.UPSTASH_REDIS_URL,
11 | token: process.env.UPSTASH_REDIS_TOKEN,
12 | });
13 |
--------------------------------------------------------------------------------
/lib/snowCache.ts:
--------------------------------------------------------------------------------
1 | import { redis } from "./redis";
2 | import { executeSnowflakeQuery } from "./snowflake";
3 |
4 | async function getCachedQueryResult(query: string): Promise {
5 | try {
6 | const cachedResult: string | null = await redis.get(query);
7 | if (cachedResult) {
8 | console.log(`Successfully retrieved and parsed cached result for query:`);
9 | return cachedResult;
10 | } else {
11 | // console.log(`No cached result for query: ${query}`);
12 | return null;
13 | }
14 | } catch (e) {
15 | console.error(
16 | `Error retrieving or parsing cached result for query: ${query}`,
17 | JSON.stringify(e, null, 2),
18 | );
19 | return null;
20 | }
21 | }
22 |
23 | async function setCachedQueryResult(query: string, result: any): Promise {
24 | try {
25 | const resultString: string = JSON.stringify(result);
26 | await redis.set(query, resultString, { ex: 3 * 60 * 60 });
27 | } catch (e) {
28 | console.error(
29 | `Error caching result for query: ${query}`,
30 | JSON.stringify(e, null, 2),
31 | );
32 | }
33 | }
34 |
35 | export async function executeQueryWithCache(query: string): Promise {
36 | let result = await getCachedQueryResult(query);
37 | if (result) {
38 | console.log("Returning cached result");
39 | return result;
40 | }
41 |
42 | console.log("Executing query and caching result");
43 | // result = test_result; // Placeholder for actual query execution
44 | result = await executeSnowflakeQuery(query);
45 | await setCachedQueryResult(query, result);
46 |
47 | return result;
48 | }
49 | const test_result = {
50 | columns: ["ORDER_DATE", "TOTAL_VALUE"],
51 | data: [
52 | { ORDER_DATE: "2023-04-01", TOTAL_VALUE: 120.99 },
53 | { ORDER_DATE: "2023-04-02", TOTAL_VALUE: 75.5 },
54 | { ORDER_DATE: "2023-04-03", TOTAL_VALUE: 140.25 },
55 | { ORDER_DATE: "2023-04-04", TOTAL_VALUE: 89.99 },
56 | { ORDER_DATE: "2023-04-05", TOTAL_VALUE: 210.45 },
57 | { ORDER_DATE: "2023-04-06", TOTAL_VALUE: 55 },
58 | { ORDER_DATE: "2023-04-07", TOTAL_VALUE: 123.75 },
59 | { ORDER_DATE: "2023-04-08", TOTAL_VALUE: 79.3 },
60 | { ORDER_DATE: "2023-04-09", TOTAL_VALUE: 45.9 },
61 | { ORDER_DATE: "2023-04-10", TOTAL_VALUE: 99.99 },
62 | ],
63 | };
64 |
--------------------------------------------------------------------------------
/lib/snowflake.ts:
--------------------------------------------------------------------------------
1 | export async function executeSnowflakeQuery(sqlText: string) {
2 | const baseUrl =
3 | process.env.NODE_ENV === "production"
4 | ? "https://snowbrain-agui.vercel.app"
5 | : "http://localhost:3000";
6 |
7 | const res = await fetch(`${baseUrl}/api/snow`, {
8 | method: "POST",
9 | headers: {
10 | "Content-Type": "application/json",
11 | "x-api-key": process.env.X_API_KEY as string,
12 | },
13 | body: JSON.stringify({ query: sqlText }),
14 | });
15 |
16 | if (!res.ok) {
17 | throw new Error("Failed to execute query");
18 | }
19 |
20 | const data = await res.json();
21 | return data;
22 | }
23 |
--------------------------------------------------------------------------------
/lib/supabase.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from "@supabase/supabase-js";
2 |
3 | export const supabase = createClient(
4 | process.env.SUPABASE_URL!,
5 | process.env.SUPABASE_SERVICE_KEY!,
6 | );
7 |
--------------------------------------------------------------------------------
/lib/utils/ddl.ts:
--------------------------------------------------------------------------------
1 | export const DDLData = [
2 | {
3 | tableName: "CUSTOMER_DETAILS",
4 | ddl: `CREATE OR REPLACE TABLE CUSTOMER_DETAILS (
5 | CUSTOMER_ID NUMBER(38,0) NOT NULL,
6 | FIRST_NAME VARCHAR(255),
7 | LAST_NAME VARCHAR(255),
8 | EMAIL VARCHAR(255),
9 | PHONE VARCHAR(20),
10 | ADDRESS VARCHAR(255),
11 | PRIMARY KEY (CUSTOMER_ID)
12 | );`,
13 | },
14 | {
15 | tableName: "ORDER_DETAILS",
16 | ddl: `CREATE OR REPLACE TABLE ORDER_DETAILS (
17 | ORDER_ID NUMBER(38,0) NOT NULL,
18 | CUSTOMER_ID NUMBER(38,0),
19 | ORDER_DATE DATE,
20 | TOTAL_AMOUNT NUMBER(10,2),
21 | PRIMARY KEY (ORDER_ID),
22 | FOREIGN KEY (CUSTOMER_ID) REFERENCES CUSTOMER_DETAILS(CUSTOMER_ID)
23 | );`,
24 | },
25 | {
26 | tableName: "PAYMENTS",
27 | ddl: `CREATE OR REPLACE TABLE PAYMENTS (
28 | PAYMENT_ID NUMBER(38,0) NOT NULL,
29 | ORDER_ID NUMBER(38,0),
30 | PAYMENT_DATE DATE,
31 | AMOUNT NUMBER(10,2),
32 | PRIMARY KEY (PAYMENT_ID),
33 | FOREIGN KEY (ORDER_ID) REFERENCES ORDER_DETAILS(ORDER_ID)
34 | );`,
35 | },
36 | {
37 | tableName: "PRODUCTS",
38 | ddl: `CREATE OR REPLACE TABLE PRODUCTS (
39 | PRODUCT_ID NUMBER(38,0) NOT NULL,
40 | PRODUCT_NAME VARCHAR(255),
41 | CATEGORY VARCHAR(255),
42 | PRICE NUMBER(10,2),
43 | PRIMARY KEY (PRODUCT_ID)
44 | );`,
45 | },
46 | {
47 | tableName: "TRANSACTIONS",
48 | ddl: `CREATE OR REPLACE TABLE TRANSACTIONS (
49 | TRANSACTION_ID NUMBER(38,0) NOT NULL,
50 | ORDER_ID NUMBER(38,0),
51 | PRODUCT_ID NUMBER(38,0),
52 | QUANTITY NUMBER(38,0),
53 | PRICE NUMBER(10,2),
54 | PRIMARY KEY (TRANSACTION_ID),
55 | FOREIGN KEY (ORDER_ID) REFERENCES ORDER_DETAILS(ORDER_ID),
56 | FOREIGN KEY (PRODUCT_ID) REFERENCES PRODUCTS(PRODUCT_ID)
57 | );`,
58 | },
59 | ];
60 |
--------------------------------------------------------------------------------
/lib/utils/index.tsx:
--------------------------------------------------------------------------------
1 | import { ToolDefinition } from "@/lib/utils/tool-definition";
2 | import { OpenAIStream } from "ai";
3 | import { clsx, type ClassValue } from "clsx";
4 | import type OpenAI from "openai";
5 | import { twMerge } from "tailwind-merge";
6 | import zodToJsonSchema from "zod-to-json-schema";
7 |
8 | const consumeStream = async (stream: ReadableStream) => {
9 | const reader = stream.getReader();
10 | while (true) {
11 | const { done } = await reader.read();
12 | if (done) break;
13 | }
14 | };
15 |
16 | export function runOpenAICompletion<
17 | T extends Omit<
18 | Parameters[0],
19 | "functions"
20 | > & {
21 | functions: ToolDefinition[];
22 | },
23 | >(openai: OpenAI, params: T) {
24 | let text = "";
25 | let hasFunction = false;
26 |
27 | type FunctionNames =
28 | T["functions"] extends Array ? T["functions"][number]["name"] : never;
29 |
30 | let onTextContent: (text: string, isFinal: boolean) => void = () => {};
31 |
32 | let onFunctionCall: Record) => void> = {};
33 |
34 | const { functions, ...rest } = params;
35 |
36 | (async () => {
37 | consumeStream(
38 | OpenAIStream(
39 | (await openai.chat.completions.create({
40 | ...rest,
41 | stream: true,
42 | functions: functions.map((fn) => ({
43 | name: fn.name,
44 | description: fn.description,
45 | parameters: zodToJsonSchema(fn.parameters) as Record<
46 | string,
47 | unknown
48 | >,
49 | })),
50 | })) as any,
51 | {
52 | async experimental_onFunctionCall(functionCallPayload) {
53 | hasFunction = true;
54 | onFunctionCall[
55 | functionCallPayload.name as keyof typeof onFunctionCall
56 | ]?.(functionCallPayload.arguments as Record);
57 | },
58 | onToken(token) {
59 | text += token;
60 | if (text.startsWith("{")) return;
61 | onTextContent(text, false);
62 | },
63 | onFinal() {
64 | if (hasFunction) return;
65 | onTextContent(text, true);
66 | },
67 | },
68 | ),
69 | );
70 | })();
71 |
72 | return {
73 | onTextContent: (
74 | callback: (text: string, isFinal: boolean) => void | Promise,
75 | ) => {
76 | onTextContent = callback;
77 | },
78 | onFunctionCall: (
79 | name: FunctionNames,
80 | callback: (args: any) => void | Promise,
81 | ) => {
82 | onFunctionCall[name] = callback;
83 | },
84 | };
85 | }
86 |
87 | export function cn(...inputs: ClassValue[]) {
88 | return twMerge(clsx(inputs));
89 | }
90 |
91 | export const formatNumber = (value: number) =>
92 | new Intl.NumberFormat("en-US", {
93 | style: "currency",
94 | currency: "USD",
95 | }).format(value);
96 |
97 | export const runAsyncFnWithoutBlocking = (
98 | fn: (...args: any) => Promise,
99 | ) => {
100 | fn();
101 | };
102 |
103 | export const sleep = (ms: number) =>
104 | new Promise((resolve) => setTimeout(resolve, ms));
105 |
106 | // Fake data
107 | export function getStockPrice(name: string) {
108 | let total = 0;
109 | for (let i = 0; i < name.length; i++) {
110 | total = (total + name.charCodeAt(i) * 9999121) % 9999;
111 | }
112 | return total / 100;
113 | }
114 |
115 | export function Template({ context }: { context: string }) {
116 | return `
117 | You're an AI assistant specializing in data analysis with Snowflake SQL. Provide me with a SQL query based on the context which details about the snowflake schema details.
118 |
119 | Context:
120 | ${context}
121 |
122 | Answer:
123 |
124 | `;
125 | }
126 |
--------------------------------------------------------------------------------
/lib/utils/tool-definition.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | /**
4 | * A tool definition contains all information required for a language model to generate tool calls.
5 | */
6 | export interface ToolDefinition {
7 | /**
8 | * The name of the tool.
9 | * Should be understandable for language models and unique among the tools that they know.
10 | *
11 | * Note: Using generics to enable result type inference when there are multiple tool calls.
12 | */
13 | name: NAME;
14 |
15 | /**
16 | * A optional description of what the tool does. Will be used by the language model to decide whether to use the tool.
17 | */
18 | description?: string;
19 |
20 | /**
21 | * The schema of the input that the tool expects. The language model will use this to generate the input.
22 | * Use descriptions to make the input understandable for the language model.
23 | */
24 | parameters: z.Schema;
25 | }
26 |
--------------------------------------------------------------------------------
/lib/validation/index.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | const chartTypes = z.enum([
4 | "area",
5 | "number",
6 | "table",
7 | "bar",
8 | "line",
9 | "donut",
10 | "scatter",
11 | ]);
12 | export type ChartType = z.infer;
13 |
14 | export const FQueryResponse = z.object({
15 | query: z.string().describe(`
16 | Creates a snowflake SQL query based on the context and given query.
17 | `),
18 | format: chartTypes.describe(
19 | "The format of the result, which determines the type of chart to generate.",
20 | ),
21 | title: z
22 | .string()
23 | .describe(
24 | "The title for the chart, which is displayed prominently above the chart.",
25 | ),
26 | timeField: z
27 | .string()
28 | .optional()
29 | .describe(
30 | "Used for time series data, designating the column that represents the time dimension. This field is used as the x-axis in charts like area and bar (if the bar chart is time-based), and potentially as the x-axis in scatter charts.",
31 | ),
32 | categories: z
33 | .array(z.string())
34 | .describe(
35 | "An array of strings that represent the numerical data series names to be visualized on the chart for 'area', 'bar', and 'line' charts. These should correspond to fields in the data that contain numerical values to plot.",
36 | ),
37 | index: z
38 | .string()
39 | .optional()
40 | .describe(
41 | "For 'bar' and 'scatter' charts, this denotes the primary categorical axis or the x-axis labels. For time series bar charts, this can often be the same as timeField.",
42 | ),
43 | // Fields specific to scatter chart
44 | category: z
45 | .string()
46 | .optional()
47 | .describe(
48 | "The category field for scatter charts, defining how data points are grouped.",
49 | ),
50 | yaxis: z
51 | .string()
52 | .optional()
53 | .describe(
54 | "The field representing the y-axis value in scatter charts. (THIS IS REQUIRED FOR SCATTER CHARTS)",
55 | ),
56 | size: z
57 | .string()
58 | .optional()
59 | .describe(
60 | "The field representing the size of the data points in scatter charts. (THIS IS REQUIRED FOR SCATTER CHARTS)",
61 | ),
62 | });
63 |
--------------------------------------------------------------------------------
/modal/main.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict
2 |
3 | from snowflake.snowpark.session import Session
4 | from snowflake.snowpark.exceptions import SnowparkSQLException
5 |
6 | import re
7 | import os
8 | import modal
9 |
10 | from typing import Optional
11 |
12 | from fastapi import Depends, FastAPI, HTTPException, status
13 | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
14 |
15 | from modal import Image, Stub, web_endpoint
16 |
17 | web_app = FastAPI()
18 | auth_scheme = HTTPBearer()
19 |
20 | stub = Stub("snowexecute")
21 |
22 | image = Image.debian_slim(python_version="3.11").pip_install(
23 | "snowflake_snowpark_python",
24 | "snowflake-snowpark-python[pandas]",
25 | )
26 |
27 | stub.sb_image = image
28 |
29 | class SnowflakeConnection:
30 | def __init__(self):
31 | self.connection_parameters = {
32 | "account": os.environ["ACCOUNT"],
33 | "user": os.environ["USER_NAME"],
34 | "password": os.environ["PASSWORD"],
35 | "warehouse": os.environ["WAREHOUSE"],
36 | "database": os.environ["DATABASE"],
37 | "schema": os.environ["SCHEMA"],
38 | "role": os.environ["ROLE"],
39 | }
40 | self.session = None
41 |
42 | def get_session(self):
43 | if self.session is None:
44 | self.session = Session.builder.configs(self.connection_parameters).create()
45 | self.session.sql_simplifier_enabled = True
46 | return self.session
47 |
48 | @stub.function(image=image, secrets=[modal.Secret.from_name("snowrun")], cpu=1)
49 | @web_endpoint(method="POST")
50 | def execute_sql(query: Dict, token: HTTPAuthorizationCredentials = Depends(auth_scheme),) -> Optional[Any]:
51 | if token.credentials != os.environ["AUTH_TOKEN"]:
52 | raise HTTPException(
53 | status_code=status.HTTP_401_UNAUTHORIZED,
54 | detail="Incorrect bearer token",
55 | headers={"WWW-Authenticate": "Bearer"},
56 | )
57 | query_text = query["query"]
58 | conn = SnowflakeConnection().get_session()
59 | if re.match(r"^\s*(drop|alter|truncate|delete|insert|update)\s", query_text, re.I):
60 | return None
61 | try:
62 | df = conn.sql(query_text)
63 | # Convert to Pandas DataFrame to easily extract column names
64 | pandas_df = df.to_pandas()
65 | # Extract column names
66 | columns = list(pandas_df.columns)
67 | # Convert rows to dictionaries
68 | data = pandas_df.to_dict('records')
69 | return {"columns": columns, "data": data}
70 | except SnowparkSQLException as e:
71 | return {"error": str(e)}
72 |
73 | if __name__ == "__main__":
74 | stub.deploy("snowexecute")
75 |
76 |
--------------------------------------------------------------------------------
/modal/requirements.txt:
--------------------------------------------------------------------------------
1 | snowflake_snowpark_python
2 | snowflake-snowpark-python[pandas]
3 | black==23.3.0
4 | fastapi
5 | pydantic==1.10.8
6 | modal-client
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | // experimental: {
4 | // serverComponentsExternalPackages: ["sharp", "onnxruntime-node"],
5 | // },
6 | };
7 |
8 | export default nextConfig;
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "snowbrainagui",
3 | "private": true,
4 | "version": "0.0.1",
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "format": "prettier --write \"**/*.{ts,tsx,js,jsx}\"",
11 | "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx}\" --cache"
12 | },
13 | "dependencies": {
14 | "@headlessui/react": "^1.7.18",
15 | "@headlessui/tailwindcss": "^0.2.0",
16 | "@mdx-js/loader": "^3.0.1",
17 | "@mdx-js/react": "^3.0.1",
18 | "@next/mdx": "^14.1.1",
19 | "@radix-ui/react-dialog": "^1.0.5",
20 | "@radix-ui/react-icons": "^1.3.0",
21 | "@radix-ui/react-label": "^2.0.2",
22 | "@radix-ui/react-separator": "^1.0.3",
23 | "@radix-ui/react-toast": "^1.1.5",
24 | "@radix-ui/react-tooltip": "^1.0.7",
25 | "@remixicon/react": "^4.2.0",
26 | "@supabase/supabase-js": "^2.39.7",
27 | "@tremor/react": "^3.14.1",
28 | "@types/mdx": "^2.0.11",
29 | "@upstash/ratelimit": "^1.0.1",
30 | "@upstash/redis": "^1.28.4",
31 | "@vercel/analytics": "^1.2.2",
32 | "ai": "3.0.5",
33 | "bright": "^0.8.4",
34 | "class-variance-authority": "^0.7.0",
35 | "clsx": "^2.1.0",
36 | "d3-scale": "^4.0.2",
37 | "date-fns": "^3.3.1",
38 | "dom-to-image": "^2.6.0",
39 | "dotenv": "^16.4.5",
40 | "geist": "^1.2.2",
41 | "html2canvas": "^1.4.1",
42 | "js-file-download": "^0.4.12",
43 | "next": "14.0.4",
44 | "next-mdx-remote": "^4.4.1",
45 | "next-themes": "^0.2.1",
46 | "openai": "^4.27.0",
47 | "react": "^18",
48 | "react-dom": "^18",
49 | "react-intersection-observer": "^9.8.0",
50 | "react-markdown": "^9.0.1",
51 | "react-textarea-autosize": "^8.5.3",
52 | "rehype-sanitize": "^6.0.0",
53 | "rehype-stringify": "^10.0.0",
54 | "remark-gfm": "^4.0.0",
55 | "remark-math": "^6.0.0",
56 | "remark-parse": "^11.0.0",
57 | "remark-rehype": "^11.1.0",
58 | "snowflake-promise": "^4.5.0",
59 | "snowflake-sdk": "^1.10.0",
60 | "sonner": "^1.4.3",
61 | "sql-formatter": "^15.2.0",
62 | "tailwind-merge": "^2.2.1",
63 | "tailwindcss-animate": "^1.0.7",
64 | "unified": "^11.0.4",
65 | "usehooks-ts": "^2.15.1",
66 | "zod": "3.22.4",
67 | "zod-to-json-schema": "3.22.4"
68 | },
69 | "devDependencies": {
70 | "@tailwindcss/forms": "^0.5.7",
71 | "@tailwindcss/typography": "^0.5.10",
72 | "@types/d3-scale": "^4.0.8",
73 | "@types/dom-to-image": "^2.6.7",
74 | "@types/node": "^20",
75 | "@types/react": "^18",
76 | "@types/react-dom": "^18",
77 | "@types/snowflake-sdk": "^1.6.20",
78 | "eslint": "8.57.0",
79 | "eslint-config-next": "14.1.0",
80 | "postcss": "^8",
81 | "prettier": "^3.2.5",
82 | "tailwindcss": "^3.4.1",
83 | "typescript": "^5"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaarthik108/snowbrain-AGUI/7aaa925816493d2bd017fb75f48b1be5daf40894/public/favicon.ico
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 | import colors from "tailwindcss/colors";
3 | import { fontFamily } from "tailwindcss/defaultTheme";
4 |
5 | const config: Config = {
6 | content: [
7 | "./ai_user_components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
9 | "./components/**/*.{js,ts,jsx,tsx}",
10 | "./ai_hooks/**/*.{js,ts,jsx,tsx}",
11 | "./node_modules/@tremor/**/*.{js,ts,jsx,tsx}",
12 | ],
13 | theme: {
14 | transparent: "transparent",
15 | current: "currentColor",
16 | extend: {
17 | fontFamily: {
18 | sans: ["var(--font-geist-sans)", ...fontFamily.sans],
19 | mono: ["var(--font-geist-mono)", ...fontFamily.mono],
20 | },
21 | backgroundImage: {
22 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
23 | "gradient-conic":
24 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
25 | },
26 | colors: {
27 | // light mode
28 | tremor: {
29 | brand: {
30 | faint: colors.blue[50],
31 | muted: colors.blue[200],
32 | subtle: colors.blue[400],
33 | DEFAULT: colors.blue[500],
34 | emphasis: colors.blue[700],
35 | inverted: colors.white,
36 | },
37 | background: {
38 | muted: colors.gray[50],
39 | subtle: colors.gray[100],
40 | DEFAULT: colors.white,
41 | emphasis: colors.gray[700],
42 | },
43 | border: {
44 | DEFAULT: colors.gray[200],
45 | },
46 | ring: {
47 | DEFAULT: colors.gray[200],
48 | },
49 | content: {
50 | subtle: colors.gray[400],
51 | DEFAULT: colors.gray[500],
52 | emphasis: colors.gray[700],
53 | strong: colors.gray[900],
54 | inverted: colors.white,
55 | },
56 | },
57 | // dark mode
58 | "dark-tremor": {
59 | brand: {
60 | faint: "#0B1229",
61 | muted: colors.blue[950],
62 | subtle: colors.blue[800],
63 | DEFAULT: colors.blue[500],
64 | emphasis: colors.blue[400],
65 | inverted: colors.blue[950],
66 | },
67 | background: {
68 | muted: "#131A2B",
69 | subtle: colors.gray[800],
70 | DEFAULT: colors.gray[900],
71 | emphasis: colors.gray[300],
72 | },
73 | border: {
74 | DEFAULT: colors.gray[800],
75 | },
76 | ring: {
77 | DEFAULT: colors.gray[800],
78 | },
79 | content: {
80 | subtle: colors.gray[600],
81 | DEFAULT: colors.gray[500],
82 | emphasis: colors.gray[200],
83 | strong: colors.gray[50],
84 | inverted: colors.gray[950],
85 | },
86 | },
87 | green: {
88 | "50": "#f0fdf6",
89 | "100": "#dbfdec",
90 | "200": "#baf8d9",
91 | "300": "#68eeac",
92 | "400": "#47e195",
93 | "500": "#1fc876",
94 | "600": "#13a65e",
95 | "700": "#13824c",
96 | "800": "#146740",
97 | "900": "#135436",
98 | "950": "#042f1c",
99 | },
100 | border: "hsl(var(--border))",
101 | input: "hsl(var(--input))",
102 | ring: "hsl(var(--ring))",
103 | background: "hsl(var(--background))",
104 | foreground: "hsl(var(--foreground))",
105 | primary: {
106 | DEFAULT: "hsl(var(--primary))",
107 | foreground: "hsl(var(--primary-foreground))",
108 | },
109 | secondary: {
110 | DEFAULT: "hsl(var(--secondary))",
111 | foreground: "hsl(var(--secondary-foreground))",
112 | },
113 | destructive: {
114 | DEFAULT: "hsl(var(--destructive))",
115 | foreground: "hsl(var(--destructive-foreground))",
116 | },
117 | muted: {
118 | DEFAULT: "hsl(var(--muted))",
119 | foreground: "hsl(var(--muted-foreground))",
120 | },
121 | accent: {
122 | DEFAULT: "hsl(var(--accent))",
123 | foreground: "hsl(var(--accent-foreground))",
124 | },
125 | popover: {
126 | DEFAULT: "hsl(var(--popover))",
127 | foreground: "hsl(var(--popover-foreground))",
128 | },
129 | card: {
130 | DEFAULT: "hsl(var(--card))",
131 | foreground: "hsl(var(--card-foreground))",
132 | },
133 | },
134 | boxShadow: {
135 | // light
136 | "tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
137 | "tremor-card":
138 | "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
139 | "tremor-dropdown":
140 | "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
141 | // dark
142 | "dark-tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
143 | "dark-tremor-card":
144 | "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
145 | "dark-tremor-dropdown":
146 | "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
147 | },
148 | borderRadius: {
149 | "tremor-small": "0.375rem",
150 | "tremor-default": "0.5rem",
151 | "tremor-full": "9999px",
152 | },
153 | fontSize: {
154 | "tremor-label": ["0.75rem", { lineHeight: "1rem" }],
155 | "tremor-default": ["0.875rem", { lineHeight: "1.25rem" }],
156 | "tremor-title": ["1.125rem", { lineHeight: "1.75rem" }],
157 | "tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }],
158 | },
159 | keyframes: {
160 | "skeleton-loading": {
161 | "0%": { backgroundPosition: "-200% 0" },
162 | "100%": { backgroundPosition: "200% 0" },
163 | },
164 | },
165 | animation: {
166 | "skeleton-loading": "skeleton-loading 1.5s infinite linear",
167 | },
168 | },
169 | },
170 | safelist: [
171 | {
172 | pattern:
173 | /^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
174 | variants: ["hover", "ui-selected"],
175 | },
176 | {
177 | pattern:
178 | /^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
179 | variants: ["hover", "ui-selected"],
180 | },
181 | {
182 | pattern:
183 | /^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
184 | variants: ["hover", "ui-selected"],
185 | },
186 | {
187 | pattern:
188 | /^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
189 | },
190 | {
191 | pattern:
192 | /^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
193 | },
194 | {
195 | pattern:
196 | /^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
197 | },
198 | ],
199 | plugins: [
200 | require("@tailwindcss/typography"),
201 | require("@headlessui/tailwindcss"),
202 | require("tailwindcss-animate"),
203 | ],
204 | };
205 | export default config;
206 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules", "modal"]
27 | }
28 |
--------------------------------------------------------------------------------
|