├── public ├── favicon.ico ├── favicon-16x16.png └── apple-touch-icon.png ├── postcss.config.js ├── .gitignore ├── next-env.d.ts ├── components ├── confetti.tsx ├── chat-list.tsx ├── llm-stocks │ ├── spinner.tsx │ ├── stocks-skeleton.tsx │ ├── events-skeleton.tsx │ ├── stock-skeleton.tsx │ ├── event.tsx │ ├── index.tsx │ ├── stocks.tsx │ ├── message.tsx │ ├── stock-purchase.tsx │ └── stock.tsx ├── providers.tsx ├── footer.tsx ├── external-link.tsx ├── ui │ ├── label.tsx │ ├── textarea.tsx │ ├── separator.tsx │ ├── input.tsx │ ├── toaster.tsx │ ├── tooltip.tsx │ ├── button.tsx │ ├── use-toast.ts │ ├── toast.tsx │ └── icons.tsx ├── header.tsx ├── pacman.tsx └── empty-screen.tsx ├── components.json ├── lib ├── rate-limit.ts ├── hooks │ ├── use-at-bottom.tsx │ ├── use-enter-submit.tsx │ └── chat-scroll-anchor.tsx └── utils │ ├── tool-definition.ts │ └── index.tsx ├── tsconfig.json ├── app ├── globals.css ├── layout.tsx ├── page.tsx └── action.tsx ├── package.json ├── tailwind.config.ts └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rauchg/genui-demo/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rauchg/genui-demo/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rauchg/genui-demo/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .turbo 4 | *.log 5 | .next 6 | *.local 7 | .env 8 | .cache 9 | .turbo 10 | .vercel 11 | .env*.local 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/confetti.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import JSConfetti from "js-confetti"; 3 | import { useEffect } from "react"; 4 | 5 | export function Confetti() { 6 | useEffect(() => { 7 | const jsConfetti = new JSConfetti(); 8 | jsConfetti.addConfetti(); 9 | }, []); 10 | 11 | return "Here you go 🎉!"; 12 | } 13 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from '@/components/ui/separator'; 2 | 3 | export function ChatList({ messages }: { messages: any[] }) { 4 | if (!messages.length) { 5 | return null; 6 | } 7 | 8 | return ( 9 |
10 | {messages.map((message, index) => ( 11 |
12 | {message.display} 13 |
14 | ))} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/llm-stocks/spinner.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | export const spinner = ( 4 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /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 | 7 | import { TooltipProvider } from '@/components/ui/tooltip'; 8 | 9 | export function Providers({ children, ...props }: ThemeProviderProps) { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /lib/rate-limit.ts: -------------------------------------------------------------------------------- 1 | import { Ratelimit } from "@upstash/ratelimit"; 2 | import { Redis } from "@upstash/redis"; 3 | 4 | if (!process.env.KV_REST_API_URL || !process.env.KV_REST_API_TOKEN) { 5 | throw new Error( 6 | "Please link a Vercel KV instance or populate `KV_REST_API_URL` and `KV_REST_API_TOKEN`", 7 | ); 8 | } 9 | 10 | const redis = new Redis({ 11 | url: process.env.KV_REST_API_URL, 12 | token: process.env.KV_REST_API_TOKEN, 13 | }); 14 | 15 | export const messageRateLimit = new Ratelimit({ 16 | redis, 17 | limiter: Ratelimit.slidingWindow(10, "15 m"), 18 | analytics: true, 19 | prefix: "ratelimit:geui:msg", 20 | }); 21 | -------------------------------------------------------------------------------- /lib/hooks/use-at-bottom.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function useAtBottom(offset = 0) { 4 | const [isAtBottom, setIsAtBottom] = React.useState(false); 5 | 6 | React.useEffect(() => { 7 | const handleScroll = () => { 8 | setIsAtBottom( 9 | window.innerHeight + window.scrollY >= 10 | document.body.offsetHeight - offset, 11 | ); 12 | }; 13 | 14 | window.addEventListener('scroll', handleScroll, { passive: true }); 15 | handleScroll(); 16 | 17 | return () => { 18 | window.removeEventListener('scroll', handleScroll); 19 | }; 20 | }, [offset]); 21 | 22 | return isAtBottom; 23 | } 24 | -------------------------------------------------------------------------------- /components/llm-stocks/stocks-skeleton.tsx: -------------------------------------------------------------------------------- 1 | export const StocksSkeleton = () => { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /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 | Open source AI chatbot built with{' '} 16 | Next.js and{' '} 17 | Vercel AI SDK. 18 |

19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/llm-stocks/events-skeleton.tsx: -------------------------------------------------------------------------------- 1 | export const EventsSkeleton = () => { 2 | return ( 3 |
4 |
5 |
6 | {'xxxxx'} 7 |
8 |
9 | {'xxxxxxxxxxx'} 10 |
11 |
12 |
13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/hooks/use-enter-submit.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, type RefObject } from 'react'; 2 | 3 | export function useEnterSubmit(): { 4 | formRef: RefObject; 5 | onKeyDown: (event: React.KeyboardEvent) => void; 6 | } { 7 | const formRef = useRef(null); 8 | 9 | const handleKeyDown = ( 10 | event: React.KeyboardEvent, 11 | ): void => { 12 | if ( 13 | event.key === 'Enter' && 14 | !event.shiftKey && 15 | !event.nativeEvent.isComposing 16 | ) { 17 | formRef.current?.requestSubmit(); 18 | event.preventDefault(); 19 | } 20 | }; 21 | 22 | return { formRef, onKeyDown: handleKeyDown }; 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/hooks/chat-scroll-anchor.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { useInView } from 'react-intersection-observer'; 5 | import { useAtBottom } from './use-at-bottom'; 6 | 7 | interface ChatScrollAnchorProps { 8 | trackVisibility?: boolean; 9 | } 10 | 11 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) { 12 | const isAtBottom = useAtBottom(); 13 | const { ref, entry, inView } = useInView({ 14 | trackVisibility, 15 | delay: 100, 16 | rootMargin: '0px 0px -50px 0px', 17 | }); 18 | 19 | React.useEffect(() => { 20 | if (isAtBottom && trackVisibility && !inView) { 21 | entry?.target.scrollIntoView({ 22 | block: 'start', 23 | }); 24 | } 25 | }, [inView, entry, isAtBottom, trackVisibility]); 26 | 27 | return
; 28 | } 29 | -------------------------------------------------------------------------------- /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 |