├── .env.example ├── .gitignore ├── postcss.config.js ├── public ├── favicon.ico ├── favicon-16x16.png └── apple-touch-icon.png ├── components ├── .DS_Store ├── chat-list.tsx ├── llm-shop │ ├── checkout-skeleton.tsx │ ├── spinner.tsx │ ├── user-purchases.tsx │ ├── product-skeleton.tsx │ ├── message.tsx │ ├── product.tsx │ ├── purchase-table.tsx │ ├── products.tsx │ └── checkout.tsx ├── providers.tsx ├── external-link.tsx ├── ui │ ├── label.tsx │ ├── textarea.tsx │ ├── separator.tsx │ ├── input.tsx │ ├── toaster.tsx │ ├── tooltip.tsx │ ├── button.tsx │ ├── table.tsx │ ├── use-toast.ts │ ├── toast.tsx │ └── icons.tsx ├── footer.tsx └── empty-screen.tsx ├── next.config.mjs ├── README.md ├── next-env.d.ts ├── lib ├── schemas │ ├── product.schema.ts │ └── purchase.schema.ts ├── hooks │ ├── use-at-bottom.tsx │ ├── use-enter-submit.tsx │ └── chat-scroll-anchor.tsx └── utils │ ├── tool-definition.ts │ └── index.tsx ├── components.json ├── tsconfig.json ├── app ├── globals.css ├── layout.tsx ├── page.tsx └── action.tsx ├── package.json └── tailwind.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .env 4 | bun.lockb 5 | .vercel 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krzysztoff1/generative-ui/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krzysztoff1/generative-ui/HEAD/components/.DS_Store -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krzysztoff1/generative-ui/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krzysztoff1/generative-ui/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Chat based store

2 | 3 | This is a demo of an chat based store. 4 | The demo is built with Next.js and the Vercel AI SDK. 5 | 6 | -------------------------------------------------------------------------------- /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/schemas/product.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const productSchema = z.object({ 4 | id: z.string(), 5 | name: z.string(), 6 | description: z.string(), 7 | price: z.number(), 8 | image: z.string(), 9 | }); 10 | 11 | export type Product = z.infer; 12 | -------------------------------------------------------------------------------- /lib/schemas/purchase.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { productSchema } from './product.schema'; 3 | 4 | export const purchaseSchema = z.object({ 5 | id: z.string(), 6 | product: productSchema, 7 | invoiceUrl: z.string(), 8 | }); 9 | 10 | export type Purchase = z.infer; 11 | -------------------------------------------------------------------------------- /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 | } 18 | -------------------------------------------------------------------------------- /components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | export function ChatList({ messages }: { messages: any[] }) { 2 | if (!messages.length) { 3 | return null; 4 | } 5 | 6 | return ( 7 |
8 | {messages.map((message, index) => ( 9 |
10 | {message.display} 11 |
12 | ))} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components/llm-shop/checkout-skeleton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | export function CheckoutSkeleton() { 4 | return ( 5 | <> 6 |
7 |
8 |
9 |
10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /components/llm-shop/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/hooks/use-at-bottom.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function useAtBottom(offset = 0) { 4 | const [isAtBottom, setIsAtBottom] = React.useState(false); 5 | 6 | React.useEffect(() => { 7 | const handleScroll = () => { 8 | setIsAtBottom( 9 | window.innerHeight + window.scrollY >= 10 | document.body.offsetHeight - offset, 11 | ); 12 | }; 13 | 14 | window.addEventListener('scroll', handleScroll, { passive: true }); 15 | handleScroll(); 16 | 17 | return () => { 18 | window.removeEventListener('scroll', handleScroll); 19 | }; 20 | }, [offset]); 21 | 22 | return isAtBottom; 23 | } 24 | -------------------------------------------------------------------------------- /lib/hooks/use-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 |