2024-03-14 20:00:52 +03:00
|
|
|
|
'use client'
|
|
|
|
|
|
|
|
|
|
|
|
import { useId, useState } from 'react'
|
|
|
|
|
|
import { useActions, useAIState, useUIState } from 'ai/rsc'
|
|
|
|
|
|
import { formatNumber } from '@/lib/utils'
|
|
|
|
|
|
|
|
|
|
|
|
import type { AI } from '@/lib/chat/actions'
|
|
|
|
|
|
|
|
|
|
|
|
interface Purchase {
|
2024-03-19 01:35:03 +03:00
|
|
|
|
numberOfShares?: number
|
|
|
|
|
|
symbol: string
|
2024-03-14 20:00:52 +03:00
|
|
|
|
price: number
|
|
|
|
|
|
status: 'requires_action' | 'completed' | 'expired'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function Purchase({
|
2024-05-15 09:39:16 -04:00
|
|
|
|
props: { numberOfShares, symbol, price, status = 'expired' }
|
2024-03-14 20:00:52 +03:00
|
|
|
|
}: {
|
|
|
|
|
|
props: Purchase
|
|
|
|
|
|
}) {
|
2024-03-19 01:35:03 +03:00
|
|
|
|
const [value, setValue] = useState(numberOfShares || 100)
|
2024-03-14 20:00:52 +03:00
|
|
|
|
const [purchasingUI, setPurchasingUI] = useState<null | React.ReactNode>(null)
|
|
|
|
|
|
const [aiState, setAIState] = useAIState<typeof AI>()
|
|
|
|
|
|
const [, setMessages] = useUIState<typeof AI>()
|
|
|
|
|
|
const { confirmPurchase } = useActions()
|
|
|
|
|
|
|
|
|
|
|
|
// Unique identifier for this UI component.
|
|
|
|
|
|
const id = useId()
|
|
|
|
|
|
|
|
|
|
|
|
// Whenever the slider changes, we need to update the local value state and the history
|
|
|
|
|
|
// so LLM also knows what's going on.
|
|
|
|
|
|
function onSliderChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
|
|
|
|
const newValue = Number(e.target.value)
|
|
|
|
|
|
setValue(newValue)
|
|
|
|
|
|
|
|
|
|
|
|
// Insert a hidden history info to the list.
|
|
|
|
|
|
const message = {
|
|
|
|
|
|
role: 'system' as const,
|
|
|
|
|
|
content: `[User has changed to purchase ${newValue} shares of ${name}. Total cost: $${(
|
|
|
|
|
|
newValue * price
|
|
|
|
|
|
).toFixed(2)}]`,
|
|
|
|
|
|
|
|
|
|
|
|
// Identifier of this UI component, so we don't insert it many times.
|
|
|
|
|
|
id
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If last history state is already this info, update it. This is to avoid
|
|
|
|
|
|
// adding every slider change to the history.
|
|
|
|
|
|
if (aiState.messages[aiState.messages.length - 1]?.id === id) {
|
|
|
|
|
|
setAIState({
|
|
|
|
|
|
...aiState,
|
|
|
|
|
|
messages: [...aiState.messages.slice(0, -1), message]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If it doesn't exist, append it to history.
|
|
|
|
|
|
setAIState({ ...aiState, messages: [...aiState.messages, message] })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2024-05-15 09:39:16 -04:00
|
|
|
|
<div className="p-4 text-green-400 border rounded-xl bg-zinc-950">
|
|
|
|
|
|
<div className="inline-block float-right px-2 py-1 text-xs rounded-full bg-white/10">
|
2024-03-14 20:00:52 +03:00
|
|
|
|
+1.23% ↑
|
|
|
|
|
|
</div>
|
2024-03-19 01:35:03 +03:00
|
|
|
|
<div className="text-lg text-zinc-300">{symbol}</div>
|
2024-03-14 20:00:52 +03:00
|
|
|
|
<div className="text-3xl font-bold">${price}</div>
|
|
|
|
|
|
{purchasingUI ? (
|
|
|
|
|
|
<div className="mt-4 text-zinc-200">{purchasingUI}</div>
|
|
|
|
|
|
) : status === 'requires_action' ? (
|
|
|
|
|
|
<>
|
2024-05-15 09:39:16 -04:00
|
|
|
|
<div className="relative pb-6 mt-6">
|
2024-03-14 20:00:52 +03:00
|
|
|
|
<p>Shares to purchase</p>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="labels-range-input"
|
|
|
|
|
|
type="range"
|
|
|
|
|
|
value={value}
|
|
|
|
|
|
onChange={onSliderChange}
|
|
|
|
|
|
min="10"
|
|
|
|
|
|
max="1000"
|
2024-05-15 09:39:16 -04:00
|
|
|
|
className="w-full h-1 rounded-lg appearance-none cursor-pointer bg-zinc-600 accent-green-500 dark:bg-zinc-700"
|
2024-03-14 20:00:52 +03:00
|
|
|
|
/>
|
2024-05-15 09:39:16 -04:00
|
|
|
|
<span className="absolute text-xs bottom-1 start-0 text-zinc-400">
|
2024-03-14 20:00:52 +03:00
|
|
|
|
10
|
|
|
|
|
|
</span>
|
2024-05-15 09:39:16 -04:00
|
|
|
|
<span className="absolute text-xs -translate-x-1/2 bottom-1 start-1/3 text-zinc-400 rtl:translate-x-1/2">
|
2024-03-14 20:00:52 +03:00
|
|
|
|
100
|
|
|
|
|
|
</span>
|
2024-05-15 09:39:16 -04:00
|
|
|
|
<span className="absolute text-xs -translate-x-1/2 bottom-1 start-2/3 text-zinc-400 rtl:translate-x-1/2">
|
2024-03-14 20:00:52 +03:00
|
|
|
|
500
|
|
|
|
|
|
</span>
|
2024-05-15 09:39:16 -04:00
|
|
|
|
<span className="absolute text-xs bottom-1 end-0 text-zinc-400">
|
2024-03-14 20:00:52 +03:00
|
|
|
|
1000
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="mt-6">
|
|
|
|
|
|
<p>Total cost</p>
|
|
|
|
|
|
<div className="flex flex-wrap items-center text-xl font-bold sm:items-end sm:gap-2 sm:text-3xl">
|
2024-05-15 09:39:16 -04:00
|
|
|
|
<div className="flex flex-col basis-1/3 tabular-nums sm:basis-auto sm:flex-row sm:items-center sm:gap-2">
|
2024-03-14 20:00:52 +03:00
|
|
|
|
{value}
|
2024-03-19 01:35:03 +03:00
|
|
|
|
<span className="mb-1 text-sm font-normal text-zinc-600 sm:mb-0 dark:text-zinc-400">
|
2024-03-14 20:00:52 +03:00
|
|
|
|
shares
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2024-05-15 09:39:16 -04:00
|
|
|
|
<div className="text-center basis-1/3 sm:basis-auto">×</div>
|
|
|
|
|
|
<span className="flex flex-col basis-1/3 tabular-nums sm:basis-auto sm:flex-row sm:items-center sm:gap-2">
|
2024-03-14 20:00:52 +03:00
|
|
|
|
${price}
|
2024-03-19 01:35:03 +03:00
|
|
|
|
<span className="mb-1 ml-1 text-sm font-normal text-zinc-600 sm:mb-0 dark:text-zinc-400">
|
2024-03-14 20:00:52 +03:00
|
|
|
|
per share
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
2024-05-15 09:39:16 -04:00
|
|
|
|
<div className="pt-2 mt-2 text-center border-t basis-full border-t-zinc-700 sm:mt-0 sm:basis-auto sm:border-0 sm:pt-0 sm:text-left">
|
2024-03-14 20:00:52 +03:00
|
|
|
|
= <span>{formatNumber(value * price)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
2024-05-15 09:39:16 -04:00
|
|
|
|
className="w-full px-4 py-2 mt-6 font-bold bg-green-400 rounded-lg text-zinc-900 hover:bg-green-500"
|
2024-03-14 20:00:52 +03:00
|
|
|
|
onClick={async () => {
|
2024-03-19 01:35:03 +03:00
|
|
|
|
const response = await confirmPurchase(symbol, price, value)
|
2024-03-14 20:00:52 +03:00
|
|
|
|
setPurchasingUI(response.purchasingUI)
|
|
|
|
|
|
|
|
|
|
|
|
// Insert a new system message to the UI.
|
|
|
|
|
|
setMessages((currentMessages: any) => [
|
|
|
|
|
|
...currentMessages,
|
|
|
|
|
|
response.newMessage
|
|
|
|
|
|
])
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Purchase
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : status === 'completed' ? (
|
|
|
|
|
|
<p className="mb-2 text-white">
|
2024-03-19 01:35:03 +03:00
|
|
|
|
You have successfully purchased {value} ${symbol}. Total cost:{' '}
|
2024-03-14 20:00:52 +03:00
|
|
|
|
{formatNumber(value * price)}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : status === 'expired' ? (
|
|
|
|
|
|
<p className="mb-2 text-white">Your checkout session has expired!</p>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|