├── .env.example
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── app
├── favicon.ico
├── globals.css
├── layout.tsx
└── page.tsx
├── components.json
├── components
├── chat-message.tsx
├── code-block.tsx
├── markdown.tsx
├── prompt-form.tsx
└── ui
│ ├── avatar.tsx
│ ├── button.tsx
│ ├── input.tsx
│ ├── scroll-area.tsx
│ └── table.tsx
├── lib
├── hooks
│ ├── use-copy-to-clipboard.tsx
│ └── use-enter-submit.tsx
└── utils.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── next.svg
└── vercel.svg
├── tailwind.config.js
├── tailwind.config.ts
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_SUPERAGENT_API_KEY=
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.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 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Ismail Pelaseyed
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ⚡ SuperAI - AI Financial Analyst
2 |
3 | **SuperAI is a web application that provides a dynamic and interactive way to visualize data using Large Language Models (LLMs) and Chart.js. This application allows users to chat with structured/unstructured data and visualize the answers with dynamically rendered charts.**
4 |
5 | https://github.com/homanp/ai-financial-analyst/assets/2464556/50186cd0-b163-40a4-a033-3444b258feac
6 |
7 | ## Features
8 | - **Dynamic Chart Generation**: Utilizes Chart.js to render charts based on model outputs.
9 | - **Superagent integration**: Seamlessly integrates with the Superagent Assistants framework.
10 | - **Interactive Chat Interface**: Features a chat-like interface where users can enter data, and see the corresponding chart update in real-time.
11 |
12 | ## Getting Started
13 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
14 |
15 | Create a [Superagent.sh account](https://beta.superagent.sh)
16 |
17 | Rename the `.env.example` to `.env` and set the required environment variables.
18 |
19 | First, run the development server:
20 |
21 | ```bash
22 | npm run dev
23 | # or
24 | yarn dev
25 | # or
26 | pnpm dev
27 | # or
28 | bun dev
29 | ```
30 |
31 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
32 |
33 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
34 |
35 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
36 |
37 | ## Learn More
38 |
39 | To learn more about Superagent.sh, take a look at the following resources:
40 |
41 | - [Superagent Documentation](https://docs.superagent.sh) - learn about Superagent features and API.
42 | - [Superagent Tutorials]([https://nextjs.org/learn](https://www.youtube.com/channel/UCBeXnF8gh2EwAmOIwpmfjmA)) - Superagent YouTube tutorials.
43 | - [Get help](https://discord.gg/mhmJUTjW4b) - join the Superagent Discord channel to get help.
44 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homanp/super-ai/871507792710fa55091aae1d3d1797408f7015df/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: 222.2 84% 4.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 222.2 84% 4.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --primary: 222.2 47.4% 11.2%;
17 | --primary-foreground: 210 40% 98%;
18 |
19 | --secondary: 210 40% 96.1%;
20 | --secondary-foreground: 222.2 47.4% 11.2%;
21 |
22 | --muted: 210 40% 96.1%;
23 | --muted-foreground: 215.4 16.3% 46.9%;
24 |
25 | --accent: 210 40% 96.1%;
26 | --accent-foreground: 222.2 47.4% 11.2%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 210 40% 98%;
30 |
31 | --border: 214.3 31.8% 91.4%;
32 | --input: 214.3 31.8% 91.4%;
33 | --ring: 222.2 84% 4.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 222.2 84% 4.9%;
40 | --foreground: 210 40% 98%;
41 |
42 | --card: 222.2 84% 4.9%;
43 | --card-foreground: 210 40% 98%;
44 |
45 | --popover: 222.2 84% 4.9%;
46 | --popover-foreground: 210 40% 98%;
47 |
48 | --primary: 210 40% 98%;
49 | --primary-foreground: 222.2 47.4% 11.2%;
50 |
51 | --secondary: 217.2 32.6% 17.5%;
52 | --secondary-foreground: 210 40% 98%;
53 |
54 | --muted: 217.2 32.6% 17.5%;
55 | --muted-foreground: 215 20.2% 65.1%;
56 |
57 | --accent: 217.2 32.6% 17.5%;
58 | --accent-foreground: 210 40% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 210 40% 98%;
62 |
63 | --border: 217.2 32.6% 17.5%;
64 | --input: 217.2 32.6% 17.5%;
65 | --ring: 212.7 26.8% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const inter = Inter({ subsets: ["latin"] });
6 |
7 | export const metadata: Metadata = {
8 | title: "Create Next App",
9 | description: "Generated by create next app",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: {
15 | children: React.ReactNode;
16 | }) {
17 | return (
18 |
19 |
{children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { fetchEventSource } from "@microsoft/fetch-event-source";
5 | import { Chart as ChartJS, ArcElement, Tooltip, Legend, Title } from "chart.js";
6 | import { Doughnut } from "react-chartjs-2";
7 |
8 | import PromptForm from "@/components/prompt-form";
9 | import ChatMessage from "@/components/chat-message";
10 |
11 | ChartJS.register(ArcElement, Tooltip, Legend, Title);
12 |
13 | // Change this to your own hosted Superagent instance
14 | const API_URL = "https://api.beta.superagent.sh/api/v1";
15 |
16 | // Change this to your Superagent Agent ID
17 | const CHAT_AGENT_ID = "e9409d15-a371-4fa7-913b-5ca114aae0e2";
18 | const STRUCTERED_PREDICTION_AGENT_ID = "c39f0a9c-6e99-47a1-a5d3-0f5604fbf720";
19 |
20 | const HEADERS = {
21 | "Content-Type": "application/json",
22 | authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPERAGENT_API_KEY}`,
23 | };
24 |
25 | const chartOptions = {
26 | plugins: {
27 | legend: {
28 | labels: {
29 | color: "white", // Legend labels
30 | },
31 | },
32 | },
33 | };
34 |
35 | const chartData = {
36 | labels: [],
37 | datasets: [
38 | {
39 | label: "",
40 | data: [],
41 | backgroundColor: [
42 | "rgba(255, 99, 132, 0.2)",
43 | "rgba(54, 162, 235, 0.2)",
44 | "rgba(255, 206, 86, 0.2)",
45 | "rgba(75, 192, 192, 0.2)",
46 | "rgba(153, 102, 255, 0.2)",
47 | "rgba(255, 159, 64, 0.2)",
48 | ],
49 | borderColor: [
50 | "rgba(255, 99, 132, 1)",
51 | "rgba(54, 162, 235, 1)",
52 | "rgba(255, 206, 86, 1)",
53 | "rgba(75, 192, 192, 1)",
54 | "rgba(153, 102, 255, 1)",
55 | "rgba(255, 159, 64, 1)",
56 | ],
57 | borderWidth: 1,
58 | },
59 | ],
60 | };
61 |
62 | export default function Home() {
63 | const [messages, setMessages] = React.useState<
64 | { type: string; message: string }[]
65 | >([{ type: "ai", message: "Hello there, how can I help you?" }]);
66 | const [data, setData] = React.useState(chartData);
67 | const [chartResponse, setChartResponseData] = React.useState(null);
68 |
69 | async function onSubmit(value: string) {
70 | let message = "";
71 |
72 | setMessages((previousMessages: any) => [
73 | ...previousMessages,
74 | { type: "human", message: value },
75 | ]);
76 |
77 | setMessages((previousMessages) => [
78 | ...previousMessages,
79 | { type: "ai", message },
80 | ]);
81 |
82 | await fetchEventSource(`${API_URL}/agents/${CHAT_AGENT_ID}/invoke`, {
83 | method: "POST",
84 | headers: HEADERS,
85 | body: JSON.stringify({
86 | input: value,
87 | enableStreaming: true,
88 | sessionId: "test",
89 | }),
90 | openWhenHidden: true,
91 | async onclose() {
92 | const chartResponse = await fetch(
93 | `${API_URL}/agents/${STRUCTERED_PREDICTION_AGENT_ID}/invoke`,
94 | {
95 | method: "POST",
96 | headers: HEADERS,
97 | body: JSON.stringify({
98 | input: message,
99 | enableStreaming: false,
100 | outputSchema:
101 | "{labels: List[str], values: List[str or int], cart_label: str}",
102 | sessionId: "test",
103 | }),
104 | }
105 | );
106 | const { data: chartResponseData } = await chartResponse.json();
107 |
108 | setChartResponseData(chartResponseData);
109 | setData({
110 | ...data,
111 | labels: chartResponseData?.labels || [],
112 | datasets: [
113 | {
114 | ...data.datasets[0],
115 | data: chartResponseData?.values || [],
116 | },
117 | ],
118 | });
119 | },
120 | async onmessage(event) {
121 | if (event.data !== "[END]") {
122 | message += event.data === "" ? `${event.data} \n` : event.data;
123 | setMessages((previousMessages) => {
124 | let updatedMessages = [...previousMessages];
125 |
126 | for (let i = updatedMessages.length - 1; i >= 0; i--) {
127 | if (updatedMessages[i].type === "ai") {
128 | updatedMessages[i].message = message;
129 | break;
130 | }
131 | }
132 |
133 | return updatedMessages;
134 | });
135 | }
136 | },
137 | });
138 | }
139 |
140 | return (
141 |
142 |
143 | {chartResponse?.values?.length > 0 && (
144 | <>
145 |
146 | {chartResponse?.chart_label}
147 |
148 |
149 |
150 |
151 | >
152 | )}
153 |
154 |
155 |
156 | {messages.map(({ type, message }, index) => (
157 |
162 | ))}
163 |
164 |
171 |
172 |
173 | );
174 | }
175 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/components/chat-message.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { motion } from "framer-motion";
4 | import remarkGfm from "remark-gfm";
5 | import remarkMath from "remark-math";
6 |
7 | import { Avatar, AvatarFallback } from "@/components/ui/avatar";
8 | import { CodeBlock } from "@/components/code-block";
9 | import { MemoizedReactMarkdown } from "@/components/markdown";
10 | import {
11 | Table,
12 | TableBody,
13 | TableCell,
14 | TableHead,
15 | TableHeader,
16 | TableRow,
17 | } from "@/components/ui/table";
18 |
19 | interface MessageProps {
20 | type: string;
21 | message: string;
22 | }
23 |
24 | function PulsatingCursor() {
25 | return (
26 |
38 | ▍
39 |
40 | );
41 | }
42 |
43 | export default function Message({ type, message }: MessageProps) {
44 | return (
45 |
46 |
47 |
50 |
51 | {message?.length === 0 &&
}
52 |
59 |
60 |
61 | );
62 | },
63 | thead({ children }) {
64 | return {children};
65 | },
66 | tbody({ children }) {
67 | return {children};
68 | },
69 | tr({ children }) {
70 | return {children};
71 | },
72 | th({ children }) {
73 | return (
74 | {children}
75 | );
76 | },
77 | td({ children }) {
78 | return (
79 | {children}
80 | );
81 | },
82 | p({ children }) {
83 | return {children}
;
84 | },
85 | a({ children, href }) {
86 | return (
87 |
93 | {children}
94 |
95 | );
96 | },
97 | ol({ children }) {
98 | return {children}
;
99 | },
100 | ul({ children }) {
101 | return ;
102 | },
103 | li({ children }) {
104 | return {children};
105 | },
106 | // @ts-ignore
107 | code({ node, inline, className, children, ...props }) {
108 | children = React.Children.map(children, (child) =>
109 | typeof child === "string" ? child.replace("`▍`", "▍") : child
110 | );
111 |
112 | const match = /language-(\w+)/.exec(className || "");
113 |
114 | if (inline) {
115 | return (
116 |
120 | {children}
121 |
122 | );
123 | }
124 |
125 | return (
126 |
132 | );
133 | },
134 | }}
135 | >
136 | {message}
137 |
138 |
139 | );
140 | }
141 |
--------------------------------------------------------------------------------
/components/code-block.tsx:
--------------------------------------------------------------------------------
1 | // Inspired by Chatbot-UI and modified to fit the needs of this project
2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Markdown/CodeBlock.tsx
3 |
4 | "use client";
5 |
6 | import { FC, memo } from "react";
7 | import { CheckIcon, CopyIcon, DownloadIcon } from "@radix-ui/react-icons";
8 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
9 | import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
10 |
11 | import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard";
12 | import { Button } from "@/components/ui/button";
13 |
14 | interface Props {
15 | language: string;
16 | value: string;
17 | }
18 |
19 | interface languageMap {
20 | [key: string]: string | undefined;
21 | }
22 |
23 | export const programmingLanguages: languageMap = {
24 | javascript: ".js",
25 | python: ".py",
26 | java: ".java",
27 | c: ".c",
28 | cpp: ".cpp",
29 | "c++": ".cpp",
30 | "c#": ".cs",
31 | ruby: ".rb",
32 | php: ".php",
33 | swift: ".swift",
34 | "objective-c": ".m",
35 | kotlin: ".kt",
36 | typescript: ".ts",
37 | go: ".go",
38 | perl: ".pl",
39 | rust: ".rs",
40 | scala: ".scala",
41 | haskell: ".hs",
42 | lua: ".lua",
43 | shell: ".sh",
44 | sql: ".sql",
45 | html: ".html",
46 | css: ".css",
47 | // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
48 | };
49 |
50 | export const generateRandomString = (length: number, lowercase = false) => {
51 | const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0
52 | let result = "";
53 | for (let i = 0; i < length; i++) {
54 | result += chars.charAt(Math.floor(Math.random() * chars.length));
55 | }
56 | return lowercase ? result.toLowerCase() : result;
57 | };
58 |
59 | const CodeBlock: FC = memo(({ language, value }) => {
60 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
61 |
62 | const downloadAsFile = () => {
63 | if (typeof window === "undefined") {
64 | return;
65 | }
66 | const fileExtension = programmingLanguages[language] || ".file";
67 | const suggestedFileName = `file-${generateRandomString(
68 | 3,
69 | true
70 | )}${fileExtension}`;
71 | const fileName = window.prompt("Enter file name" || "", suggestedFileName);
72 |
73 | if (!fileName) {
74 | // User pressed cancel on prompt.
75 | return;
76 | }
77 |
78 | const blob = new Blob([value], { type: "text/plain" });
79 | const url = URL.createObjectURL(blob);
80 | const link = document.createElement("a");
81 | link.download = fileName;
82 | link.href = url;
83 | link.style.display = "none";
84 | document.body.appendChild(link);
85 | link.click();
86 | document.body.removeChild(link);
87 | URL.revokeObjectURL(url);
88 | };
89 |
90 | const onCopy = () => {
91 | if (isCopied) return;
92 | copyToClipboard(value);
93 | };
94 |
95 | return (
96 |
97 |
98 |
{language}
99 |
100 |
109 |
117 |
118 |
119 |
138 | {value}
139 |
140 |
141 | );
142 | });
143 | CodeBlock.displayName = "CodeBlock";
144 |
145 | export { CodeBlock };
146 |
--------------------------------------------------------------------------------
/components/markdown.tsx:
--------------------------------------------------------------------------------
1 | import { FC, memo } from "react";
2 | import ReactMarkdown, { Options } from "react-markdown";
3 |
4 | export const MemoizedReactMarkdown: FC = memo(
5 | ReactMarkdown,
6 | (prevProps, nextProps) =>
7 | prevProps.children === nextProps.children &&
8 | prevProps.className === nextProps.className
9 | );
10 |
--------------------------------------------------------------------------------
/components/prompt-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ArrowUpIcon } from "@radix-ui/react-icons";
5 | import Textarea from "react-textarea-autosize";
6 |
7 | import { useEnterSubmit } from "@/lib/hooks/use-enter-submit";
8 | import { Button } from "@/components/ui/button";
9 |
10 | export interface PromptProps {
11 | onSubmit: (value: string) => Promise;
12 | }
13 |
14 | export default function PromptFrom({ onSubmit }: PromptProps) {
15 | const [input, setInput] = React.useState();
16 | const { formRef, onKeyDown } = useEnterSubmit();
17 | const inputRef = React.useRef(null);
18 |
19 | React.useEffect(() => {
20 | if (inputRef.current) {
21 | inputRef.current.focus();
22 | }
23 | }, []);
24 |
25 | return (
26 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
49 |
50 | ))
51 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
52 |
53 | export { ScrollArea, ScrollBar }
54 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 |
48 | ))
49 | TableFooter.displayName = "TableFooter"
50 |
51 | const TableRow = React.forwardRef<
52 | HTMLTableRowElement,
53 | React.HTMLAttributes
54 | >(({ className, ...props }, ref) => (
55 |
63 | ))
64 | TableRow.displayName = "TableRow"
65 |
66 | const TableHead = React.forwardRef<
67 | HTMLTableCellElement,
68 | React.ThHTMLAttributes
69 | >(({ className, ...props }, ref) => (
70 | |
78 | ))
79 | TableHead.displayName = "TableHead"
80 |
81 | const TableCell = React.forwardRef<
82 | HTMLTableCellElement,
83 | React.TdHTMLAttributes
84 | >(({ className, ...props }, ref) => (
85 | |
90 | ))
91 | TableCell.displayName = "TableCell"
92 |
93 | const TableCaption = React.forwardRef<
94 | HTMLTableCaptionElement,
95 | React.HTMLAttributes
96 | >(({ className, ...props }, ref) => (
97 |
102 | ))
103 | TableCaption.displayName = "TableCaption"
104 |
105 | export {
106 | Table,
107 | TableHeader,
108 | TableBody,
109 | TableFooter,
110 | TableHead,
111 | TableRow,
112 | TableCell,
113 | TableCaption,
114 | }
115 |
--------------------------------------------------------------------------------
/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/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {}
3 |
4 | module.exports = nextConfig
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ai-financial-analyst",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@microsoft/fetch-event-source": "^2.0.1",
13 | "@radix-ui/react-avatar": "^1.0.4",
14 | "@radix-ui/react-icons": "^1.3.0",
15 | "@radix-ui/react-scroll-area": "^1.0.5",
16 | "@radix-ui/react-slot": "^1.0.2",
17 | "chart.js": "^4.4.0",
18 | "class-variance-authority": "^0.7.0",
19 | "clsx": "^2.0.0",
20 | "framer-motion": "^10.16.4",
21 | "lucide-react": "^0.292.0",
22 | "next": "14.0.2",
23 | "react": "^18",
24 | "react-chartjs-2": "^5.2.0",
25 | "react-dom": "^18",
26 | "react-loader-spinner": "^5.4.5",
27 | "react-markdown": "^9.0.0",
28 | "react-syntax-highlighter": "^15.5.0",
29 | "react-textarea-autosize": "^8.5.3",
30 | "remark-gfm": "^4.0.0",
31 | "remark-math": "^6.0.0",
32 | "tailwind-merge": "^2.0.0",
33 | "tailwindcss-animate": "^1.0.7",
34 | "uuid": "^9.0.1"
35 | },
36 | "devDependencies": {
37 | "@types/node": "^20",
38 | "@types/react": "^18",
39 | "@types/react-dom": "^18",
40 | "@types/react-syntax-highlighter": "^15.5.10",
41 | "@types/uuid": "^9.0.7",
42 | "autoprefixer": "^10.0.1",
43 | "eslint": "^8",
44 | "eslint-config-next": "14.0.2",
45 | "postcss": "^8",
46 | "tailwindcss": "^3.3.0",
47 | "typescript": "^5"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | input: "hsl(var(--input))",
22 | ring: "hsl(var(--ring))",
23 | background: "hsl(var(--background))",
24 | foreground: "hsl(var(--foreground))",
25 | primary: {
26 | DEFAULT: "hsl(var(--primary))",
27 | foreground: "hsl(var(--primary-foreground))",
28 | },
29 | secondary: {
30 | DEFAULT: "hsl(var(--secondary))",
31 | foreground: "hsl(var(--secondary-foreground))",
32 | },
33 | destructive: {
34 | DEFAULT: "hsl(var(--destructive))",
35 | foreground: "hsl(var(--destructive-foreground))",
36 | },
37 | muted: {
38 | DEFAULT: "hsl(var(--muted))",
39 | foreground: "hsl(var(--muted-foreground))",
40 | },
41 | accent: {
42 | DEFAULT: "hsl(var(--accent))",
43 | foreground: "hsl(var(--accent-foreground))",
44 | },
45 | popover: {
46 | DEFAULT: "hsl(var(--popover))",
47 | foreground: "hsl(var(--popover-foreground))",
48 | },
49 | card: {
50 | DEFAULT: "hsl(var(--card))",
51 | foreground: "hsl(var(--card-foreground))",
52 | },
53 | },
54 | borderRadius: {
55 | lg: "var(--radius)",
56 | md: "calc(var(--radius) - 2px)",
57 | sm: "calc(var(--radius) - 4px)",
58 | },
59 | keyframes: {
60 | "accordion-down": {
61 | from: { height: 0 },
62 | to: { height: "var(--radix-accordion-content-height)" },
63 | },
64 | "accordion-up": {
65 | from: { height: "var(--radix-accordion-content-height)" },
66 | to: { height: 0 },
67 | },
68 | },
69 | animation: {
70 | "accordion-down": "accordion-down 0.2s ease-out",
71 | "accordion-up": "accordion-up 0.2s ease-out",
72 | },
73 | },
74 | },
75 | plugins: [require("tailwindcss-animate")],
76 | }
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config: Config = {
4 | content: [
5 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 | 'gradient-conic':
14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15 | },
16 | },
17 | },
18 | plugins: [],
19 | }
20 | export default config
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
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"]
27 | }
28 |
--------------------------------------------------------------------------------