├── .env.example ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── Backend.md ├── README.md ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── prisma └── schema.prisma ├── public ├── font │ └── Agbalumo.ttf ├── next.svg └── vercel.svg ├── src ├── app │ ├── actions │ │ ├── auth.js │ │ ├── chat.ts │ │ ├── middleware.js │ │ ├── plaidService.js │ │ ├── stripe.js │ │ ├── user.js │ │ └── userService.js │ ├── api │ │ ├── aiResponse │ │ │ └── route.js │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.js │ │ ├── public │ │ │ └── user │ │ │ │ └── [userId] │ │ │ │ ├── accounts │ │ │ │ └── route.ts │ │ │ │ ├── getGptSpec │ │ │ │ └── route.ts │ │ │ │ └── transactions │ │ │ │ └── route.ts │ │ └── v1 │ │ │ ├── chat │ │ │ ├── [id] │ │ │ │ └── route.js │ │ │ ├── all │ │ │ │ └── route.js │ │ │ └── route.js │ │ │ ├── plaid │ │ │ ├── accounts │ │ │ │ └── route.js │ │ │ ├── categories │ │ │ │ └── route.js │ │ │ ├── create_link_token │ │ │ │ └── route.js │ │ │ ├── set_access_token │ │ │ │ └── route.js │ │ │ └── transactions │ │ │ │ ├── all │ │ │ │ └── route.js │ │ │ │ └── route.js │ │ │ ├── transaction │ │ │ └── getData │ │ │ │ └── route.js │ │ │ └── user │ │ │ ├── charts │ │ │ └── route.js │ │ │ ├── dashboard │ │ │ └── route.js │ │ │ ├── item │ │ │ └── [id] │ │ │ │ └── route.js │ │ │ ├── route.js │ │ │ └── users │ │ │ ├── pay │ │ │ └── route.js │ │ │ └── route.js │ ├── dashboard │ │ ├── charts │ │ │ ├── Summary.tsx │ │ │ ├── page.tsx │ │ │ ├── recurringSpend │ │ │ │ ├── RecurringSpend.tsx │ │ │ │ ├── RecurringTransaction.tsx │ │ │ │ └── SpendByChannel.tsx │ │ │ ├── spendByCategory │ │ │ │ ├── SpendCategory.tsx │ │ │ │ ├── TopPurchaseCategory.tsx │ │ │ │ └── TransactionsByCategory.tsx │ │ │ └── spendOverTime │ │ │ │ ├── GithubGraph.tsx │ │ │ │ ├── MonthlySpend.tsx │ │ │ │ └── SumSpend.tsx │ │ ├── chat │ │ │ ├── [id] │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── new │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── checkout │ │ │ ├── cancel │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── setting │ │ │ └── page.tsx │ │ ├── transaction │ │ │ ├── components │ │ │ │ ├── Browse.tsx │ │ │ │ └── Table.tsx │ │ │ └── page.tsx │ │ └── users │ │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ └── template.tsx ├── components │ ├── Basic │ │ ├── Button.js │ │ ├── CheckBox.tsx │ │ ├── Modal.js │ │ ├── Pagination.js │ │ └── SearchInput.js │ ├── ConnectButton.tsx │ ├── CreditCard.tsx │ ├── DarkModeSwitcher.tsx │ ├── DropdownUser.tsx │ ├── FullConnectButton.tsx │ ├── MobileNavbar.tsx │ ├── Navbar.tsx │ ├── Table.tsx │ ├── button-scroll-to-bottom.tsx │ ├── chat-history.tsx │ ├── chat-list.tsx │ ├── chat-message-actions.tsx │ ├── chat-panel.tsx │ ├── chat-share-dialog.tsx │ ├── chat.tsx │ ├── chatui │ │ ├── account-card-skeleton.tsx │ │ ├── account-card.tsx │ │ ├── account-cards-skeleton.tsx │ │ ├── account-cards.tsx │ │ ├── account-detail-skeleton.tsx │ │ ├── account-detail.tsx │ │ ├── category-transaction-skeleton.tsx │ │ ├── category-transaction.tsx │ │ ├── message.tsx │ │ ├── recurring-transactions-skeleton.tsx │ │ ├── recurring-transactions.tsx │ │ └── spinner.tsx │ ├── clear-history.tsx │ ├── empty-screen.tsx │ ├── external-link.tsx │ ├── footer.tsx │ ├── markdown.tsx │ ├── prompt-form.tsx │ ├── providers.tsx │ ├── sidebar-actions.tsx │ ├── sidebar-desktop.tsx │ ├── sidebar-footer.tsx │ ├── sidebar-item.tsx │ ├── sidebar-items.tsx │ ├── sidebar-list.tsx │ ├── sidebar-mobile.tsx │ ├── sidebar-toggle.tsx │ ├── sidebar.tsx │ ├── tailwind-indicator.tsx │ ├── theme-toggle.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── codeblock.tsx │ │ ├── collapsible.tsx │ │ ├── combobox.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── icons.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ └── tooltip.tsx ├── hooks │ ├── actions.js │ ├── use-mobile.tsx │ ├── use-toast.ts │ ├── useGetAccounts.js │ ├── useGetAssets.js │ ├── useGetAuth.js │ ├── useGetBalance.js │ ├── useGetHoldings.js │ ├── useGetIdentity.js │ ├── useGetInvestTransactions.js │ ├── useGetLiabilities.js │ ├── useGetPayment.js │ ├── useGetTransactionsSync.js │ ├── useGetTransfer.js │ ├── usePlaidInit.js │ └── useUpdateSession.js ├── lib │ ├── analytics.ts │ ├── auth.js │ ├── chat │ │ └── actions.tsx │ ├── config.ts │ ├── db.ts │ ├── fonts.ts │ ├── hooks │ │ ├── use-at-bottom.tsx │ │ ├── use-copy-to-clipboard.tsx │ │ ├── use-enter-submit.tsx │ │ ├── use-local-storage.ts │ │ ├── use-scroll-anchor.tsx │ │ ├── use-sidebar.tsx │ │ └── use-streamable-text.ts │ ├── plaid.js │ ├── stripe.ts │ ├── types.ts │ └── utils.ts ├── middleware.js ├── server │ ├── auth.js │ ├── chat.js │ ├── plaid.js │ ├── transaction.js │ ├── twilio │ │ ├── retrievalAugmentedGeneration.js │ │ └── tools │ │ │ ├── expendituresByCategory.js │ │ │ ├── lib.js │ │ │ └── sumTransactions.js │ ├── user.js │ └── utils.js ├── store │ ├── actions │ │ ├── usePlaid.ts │ │ ├── useTheme.ts │ │ ├── useTransaction.ts │ │ └── useUser.ts │ ├── constants │ │ ├── plaidConstants.ts │ │ ├── themeConstants.ts │ │ ├── transactionConstants.ts │ │ └── userConstants.ts │ ├── index.ts │ └── reducers │ │ ├── plaidReducer.ts │ │ ├── themeReducer.ts │ │ ├── transactionReducer.ts │ │ └── userReducer.ts └── utils │ ├── apiCall.js │ ├── stripe-helpers.ts │ └── util.js ├── tailwind.config.js ├── tsconfig.json ├── types └── next-auth.d.ts └── vercel.json /.env.example: -------------------------------------------------------------------------------- 1 | NEXTAUTH_URL=http://localhost:3000/api/auth 2 | # Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32` 3 | NEXTAUTH_SECRET=your_nextauth_secret 4 | 5 | GOOGLE_CLIENT_ID=your_google_client_id 6 | GOOGLE_CLIENT_SECRET=your_google_client_secret 7 | 8 | # Stripe keys 9 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key 10 | STRIPE_SECRET_KEY=your_stripe_secret_key 11 | WEBHOOK_SECRET_KEY=your_webhook_secret_key 12 | 13 | # You must first activate a Billing Account here: https://platform.openai.com/account/billing/overview 14 | # Then get your OpenAI API Key here: https://platform.openai.com/account/api-keys 15 | OPENAI_API_KEY=your_openai_api_key 16 | 17 | # This was inserted by `prisma init`: 18 | # Environment variables declared in this file are automatically made available to Prisma. 19 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 20 | 21 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 22 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 23 | 24 | # vercel 25 | POSTGRES_PRISMA_URL=your_postgres_prisma_url 26 | 27 | # plaid 28 | PLAID_ENV=sandbox 29 | PLAID_CLIENT_ID=your_plaid_client_id 30 | PLAID_SECRET=your_plaid_secret 31 | PLAID_PRODUCTS=auth,transactions,investments,liabilities 32 | PLAID_COUNTRY_CODES=US,CA 33 | PLAID_REDIRECT_URI=your_plaid_redirect_uri 34 | 35 | ADMIN_EMAIL=your_admin_email 36 | 37 | KV_URL=your_kv_url 38 | KV_REST_API_URL=your_kv_rest_api_url 39 | KV_REST_API_TOKEN=your_kv_rest_api_token 40 | KV_REST_API_READ_ONLY_TOKEN=your_kv_rest_api_read_only_token -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ["images.unsplash.com", "via.placeholder.com", "lh3.googleusercontent.com"] 5 | } 6 | }; 7 | 8 | export default nextConfig; 9 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/font/Agbalumo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cameronking4/shadcn-openai-plaid-dashboard/5facc479a3df5c43338cd174fd84c4a3d81e68ac/public/font/Agbalumo.ttf -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/actions/auth.js: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import axios from "axios"; 4 | import { getServerSession } from "next-auth"; 5 | import { authOptions } from "@/lib/auth"; 6 | 7 | export async function getFullUserInfo() { 8 | const { user } = await getServerSession(authOptions); 9 | return user; 10 | } 11 | 12 | export async function getAccessToken() { 13 | const { accessToken } = await getServerSession(authOptions); 14 | return accessToken; 15 | } 16 | 17 | const CancelToken = axios.CancelToken; 18 | const source = CancelToken.source(); 19 | 20 | const apiCall = axios.create({ 21 | headers: { 22 | "Content-Type": "application/json", 23 | Accept: "application/json" 24 | }, 25 | timeout: 240000, 26 | cancelToken: source.token 27 | }); 28 | 29 | apiCall.interceptors.request.use(async config => { 30 | const session = await getServerSession(authOptions); 31 | const lang = global.window && window.location.href.split("/")[3]; 32 | 33 | if (session?.accessToken) { 34 | config.headers.Authorization = `Bearer ${session?.accessToken}`; 35 | } 36 | config.headers.lang = lang ? lang : "en"; 37 | return config; 38 | }); 39 | 40 | apiCall.interceptors.response.use(response => { 41 | return response; 42 | }); 43 | 44 | export default apiCall; -------------------------------------------------------------------------------- /src/app/actions/chat.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { revalidatePath } from 'next/cache' 4 | import { redirect } from 'next/navigation' 5 | import { type Chat } from '@/lib/types' 6 | import { getFullUserInfo } from './auth' 7 | import { kv } from '@vercel/kv' 8 | 9 | export async function getChats(userId?: string | null) { 10 | if (!userId) { 11 | return [] 12 | } 13 | 14 | try { 15 | const pipeline = kv.pipeline() 16 | const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, { 17 | rev: true 18 | }) 19 | 20 | for (const chat of chats) { 21 | pipeline.hgetall(chat) 22 | } 23 | 24 | const results = await pipeline.exec() 25 | 26 | return results as Chat[] 27 | } catch (err) { 28 | return [] 29 | } 30 | } 31 | 32 | export async function getChat(id: string, userId: string) { 33 | const chat = await kv.hgetall(`chat:${id}`) 34 | 35 | if (!chat || (userId && chat.userId !== userId)) { 36 | return null 37 | } 38 | 39 | return chat 40 | } 41 | 42 | export async function removeChat({ id, path }: { id: string; path: string }) { 43 | const session = await getFullUserInfo() 44 | 45 | if (!session) { 46 | return { 47 | error: 'Unauthorized' 48 | } 49 | } 50 | 51 | const uid = String(await kv.hget(`chat:${id}`, 'userId')) 52 | 53 | if (uid !== session?.id) { 54 | return { 55 | error: 'Unauthorized' 56 | } 57 | } 58 | 59 | await kv.del(`chat:${id}`) 60 | await kv.zrem(`user:chat:${session.id}`, `chat:${id}`) 61 | 62 | revalidatePath('/') 63 | return revalidatePath(path) 64 | } 65 | 66 | export async function clearChats() { 67 | const session = await getFullUserInfo(); 68 | 69 | if (!session) { 70 | return { 71 | error: 'Unauthorized' 72 | } 73 | } 74 | 75 | const chats: string[] = await kv.zrange(`user:chat:${session.id}`, 0, -1) 76 | if (!chats.length) { 77 | return redirect('/') 78 | } 79 | const pipeline = kv.pipeline() 80 | 81 | for (const chat of chats) { 82 | pipeline.del(chat) 83 | pipeline.zrem(`user:chat:${session.id}`, chat) 84 | } 85 | 86 | await pipeline.exec() 87 | 88 | revalidatePath('/dashboard/chat') 89 | return redirect('/dashboard/chat') 90 | } 91 | 92 | export async function saveChat(chat: Chat) { 93 | const session = await getFullUserInfo() 94 | 95 | if (session) { 96 | try { 97 | const pipeline = kv.pipeline() 98 | pipeline.hmset(`chat:${chat.id}`, chat) 99 | pipeline.zadd(`user:chat:${chat.userId}`, { 100 | score: Date.now(), 101 | member: `chat:${chat.id}` 102 | }) 103 | await pipeline.exec() 104 | } catch (err) { 105 | console.log(err) 106 | } 107 | } else { 108 | return 109 | } 110 | } 111 | 112 | export async function refreshHistory(path: string) { 113 | redirect(path) 114 | } 115 | 116 | export async function getMissingKeys() { 117 | const keysRequired = ['OPENAI_API_KEY'] 118 | return keysRequired 119 | .map(key => (process.env[key] ? '' : key)) 120 | .filter(key => key !== '') 121 | } 122 | -------------------------------------------------------------------------------- /src/app/actions/middleware.js: -------------------------------------------------------------------------------- 1 | export const authenticateRequest = (req) => { 2 | const apiKey = req.headers['x-api-key']; 3 | return apiKey === process.env.PUBLIC_API_KEY; 4 | }; -------------------------------------------------------------------------------- /src/app/actions/stripe.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { redirect } from "next/navigation"; 4 | import { headers } from "next/headers"; 5 | 6 | import { APP_PRICE, CURRENCY } from "@/lib/config"; 7 | import { formatAmountForStripe } from "@/utils/stripe-helpers"; 8 | import { stripe } from "@/lib/stripe"; 9 | import { getServerSession } from "next-auth"; 10 | import { authOptions } from "@/lib/auth"; 11 | import { getFullUserInfo, updateUserInfoServerSide } from "@/app/actions/user"; 12 | 13 | export async function createCheckoutSession() { 14 | const { user } = await getServerSession(authOptions); 15 | const checkoutSession = await stripe.checkout.sessions.create({ 16 | mode: "subscription", 17 | customer_email: user.email, 18 | line_items: [ 19 | { 20 | quantity: 1, 21 | price_data: { 22 | currency: CURRENCY, 23 | product_data: { 24 | name: "Subscribe to Qashboard" 25 | }, 26 | unit_amount: formatAmountForStripe(Number(APP_PRICE), CURRENCY), 27 | recurring: { 28 | interval: "month" 29 | } 30 | } 31 | } 32 | ], 33 | success_url: `${headers().get( 34 | "origin" 35 | )}/dashboard/checkout/success?success=true&session_id={CHECKOUT_SESSION_ID}`, 36 | cancel_url: `${headers().get("origin")}/dashboard/checkout?canceled=true` 37 | }); 38 | 39 | redirect(checkoutSession.url); 40 | } 41 | 42 | export async function cancelCheckoutSession() { 43 | const { user } = await getFullUserInfo(); 44 | const deleted = await stripe.subscriptions.cancel(user.subscription); 45 | 46 | if (deleted.status === "canceled") { 47 | await updateUserInfoServerSide({ 48 | userInfo: { isPro: false, subscription: null } 49 | }); 50 | redirect("/"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/actions/user.js: -------------------------------------------------------------------------------- 1 | import { getUserInfo, updateUserAccount } from "@/server/user"; 2 | 3 | export async function getFullUserInfo() { 4 | const userInfo = await getUserInfo(); 5 | return userInfo; 6 | } 7 | 8 | export async function updateUserInfoServerSide(data) { 9 | const { userInfo } = data; 10 | await updateUserAccount(userInfo); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/actions/userService.js: -------------------------------------------------------------------------------- 1 | import db from '@/lib/db'; 2 | 3 | export const getUserAccessToken = async (userId) => { 4 | const user = await db.user.findUnique({ 5 | where: { 6 | id: userId, 7 | }, 8 | select: { 9 | ACCESS_TOKEN: true, 10 | }, 11 | }); 12 | return user?.ACCESS_TOKEN || null; 13 | }; 14 | 15 | export const authenticateRequest = (req) => { 16 | const apiKey = req.headers['x-api-key']; 17 | return apiKey === process.env.PUBLIC_API_KEY; 18 | }; -------------------------------------------------------------------------------- /src/app/api/aiResponse/route.js: -------------------------------------------------------------------------------- 1 | import { OpenAI } from "openai"; 2 | import { getFullUserInfo } from "@/app/actions/user"; 3 | import { NextResponse } from "next/server"; 4 | 5 | async function GPT4(info, key) { 6 | const openai = new OpenAI({ apiKey: key, dangerouslyAllowBrowser: true }); 7 | 8 | const GPT4Message = [ 9 | { 10 | role: "system", 11 | content: 12 | "You are personal finance assistant. It's your job to write two short paragraphs containing insights for me to understand my financial position overall. KPIS are live account data and includes the data from last check in. Accounts have all my connected accounts (checkings, credit cards, etc). Provide insights by not just summarizing account balances but instead, providing meaningful analysis for me to take action and reflect on my current financial position. Include overview of balances, changes and a profile based on my spend." 13 | }, 14 | { 15 | role: "user", 16 | content: info 17 | } 18 | ]; 19 | 20 | const chat_res = await openai.chat.completions.create({ 21 | model: "gpt-4o", 22 | messages: GPT4Message, 23 | temperature: .5, 24 | max_tokens: 3000, 25 | }); 26 | 27 | // const stream = OpenAIStream(chat_res); 28 | return chat_res.choices[0]; 29 | } 30 | 31 | const handler = async (req, res) => { 32 | try { 33 | const { data } = req.body; 34 | const { user } = await getFullUserInfo(); 35 | const aiSummaryResponse = await GPT4(JSON.stringify(user), process.env.OPENAI_API_KEY); 36 | console.log(aiSummaryResponse); 37 | return NextResponse.json({ message: aiSummaryResponse.message.content }, { status: 200 }); 38 | } catch (err) { 39 | return NextResponse.json({ message: err?.error?.message }, { status: 403 }); 40 | } 41 | }; 42 | 43 | export { handler as POST }; 44 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.js: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import { authOptions } from "@/lib/auth"; 3 | 4 | const handler = NextAuth(authOptions); 5 | 6 | export { handler as GET, handler as POST }; 7 | -------------------------------------------------------------------------------- /src/app/api/public/user/[userId]/accounts/route.ts: -------------------------------------------------------------------------------- 1 | import { accounts } from '@/app/actions/plaidService'; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function GET(req: Request, { params }: { params: { userId: string } }) { 5 | const { userId } = params; // Extract userId from dynamic route 6 | 7 | // Validate API Key from Headers 8 | const apiKey = req.headers.get('x-api-key'); 9 | const VALID_API_KEY = process.env.PUBLIC_API_KEY; // This is the API key you defined in the .env file 10 | if (apiKey !== VALID_API_KEY) { 11 | return NextResponse.json({ 12 | message: "Unauthorized", 13 | status: 401, 14 | }); 15 | } 16 | 17 | if (!userId) { 18 | return NextResponse.json({ 19 | message: "Missing userId in request.", 20 | status: 400, 21 | }); 22 | } 23 | 24 | try { 25 | const data = await accounts(userId); 26 | return NextResponse.json({ 27 | message: "Authorized", 28 | data: data, 29 | status: 200, 30 | }); 31 | } catch (error) { 32 | console.error('Error fetching user data:', error); 33 | return NextResponse.json({ 34 | message: "Failed to fetch user data.", 35 | status: 500, 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/api/public/user/[userId]/transactions/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { transactionsSyncAll } from '@/app/actions/plaidService'; 3 | import db from '@/lib/db'; 4 | 5 | export async function GET(req: Request, { params }: { params: { userId: string } }) { 6 | const { userId } = params; // Extract userId from dynamic route 7 | 8 | // Validate API Key from Headers 9 | const apiKey = req.headers.get('x-api-key'); 10 | const VALID_API_KEY = process.env.PUBLIC_API_KEY; // This is the API key you defined in the .env file 11 | if (apiKey !== VALID_API_KEY) { 12 | return NextResponse.json({ 13 | message: "Unauthorized", 14 | status: 401, 15 | }); 16 | } 17 | 18 | if (!userId) { 19 | return NextResponse.json({ 20 | message: "Missing userId in request.", 21 | status: 400, 22 | }); 23 | } 24 | 25 | try { 26 | // First sync the transactions 27 | await transactionsSyncAll(userId); 28 | 29 | // Now fetch all transactions for the user from the database 30 | const transactions = await db.transaction.findMany({ 31 | where: { 32 | userId: userId 33 | }, 34 | include: { 35 | personal_finance_category: true 36 | }, 37 | orderBy: { 38 | date: 'desc' 39 | } 40 | }); 41 | 42 | return NextResponse.json({ 43 | message: "Authorized", 44 | data: transactions, 45 | status: 200, 46 | }); 47 | } catch (error) { 48 | console.error('Error fetching transactions:', error); 49 | return NextResponse.json({ 50 | message: "Failed to fetch transactions.", 51 | status: 500, 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/api/v1/chat/[id]/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { 3 | getChatInfoById, 4 | deleteChatChannel, 5 | } from "../../../../../server/chat"; 6 | 7 | export async function GET(req, { params }) { 8 | try { 9 | const data = await getChatInfoById(params.id); 10 | return NextResponse.json(data); 11 | } catch (err) { 12 | return NextResponse.json({ 13 | message: err.message, 14 | status: 500, 15 | }); 16 | } 17 | } 18 | 19 | export async function DELETE(req, { params }) { 20 | try { 21 | const data = await deleteChatChannel(params.id); 22 | return NextResponse.json(data); 23 | } catch (err) { 24 | return NextResponse.json({ 25 | message: err.message, 26 | status: 500, 27 | }); 28 | } 29 | } -------------------------------------------------------------------------------- /src/app/api/v1/chat/all/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { 3 | clearChatHistory 4 | } from "../../../../../server/chat"; 5 | 6 | export async function DELETE(req) { 7 | try { 8 | const data = await clearChatHistory(); 9 | return NextResponse.json(data); 10 | } catch (err) { 11 | return NextResponse.json({ 12 | message: err.message, 13 | status: 500, 14 | }); 15 | } 16 | } -------------------------------------------------------------------------------- /src/app/api/v1/chat/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { getChatInfo } from "../../../../server/chat"; 3 | 4 | export async function GET(req) { 5 | try { 6 | const data = await getChatInfo(); 7 | return NextResponse.json(data); 8 | } catch (err) { 9 | return NextResponse.json({ 10 | message: err.message, 11 | status: 500, 12 | }); 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/api/v1/plaid/accounts/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { accounts } from "../../../../../server/plaid"; 3 | 4 | export async function GET(req) { 5 | try { 6 | const data = await accounts(); 7 | return NextResponse.json(data); 8 | } catch (err) { 9 | return NextResponse.json({ 10 | message: err.message, 11 | status: 500, 12 | }); 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/api/v1/plaid/categories/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { getAllCategories } from "../../../../../server/plaid"; 3 | 4 | export async function GET(req) { 5 | try { 6 | const data = await getAllCategories(); 7 | return NextResponse.json(data); 8 | } catch (err) { 9 | return NextResponse.json({ 10 | message: err.message, 11 | status: 500, 12 | }); 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/api/v1/plaid/create_link_token/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { createLinkToken } from "../../../../../server/plaid"; 3 | 4 | export async function GET(req) { 5 | try { 6 | const data = await createLinkToken(); 7 | return NextResponse.json(data); 8 | } catch (err) { 9 | return NextResponse.json({ 10 | message: err.message, 11 | status: 500, 12 | }); 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/api/v1/plaid/set_access_token/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { setAccessToken } from "../../../../../server/plaid"; 3 | 4 | export async function POST(req) { 5 | try { 6 | const reqInfo = await req.json(); 7 | const data = await setAccessToken(reqInfo); 8 | return NextResponse.json(data); 9 | } catch (err) { 10 | return NextResponse.json({ 11 | message: err.message, 12 | status: 500, 13 | }); 14 | } 15 | } -------------------------------------------------------------------------------- /src/app/api/v1/plaid/transactions/all/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { transactionsSyncAll } from "../../../../../../server/plaid"; 3 | 4 | export async function GET(req) { 5 | try { 6 | const data = await transactionsSyncAll(); 7 | return NextResponse.json(data); 8 | } catch (err) { 9 | return NextResponse.json({ 10 | message: err.message, 11 | status: 500, 12 | }); 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/api/v1/plaid/transactions/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { transactions } from "../../../../../server/plaid"; 3 | 4 | export async function GET(req) { 5 | try { 6 | const data = await transactions(); 7 | return NextResponse.json(data); 8 | } catch (err) { 9 | return NextResponse.json({ 10 | message: err.message, 11 | status: 500, 12 | }); 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/api/v1/transaction/getData/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { getTransaction } from "../../../../../server/transaction"; 3 | 4 | export async function POST(req) { 5 | try { 6 | const { filter } = await req.json(); 7 | const data = await getTransaction(filter); 8 | console.log(JSON.stringify(data, null, 2)); 9 | return NextResponse.json(data); 10 | } catch (err) { 11 | return NextResponse.json({ 12 | message: err.message, 13 | status: 500, 14 | }); 15 | } 16 | } -------------------------------------------------------------------------------- /src/app/api/v1/user/charts/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { getChartInfo } from "../../../../../server/user"; 3 | 4 | export async function POST(req) { 5 | try { 6 | const reqInfo = await req.json(); 7 | const data = await getChartInfo(reqInfo); 8 | return NextResponse.json(data); 9 | } catch (err) { 10 | return NextResponse.json({ 11 | message: err.message, 12 | status: 500, 13 | }); 14 | } 15 | } -------------------------------------------------------------------------------- /src/app/api/v1/user/dashboard/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { getDashboard } from "../../../../../server/user"; 3 | 4 | export async function GET(req) { 5 | try { 6 | const data = await getDashboard(); 7 | return NextResponse.json(data); 8 | } catch (err) { 9 | return NextResponse.json({ 10 | message: err.message, 11 | status: 500, 12 | }); 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/api/v1/user/item/[id]/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { deleteItemInfoById } from "../../../../../../server/user"; 3 | 4 | export async function DELETE(req, { params }) { 5 | try { 6 | const data = await deleteItemInfoById(params.id); 7 | return NextResponse.json(data); 8 | } catch (err) { 9 | return NextResponse.json({ 10 | message: err.message, 11 | status: 500, 12 | }); 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/api/v1/user/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { 3 | getUserInfo, 4 | updateUserAccount, 5 | deleteUserAccount, 6 | } from "../../../../server/user"; 7 | 8 | export async function GET(req) { 9 | try { 10 | const data = await getUserInfo(); 11 | return NextResponse.json(data); 12 | } catch (err) { 13 | return NextResponse.json({ 14 | message: err.message, 15 | status: 500, 16 | }); 17 | } 18 | } 19 | 20 | export async function POST(req) { 21 | try { 22 | const { userInfo } = await req.json(); 23 | await updateUserAccount(userInfo); 24 | return NextResponse.json({ 25 | message: "User account updated successfully", 26 | status: 200, 27 | }); 28 | } catch (err) { 29 | return NextResponse.json({ 30 | message: err.message, 31 | status: 500, 32 | }); 33 | } 34 | } 35 | 36 | export async function DELETE(req) { 37 | try { 38 | const data = await deleteUserAccount(); 39 | return NextResponse.json(data); 40 | } catch (err) { 41 | return NextResponse.json({ 42 | message: err.message, 43 | status: 500, 44 | }); 45 | } 46 | } -------------------------------------------------------------------------------- /src/app/api/v1/user/users/pay/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { setUserPayByEmail } from "../../../../../../server/user"; 3 | 4 | export async function POST(req) { 5 | try { 6 | const reqInfo = await req.json(); 7 | const data = await setUserPayByEmail(reqInfo); 8 | return NextResponse.json(data); 9 | } catch (err) { 10 | return NextResponse.json({ 11 | message: err.message, 12 | status: 500, 13 | }); 14 | } 15 | } -------------------------------------------------------------------------------- /src/app/api/v1/user/users/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { getAllUsers } from "../../../../../server/user"; 3 | 4 | export async function POST(req) { 5 | try { 6 | const { filter } = await req.json(); 7 | const data = await getAllUsers(filter); 8 | return NextResponse.json(data); 9 | } catch (err) { 10 | return NextResponse.json({ 11 | message: err.message, 12 | status: 500, 13 | }); 14 | } 15 | } -------------------------------------------------------------------------------- /src/app/dashboard/charts/Summary.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardHeader, 4 | CardTitle, 5 | CardDescription, 6 | } from "@/components/ui/card"; 7 | import { useSelector } from "react-redux"; 8 | import { RootState } from "../../../store"; 9 | 10 | const Summary = (): JSX.Element => { 11 | const { analyzeSummary } = useSelector((state: RootState) => state.user); 12 | return ( 13 | 14 |
15 |                 

{analyzeSummary}

16 |
17 |
18 | {analyzeSummary?.length < 50 ? ( 19 | 26 | 27 | 28 | 29 | 30 | 38 | 39 | 40 | 41 | 42 | ) : null} 43 |
44 | ) 45 | } 46 | 47 | export default Summary 48 | -------------------------------------------------------------------------------- /src/app/dashboard/charts/recurringSpend/RecurringSpend.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import { 3 | BarList, 4 | } from "@tremor/react"; 5 | import { FC } from 'react'; 6 | 7 | function filterAndSortBarListData(barListData: any[], searchQuery: string) { 8 | const filteredData = barListData.filter((item: { name: string | null; }) => item.name !== null && item.name.toLowerCase().includes(searchQuery.toLowerCase())); 9 | 10 | const sortedFilteredData = filteredData.sort((a: { value: number; }, b: { value: number; }) => a.value - b.value); 11 | 12 | return sortedFilteredData; 13 | } 14 | 15 | interface RecurringSpendProps { 16 | searchQuery: string; 17 | } 18 | 19 | const RecurringSpend: FC = ({ 20 | searchQuery 21 | }) => { 22 | const { 23 | barListData 24 | } = useSelector((state: any) => state.user); 25 | 26 | const filteredpages = filterAndSortBarListData(barListData, searchQuery); 27 | 28 | return ( 29 |
30 | 35 |
36 | ) 37 | } 38 | 39 | export default RecurringSpend; 40 | -------------------------------------------------------------------------------- /src/app/dashboard/charts/spendByCategory/SpendCategory.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import { 3 | BarList 4 | } from "@tremor/react"; 5 | import { RootState } from "@/store"; 6 | 7 | const SpendCategory = ({ 8 | searchQuery 9 | }: { 10 | searchQuery: string 11 | }) => { 12 | const { 13 | donutAsBarData, 14 | } = useSelector((state: RootState) => state.user); 15 | 16 | const barListChartConfig = { 17 | value: { 18 | label: "Value", 19 | color: "hsl(var(--chart-2))", 20 | }, 21 | name: { 22 | label: "Name", 23 | color: "hsl(var(--chart-2))", 24 | }, 25 | label: { 26 | color: "hsl(var(--background))", 27 | }, 28 | } 29 | 30 | return ( 31 |
32 | 34 | item.name !== null && 35 | item.name 36 | .toLowerCase() 37 | .includes(searchQuery.toLowerCase()) 38 | )} 39 | className="mr-4 sm:min-w-full" 40 | showAnimation={true} 41 | /> 42 |
43 | ) 44 | } 45 | 46 | export default SpendCategory; 47 | -------------------------------------------------------------------------------- /src/app/dashboard/charts/spendByCategory/TransactionsByCategory.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useSelector } from "react-redux"; 3 | import { 4 | Button, 5 | Card, 6 | Flex, 7 | Title, 8 | Text, 9 | Bold, 10 | BarList, 11 | } from "@tremor/react"; 12 | import { 13 | ArrowNarrowRightIcon, 14 | } from "@heroicons/react/solid"; 15 | import { RootState } from "@/store"; 16 | 17 | const TransactionsByCategory = () => { 18 | const { 19 | chartDataByMonth, 20 | filterDate, 21 | selectedAccounts, 22 | } = useSelector((state: RootState) => state.user); 23 | 24 | return ( 25 | 26 | Transactions by Category 27 | Types of purchases made 28 | 29 | 30 | Merchant 31 | 32 | 33 | Total Spend 34 | 35 | 36 | 41 | 42 | 50 | 59 | 60 | 61 |
62 |
63 | ) 64 | } 65 | 66 | export default TransactionsByCategory; 67 | -------------------------------------------------------------------------------- /src/app/dashboard/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from 'next' 2 | import { notFound, redirect } from 'next/navigation' 3 | 4 | // import { auth } from '@/auth' 5 | import { getFullUserInfo } from '@/app/actions/auth' 6 | import { getChat, getMissingKeys } from '@/app/actions/chat' 7 | import { Chat } from '@/components/chat' 8 | import { AI } from '@/lib/chat/actions' 9 | import { UserSession } from '@/lib/types' 10 | 11 | export interface ChatPageProps { 12 | params: { 13 | id: string 14 | } 15 | } 16 | 17 | export async function generateMetadata({ 18 | params 19 | }: ChatPageProps): Promise { 20 | // const session = await auth() 21 | const session = await getFullUserInfo(); 22 | 23 | if (!session?.id) { 24 | return {} 25 | } 26 | 27 | const chat = await getChat(params.id, session.id) 28 | return { 29 | title: chat?.title.toString().slice(0, 50) ?? 'Chat' 30 | } 31 | } 32 | 33 | export default async function ChatPage({ params }: ChatPageProps) { 34 | // const session = (await auth()) as Session 35 | const session = await getFullUserInfo(); 36 | const missingKeys = await getMissingKeys() 37 | 38 | if (!session?.id) { 39 | redirect(`/login?next=/dashboard/chat/${params.id}`) 40 | } 41 | 42 | const userId = session.id as string 43 | const chat = await getChat(params.id, userId) 44 | 45 | if (!chat) { 46 | redirect('/dashboard/chat') 47 | } 48 | 49 | // if (chat?.userId !== session?.id) { 50 | // notFound() 51 | // } 52 | 53 | return ( 54 | 55 | 61 | 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /src/app/dashboard/chat/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarDesktop } from '@/components/sidebar-desktop' 2 | 3 | interface ChatLayoutProps { 4 | children: React.ReactNode 5 | } 6 | 7 | export default async function ChatLayout({ children }: ChatLayoutProps) { 8 | return ( 9 |
10 | 11 | {children} 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/app/dashboard/chat/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation' 2 | 3 | export default async function NewPage() { 4 | redirect('/dashboard/chat') 5 | } 6 | -------------------------------------------------------------------------------- /src/app/dashboard/chat/page.tsx: -------------------------------------------------------------------------------- 1 | import { nanoid } from '@/lib/utils' 2 | import { Chat } from '@/components/chat' 3 | import { AI } from '@/lib/chat/actions' 4 | // import { auth } from '@/lib/auth' 5 | import { UserSession } from '@/lib/types' 6 | import { getMissingKeys } from '@/app/actions/chat'; 7 | import { getFullUserInfo } from '@/app/actions/auth'; 8 | 9 | export default async function IndexPage() { 10 | const id = nanoid() 11 | // const session = await auth() 12 | const session = await getFullUserInfo(); 13 | const missingKeys = await getMissingKeys() 14 | 15 | return ( 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Navbar from "@/components/Navbar"; 4 | import MobileNavbar from "@/components/MobileNavbar"; 5 | import { Toaster } from "sonner"; 6 | 7 | import { ReactNode } from "react"; 8 | 9 | interface RootLayoutProps { 10 | children: ReactNode; 11 | } 12 | 13 | export default function RootLayout({ children }: RootLayoutProps) { 14 | return ( 15 | <> 16 |
17 |
18 | 19 | {children} 20 | 21 |
22 |
23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cameronking4/shadcn-openai-plaid-dashboard/5facc479a3df5c43338cd174fd84c4a3d81e68ac/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import { agbalumo, inter } from "@/lib/fonts"; 3 | import { ReactNode } from 'react'; 4 | 5 | interface RootLayoutProps { 6 | children: ReactNode; 7 | } 8 | 9 | export const metadata = { 10 | description: 11 | "Connect all of your accounts to browse transactions, analyze spend & use your personal AI assistant to ask Q&A for precise insights to your financial position", 12 | title: "Plaid AI Dashboard | Personal Finance AI Assistant", 13 | icons: { 14 | icon: '/favicon.ico', 15 | } 16 | }; 17 | 18 | export default function RootLayout({ children }: RootLayoutProps) { 19 | return ( 20 | 21 | {children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/template.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import store from "../store"; 4 | import { SessionProvider } from "next-auth/react"; 5 | import { Provider } from "react-redux"; 6 | import { Toaster } from "react-hot-toast"; 7 | import { Providers } from "@/components/providers"; 8 | 9 | import { ReactNode } from "react"; 10 | 11 | interface TemplateProps { 12 | children: ReactNode; 13 | } 14 | 15 | export default function Template({ children }: TemplateProps) { 16 | return ( 17 | 18 | 19 | 20 | 25 | {children} 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/Basic/Button.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | 5 | const Button = ({ 6 | type, 7 | disabled, 8 | onClick, 9 | name, 10 | href, 11 | className, 12 | ...rest 13 | }) => { 14 | return ( 15 | <> 16 | {href && href !== "" ? ( 17 | 24 | {rest.children} 25 | 26 | ) : ( 27 | 37 | )} 38 | 39 | ); 40 | }; 41 | 42 | export default Button; 43 | -------------------------------------------------------------------------------- /src/components/Basic/CheckBox.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // import { Checkbox } from "@heroicons/react/solid"; 3 | 4 | const Checkbox = ({ 5 | label, 6 | name, 7 | checked, 8 | handleChange, 9 | disabled, 10 | className 11 | } : { 12 | label?: string; 13 | name?: string; 14 | checked: boolean; 15 | handleChange: (event: React.FormEvent) => void; 16 | disabled?: boolean; 17 | className?: string; 18 | 19 | }) => { 20 | return ( 21 | 33 | ); 34 | }; 35 | 36 | export default Checkbox; 37 | -------------------------------------------------------------------------------- /src/components/Basic/Modal.js: -------------------------------------------------------------------------------- 1 | export default function Modal({ 2 | showModal, 3 | setShowModal, 4 | type, 5 | title, 6 | content, 7 | onOk, 8 | }) { 9 | return ( 10 | <> 11 | {showModal && ( 12 |
13 |
setShowModal(false)} 16 | /> 17 |
18 |
19 |
20 | {type === "delete" ? ( 21 |
22 | 28 | 33 | 34 |
35 | ) : ( 36 |
37 | 43 | 48 | 49 |
50 | )} 51 |
52 |

53 | {title} 54 |

55 |

56 | {content} 57 |

58 |
59 | 65 | 71 |
72 |
73 |
74 |
75 |
76 |
77 | )} 78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/components/Basic/SearchInput.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextInput } from "@tremor/react"; 3 | import { SearchIcon } from "@heroicons/react/solid"; 4 | 5 | export default function SearchInput({ onSearch, ...rest }) { 6 | const handleEnterKeyDown = (e) => { 7 | var keycode = e.keyCode ? e.keyCode : e.which; 8 | if (keycode == 13) { 9 | onSearch(); 10 | } 11 | }; 12 | 13 | return ( 14 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/ConnectButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCallback } from "react"; 4 | import { usePlaidLink } from "react-plaid-link"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { setPlaidState } from "@/store/actions/usePlaid"; 7 | import apiCall from "@/utils/apiCall"; 8 | import { setUserInfoState } from "@/store/actions/useUser"; 9 | import { isEmpty } from "@/utils/util"; 10 | import { RootState } from "@/store"; 11 | import { AnyAction } from 'redux'; 12 | import { Dispatch } from 'redux'; 13 | import { Button } from "./ui/button"; 14 | 15 | const ConnectButton = ({ children, type, setShowConnectModal }: { children: React.ReactNode, type: string | number, setShowConnectModal: (show: boolean) => void }) => { 16 | const { linkToken } = useSelector((state: RootState) => state.plaid); 17 | const { items: linkInfo } = useSelector((state: RootState) => state.user); 18 | 19 | const dispatch = useDispatch>(); 20 | 21 | const onSuccess = useCallback( 22 | (public_token: any, metadata: any) => { 23 | const exchangePublicTokenForAccessToken = async () => { 24 | const response = await apiCall.post("/api/v1/plaid/set_access_token", { public_token, metadata, type }); 25 | if (response.status !== 200) { 26 | dispatch(setPlaidState({ isItemAccess: false }) as unknown as AnyAction); 27 | return; 28 | } 29 | const { isItemAccess, item_id, accounts } = response.data; 30 | dispatch(setPlaidState({ isItemAccess: isItemAccess }) as unknown as AnyAction); 31 | if (!isEmpty(item_id)) { 32 | dispatch( 33 | setUserInfoState({ 34 | items: [...linkInfo, { ...metadata, accounts }] 35 | }) 36 | ); 37 | } 38 | setShowConnectModal(false); 39 | }; 40 | exchangePublicTokenForAccessToken(); 41 | }, 42 | [dispatch, linkInfo, setShowConnectModal, type] 43 | ); 44 | 45 | const config = { 46 | token: !isEmpty(linkToken) ? linkToken[type].link_token : null, 47 | onSuccess 48 | }; 49 | 50 | const { open, ready } = usePlaidLink(config); 51 | 52 | const handleOpenPlaidLink = () => { 53 | dispatch(setPlaidState({ isItemAccess: false, linkSuccess: false }) as unknown as AnyAction); 54 | open(); 55 | }; 56 | 57 | const setupConfig = { 58 | token: !isEmpty(linkToken) ? linkToken[type].link_token : "Token not found", 59 | open: open, 60 | ready: ready, 61 | onSuccess: onSuccess, 62 | linkInfo: linkInfo, 63 | } 64 | 65 | console.log(setupConfig); 66 | 67 | return ( 68 | 74 | ); 75 | }; 76 | 77 | export default ConnectButton; 78 | -------------------------------------------------------------------------------- /src/components/CreditCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CreditCardComponent = () => { 4 | return ( 5 |
6 |

How should I tackle my credit card debt?

7 |

We can help with that.

8 | 9 |
10 |
Slope
11 |

PATRIC BATEMAN

12 |

0004 4889 3989 5660

13 |
14 |
15 | ); 16 | } 17 | 18 | export default CreditCardComponent; -------------------------------------------------------------------------------- /src/components/FullConnectButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogHeader, 8 | DialogTitle 9 | } from "./ui/dialog"; 10 | import { PlusCircleIcon, PlusIcon } from "lucide-react"; 11 | import ConnectButton from "./ConnectButton"; 12 | import { Button } from "./ui/button"; 13 | const ConnectButtonModal = () => { 14 | const [showConnectModal, setShowConnectModal] = useState(false); 15 | 16 | return ( 17 |
18 | 28 | 33 | 34 | 35 | 36 | Connect Account 37 | 38 | 42 | Connect Bank Account 43 | 44 | 48 | Connect Credit Card or Loan 49 | 50 | 54 | Connect Investment 55 | 56 | 57 | 58 | 59 |
60 | ); 61 | }; 62 | 63 | export default ConnectButtonModal; 64 | -------------------------------------------------------------------------------- /src/components/MobileNavbar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Link from "next/link"; 3 | import { useEffect, useCallback } from "react"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import DarkModeSwitcher from "./DarkModeSwitcher"; 6 | import DropdownUser from "./DropdownUser"; 7 | import usePlaidInit from "@/hooks/usePlaidInit"; 8 | import useGetTransactionsSync from "@/hooks/useGetTransactionsSync"; 9 | import useGetAccounts from "@/hooks/useGetAccounts"; 10 | import { getUserInfo } from "@/store/actions/useUser"; 11 | import { usePathname } from "next/navigation"; 12 | 13 | const Navbar = () => { 14 | const { isTransactionsLoaded } = useSelector((state: { plaid: { isTransactionsLoaded: boolean } }) => state.plaid); 15 | 16 | const pathname = usePathname(); 17 | 18 | const navItems = [ 19 | { 20 | label: "Home", 21 | href: "/dashboard" 22 | }, 23 | { 24 | label: "Chat", 25 | href: "/dashboard/chat" 26 | }, 27 | { 28 | label: "Explore", 29 | href: "/dashboard/transaction" 30 | }, 31 | // { 32 | // label: "Analyze", 33 | // href: "/dashboard/charts" 34 | // } 35 | ]; 36 | 37 | return ( 38 | 58 | ); 59 | }; 60 | 61 | export default Navbar; 62 | -------------------------------------------------------------------------------- /src/components/Table.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/button-scroll-to-bottom.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { cn } from '@/lib/utils' 6 | import { Button, type ButtonProps } from '@/components/ui/button' 7 | import { IconArrowDown } from '@/components/ui/icons' 8 | 9 | interface ButtonScrollToBottomProps extends ButtonProps { 10 | isAtBottom: boolean 11 | scrollToBottom: () => void 12 | } 13 | 14 | export function ButtonScrollToBottom({ 15 | className, 16 | isAtBottom, 17 | scrollToBottom, 18 | ...props 19 | }: ButtonScrollToBottomProps) { 20 | return ( 21 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/components/chat-history.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import Link from 'next/link' 4 | 5 | import { cn } from '@/lib/utils' 6 | import { SidebarList } from '@/components/sidebar-list' 7 | import { buttonVariants } from '@/components/ui/button' 8 | import { PlusCircleIcon } from 'lucide-react' 9 | 10 | interface ChatHistoryProps { 11 | userId?: string 12 | } 13 | 14 | export async function ChatHistory({ userId }: ChatHistoryProps) { 15 | return ( 16 |
17 |
18 |

Chat History

19 |
20 |
21 | 28 | 29 | New Chat 30 | 31 |
32 | 35 | {Array.from({ length: 10 }).map((_, i) => ( 36 |
40 | ))} 41 |
42 | } 43 | > 44 | {/* @ts-ignore */} 45 | 46 |
47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from '@/components/ui/separator' 2 | import { UIState } from '@/lib/chat/actions' 3 | import { UserSession } from '@/lib/types' 4 | import Link from 'next/link' 5 | import { ExclamationTriangleIcon } from '@radix-ui/react-icons' 6 | 7 | export interface ChatList { 8 | messages: UIState 9 | session?: UserSession 10 | isShared: boolean 11 | } 12 | 13 | export function ChatList({ messages, session, isShared }: ChatList) { 14 | if (!messages.length) { 15 | return null 16 | } 17 | 18 | return ( 19 |
20 | {/* {!isShared && !session ? ( 21 | <> 22 |
23 |
24 | 25 |
26 |
27 |

28 | Please{' '} 29 | 30 | log in 31 | {' '} 32 | or{' '} 33 | 34 | sign up 35 | {' '} 36 | to save and revisit your chat history! 37 |

38 |
39 |
40 | 41 | 42 | ) : null} */} 43 | 44 | {messages.map((message, index) => ( 45 |
46 | {message.display} 47 | {index < messages.length - 1 && } 48 |
49 | ))} 50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/components/chat-message-actions.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { type Message } from 'ai' 4 | 5 | import { Button } from '@/components/ui/button' 6 | import { IconCheck, IconCopy } from '@/components/ui/icons' 7 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' 8 | import { cn } from '@/lib/utils' 9 | 10 | interface ChatMessageActionsProps extends React.ComponentProps<'div'> { 11 | message: Message 12 | } 13 | 14 | export function ChatMessageActions({ 15 | message, 16 | className, 17 | ...props 18 | }: ChatMessageActionsProps) { 19 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) 20 | 21 | const onCopy = () => { 22 | if (isCopied) return 23 | copyToClipboard(message.content) 24 | } 25 | 26 | return ( 27 |
34 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/chat-share-dialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import type { DialogProps } from '@radix-ui/react-dialog'; 5 | import { toast } from 'sonner' 6 | 7 | import { ServerActionResult, type Chat } from '@/lib/types' 8 | import { Button } from '@/components/ui/button' 9 | import { 10 | Dialog, 11 | DialogContent, 12 | DialogDescription, 13 | DialogFooter, 14 | DialogHeader, 15 | DialogTitle 16 | } from '@/components/ui/dialog' 17 | import { IconSpinner } from '@/components/ui/icons' 18 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' 19 | 20 | interface ChatShareDialogProps extends DialogProps { 21 | chat: Pick 22 | shareChat: (id: string) => ServerActionResult 23 | onCopy: () => void 24 | } 25 | 26 | export function ChatShareDialog({ 27 | chat, 28 | shareChat, 29 | onCopy, 30 | ...props 31 | }: ChatShareDialogProps) { 32 | const { copyToClipboard } = useCopyToClipboard({ timeout: 1000 }) 33 | const [isSharePending, startShareTransition] = React.useTransition() 34 | 35 | const copyShareLink = React.useCallback( 36 | async (chat: Chat) => { 37 | if (!chat.sharePath) { 38 | return toast.error('Could not copy share link to clipboard') 39 | } 40 | 41 | const url = new URL(window.location.href) 42 | url.pathname = chat.sharePath 43 | copyToClipboard(url.toString()) 44 | onCopy() 45 | toast.success('Share link copied to clipboard') 46 | }, 47 | [copyToClipboard, onCopy] 48 | ) 49 | 50 | return ( 51 | 52 | 53 | 54 | Share link to chat 55 | 56 | Anyone with the URL will be able to view the shared chat. 57 | 58 | 59 |
60 |
{chat.title}
61 |
62 | {chat.messages.length} messages 63 |
64 |
65 | 66 | 91 | 92 |
93 |
94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /src/components/chat.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { cn } from '@/lib/utils' 4 | import { ChatList } from '@/components/chat-list' 5 | import { ChatPanel } from '@/components/chat-panel' 6 | import { EmptyScreen } from '@/components/empty-screen' 7 | import { useLocalStorage } from '@/lib/hooks/use-local-storage' 8 | import { useEffect, useState } from 'react' 9 | import { useUIState, useAIState } from 'ai/rsc' 10 | import { UserSession } from '@/lib/types' 11 | import { usePathname, useRouter } from 'next/navigation' 12 | import { Message } from '@/lib/chat/actions' 13 | import { useScrollAnchor } from '@/lib/hooks/use-scroll-anchor' 14 | import { toast } from 'sonner' 15 | 16 | export interface ChatProps extends React.ComponentProps<'div'> { 17 | initialMessages?: Message[] 18 | id?: string 19 | session?: UserSession 20 | missingKeys: string[] 21 | } 22 | 23 | export function Chat({ id, className, session, missingKeys }: ChatProps) { 24 | const router = useRouter() 25 | const path = usePathname() 26 | const [input, setInput] = useState('') 27 | const [messages] = useUIState() 28 | const [aiState] = useAIState() 29 | 30 | const [_, setNewChatId] = useLocalStorage('newChatId', id) 31 | 32 | useEffect(() => { 33 | if (session) { 34 | if (path === `/dashboard/chat` && messages.length === 1) { 35 | window.history.replaceState({}, '', `/dashboard/chat/${id}`) 36 | } 37 | } 38 | }, [id, path, session, messages]) 39 | 40 | useEffect(() => { 41 | const messagesLength = aiState.messages?.length 42 | if (messagesLength === 2) { 43 | setTimeout(() => { 44 | router.refresh() 45 | }, 1000) 46 | } 47 | }, [aiState.messages, router]) 48 | 49 | useEffect(() => { 50 | setNewChatId(id) 51 | }) 52 | 53 | useEffect(() => { 54 | missingKeys.map(key => { 55 | toast.error(`Missing ${key} environment variable!`) 56 | }) 57 | }, [missingKeys]) 58 | 59 | const { messagesRef, scrollRef, visibilityRef, isAtBottom, scrollToBottom } = 60 | useScrollAnchor() 61 | 62 | return ( 63 |
67 |
71 | {messages.length ? ( 72 | 73 | ) : ( 74 | 75 | )} 76 |
77 |
78 | 85 |
86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /src/components/chatui/account-card-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from '@tremor/react'; 2 | 3 | const AccountCardSkeleton = () => { 4 | return ( 5 | 10 |
11 |
12 |
13 |
14 |
15 | 16 |
17 | ) 18 | } 19 | 20 | export default AccountCardSkeleton; -------------------------------------------------------------------------------- /src/components/chatui/account-card.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from '@tremor/react'; 2 | import { OfficeBuildingIcon, CurrencyDollarIcon } from '@heroicons/react/outline'; 3 | 4 | export interface Account { 5 | name: string; 6 | type: string; 7 | balance: number; 8 | available: number; 9 | } 10 | 11 | const AccountCard = ({ props: account }: { props: Account }) => { 12 | return ( 13 | 18 |
19 | 20 |
21 |

{account.name}

22 |

{account.type}

23 |
24 |
25 |
26 | 27 |

Balance: {account.balance}

28 |
29 |
30 | 31 |

Available: {account.available}

32 |
33 |
34 | ) 35 | } 36 | 37 | export default AccountCard; -------------------------------------------------------------------------------- /src/components/chatui/account-cards-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import AccountCardSkeleton from "./account-card-skeleton" 2 | 3 | const AccountCardsSkeleton = () => { 4 | return ( 5 |
6 | 7 | 8 |
9 | ) 10 | } 11 | 12 | export default AccountCardsSkeleton -------------------------------------------------------------------------------- /src/components/chatui/account-cards.tsx: -------------------------------------------------------------------------------- 1 | import AccountCard, { type Account } from "./account-card" 2 | 3 | const AccountCards = ({ props: accounts }: { props: Account[] }) => { 4 | return ( 5 |
6 | {accounts && accounts.map((account: Account, idx: number) => )} 7 |
8 | ) 9 | } 10 | 11 | export default AccountCards -------------------------------------------------------------------------------- /src/components/chatui/account-detail-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Text, Metric } from '@tremor/react' 2 | import { OfficeBuildingIcon, CreditCardIcon } from '@heroicons/react/outline'; 3 | 4 | const AccountDetailSkeleton = () => { 5 | return ( 6 | 11 |
12 |
13 |
14 |
15 |
16 | 17 |

Recent Transactions

18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 35 | 38 | 39 | 40 | 43 | 46 | 49 | 50 | 51 | 54 | 57 | 60 | 61 | 62 |
NameDateAmount
30 |
31 |
33 |
34 |
36 |
37 |
41 |
42 |
44 |
45 |
47 |
48 |
52 |
53 |
55 |
56 |
58 |
59 |
63 |
64 | ) 65 | } 66 | 67 | export default AccountDetailSkeleton -------------------------------------------------------------------------------- /src/components/chatui/account-detail.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { Card, Text, Metric, Flex } from '@tremor/react' 3 | import { OfficeBuildingIcon, CreditCardIcon } from '@heroicons/react/outline'; 4 | import { ArrowNarrowRightIcon } from '@heroicons/react/outline'; 5 | 6 | interface Transaction { 7 | name: string; 8 | value: number; 9 | date: string; 10 | } 11 | 12 | interface Account { 13 | id: string; 14 | bank: string; 15 | name: string; 16 | type: string; 17 | balance: number; 18 | transactions: Transaction[] 19 | } 20 | 21 | const AccountDetail = ({ props: account } : { props: Account }) => { 22 | return ( 23 | 28 |
29 | 30 |
31 |

{account.bank}

32 |

{account.name} - {account.type}

33 |
34 |
35 |
36 | 37 | Current Balance 38 | ${account.balance} 39 | 40 |
41 |
42 | 43 |

Recent Transactions

44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | {account.transactions && account.transactions.map((tx: Transaction, idx: number) => ( 55 | 56 | 57 | 58 | ))} 59 | 60 |
NameDateAmount
{tx.name}{new Date(tx.date).toLocaleDateString()}{tx.value}
61 | 62 | 63 |
64 |

View in Explorer

65 | 66 |
67 | 68 |
69 |
70 | ) 71 | } 72 | 73 | export default AccountDetail -------------------------------------------------------------------------------- /src/components/chatui/category-transaction-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | Text, 4 | Title, 5 | Flex, 6 | Bold, 7 | } from "@tremor/react"; 8 | 9 | const CategoryTransactionsSkeleton = () => { 10 | return ( 11 | 12 | Transactions by Category 13 | Types of purchases made 14 | 15 | 16 | Merchant 17 | 18 | 19 | Total Spend 20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ) 32 | } 33 | 34 | export default CategoryTransactionsSkeleton; -------------------------------------------------------------------------------- /src/components/chatui/category-transaction.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { 3 | Card, 4 | Text, 5 | BarList, 6 | Title, 7 | Flex, 8 | Bold, 9 | } from "@tremor/react"; 10 | import { 11 | ArrowNarrowRightIcon, 12 | } from "@heroicons/react/solid"; 13 | 14 | interface CategoryTransaction { 15 | name: string; 16 | count: number; 17 | value: number; 18 | } 19 | 20 | const CategoryTransactions = ({ 21 | props 22 | } : { 23 | props: { 24 | chartDataByMonth: CategoryTransaction[], 25 | filterDate: { 26 | startDate: string, 27 | endDate: string, 28 | } 29 | } 30 | }) => { 31 | const { chartDataByMonth, filterDate } = props; 32 | 33 | return ( 34 | 35 | Transactions by Category 36 | Types of purchases made 37 | 38 | 39 | Merchant 40 | 41 | 42 | Total Spend 43 | 44 | 45 | 50 | 51 | 59 |
60 |

View in Explorer

61 | 62 |
63 | 64 |
65 |
66 |
67 | ) 68 | } 69 | 70 | export default CategoryTransactions; -------------------------------------------------------------------------------- /src/components/chatui/recurring-transactions-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Card, 4 | Text, 5 | Title, 6 | Flex, 7 | Bold, 8 | } from "@tremor/react"; 9 | 10 | const RecurringTransactionsSkeleton = () => { 11 | return ( 12 | 13 | Recurring Transactions 14 | 15 | 16 | Merchant 17 | 18 | 19 | Total Spend 20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ) 31 | } 32 | 33 | export default RecurringTransactionsSkeleton; -------------------------------------------------------------------------------- /src/components/chatui/recurring-transactions.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | Text, 4 | BarList, 5 | Title, 6 | Flex, 7 | Bold, 8 | } from "@tremor/react"; 9 | 10 | interface RecurringTransaction { 11 | name: string; 12 | count: number; 13 | value: number; 14 | } 15 | 16 | const RecurringTransactions = ({ 17 | props: barListData, 18 | } : { 19 | props: RecurringTransaction[]; 20 | }) => { 21 | return ( 22 | 23 | Recurring Transactions 24 | 25 | 26 | Merchant 27 | 28 | 29 | Total Spend 30 | 31 | 32 | 37 | 38 | ) 39 | } 40 | 41 | export default RecurringTransactions; -------------------------------------------------------------------------------- /src/components/chatui/spinner.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export const Spinner = ( 4 |
5 | 7 | 10 | 13 | 14 | 15 |
16 | ) 17 | 18 | 19 | export const spinner = ( 20 |
21 | 23 | 26 | 29 | 30 | 31 |
32 | ) -------------------------------------------------------------------------------- /src/components/clear-history.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useRouter } from 'next/navigation' 5 | import { toast } from 'sonner' 6 | 7 | import { ServerActionResult } from '@/lib/types' 8 | import { Button } from '@/components/ui/button' 9 | import { 10 | AlertDialog, 11 | AlertDialogAction, 12 | AlertDialogCancel, 13 | AlertDialogContent, 14 | AlertDialogDescription, 15 | AlertDialogFooter, 16 | AlertDialogHeader, 17 | AlertDialogTitle, 18 | AlertDialogTrigger 19 | } from '@/components/ui/alert-dialog' 20 | import { IconSpinner } from '@/components/ui/icons' 21 | 22 | interface ClearHistoryProps { 23 | isEnabled: boolean 24 | clearChats: () => ServerActionResult 25 | } 26 | 27 | export function ClearHistory({ 28 | isEnabled = false, 29 | clearChats 30 | }: ClearHistoryProps) { 31 | const [open, setOpen] = React.useState(false) 32 | const [isPending, startTransition] = React.useTransition() 33 | const router = useRouter() 34 | 35 | return ( 36 | 37 | 38 | 42 | 43 | 44 | 45 | Are you absolutely sure? 46 | 47 | This will permanently delete your chat history and remove your data 48 | from our servers. 49 | 50 | 51 | 52 | Cancel 53 | { 56 | event.preventDefault() 57 | startTransition(async () => { 58 | const result = await clearChats() 59 | if (result && 'error' in result) { 60 | toast.error(result.error) 61 | return 62 | } 63 | 64 | setOpen(false) 65 | }) 66 | }} 67 | > 68 | {isPending && } 69 | Delete 70 | 71 | 72 | 73 | 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /src/components/empty-screen.tsx: -------------------------------------------------------------------------------- 1 | import { UseChatHelpers } from 'ai/react' 2 | 3 | import { Button } from '@/components/ui/button' 4 | import { ExternalLink } from '@/components/external-link' 5 | import { IconArrowRight } from '@/components/ui/icons' 6 | 7 | const exampleMessages = [ 8 | { 9 | heading: 'Explain technical concepts', 10 | message: `What is a "serverless function"?` 11 | }, 12 | { 13 | heading: 'Summarize an article', 14 | message: 'Summarize the following article for a 2nd grader: \n' 15 | }, 16 | { 17 | heading: 'Draft an email', 18 | message: `Draft an email to my boss about the following: \n` 19 | } 20 | ] 21 | 22 | export function EmptyScreen() { 23 | return ( 24 |
25 |
26 |

27 | Personal Finance GPT 28 |

29 |

30 | Ask your personal finance assistant anything to learn more about your financial position. {' '} 31 | 32 | Learn more 33 | 34 | . 35 |

36 |
37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/components/external-link.tsx: -------------------------------------------------------------------------------- 1 | export function ExternalLink({ 2 | href, 3 | children 4 | }: { 5 | href: string 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | 14 | {children} 15 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | import { ExternalLink } from '@/components/external-link' 5 | 6 | export function FooterText({ className, ...props }: React.ComponentProps<'p'>) { 7 | return ( 8 |

15 | AI generated content may be inaccurate, please check for accuracy.{' '} 16 | 17 | Vercel AI SDK 18 | 19 | . 20 |

21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/components/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { ThemeProvider as NextThemesProvider } from 'next-themes' 5 | import { ThemeProviderProps } from 'next-themes/dist/types' 6 | import { SidebarProvider } from '@/lib/hooks/use-sidebar' 7 | import { TooltipProvider } from '@/components/ui/tooltip' 8 | 9 | export function Providers({ children, ...props }: ThemeProviderProps) { 10 | return ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/components/sidebar-desktop.tsx: -------------------------------------------------------------------------------- 1 | import { Sidebar } from '@/components/sidebar' 2 | 3 | // import { auth } from '@/lib/auth' 4 | import { ChatHistory } from '@/components/chat-history' 5 | import { getFullUserInfo } from '@/app/actions/auth' 6 | import { SidebarMobile } from './sidebar-mobile'; 7 | 8 | export async function SidebarDesktop() { 9 | // const session = await auth() 10 | const session = await getFullUserInfo(); 11 | 12 | if (!session?.id) { 13 | return null 14 | } 15 | 16 | return ( 17 | <> 18 | 19 | {/* @ts-ignore */} 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/sidebar-footer.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | 3 | export function SidebarFooter({ 4 | children, 5 | className, 6 | ...props 7 | }: React.ComponentProps<'div'>) { 8 | return ( 9 |
13 | {children} 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/sidebar-items.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Chat } from '@/lib/types' 4 | import { AnimatePresence, motion } from 'framer-motion' 5 | 6 | import { removeChat } from '@/app/actions/chat' 7 | 8 | import { SidebarActions } from '@/components/sidebar-actions' 9 | import { SidebarItem } from '@/components/sidebar-item' 10 | 11 | interface SidebarItemsProps { 12 | chats?: Chat[] 13 | } 14 | 15 | export function SidebarItems({ chats }: SidebarItemsProps) { 16 | if (!chats?.length) return null 17 | 18 | return ( 19 | 20 | {chats.map( 21 | (chat, index) => 22 | chat && ( 23 | 30 | 31 | 36 | 37 | 38 | ) 39 | )} 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/components/sidebar-list.tsx: -------------------------------------------------------------------------------- 1 | import { clearChats, getChats } from '@/app/actions/chat' 2 | import { ClearHistory } from '@/components/clear-history' 3 | import { SidebarItems } from '@/components/sidebar-items' 4 | import { cache } from 'react' 5 | import { type Chat } from '@/lib/types' 6 | 7 | interface SidebarListProps { 8 | userId?: string 9 | children?: React.ReactNode 10 | } 11 | 12 | const loadChats = cache(async (userId?: string) => { 13 | return await getChats(userId) 14 | }) 15 | 16 | export async function SidebarList({ userId }: SidebarListProps) { 17 | const chats = await loadChats(userId); 18 | 19 | return ( 20 |
21 |
22 | {chats?.length ? ( 23 |
24 | 25 |
26 | ) : ( 27 |
28 |

No chat history

29 |
30 | )} 31 |
32 |
33 | 0} /> 34 |
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/components/sidebar-mobile.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet' 4 | 5 | import { Sidebar } from '@/components/sidebar' 6 | import { Button } from '@/components/ui/button' 7 | 8 | import { IconSidebar } from '@/components/ui/icons' 9 | 10 | interface SidebarMobileProps { 11 | children: React.ReactNode 12 | } 13 | 14 | export function SidebarMobile({ children }: SidebarMobileProps) { 15 | return ( 16 | 17 | 18 | 22 | 23 | 27 | {children} 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/components/sidebar-toggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { useSidebar } from '@/lib/hooks/use-sidebar' 6 | import { Button } from '@/components/ui/button' 7 | import { IconSidebar } from '@/components/ui/icons' 8 | 9 | export function SidebarToggle() { 10 | const { toggleSidebar } = useSidebar() 11 | 12 | return ( 13 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | // import { useSidebar } from '@/lib/hooks/use-sidebar' 6 | import { cn } from '@/lib/utils' 7 | 8 | export interface SidebarProps extends React.ComponentProps<'div'> {} 9 | 10 | export function Sidebar({ className, children }: SidebarProps) { 11 | // const { isSidebarOpen, isLoading } = useSidebar() 12 | 13 | return ( 14 |
18 | {children} 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/tailwind-indicator.tsx: -------------------------------------------------------------------------------- 1 | export function TailwindIndicator() { 2 | if (process.env.NODE_ENV === 'production') return null 3 | 4 | return ( 5 |
6 |
xs
7 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/components/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useTheme } from 'next-themes' 5 | 6 | import { Button } from '@/components/ui/button' 7 | import { IconMoon, IconSun } from '@/components/ui/icons' 8 | 9 | export function ThemeToggle() { 10 | const { setTheme, theme } = useTheme() 11 | const [_, startTransition] = React.useTransition() 12 | 13 | return ( 14 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { cn } from "@/lib/utils" 6 | import { ChevronDownIcon } from "@radix-ui/react-icons" 7 | 8 | const Accordion = AccordionPrimitive.Root 9 | 10 | const AccordionItem = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 19 | )) 20 | AccordionItem.displayName = "AccordionItem" 21 | 22 | const AccordionTrigger = React.forwardRef< 23 | React.ElementRef, 24 | React.ComponentPropsWithoutRef 25 | >(({ className, children, ...props }, ref) => ( 26 | 27 | svg]:rotate-180", 31 | className 32 | )} 33 | {...props} 34 | > 35 | {children} 36 | 37 | 38 | 39 | )) 40 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 41 | 42 | const AccordionContent = React.forwardRef< 43 | React.ElementRef, 44 | React.ComponentPropsWithoutRef 45 | >(({ className, children, ...props }, ref) => ( 46 | 51 |
{children}
52 |
53 | )) 54 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 55 | 56 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 57 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cn } from "@/lib/utils" 4 | import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons" 5 | 6 | const Breadcrumb = React.forwardRef< 7 | HTMLElement, 8 | React.ComponentPropsWithoutRef<"nav"> & { 9 | separator?: React.ReactNode 10 | } 11 | >(({ ...props }, ref) =>