├── public ├── favicon.ico ├── favicon-16x16.png ├── apple-touch-icon.png ├── vercel.svg ├── thirteen.svg └── next.svg ├── app ├── opengraph-image.png ├── twitter-image.png ├── new │ └── page.tsx ├── (chat) │ ├── layout.tsx │ ├── page.tsx │ └── chat │ │ └── [id] │ │ └── page.tsx ├── api │ └── auth │ │ ├── callback │ │ └── route.ts │ │ └── actions.ts ├── login │ └── page.tsx ├── signup │ └── page.tsx ├── globals.css ├── layout.tsx └── share │ └── [id] │ └── page.tsx ├── postcss.config.js ├── .env.example ├── next-env.d.ts ├── lib ├── supabase │ ├── client.ts │ ├── server.ts │ └── middleware.ts ├── hooks │ ├── use-enter-submit.tsx │ ├── use-local-storage.ts │ ├── use-streamable-text.ts │ ├── use-copy-to-clipboard.tsx │ ├── use-get-user.tsx │ ├── use-sidebar.tsx │ └── use-scroll-anchor.tsx ├── types.ts ├── utils.ts ├── db_types.ts └── chat │ └── actions.tsx ├── next.config.js ├── components ├── ui │ ├── markdown.tsx │ ├── providers.tsx │ ├── tailwind-indicator.tsx │ ├── auth-button.tsx │ ├── external-link.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── theme-toggle.tsx │ ├── input.tsx │ ├── separator.tsx │ ├── user-login.tsx │ ├── sonner.tsx │ ├── button-scroll-to-bottom.tsx │ ├── tooltip.tsx │ ├── switch.tsx │ ├── profile-dropdown.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── avatar.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── codeblock.tsx │ ├── sheet.tsx │ ├── alert-dialog.tsx │ ├── select.tsx │ └── dropdown-menu.tsx ├── sidebar │ ├── sidebar-footer.tsx │ ├── sidebar-toggle.tsx │ ├── sidebar-mobile.tsx │ ├── sidebar.tsx │ ├── sidebar-items.tsx │ ├── sidebar-list.tsx │ ├── clear-history.tsx │ ├── sidebar-item.tsx │ └── sidebar-actions.tsx ├── stocks │ ├── spinner.tsx │ ├── stocks-skeleton.tsx │ ├── stock-skeleton.tsx │ ├── events.tsx │ ├── events-skeleton.tsx │ ├── index.tsx │ ├── stocks.tsx │ ├── message.tsx │ ├── stock-sale.tsx │ ├── stock-purchase.tsx │ └── stock.tsx ├── footer │ └── footer.tsx ├── header │ └── header.tsx ├── chat │ ├── empty-screen.tsx │ ├── chat-message-actions.tsx │ ├── chat-history.tsx │ ├── chat-list.tsx │ ├── chat.tsx │ ├── chat-message.tsx │ ├── chat-share-dialog.tsx │ ├── prompt-form.tsx │ └── chat-panel.tsx └── auth │ ├── login-form.tsx │ └── signup-form.tsx ├── components.json ├── .gitignore ├── LICENSE ├── .eslintrc.json ├── middleware.ts ├── tsconfig.json ├── prettier.config.cjs ├── tailwind.config.ts ├── package.json └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuanseHurtadoF/nextjs-chat/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuanseHurtadoF/nextjs-chat/HEAD/app/opengraph-image.png -------------------------------------------------------------------------------- /app/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuanseHurtadoF/nextjs-chat/HEAD/app/twitter-image.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuanseHurtadoF/nextjs-chat/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuanseHurtadoF/nextjs-chat/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /app/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation' 2 | 3 | export default async function NewPage() { 4 | redirect('/') 5 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # OPENAI 2 | OPENAI_API_KEY="your_openai_api_key" 3 | 4 | # SUPABASE 5 | NEXT_PUBLIC_SUPABASE_URL="your_supabase_url" 6 | NEXT_PUBLIC_SUPABASE_ANON_KEY="your_supabase_anon_key" -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /lib/supabase/client.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserClient } from '@supabase/ssr' 2 | 3 | export function createClient() { 4 | return createBrowserClient( 5 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 6 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'avatars.githubusercontent.com', 8 | port: '', 9 | pathname: '**' 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /components/ui/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/sidebar/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 | -------------------------------------------------------------------------------- /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.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /app/(chat)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from '@/components/header/header' 2 | 3 | interface ChatLayoutProps { 4 | children: React.ReactNode 5 | } 6 | 7 | export default async function ChatLayout({ children }: ChatLayoutProps) { 8 | return ( 9 | <> 10 |
11 | 12 |
13 | {children} 14 |
15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /components/stocks/spinner.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export const spinner = ( 4 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | .env 36 | .vercel 37 | .vscode 38 | .env*.local 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Vercel, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/(chat)/page.tsx: -------------------------------------------------------------------------------- 1 | import { nanoid } from '@/lib/utils' 2 | import { Chat } from '@/components/chat/chat' 3 | import { AI } from '@/lib/chat/actions' 4 | import { createClient } from '@/lib/supabase/server' 5 | 6 | export const metadata = { 7 | title: 'Next.js AI Chatbot' 8 | } 9 | 10 | export default async function IndexPage() { 11 | const supabase = createClient() 12 | const id = nanoid() 13 | 14 | const { 15 | data: { user } 16 | } = await supabase.auth.getUser() 17 | 18 | return ( 19 | 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /components/ui/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 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc", 3 | "root": true, 4 | "extends": [ 5 | "next/core-web-vitals", 6 | "prettier", 7 | "plugin:tailwindcss/recommended" 8 | ], 9 | "plugins": ["tailwindcss"], 10 | "rules": { 11 | "tailwindcss/no-custom-classname": "off", 12 | "tailwindcss/classnames-order": "off" 13 | }, 14 | "settings": { 15 | "tailwindcss": { 16 | "callees": ["cn", "cva"], 17 | "config": "tailwind.config.js" 18 | } 19 | }, 20 | "overrides": [ 21 | { 22 | "files": ["*.ts", "*.tsx"], 23 | "parser": "@typescript-eslint/parser" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /components/stocks/stocks-skeleton.tsx: -------------------------------------------------------------------------------- 1 | export const StocksSkeleton = () => { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /components/sidebar/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 | -------------------------------------------------------------------------------- /components/ui/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 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest } from 'next/server' 2 | import { updateSession } from '@/lib/supabase/middleware' 3 | 4 | export async function middleware(request: NextRequest) { 5 | return await updateSession(request) 6 | } 7 | 8 | export const config = { 9 | matcher: [ 10 | /* 11 | * Match all request paths except for the ones starting with: 12 | * - _next/static (static files) 13 | * - _next/image (image optimization files) 14 | * - favicon.ico (favicon file) 15 | * Feel free to modify this pattern to include more paths. 16 | */ 17 | '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)' 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /lib/hooks/use-local-storage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export const useLocalStorage = ( 4 | key: string, 5 | initialValue: T 6 | ): [T, (value: T) => void] => { 7 | const [storedValue, setStoredValue] = useState(initialValue) 8 | 9 | useEffect(() => { 10 | // Retrieve from localStorage 11 | const item = window.localStorage.getItem(key) 12 | if (item) { 13 | setStoredValue(JSON.parse(item)) 14 | } 15 | }, [key]) 16 | 17 | const setValue = (value: T) => { 18 | // Save state 19 | setStoredValue(value) 20 | // Save to localStorage 21 | window.localStorage.setItem(key, JSON.stringify(value)) 22 | } 23 | return [storedValue, setValue] 24 | } 25 | -------------------------------------------------------------------------------- /components/footer/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | import { ExternalLink } from '@/components/ui/external-link' 5 | 6 | export function FooterText({ className, ...props }: React.ComponentProps<'p'>) { 7 | return ( 8 |

15 | Financial AI chatbot built with{' '} 16 | Next.js and{' '} 17 | 18 | Vercel AI SDK 19 | 20 | . 21 |

22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'ai' 2 | 3 | export interface Chat extends Record { 4 | id: string 5 | title: string 6 | created_at: Date 7 | user_id: string 8 | path: string 9 | messages: Message[] 10 | sharePath?: string 11 | } 12 | 13 | export type ServerActionResult = Promise< 14 | | Result 15 | | { 16 | error: string 17 | } 18 | > 19 | 20 | export interface Session { 21 | user: { 22 | id: string 23 | email: string 24 | } 25 | } 26 | 27 | export interface AuthResult { 28 | type: string 29 | message: string 30 | } 31 | 32 | export interface User extends Record { 33 | id: string 34 | email: string 35 | password: string 36 | salt: string 37 | } 38 | -------------------------------------------------------------------------------- /components/ui/auth-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { IconSpinner } from "./icons"; 4 | import { useFormStatus } from "react-dom"; 5 | 6 | export interface AuthButtonProps { 7 | label: string; 8 | } 9 | 10 | export default function AuthButton({ label }: AuthButtonProps): JSX.Element { 11 | const { pending }: { pending: boolean } = useFormStatus(); 12 | 13 | return ( 14 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /lib/hooks/use-streamable-text.ts: -------------------------------------------------------------------------------- 1 | import { StreamableValue, readStreamableValue } from 'ai/rsc' 2 | import { useEffect, useState } from 'react' 3 | 4 | export const useStreamableText = ( 5 | content: string | StreamableValue 6 | ) => { 7 | const [rawContent, setRawContent] = useState( 8 | typeof content === 'string' ? content : '' 9 | ) 10 | 11 | useEffect(() => { 12 | ;(async () => { 13 | if (typeof content === 'object') { 14 | let value = '' 15 | for await (const delta of readStreamableValue(content)) { 16 | if (typeof delta === 'string') { 17 | setRawContent((value = value + delta)) 18 | } 19 | } 20 | } 21 | })() 22 | }, [content]) 23 | 24 | return rawContent 25 | } 26 | -------------------------------------------------------------------------------- /app/api/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/lib/supabase/server"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function GET(request: Request) { 5 | // The `/auth/callback` route is required for the server-side auth flow implemented 6 | // by the SSR package. It exchanges an auth code for the user's session. 7 | // https://supabase.com/docs/guides/auth/server-side/nextjs 8 | const requestUrl = new URL(request.url); 9 | const code = requestUrl.searchParams.get("code"); 10 | 11 | if (code) { 12 | const supabase = createClient(); 13 | await supabase.auth.exchangeCodeForSession(code); 14 | } 15 | 16 | // URL to redirect to after sign in process completes 17 | return NextResponse.redirect(requestUrl.origin); 18 | } 19 | -------------------------------------------------------------------------------- /app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from '@/components/auth/login-form' 2 | import { createClient } from '@/lib/supabase/server' 3 | import { redirect } from 'next/navigation' 4 | 5 | export default async function LoginPage({ 6 | searchParams 7 | }: { 8 | searchParams: { message: string } 9 | }) { 10 | const supabase = createClient() 11 | const { 12 | data: { user } 13 | } = await supabase.auth.getUser() 14 | 15 | if (user) { 16 | return redirect('/') 17 | } 18 | 19 | return ( 20 |
21 | 22 | {searchParams?.message && ( 23 |

24 | {searchParams.message} 25 |

26 | )} 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /components/ui/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 | -------------------------------------------------------------------------------- /app/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUpForm } from '@/components/auth/signup-form' 2 | import { redirect } from 'next/navigation' 3 | import { createClient } from '@/lib/supabase/server' 4 | 5 | export default async function SignUpPage({ 6 | searchParams 7 | }: { 8 | searchParams: { message: string } 9 | }) { 10 | const supabase = createClient() 11 | const { 12 | data: { user } 13 | } = await supabase.auth.getUser() 14 | 15 | if (user) { 16 | return redirect('/') 17 | } 18 | 19 | return ( 20 |
21 | 22 | {searchParams?.message && ( 23 |

24 | {searchParams.message} 25 |

26 | )} 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |