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>
<AI>
<ChatList messages={uiState} />
<ChatList messages={uiState} isShared={true} />
</AI>
</div>
<FooterText className="py-8" />

View file

@ -1,17 +1,37 @@
import { Separator } from '@/components/ui/separator'
import { UIState } from '@/lib/chat/actions'
import { Session } from '@/lib/types'
import Link from 'next/link'
export interface ChatList {
messages: UIState
session?: Session
isShared: boolean
}
export function ChatList({ messages }: ChatList) {
export function ChatList({ messages, session, isShared }: ChatList) {
if (!messages.length) {
return null
}
return (
<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) => (
<div key={message.id}>
{message.display}

View file

@ -27,24 +27,24 @@ export function ChatPanel({ id, title, input, setInput }: ChatPanelProps) {
const exampleMessages = [
{
heading: 'Explain the concept',
subheading: 'of a serverless function',
message: `Explain the concept of a serverless function`
heading: 'What are the',
subheading: 'trending memecoins today?',
message: `What are the trending memecoins today?`
},
{
heading: 'What are the benefits',
subheading: 'of using turborepo in my codebase?',
message: 'What are the benefits of using turborepo in my codebase?'
heading: 'What is the price of',
subheading: 'DOGE in the stock market?',
message: 'What is the price of DOGE in the stock market?'
},
{
heading: 'List differences between',
subheading: 'pages and app router in Next.js',
message: `List differences between pages and app router in Next.js`
heading: 'I would like to buy',
subheading: '42 DOGE coins',
message: `I would like to buy 42 DOGE coins`
},
{
heading: 'What is the price',
subheading: `of VRCL in the stock market?`,
message: `What is the price of VRCL in the stock market?`
heading: 'What are some',
subheading: `recent events about DOGE?`,
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)}>
{messages.length ? (
<>
<ChatList messages={messages} />
<ChatList messages={messages} isShared={false} session={session} />
<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">
React Server Components
</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
as they happen.
</p>

View file

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

View file

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

View file

@ -72,9 +72,9 @@ export function PromptForm({
<Button
variant="outline"
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={() => {
router.push('/')
router.push('/new')
}}
>
<IconPlus />
@ -98,7 +98,7 @@ export function PromptForm({
value={input}
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>
<TooltipTrigger asChild>
<Button type="submit" size="icon" disabled={input === ''}>

View file

@ -7,13 +7,13 @@ const placeholderEvents = [
}
]
export const EventsSkeleton = ({ events = placeholderEvents }) => {
export const EventsSkeleton = () => {
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 => (
<div
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">
{event.date}

View file

@ -8,11 +8,11 @@ interface Event {
export function Events({ props: events }: { props: Event[] }) {
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 => (
<div
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">
{format(parseISO(event.date), 'dd LLL, yyyy')}

View file

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

View file

@ -7,18 +7,18 @@ import { formatNumber } from '@/lib/utils'
import type { AI } from '@/lib/chat/actions'
interface Purchase {
defaultAmount?: number
name: string
numberOfShares?: number
symbol: string
price: number
status: 'requires_action' | 'completed' | 'expired'
}
export function Purchase({
props: { defaultAmount, name, price, status = 'expired' }
props: { numberOfShares, symbol, price, status = 'requires_action' }
}: {
props: Purchase
}) {
const [value, setValue] = useState(defaultAmount || 100)
const [value, setValue] = useState(numberOfShares || 100)
const [purchasingUI, setPurchasingUI] = useState<null | React.ReactNode>(null)
const [aiState, setAIState] = useAIState<typeof AI>()
const [, setMessages] = useUIState<typeof AI>()
@ -60,11 +60,11 @@ export function Purchase({
}
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">
+1.23%
</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>
{purchasingUI ? (
<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 basis-1/3 flex-col tabular-nums sm:basis-auto sm:flex-row sm:items-center sm:gap-2">
{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
</span>
</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">
${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
</span>
</span>
@ -118,9 +118,9 @@ export function Purchase({
</div>
<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 () => {
const response = await confirmPurchase(name, price, value)
const response = await confirmPurchase(symbol, price, value)
setPurchasingUI(response.purchasingUI)
// Insert a new system message to the UI.
@ -135,7 +135,7 @@ export function Purchase({
</>
) : status === 'completed' ? (
<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)}
</p>
) : status === 'expired' ? (

View file

@ -1,9 +1,9 @@
export const StocksSkeleton = () => {
return (
<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-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-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-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-800 p-2 text-left hover:bg-zinc-800 sm:w-[208px]"></div>
</div>
)
}

View file

@ -15,6 +15,7 @@ export function Stocks({ props: stocks }: { props: Stock[] }) {
const { submitUserMessage } = useActions()
return (
<div>
<div className="mb-4 flex flex-col gap-2 overflow-y-scroll pb-4 text-sm sm:flex-row">
{stocks.map(stock => (
<button
@ -34,7 +35,9 @@ export function Stocks({ props: stocks }: { props: Stock[] }) {
</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 className="text-base text-zinc-500">
${stock.price.toExponential(1)}
</div>
</div>
<div className="ml-auto flex flex-col">
<div
@ -42,18 +45,23 @@ export function Stocks({ props: stocks }: { props: Stock[] }) {
stock.delta > 0 ? 'text-green-600' : 'text-red-600'
} bold text-right uppercase`}
>
{` ${((stock.delta / stock.price) * 100).toFixed(2)}%`}
{` ${((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}
{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>
)
}

View file

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