Update examples and improve stability (#271)
This commit is contained in:
parent
25e4128e25
commit
70ed5c4f47
16 changed files with 118 additions and 101 deletions
5
app/new/page.tsx
Normal file
5
app/new/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default async function NewPage() {
|
||||
redirect('/')
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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?`
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 === ''}>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
@ -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 />
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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' ? (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue