Update examples and improve stability (#271)

This commit is contained in:
Jeremy 2024-03-19 01:35:03 +03:00 committed by GitHub
parent 25e4128e25
commit 70ed5c4f47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 118 additions and 101 deletions

5
app/new/page.tsx Normal file
View file

@ -0,0 +1,5 @@
import { redirect } from 'next/navigation'
export default async function NewPage() {
redirect('/')
}

View file

@ -49,7 +49,7 @@ export default async function SharePage({ params }: SharePageProps) {
</div> </div>
</div> </div>
<AI> <AI>
<ChatList messages={uiState} /> <ChatList messages={uiState} isShared={true} />
</AI> </AI>
</div> </div>
<FooterText className="py-8" /> <FooterText className="py-8" />

View file

@ -1,17 +1,37 @@
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { UIState } from '@/lib/chat/actions' import { UIState } from '@/lib/chat/actions'
import { Session } from '@/lib/types'
import Link from 'next/link'
export interface ChatList { export interface ChatList {
messages: UIState messages: UIState
session?: Session
isShared: boolean
} }
export function ChatList({ messages }: ChatList) { export function ChatList({ messages, session, isShared }: ChatList) {
if (!messages.length) { if (!messages.length) {
return null return null
} }
return ( return (
<div className="relative mx-auto max-w-2xl px-4"> <div className="relative mx-auto max-w-2xl px-4">
{!isShared && !session ? (
<div className="mb-8 rounded-lg border bg-white p-4 dark:bg-zinc-950">
<p className="text-muted-foreground leading-normal">
Please{' '}
<Link href="/login" className="underline">
log in
</Link>{' '}
or{' '}
<Link href="/signup" className="underline">
sign up
</Link>{' '}
to save and revisit your chat history!
</p>
</div>
) : null}
{messages.map((message, index) => ( {messages.map((message, index) => (
<div key={message.id}> <div key={message.id}>
{message.display} {message.display}

View file

@ -27,24 +27,24 @@ export function ChatPanel({ id, title, input, setInput }: ChatPanelProps) {
const exampleMessages = [ const exampleMessages = [
{ {
heading: 'Explain the concept', heading: 'What are the',
subheading: 'of a serverless function', subheading: 'trending memecoins today?',
message: `Explain the concept of a serverless function` message: `What are the trending memecoins today?`
}, },
{ {
heading: 'What are the benefits', heading: 'What is the price of',
subheading: 'of using turborepo in my codebase?', subheading: 'DOGE in the stock market?',
message: 'What are the benefits of using turborepo in my codebase?' message: 'What is the price of DOGE in the stock market?'
}, },
{ {
heading: 'List differences between', heading: 'I would like to buy',
subheading: 'pages and app router in Next.js', subheading: '42 DOGE coins',
message: `List differences between pages and app router in Next.js` message: `I would like to buy 42 DOGE coins`
}, },
{ {
heading: 'What is the price', heading: 'What are some',
subheading: `of VRCL in the stock market?`, subheading: `recent events about DOGE?`,
message: `What is the price of VRCL in the stock market?` message: `What are some recent events about DOGE?`
} }
] ]

View file

@ -60,7 +60,7 @@ export function Chat({ id, className, session, missingKeys }: ChatProps) {
<div className={cn('pb-[200px] pt-4 md:pt-10', className)}> <div className={cn('pb-[200px] pt-4 md:pt-10', className)}>
{messages.length ? ( {messages.length ? (
<> <>
<ChatList messages={messages} /> <ChatList messages={messages} isShared={false} session={session} />
<ChatScrollAnchor trackVisibility={isLoading} /> <ChatScrollAnchor trackVisibility={isLoading} />
</> </>
) : ( ) : (

View file

@ -43,7 +43,7 @@ export function EmptyScreen({ setInput }: Pick<UseChatHelpers, 'setInput'>) {
<ExternalLink href="https://vercel.com/blog/ai-sdk-3-generative-ui"> <ExternalLink href="https://vercel.com/blog/ai-sdk-3-generative-ui">
React Server Components React Server Components
</ExternalLink>{' '} </ExternalLink>{' '}
to combine text with UI generated as output of the LLM. The UI state to combine text with generative UI as output of the LLM. The UI state
is synced through the SDK so the model is aware of your interactions is synced through the SDK so the model is aware of your interactions
as they happen. as they happen.
</p> </p>

View file

@ -14,8 +14,8 @@ export function FooterText({ className, ...props }: React.ComponentProps<'p'>) {
> >
Open source AI chatbot built with{' '} Open source AI chatbot built with{' '}
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '} <ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
<ExternalLink href="https://vercel.com/storage/kv"> <ExternalLink href="https://github.com/vercel/ai">
Vercel KV Vercel AI SDK
</ExternalLink> </ExternalLink>
. .
</p> </p>

View file

@ -28,7 +28,7 @@ async function UserOrLogin() {
<SidebarToggle /> <SidebarToggle />
</> </>
) : ( ) : (
<Link href="/" rel="nofollow"> <Link href="/new" rel="nofollow">
<IconNextChat className="size-6 mr-2 dark:hidden" inverted /> <IconNextChat className="size-6 mr-2 dark:hidden" inverted />
<IconNextChat className="hidden size-6 mr-2 dark:block" /> <IconNextChat className="hidden size-6 mr-2 dark:block" />
</Link> </Link>

View file

@ -72,9 +72,9 @@ export function PromptForm({
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
className="absolute left-0 top-4 size-8 rounded-full bg-background p-0 sm:left-4" className="absolute left-0 top-[14px] size-8 rounded-full bg-background p-0 sm:left-4"
onClick={() => { onClick={() => {
router.push('/') router.push('/new')
}} }}
> >
<IconPlus /> <IconPlus />
@ -98,7 +98,7 @@ export function PromptForm({
value={input} value={input}
onChange={e => setInput(e.target.value)} onChange={e => setInput(e.target.value)}
/> />
<div className="absolute right-0 top-4 sm:right-4"> <div className="absolute right-0 top-[13px] sm:right-4">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button type="submit" size="icon" disabled={input === ''}> <Button type="submit" size="icon" disabled={input === ''}>

View file

@ -7,13 +7,13 @@ const placeholderEvents = [
} }
] ]
export const EventsSkeleton = ({ events = placeholderEvents }) => { export const EventsSkeleton = () => {
return ( return (
<div className="-mt-2 flex flex-col gap-2 py-4"> <div className="-mt-2 flex w-full flex-col gap-2 py-4">
{placeholderEvents.map(event => ( {placeholderEvents.map(event => (
<div <div
key={event.date} key={event.date}
className="max-w-96 flex gap-1 shrink-0 flex-col rounded-lg bg-zinc-800 p-4" className="flex shrink-0 flex-col gap-1 rounded-lg bg-zinc-800 p-4"
> >
<div className="w-fit rounded-md bg-zinc-700 text-sm text-transparent"> <div className="w-fit rounded-md bg-zinc-700 text-sm text-transparent">
{event.date} {event.date}

View file

@ -8,11 +8,11 @@ interface Event {
export function Events({ props: events }: { props: Event[] }) { export function Events({ props: events }: { props: Event[] }) {
return ( return (
<div className="-mt-2 flex flex-col gap-2"> <div className="-mt-2 flex w-full flex-col gap-2 py-4">
{events.map(event => ( {events.map(event => (
<div <div
key={event.date} key={event.date}
className="max-w-96 flex shrink-0 flex-col rounded-lg bg-zinc-800 p-4 gap-1" className="flex shrink-0 flex-col gap-1 rounded-lg bg-zinc-800 p-4"
> >
<div className="text-sm text-zinc-400"> <div className="text-sm text-zinc-400">
{format(parseISO(event.date), 'dd LLL, yyyy')} {format(parseISO(event.date), 'dd LLL, yyyy')}

View file

@ -18,9 +18,7 @@ const Purchase = dynamic(
{ {
ssr: false, ssr: false,
loading: () => ( loading: () => (
<div className="rounded-lg bg-zinc-900 px-4 py-5 text-center text-xs"> <div className="h-[375px] rounded-xl border bg-zinc-950 p-4 text-green-400 sm:h-[314px]" />
Loading stock info...
</div>
) )
} }
) )
@ -30,7 +28,7 @@ const Stocks = dynamic(() => import('./stocks').then(mod => mod.Stocks), {
loading: () => <StocksSkeleton /> loading: () => <StocksSkeleton />
}) })
const Events = dynamic(() => import('./event').then(mod => mod.Events), { const Events = dynamic(() => import('./events').then(mod => mod.Events), {
ssr: false, ssr: false,
loading: () => <EventsSkeleton /> loading: () => <EventsSkeleton />
}) })

View file

@ -7,18 +7,18 @@ import { formatNumber } from '@/lib/utils'
import type { AI } from '@/lib/chat/actions' import type { AI } from '@/lib/chat/actions'
interface Purchase { interface Purchase {
defaultAmount?: number numberOfShares?: number
name: string symbol: string
price: number price: number
status: 'requires_action' | 'completed' | 'expired' status: 'requires_action' | 'completed' | 'expired'
} }
export function Purchase({ export function Purchase({
props: { defaultAmount, name, price, status = 'expired' } props: { numberOfShares, symbol, price, status = 'requires_action' }
}: { }: {
props: Purchase props: Purchase
}) { }) {
const [value, setValue] = useState(defaultAmount || 100) const [value, setValue] = useState(numberOfShares || 100)
const [purchasingUI, setPurchasingUI] = useState<null | React.ReactNode>(null) const [purchasingUI, setPurchasingUI] = useState<null | React.ReactNode>(null)
const [aiState, setAIState] = useAIState<typeof AI>() const [aiState, setAIState] = useAIState<typeof AI>()
const [, setMessages] = useUIState<typeof AI>() const [, setMessages] = useUIState<typeof AI>()
@ -60,11 +60,11 @@ export function Purchase({
} }
return ( return (
<div className="rounded-xl border bg-zinc-950 p-4 text-green-400 mt-2"> <div className="rounded-xl border bg-zinc-950 p-4 text-green-400">
<div className="float-right inline-block rounded-full bg-white/10 px-2 py-1 text-xs"> <div className="float-right inline-block rounded-full bg-white/10 px-2 py-1 text-xs">
+1.23% +1.23%
</div> </div>
<div className="text-lg text-zinc-300">{name}</div> <div className="text-lg text-zinc-300">{symbol}</div>
<div className="text-3xl font-bold">${price}</div> <div className="text-3xl font-bold">${price}</div>
{purchasingUI ? ( {purchasingUI ? (
<div className="mt-4 text-zinc-200">{purchasingUI}</div> <div className="mt-4 text-zinc-200">{purchasingUI}</div>
@ -100,14 +100,14 @@ export function Purchase({
<div className="flex flex-wrap items-center text-xl font-bold sm:items-end sm:gap-2 sm:text-3xl"> <div className="flex flex-wrap items-center text-xl font-bold sm:items-end sm:gap-2 sm:text-3xl">
<div className="flex basis-1/3 flex-col tabular-nums sm:basis-auto sm:flex-row sm:items-center sm:gap-2"> <div className="flex basis-1/3 flex-col tabular-nums sm:basis-auto sm:flex-row sm:items-center sm:gap-2">
{value} {value}
<span className="mb-1 text-sm font-normal text-zinc-600 dark:text-zinc-400 sm:mb-0"> <span className="mb-1 text-sm font-normal text-zinc-600 sm:mb-0 dark:text-zinc-400">
shares shares
</span> </span>
</div> </div>
<div className="basis-1/3 text-center sm:basis-auto">×</div> <div className="basis-1/3 text-center sm:basis-auto">×</div>
<span className="flex basis-1/3 flex-col tabular-nums sm:basis-auto sm:flex-row sm:items-center sm:gap-2"> <span className="flex basis-1/3 flex-col tabular-nums sm:basis-auto sm:flex-row sm:items-center sm:gap-2">
${price} ${price}
<span className="mb-1 ml-1 text-sm font-normal text-zinc-600 dark:text-zinc-400 sm:mb-0"> <span className="mb-1 ml-1 text-sm font-normal text-zinc-600 sm:mb-0 dark:text-zinc-400">
per share per share
</span> </span>
</span> </span>
@ -118,9 +118,9 @@ export function Purchase({
</div> </div>
<button <button
className="mt-6 w-full rounded-lg bg-green-500 px-4 py-2 text-zinc-900 dark:bg-green-500" className="mt-6 w-full rounded-lg bg-green-500 px-4 py-2 font-bold text-zinc-900 hover:bg-green-600"
onClick={async () => { onClick={async () => {
const response = await confirmPurchase(name, price, value) const response = await confirmPurchase(symbol, price, value)
setPurchasingUI(response.purchasingUI) setPurchasingUI(response.purchasingUI)
// Insert a new system message to the UI. // Insert a new system message to the UI.
@ -135,7 +135,7 @@ export function Purchase({
</> </>
) : status === 'completed' ? ( ) : status === 'completed' ? (
<p className="mb-2 text-white"> <p className="mb-2 text-white">
You have successfully purchased {value} ${name}. Total cost:{' '} You have successfully purchased {value} ${symbol}. Total cost:{' '}
{formatNumber(value * price)} {formatNumber(value * price)}
</p> </p>
) : status === 'expired' ? ( ) : status === 'expired' ? (

View file

@ -1,9 +1,9 @@
export const StocksSkeleton = () => { export const StocksSkeleton = () => {
return ( return (
<div className="mb-4 flex flex-col gap-2 overflow-y-scroll pb-4 text-sm sm:flex-row"> <div className="mb-4 flex flex-col gap-2 overflow-y-scroll pb-4 text-sm sm:flex-row">
<div className="flex h-[60px] w-full cursor-pointer flex-row gap-2 rounded-lg bg-zinc-900 p-2 text-left hover:bg-zinc-800 sm:w-[208px]"></div> <div className="flex h-[60px] w-full cursor-pointer flex-row gap-2 rounded-lg bg-zinc-800 p-2 text-left hover:bg-zinc-800 sm:w-[208px]"></div>
<div className="flex h-[60px] w-full cursor-pointer flex-row gap-2 rounded-lg bg-zinc-900 p-2 text-left hover:bg-zinc-800 sm:w-[208px]"></div> <div className="flex h-[60px] w-full cursor-pointer flex-row gap-2 rounded-lg bg-zinc-800 p-2 text-left hover:bg-zinc-800 sm:w-[208px]"></div>
<div className="flex h-[60px] w-full cursor-pointer flex-row gap-2 rounded-lg bg-zinc-900 p-2 text-left hover:bg-zinc-800 sm:w-[208px]"></div> <div className="flex h-[60px] w-full cursor-pointer flex-row gap-2 rounded-lg bg-zinc-800 p-2 text-left hover:bg-zinc-800 sm:w-[208px]"></div>
</div> </div>
) )
} }

View file

@ -15,45 +15,53 @@ export function Stocks({ props: stocks }: { props: Stock[] }) {
const { submitUserMessage } = useActions() const { submitUserMessage } = useActions()
return ( return (
<div className="mb-4 flex flex-col gap-2 overflow-y-scroll pb-4 text-sm sm:flex-row"> <div>
{stocks.map(stock => ( <div className="mb-4 flex flex-col gap-2 overflow-y-scroll pb-4 text-sm sm:flex-row">
<button {stocks.map(stock => (
key={stock.symbol} <button
className="flex cursor-pointer flex-row gap-2 rounded-lg bg-zinc-800 p-2 text-left hover:bg-zinc-700 sm:w-52" key={stock.symbol}
onClick={async () => { className="flex cursor-pointer flex-row gap-2 rounded-lg bg-zinc-800 p-2 text-left hover:bg-zinc-700 sm:w-52"
const response = await submitUserMessage(`View ${stock.symbol}`) onClick={async () => {
setMessages(currentMessages => [...currentMessages, response]) const response = await submitUserMessage(`View ${stock.symbol}`)
}} setMessages(currentMessages => [...currentMessages, response])
> }}
<div
className={`text-xl ${
stock.delta > 0 ? 'text-green-600' : 'text-red-600'
} flex w-11 flex-row justify-center rounded-md bg-white/10 p-2`}
> >
{stock.delta > 0 ? '↑' : '↓'}
</div>
<div className="flex flex-col">
<div className="bold uppercase text-zinc-300">{stock.symbol}</div>
<div className="text-base text-zinc-500">${stock.price}</div>
</div>
<div className="ml-auto flex flex-col">
<div <div
className={`${ className={`text-xl ${
stock.delta > 0 ? 'text-green-600' : 'text-red-600' stock.delta > 0 ? 'text-green-600' : 'text-red-600'
} bold text-right uppercase`} } flex w-11 flex-row justify-center rounded-md bg-white/10 p-2`}
> >
{` ${((stock.delta / stock.price) * 100).toFixed(2)}%`} {stock.delta > 0 ? '↑' : '↓'}
</div> </div>
<div <div className="flex flex-col">
className={`${ <div className="bold uppercase text-zinc-300">{stock.symbol}</div>
stock.delta > 0 ? 'text-green-700' : 'text-red-700' <div className="text-base text-zinc-500">
} text-right text-base`} ${stock.price.toExponential(1)}
> </div>
{stock.delta}
</div> </div>
</div> <div className="ml-auto flex flex-col">
</button> <div
))} className={`${
stock.delta > 0 ? 'text-green-600' : 'text-red-600'
} bold text-right uppercase`}
>
{` ${((stock.delta / stock.price) * 100).toExponential(1)}%`}
</div>
<div
className={`${
stock.delta > 0 ? 'text-green-700' : 'text-red-700'
} text-right text-base`}
>
{stock.delta.toExponential(1)}
</div>
</div>
</button>
))}
</div>
<div className="p-4 text-center text-sm text-zinc-500">
Note: Data and latency are simulated for illustrative purposes and
should not be considered as financial advice.
</div>
</div> </div>
) )
} }

View file

@ -21,7 +21,7 @@ import {
import { z } from 'zod' import { z } from 'zod'
import { EventsSkeleton } from '@/components/stocks/events-skeleton' import { EventsSkeleton } from '@/components/stocks/events-skeleton'
import { Events } from '@/components/stocks/event' import { Events } from '@/components/stocks/events'
import { StocksSkeleton } from '@/components/stocks/stocks-skeleton' import { StocksSkeleton } from '@/components/stocks/stocks-skeleton'
import { Stocks } from '@/components/stocks/stocks' import { Stocks } from '@/components/stocks/stocks'
import { StockSkeleton } from '@/components/stocks/stock-skeleton' import { StockSkeleton } from '@/components/stocks/stock-skeleton'
@ -95,7 +95,7 @@ async function confirmPurchase(symbol: string, price: number, amount: number) {
role: 'function', role: 'function',
name: 'showStockPurchase', name: 'showStockPurchase',
content: JSON.stringify({ content: JSON.stringify({
name: symbol, symbol,
price, price,
defaultAmount: amount, defaultAmount: amount,
status: 'completed' status: 'completed'
@ -328,25 +328,16 @@ Besides that, you can also chat with users and do some calculations if needed.`
}) })
return ( return (
<> <BotCard>
<BotMessage
content={`Sure!
${
typeof numberOfShares === 'number'
? `Click the button below to purchase ${numberOfShares} shares of $${symbol}:`
: `How many $${symbol} would you like to purchase?`
}`}
/>
<Purchase <Purchase
props={{ props={{
defaultAmount: numberOfShares, numberOfShares,
name: symbol, symbol,
price: +price, price: +price,
status: 'requires_action' status: 'requires_action'
}} }}
/> />
</> </BotCard>
) )
} }
}, },
@ -405,18 +396,13 @@ Besides that, you can also chat with users and do some calculations if needed.`
export type Message = { export type Message = {
role: 'user' | 'assistant' | 'system' | 'function' | 'data' | 'tool' role: 'user' | 'assistant' | 'system' | 'function' | 'data' | 'tool'
content: string content: string
id?: string id: string
name?: string name?: string
} }
export type AIState = { export type AIState = {
chatId: string chatId: string
messages: { messages: Message[]
role: 'user' | 'assistant' | 'system' | 'function' | 'data' | 'tool'
content: string
id: string
name?: string
}[]
} }
export type UIState = { export type UIState = {
@ -447,7 +433,7 @@ export const AI = createAI<AIState, UIState>({
return return
} }
}, },
unstable_onSetAIState: async ({ state }) => { unstable_onSetAIState: async ({ state, done }) => {
'use server' 'use server'
const session = await auth() const session = await auth()