import 'server-only' import { createAI, createStreamableUI, getMutableAIState, getAIState, render, createStreamableValue } from 'ai/rsc' import OpenAI from 'openai' import { spinner, BotCard, BotMessage, SystemMessage, Stock, Purchase } from '@/components/stocks' import { z } from 'zod' import { EventsSkeleton } from '@/components/stocks/events-skeleton' 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' import { formatNumber, runAsyncFnWithoutBlocking, sleep, nanoid } from '@/lib/utils' import { saveChat } from '@/app/actions' import { SpinnerMessage, UserMessage } from '@/components/stocks/message' import { Chat } from '@/lib/types' import { auth } from '@/auth' const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY || '' }) async function confirmPurchase(symbol: string, price: number, amount: number) { 'use server' const aiState = getMutableAIState() const purchasing = createStreamableUI(
{spinner}

Purchasing {amount} ${symbol}...

) const systemMessage = createStreamableUI(null) runAsyncFnWithoutBlocking(async () => { await sleep(1000) purchasing.update(
{spinner}

Purchasing {amount} ${symbol}... working on it...

) await sleep(1000) purchasing.done(

You have successfully purchased {amount} ${symbol}. Total cost:{' '} {formatNumber(amount * price)}

) systemMessage.done( You have purchased {amount} shares of {symbol} at ${price}. Total cost ={' '} {formatNumber(amount * price)}. ) aiState.done({ ...aiState.get(), messages: [ ...aiState.get().messages.slice(0, -1), { id: nanoid(), role: 'function', name: 'showStockPurchase', content: JSON.stringify({ symbol, price, defaultAmount: amount, status: 'completed' }) }, { id: nanoid(), role: 'system', content: `[User has purchased ${amount} shares of ${symbol} at ${price}. Total cost = ${ amount * price }]` } ] }) }) return { purchasingUI: purchasing.value, newMessage: { id: nanoid(), display: systemMessage.value } } } async function submitUserMessage(content: string) { 'use server' const aiState = getMutableAIState() aiState.update({ ...aiState.get(), messages: [ ...aiState.get().messages, { id: nanoid(), role: 'user', content } ] }) let textStream: undefined | ReturnType> let textNode: undefined | React.ReactNode const ui = render({ model: 'gpt-3.5-turbo', provider: openai, initial: , messages: [ { role: 'system', content: `\ You are a menu suggestion bot for Gateway Subs in New york City. Messages inside [] means that it's a UI element or a user event. For example: - "[Price of AAPL = 100]" means that an interface of the stock price of AAPL is shown to the user. - "[User has changed the amount of AAPL to 10]" means that the user has changed the amount of AAPL to 10 in the UI. If the user requests purchasing a stock, call \`show_stock_purchase_ui\` to show the purchase UI. If the user just wants the price, call \`show_stock_price\` to show the price. If you want to show trending stocks, call \`list_stocks\`. If you want to show events, call \`get_events\`. If the user wants to sell stock, or complete another impossible task, respond that you are a demo and cannot do that. Besides that, you can also chat with users and do some calculations if needed.` }, ...aiState.get().messages.map((message: any) => ({ role: message.role, content: message.content, name: message.name })) ], text: ({ content, done, delta }) => { if (!textStream) { textStream = createStreamableValue('') textNode = } if (done) { textStream.done() aiState.done({ ...aiState.get(), messages: [ ...aiState.get().messages, { id: nanoid(), role: 'assistant', content } ] }) } else { textStream.update(delta) } return textNode }, functions: { listStocks: { description: 'List three imaginary stocks that are trending.', parameters: z.object({ stocks: z.array( z.object({ symbol: z.string().describe('The symbol of the stock'), price: z.number().describe('The price of the stock'), delta: z.number().describe('The change in price of the stock') }) ) }), render: async function* ({ stocks }) { yield ( ) await sleep(1000) aiState.done({ ...aiState.get(), messages: [ ...aiState.get().messages, { id: nanoid(), role: 'function', name: 'listStocks', content: JSON.stringify(stocks) } ] }) return ( ) } }, showStockPrice: { description: 'Get the current stock price of a given stock or currency. Use this to show the price to the user.', parameters: z.object({ symbol: z .string() .describe( 'The name or symbol of the stock or currency. e.g. DOGE/AAPL/USD.' ), price: z.number().describe('The price of the stock.'), delta: z.number().describe('The change in price of the stock') }), render: async function* ({ symbol, price, delta }) { yield ( ) await sleep(1000) aiState.done({ ...aiState.get(), messages: [ ...aiState.get().messages, { id: nanoid(), role: 'function', name: 'showStockPrice', content: JSON.stringify({ symbol, price, delta }) } ] }) return ( ) } }, showStockPurchase: { description: 'Show price and the UI to purchase a stock or currency. Use this if the user wants to purchase a stock or currency.', parameters: z.object({ symbol: z .string() .describe( 'The name or symbol of the stock or currency. e.g. DOGE/AAPL/USD.' ), price: z.number().describe('The price of the stock.'), numberOfShares: z .number() .describe( 'The **number of shares** for a stock or currency to purchase. Can be optional if the user did not specify it.' ) }), render: async function* ({ symbol, price, numberOfShares = 100 }) { if (numberOfShares <= 0 || numberOfShares > 1000) { aiState.done({ ...aiState.get(), messages: [ ...aiState.get().messages, { id: nanoid(), role: 'system', content: `[User has selected an invalid amount]` } ] }) return } aiState.done({ ...aiState.get(), messages: [ ...aiState.get().messages, { id: nanoid(), role: 'function', name: 'showStockPurchase', content: JSON.stringify({ symbol, price, numberOfShares }) } ] }) return ( ) } }, getEvents: { description: 'List funny imaginary events between user highlighted dates that describe stock activity.', parameters: z.object({ events: z.array( z.object({ date: z .string() .describe('The date of the event, in ISO-8601 format'), headline: z.string().describe('The headline of the event'), description: z.string().describe('The description of the event') }) ) }), render: async function* ({ events }) { yield ( ) await sleep(1000) aiState.done({ ...aiState.get(), messages: [ ...aiState.get().messages, { id: nanoid(), role: 'function', name: 'getEvents', content: JSON.stringify(events) } ] }) return ( ) } } } }) return { id: nanoid(), display: ui } } export type Message = { role: 'user' | 'assistant' | 'system' | 'function' | 'data' | 'tool' content: string id: string name?: string } export type AIState = { chatId: string messages: Message[] } export type UIState = { id: string display: React.ReactNode }[] export const AI = createAI({ actions: { submitUserMessage, confirmPurchase }, initialUIState: [], initialAIState: { chatId: nanoid(), messages: [] }, unstable_onGetUIState: async () => { 'use server' const session = await auth() if (session && session.user) { const aiState = getAIState() if (aiState) { const uiState = getUIStateFromAIState(aiState) return uiState } } else { return } }, unstable_onSetAIState: async ({ state, done }) => { 'use server' const session = await auth() if (session && session.user) { const { chatId, messages } = state const createdAt = new Date() const userId = session.user.id as string const path = `/chat/${chatId}` const title = messages[0].content.substring(0, 100) const chat: Chat = { id: chatId, title, userId, createdAt, messages, path } await saveChat(chat) } else { return } } }) export const getUIStateFromAIState = (aiState: Chat) => { return aiState.messages .filter(message => message.role !== 'system') .map((message, index) => ({ id: `${aiState.chatId}-${index}`, display: message.role === 'function' ? ( message.name === 'listStocks' ? ( ) : message.name === 'showStockPrice' ? ( ) : message.name === 'showStockPurchase' ? ( ) : message.name === 'getEvents' ? ( ) : null ) : message.role === 'user' ? ( {message.content} ) : ( ) })) }