87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]',
92 | className,
93 | )}
94 | {...props}
95 | />
96 | ));
97 | TableCell.displayName = 'TableCell';
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | TableCaption.displayName = 'TableCaption';
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | };
121 |
--------------------------------------------------------------------------------
/lib/utils/index.tsx:
--------------------------------------------------------------------------------
1 | import { ToolDefinition } from '@/lib/utils/tool-definition';
2 | import { OpenAIStream } from 'ai';
3 | import type OpenAI from 'openai';
4 | import zodToJsonSchema from 'zod-to-json-schema';
5 | import { type ClassValue, clsx } from 'clsx';
6 | import { twMerge } from 'tailwind-merge';
7 |
8 | const consumeStream = async (stream: ReadableStream) => {
9 | const reader = stream.getReader();
10 | while (true) {
11 | const { done } = await reader.read();
12 | if (done) break;
13 | }
14 | };
15 |
16 | export function runOpenAICompletion<
17 | T extends Omit<
18 | Parameters[0],
19 | 'functions'
20 | > & {
21 | functions: ToolDefinition[];
22 | },
23 | >(openai: OpenAI, params: T) {
24 | let text = '';
25 | let hasFunction = false;
26 |
27 | type FunctionNames =
28 | T['functions'] extends Array ? T['functions'][number]['name'] : never;
29 |
30 | let onTextContent: (text: string, isFinal: boolean) => void = () => {};
31 |
32 | let onFunctionCall: Record) => void> = {};
33 |
34 | const { functions, ...rest } = params;
35 |
36 | (async () => {
37 | consumeStream(
38 | OpenAIStream(
39 | (await openai.chat.completions.create({
40 | ...rest,
41 | stream: true,
42 | functions: functions.map((fn) => ({
43 | name: fn.name,
44 | description: fn.description,
45 | parameters: zodToJsonSchema(fn.parameters) as Record<
46 | string,
47 | unknown
48 | >,
49 | })),
50 | })) as any,
51 | {
52 | async experimental_onFunctionCall(functionCallPayload) {
53 | hasFunction = true;
54 | onFunctionCall[
55 | functionCallPayload.name as keyof typeof onFunctionCall
56 | ]?.(functionCallPayload.arguments as Record);
57 | },
58 | onToken(token) {
59 | text += token;
60 | if (text.startsWith('{')) return;
61 | onTextContent(text, false);
62 | },
63 | onFinal() {
64 | if (hasFunction) return;
65 | onTextContent(text, true);
66 | },
67 | },
68 | ),
69 | );
70 | })();
71 |
72 | return {
73 | onTextContent: (
74 | callback: (text: string, isFinal: boolean) => void | Promise,
75 | ) => {
76 | onTextContent = callback;
77 | },
78 | onFunctionCall: (
79 | name: FunctionNames,
80 | callback: (args: any) => void | Promise,
81 | ) => {
82 | onFunctionCall[name] = callback;
83 | },
84 | };
85 | }
86 |
87 | export function cn(...inputs: ClassValue[]) {
88 | return twMerge(clsx(inputs));
89 | }
90 |
91 | export const formatNumber = (value: number) =>
92 | new Intl.NumberFormat('en-US', {
93 | style: 'currency',
94 | currency: 'USD',
95 | }).format(value);
96 |
97 | export const runAsyncFnWithoutBlocking = (
98 | fn: (...args: any) => Promise,
99 | ) => {
100 | fn();
101 | };
102 |
103 | export const sleep = (ms: number) =>
104 | new Promise((resolve) => setTimeout(resolve, ms));
105 |
106 | // Fake data
107 | export function getStockPrice(name: string) {
108 | let total = 0;
109 | for (let i = 0; i < name.length; i++) {
110 | total = (total + name.charCodeAt(i) * 9999121) % 9999;
111 | }
112 | return total / 100;
113 | }
114 |
--------------------------------------------------------------------------------
/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from 'react';
3 |
4 | import type { ToastActionElement, ToastProps } from '@/components/ui/toast';
5 |
6 | const TOAST_LIMIT = 1;
7 | const TOAST_REMOVE_DELAY = 1000000;
8 |
9 | type ToasterToast = ToastProps & {
10 | id: string;
11 | title?: React.ReactNode;
12 | description?: React.ReactNode;
13 | action?: ToastActionElement;
14 | };
15 |
16 | const actionTypes = {
17 | ADD_TOAST: 'ADD_TOAST',
18 | UPDATE_TOAST: 'UPDATE_TOAST',
19 | DISMISS_TOAST: 'DISMISS_TOAST',
20 | REMOVE_TOAST: 'REMOVE_TOAST',
21 | } as const;
22 |
23 | let count = 0;
24 |
25 | function genId() {
26 | count = (count + 1) % Number.MAX_SAFE_INTEGER;
27 | return count.toString();
28 | }
29 |
30 | type ActionType = typeof actionTypes;
31 |
32 | type Action =
33 | | {
34 | type: ActionType['ADD_TOAST'];
35 | toast: ToasterToast;
36 | }
37 | | {
38 | type: ActionType['UPDATE_TOAST'];
39 | toast: Partial;
40 | }
41 | | {
42 | type: ActionType['DISMISS_TOAST'];
43 | toastId?: ToasterToast['id'];
44 | }
45 | | {
46 | type: ActionType['REMOVE_TOAST'];
47 | toastId?: ToasterToast['id'];
48 | };
49 |
50 | interface State {
51 | toasts: ToasterToast[];
52 | }
53 |
54 | const toastTimeouts = new Map>();
55 |
56 | const addToRemoveQueue = (toastId: string) => {
57 | if (toastTimeouts.has(toastId)) {
58 | return;
59 | }
60 |
61 | const timeout = setTimeout(() => {
62 | toastTimeouts.delete(toastId);
63 | dispatch({
64 | type: 'REMOVE_TOAST',
65 | toastId: toastId,
66 | });
67 | }, TOAST_REMOVE_DELAY);
68 |
69 | toastTimeouts.set(toastId, timeout);
70 | };
71 |
72 | export const reducer = (state: State, action: Action): State => {
73 | switch (action.type) {
74 | case 'ADD_TOAST':
75 | return {
76 | ...state,
77 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
78 | };
79 |
80 | case 'UPDATE_TOAST':
81 | return {
82 | ...state,
83 | toasts: state.toasts.map((t) =>
84 | t.id === action.toast.id ? { ...t, ...action.toast } : t,
85 | ),
86 | };
87 |
88 | case 'DISMISS_TOAST': {
89 | const { toastId } = action;
90 |
91 | // ! Side effects ! - This could be extracted into a dismissToast() action,
92 | // but I'll keep it here for simplicity
93 | if (toastId) {
94 | addToRemoveQueue(toastId);
95 | } else {
96 | state.toasts.forEach((toast) => {
97 | addToRemoveQueue(toast.id);
98 | });
99 | }
100 |
101 | return {
102 | ...state,
103 | toasts: state.toasts.map((t) =>
104 | t.id === toastId || toastId === undefined
105 | ? {
106 | ...t,
107 | open: false,
108 | }
109 | : t,
110 | ),
111 | };
112 | }
113 | case 'REMOVE_TOAST':
114 | if (action.toastId === undefined) {
115 | return {
116 | ...state,
117 | toasts: [],
118 | };
119 | }
120 | return {
121 | ...state,
122 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
123 | };
124 | }
125 | };
126 |
127 | const listeners: Array<(state: State) => void> = [];
128 |
129 | let memoryState: State = { toasts: [] };
130 |
131 | function dispatch(action: Action) {
132 | memoryState = reducer(memoryState, action);
133 | listeners.forEach((listener) => {
134 | listener(memoryState);
135 | });
136 | }
137 |
138 | type Toast = Omit;
139 |
140 | function toast({ ...props }: Toast) {
141 | const id = genId();
142 |
143 | const update = (props: ToasterToast) =>
144 | dispatch({
145 | type: 'UPDATE_TOAST',
146 | toast: { ...props, id },
147 | });
148 | const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });
149 |
150 | dispatch({
151 | type: 'ADD_TOAST',
152 | toast: {
153 | ...props,
154 | id,
155 | open: true,
156 | onOpenChange: (open) => {
157 | if (!open) dismiss();
158 | },
159 | },
160 | });
161 |
162 | return {
163 | id: id,
164 | dismiss,
165 | update,
166 | };
167 | }
168 |
169 | function useToast() {
170 | const [state, setState] = React.useState(memoryState);
171 |
172 | React.useEffect(() => {
173 | listeners.push(setState);
174 | return () => {
175 | const index = listeners.indexOf(setState);
176 | if (index > -1) {
177 | listeners.splice(index, 1);
178 | }
179 | };
180 | }, [state]);
181 |
182 | return {
183 | ...state,
184 | toast,
185 | dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
186 | };
187 | }
188 |
189 | export { useToast, toast };
190 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Cross2Icon } from '@radix-ui/react-icons';
3 | import * as ToastPrimitives from '@radix-ui/react-toast';
4 | import { cva, type VariantProps } from 'class-variance-authority';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const ToastProvider = ToastPrimitives.Provider;
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
24 |
25 | const toastVariants = cva(
26 | 'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
27 | {
28 | variants: {
29 | variant: {
30 | default: 'border bg-background text-foreground',
31 | destructive:
32 | 'destructive group border-destructive bg-destructive text-destructive-foreground',
33 | },
34 | },
35 | defaultVariants: {
36 | variant: 'default',
37 | },
38 | },
39 | );
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | );
53 | });
54 | Toast.displayName = ToastPrimitives.Root.displayName;
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ));
69 | ToastAction.displayName = ToastPrimitives.Action.displayName;
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ));
87 | ToastClose.displayName = ToastPrimitives.Close.displayName;
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ));
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName;
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ));
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName;
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef;
114 |
115 | type ToastActionElement = React.ReactElement;
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | };
128 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect, useRef, useState } from 'react';
4 |
5 | import { useUIState, useActions } from 'ai/rsc';
6 | import { UserMessage } from '@/components/llm-shop/message';
7 |
8 | import { type AI } from './action';
9 | import { ChatScrollAnchor } from '@/lib/hooks/chat-scroll-anchor';
10 | import { FooterText } from '@/components/footer';
11 | import Textarea from 'react-textarea-autosize';
12 | import { useEnterSubmit } from '@/lib/hooks/use-enter-submit';
13 | import {
14 | Tooltip,
15 | TooltipContent,
16 | TooltipTrigger,
17 | } from '@/components/ui/tooltip';
18 | import { IconArrowElbow, IconPlus } from '@/components/ui/icons';
19 | import { Button } from '@/components/ui/button';
20 | import { ChatList } from '@/components/chat-list';
21 | import { EmptyScreen } from '@/components/empty-screen';
22 |
23 | export default function Page() {
24 | const [messages, setMessages] = useUIState();
25 | const { submitUserMessage } = useActions();
26 | const [inputValue, setInputValue] = useState('');
27 | const { formRef, onKeyDown } = useEnterSubmit();
28 | const inputRef = useRef(null);
29 |
30 | useEffect(() => {
31 | const handleKeyDown = (e: KeyboardEvent) => {
32 | if (e.key === '/') {
33 | if (
34 | e.target &&
35 | ['INPUT', 'TEXTAREA'].includes((e.target as any).nodeName)
36 | ) {
37 | return;
38 | }
39 | e.preventDefault();
40 | e.stopPropagation();
41 | if (inputRef?.current) {
42 | inputRef.current.focus();
43 | }
44 | }
45 | };
46 |
47 | document.addEventListener('keydown', handleKeyDown);
48 |
49 | return () => {
50 | document.removeEventListener('keydown', handleKeyDown);
51 | };
52 | }, [inputRef]);
53 |
54 | return (
55 |
56 |
57 | {messages.length ? (
58 | <>
59 |
60 | >
61 | ) : (
62 | {
64 | // Add user message UI
65 | setMessages((currentMessages) => [
66 | ...currentMessages,
67 | {
68 | id: Date.now(),
69 | display: {message},
70 | },
71 | ]);
72 |
73 | // Submit and get response message
74 | const responseMessage = await submitUserMessage(message);
75 | setMessages((currentMessages) => [
76 | ...currentMessages,
77 | responseMessage,
78 | ]);
79 | }}
80 | />
81 | )}
82 |
83 |
84 |
177 |
178 | );
179 | }
180 |
--------------------------------------------------------------------------------
/components/llm-shop/checkout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { AI } from '@/app/action';
4 | import { Button } from '../ui/button';
5 | import { useId, useState } from 'react';
6 | import { useAIState } from 'ai/rsc';
7 | import { cn, sleep } from '@/lib/utils';
8 | import { OTPInput, SlotProps } from 'input-otp';
9 | import { Purchase } from '@/lib/schemas/purchase.schema';
10 | import { Product } from '@/lib/schemas/product.schema';
11 | import { SystemMessage } from './message';
12 |
13 | enum PaymentStatus {
14 | Idle,
15 | Pending,
16 | Success,
17 | Failed,
18 | }
19 |
20 | enum Views {
21 | EnterOtp,
22 | Success,
23 | Failed,
24 | }
25 |
26 | export function Checkout({ product }: { product: Product }) {
27 | const [history, setHistory] = useAIState();
28 |
29 | const [view, setView] = useState(Views.EnterOtp);
30 | const [systemMessages, setSystemMessages] = useState([]);
31 | const [status, setStatus] = useState(PaymentStatus.Idle);
32 | const [otp, setOtp] = useState('');
33 |
34 | const id = useId();
35 |
36 | const submit = async (e: React.FormEvent) => {
37 | e.preventDefault();
38 |
39 | setStatus(PaymentStatus.Pending);
40 |
41 | if (otp.length !== 6) {
42 | setStatus(PaymentStatus.Idle);
43 | return;
44 | }
45 |
46 | try {
47 | await sleep(500);
48 |
49 | if (Math.random() > 0.8) {
50 | throw new Error('Payment failed');
51 | }
52 |
53 | const invoiceId = String(Math.floor(Math.random() * 1000));
54 | const purchase: Purchase = {
55 | invoiceUrl: `https://example.com/invoice/${invoiceId}`,
56 | product,
57 | id: invoiceId,
58 | };
59 |
60 | const info = {
61 | role: 'system' as const,
62 | content: `User has successfully purchased ${product.name} with id ${product.id}. Full purchase ${JSON.stringify(
63 | purchase,
64 | )}`,
65 | id,
66 | };
67 |
68 | setHistory([...history, info]);
69 | setStatus(PaymentStatus.Success);
70 | setView(Views.Success);
71 | setSystemMessages((prev) => [
72 | ...prev,
73 | `You have successfully purchased ${product.name} for a total of ${new Intl.NumberFormat(
74 | 'en-US',
75 | { style: 'currency', currency: 'USD' },
76 | ).format(product.price)}`,
77 | ]);
78 | } catch (error) {
79 | const info = {
80 | role: 'system' as const,
81 | content: `User has failed to purchase ${product.name} with id ${product.id}`,
82 | id,
83 | };
84 |
85 | if (history[history.length - 1]?.id === id) {
86 | setHistory([...history.slice(0, -1), info]);
87 | return;
88 | } else {
89 | setHistory([...history, info]);
90 | }
91 |
92 | setSystemMessages((prev) => [
93 | ...prev,
94 | `Payment for ${product.name} failed. Please try again.`,
95 | ]);
96 | setStatus(PaymentStatus.Failed);
97 | setView(Views.Failed);
98 | }
99 | };
100 |
101 | return (
102 | <>
103 |
104 | Checkout
105 |
106 |
107 | {product.name}
108 |
109 | {new Intl.NumberFormat('en-US', {
110 | style: 'currency',
111 | currency: 'USD',
112 | }).format(product.price)}
113 |
114 |
115 |
116 | {view === Views.EnterOtp && (
117 |
118 |
119 |
120 | Complete your payment by entering the OTP sent to your phone
121 |
122 |
123 |
124 | An OTP has been sent to your phone number ending in{' '}
125 | **** 1234
126 |
127 |
128 |
176 |
177 | )}
178 |
179 | {view === Views.Success && (
180 |
181 | Payment successful
182 |
183 |
184 | Your payment has been successfully processed. You will receive a
185 | confirmation email shortly.
186 |
187 |
188 | )}
189 |
190 | {view === Views.Failed && (
191 |
192 | Payment failed
193 |
202 |
203 | )}
204 |
205 | {systemMessages.map((message, id) => (
206 | {message}
207 | ))}
208 | >
209 | );
210 | }
211 |
212 | function Slot(props: SlotProps) {
213 | return (
214 |
225 | {props.char !== null && {props.char} }
226 | {props.hasFakeCaret && }
227 |
228 | );
229 | }
230 |
231 | function FakeCaret() {
232 | return (
233 |
236 | );
237 | }
238 |
239 | function FakeDash() {
240 | return (
241 |
244 | );
245 | }
246 |
--------------------------------------------------------------------------------
/app/action.tsx:
--------------------------------------------------------------------------------
1 | import 'server-only';
2 |
3 | import { createAI, createStreamableUI, getMutableAIState } from 'ai/rsc';
4 | import OpenAI from 'openai';
5 |
6 | import { runOpenAICompletion, sleep } from '@/lib/utils';
7 | import { z } from 'zod';
8 | import { BotCard, BotMessage } from '@/components/llm-shop/message';
9 | import { spinner } from '@/components/llm-shop/spinner';
10 | import { Products } from '@/components/llm-shop/products';
11 | import { Checkout } from '@/components/llm-shop/checkout';
12 | import { productSchema } from '@/lib/schemas/product.schema';
13 | import { Product as ProductComponent } from '@/components/llm-shop/product';
14 | import { Purchase, purchaseSchema } from '@/lib/schemas/purchase.schema';
15 | import { UserPurchases } from '@/components/llm-shop/user-purchases';
16 | import { CheckoutSkeleton } from '@/components/llm-shop/checkout-skeleton';
17 | import { ProductSkeleton } from '@/components/llm-shop/product-skeleton';
18 |
19 | const products = [
20 | {
21 | id: '1',
22 | name: 'Book',
23 | description: 'A book',
24 | price: 10,
25 | image:
26 | 'https://plus.unsplash.com/premium_photo-1667251760504-096946b820af?q=80&w=2487&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
27 | },
28 | {
29 | id: '2',
30 | name: 'Shoes',
31 | description: 'A pair of shoes',
32 | price: 20,
33 | image:
34 | 'https://images.unsplash.com/photo-1491553895911-0055eca6402d?q=80&w=2380&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
35 | },
36 | {
37 | id: '3',
38 | name: 'Phone',
39 | description: 'A phone',
40 | price: 30,
41 | image:
42 | 'https://images.unsplash.com/photo-1525598912003-663126343e1f?q=80&w=2370&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
43 | },
44 | {
45 | id: '4',
46 | name: 'Laptop',
47 | description: 'A laptop',
48 | price: 40,
49 | image:
50 | 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?q=80&w=2371&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
51 | },
52 | {
53 | id: '5',
54 | name: 'Headphones',
55 | description: 'A pair of headphones',
56 | price: 50,
57 | image:
58 | 'https://images.unsplash.com/photo-1606400082777-ef05f3c5cde2?q=80&w=2370&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
59 | },
60 | ];
61 |
62 | const openai = new OpenAI({
63 | apiKey: process.env.OPENAI_API_KEY || '',
64 | });
65 |
66 | async function submitUserMessage(content: string) {
67 | 'use server';
68 |
69 | const aiState = getMutableAIState();
70 | aiState.update([
71 | ...aiState.get(),
72 | {
73 | role: 'user',
74 | content,
75 | },
76 | ]);
77 |
78 | const reply = createStreamableUI(
79 | {spinner},
80 | );
81 |
82 | const completion = runOpenAICompletion(openai, {
83 | model: process.env.NODE_ENV === 'production' ? 'gpt-3.5-turbo' : 'gpt-4',
84 | stream: true,
85 | messages: [
86 | {
87 | role: 'system',
88 | content: `
89 | You are a friendly ecommerce assistant. You can help users with purchasing products, showing products, purchasing products, and other ecommerce-related tasks. You can also chat with users to request additional information or provide help.
90 |
91 | Messages inside [] means that it's a UI element or a user event. For example:
92 | - "[Showing product card - book with id 123]" means that the UI is showing a product card for a book with id 123.
93 | - "[Showing images of book with id 123]" means that the UI is showing images of a book with id 123.
94 | - "[Purchasing book with id 123]" means that the user is purchasing a book with id 123.
95 | - "[User has successfully purchased book with id 123]" means that the user has successfully purchased a book with id 123.
96 | - "[User has failed to purchase book with id 123]" means that the user has failed to purchase a book with id 123.
97 |
98 | If you want to show list of products, call \`show_products\`.
99 | If user requests to buy a certain product, show purchase UI using \`show_purchase_ui\`. Always use the interface to show the purchase UI. Make sure to respond to every request with the \`show_purchase_ui\` function. NEVER say 'Let's proceed with the purchase' or similar. Always use the function.
100 | If user searches for a result that returns only one product, directly show product using \`show_product\`. Before that indicate that search returned only one product.
101 | If user wants to show their purchases, respond with a list of their purchases using \`show_users_purchases\`.
102 |
103 | Users don't need to know the id of product you can use the name.
104 |
105 | Products: ${products.map((product) => Object.values(product).join(', ')).join('; ')}
106 | `,
107 | },
108 | ...aiState.get().map((info: any) => ({
109 | role: info.role,
110 | content: info.content,
111 | name: info.name,
112 | })),
113 | ],
114 | functions: [
115 | {
116 | name: 'show_products',
117 | description: `
118 | Show a list of products to the user.
119 | The user can then click on a product to view more details.
120 | `,
121 | parameters: z.object({
122 | products: productSchema.array(),
123 | }),
124 | },
125 | {
126 | name: 'show_product',
127 | description: `
128 | Show a product to the user.
129 |
130 | The user can then click on a purchase button to purchase the product.
131 | `,
132 | parameters: z.object({
133 | product: productSchema,
134 | }),
135 | },
136 | {
137 | name: 'show_purchase_ui',
138 | description: `Show a purchase UI to the user.`,
139 | parameters: z.object({
140 | product: productSchema,
141 | }),
142 | },
143 | {
144 | name: 'show_users_purchases',
145 | description: `Show a list of the user's purchases.`,
146 | parameters: z.object({
147 | purchases: purchaseSchema.array(),
148 | }),
149 | },
150 | ],
151 | temperature: 0,
152 | });
153 |
154 | completion.onTextContent((content: string, isFinal: boolean) => {
155 | reply.update({content});
156 | if (isFinal) {
157 | reply.done();
158 | aiState.done([...aiState.get(), { role: 'assistant', content }]);
159 | }
160 | });
161 |
162 | completion.onFunctionCall('show_products', async ({ products }) => {
163 | reply.update(Loading products...);
164 |
165 | reply.done(
166 |
167 |
168 | ,
169 | );
170 |
171 | aiState.done([
172 | ...aiState.get(),
173 | {
174 | role: 'function',
175 | name: 'show_products',
176 | content: JSON.stringify(products),
177 | },
178 | ]);
179 | });
180 |
181 | completion.onFunctionCall('show_product', async ({ product }) => {
182 | reply.update(
183 |
184 |
185 | ,
186 | );
187 |
188 | reply.done(
189 |
190 |
191 | ,
192 | );
193 |
194 | aiState.done([
195 | ...aiState.get(),
196 | {
197 | role: 'function',
198 | name: 'show_product',
199 | content: JSON.stringify(product),
200 | },
201 | ]);
202 | });
203 |
204 | completion.onFunctionCall('show_purchase_ui', async ({ product }) => {
205 | reply.update(
206 |
207 |
208 | ,
209 | );
210 |
211 | reply.done(
212 |
213 |
214 | ,
215 | );
216 |
217 | aiState.done([
218 | ...aiState.get(),
219 | {
220 | role: 'function',
221 | name: 'show_purchase_ui',
222 | content: `[UI for purchasing ${product.name} with id ${product.id}]`,
223 | },
224 | ]);
225 | });
226 |
227 | completion.onFunctionCall(
228 | 'show_users_purchases',
229 | async ({ purchases }: { purchases: Purchase[] }) => {
230 | reply.update(Preparing checkout...);
231 |
232 | reply.done(
233 |
234 |
235 | ,
236 | );
237 |
238 | aiState.done([
239 | ...aiState.get(),
240 | {
241 | role: 'function',
242 | name: 'show_users_purchases',
243 | content: `[UI for showing purchases]`,
244 | },
245 | ]);
246 | },
247 | );
248 |
249 | return {
250 | id: Date.now(),
251 | display: reply.value,
252 | };
253 | }
254 |
255 | // Define necessary types and create the AI.
256 |
257 | const initialAIState: {
258 | role: 'user' | 'assistant' | 'system' | 'function';
259 | content: string;
260 | id?: string;
261 | name?: string;
262 | }[] = [];
263 |
264 | const initialUIState: {
265 | id: number;
266 | display: React.ReactNode;
267 | }[] = [];
268 |
269 | export const AI = createAI({
270 | actions: {
271 | submitUserMessage,
272 | },
273 | initialUIState,
274 | initialAIState,
275 | });
276 |
--------------------------------------------------------------------------------
/components/ui/icons.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | function IconAI({ className, ...props }: React.ComponentProps<'svg'>) {
8 | return (
9 |
19 | );
20 | }
21 |
22 | function IconVercel({ className, ...props }: React.ComponentProps<'svg'>) {
23 | return (
24 |
36 | );
37 | }
38 |
39 | function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
40 | return (
41 |
52 | );
53 | }
54 |
55 | function IconSeparator({ className, ...props }: React.ComponentProps<'svg'>) {
56 | return (
57 |
71 | );
72 | }
73 |
74 | function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
75 | return (
76 |
85 | );
86 | }
87 |
88 | function IconUser({ className, ...props }: React.ComponentProps<'svg'>) {
89 | return (
90 |
99 | );
100 | }
101 |
102 | function IconPlus({ className, ...props }: React.ComponentProps<'svg'>) {
103 | return (
104 |
113 | );
114 | }
115 |
116 | function IconArrowElbow({ className, ...props }: React.ComponentProps<'svg'>) {
117 | return (
118 |
127 | );
128 | }
129 |
130 | function IconSpinner({ className, ...props }: React.ComponentProps<'svg'>) {
131 | return (
132 |
141 | );
142 | }
143 |
144 | function IconMessage({ className, ...props }: React.ComponentProps<'svg'>) {
145 | return (
146 |
155 | );
156 | }
157 |
158 | function IconCopy({ className, ...props }: React.ComponentProps<'svg'>) {
159 | return (
160 |
169 | );
170 | }
171 |
172 | function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
173 | return (
174 |
183 | );
184 | }
185 |
186 | function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
187 | return (
188 |
197 | );
198 | }
199 |
200 | function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
201 | return (
202 |
211 | );
212 | }
213 |
214 | function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
215 | return (
216 |
225 | );
226 | }
227 |
228 | function IconExternalLink({
229 | className,
230 | ...props
231 | }: React.ComponentProps<'svg'>) {
232 | return (
233 |
242 | );
243 | }
244 |
245 | function IconChevronUpDown({
246 | className,
247 | ...props
248 | }: React.ComponentProps<'svg'>) {
249 | return (
250 |
259 | );
260 | }
261 |
262 | function IconSparkles({ className, ...props }: React.ComponentProps<'svg'>) {
263 | return (
264 |
276 | );
277 | }
278 |
279 | export {
280 | IconAI,
281 | IconVercel,
282 | IconGitHub,
283 | IconSeparator,
284 | IconUser,
285 | IconPlus,
286 | IconArrowRight,
287 | IconArrowElbow,
288 | IconSpinner,
289 | IconMessage,
290 | IconCopy,
291 | IconCheck,
292 | IconClose,
293 | IconShare,
294 | IconUsers,
295 | IconExternalLink,
296 | IconChevronUpDown,
297 | IconSparkles,
298 | };
299 |
--------------------------------------------------------------------------------
|