diff --git a/components/stocks/events.tsx b/components/stocks/events.tsx index eebf7632f..74e94279b 100644 --- a/components/stocks/events.tsx +++ b/components/stocks/events.tsx @@ -1,12 +1,7 @@ +import { GetEventProps } from '@/lib/types' import { format, parseISO } from 'lib/utils' -interface Event { - date: string - headline: string - description: string -} - -export function Events({ props: events }: { props: Event[] }) { +export function Events({ events }: GetEventProps) { return (
{events.map(event => ( diff --git a/components/stocks/stock-purchase.tsx b/components/stocks/stock-purchase.tsx index 0bf922ead..ef7a387a6 100644 --- a/components/stocks/stock-purchase.tsx +++ b/components/stocks/stock-purchase.tsx @@ -5,19 +5,10 @@ import { useActions, useAIState, useUIState } from 'ai/rsc' import { formatNumber } from '@/lib/utils' import type { AI } from '@/lib/chat/actions' +import { PurchaseProps } from '@/lib/types' -interface Purchase { - numberOfShares?: number - symbol: string - price: number - status: 'requires_action' | 'completed' | 'expired' -} -export function Purchase({ - props: { numberOfShares, symbol, price, status = 'expired' } -}: { - props: Purchase -}) { +export function Purchase({ numberOfShares, symbol, price, status = 'expired' }: PurchaseProps) { const [value, setValue] = useState(numberOfShares || 100) const [purchasingUI, setPurchasingUI] = useState(null) const [aiState, setAIState] = useAIState() diff --git a/components/stocks/stock.tsx b/components/stocks/stock.tsx index e895357ad..23f6c6801 100644 --- a/components/stocks/stock.tsx +++ b/components/stocks/stock.tsx @@ -3,12 +3,8 @@ import { useState, useRef, useEffect, useId } from 'react' import { subMonths, format } from 'lib/utils' import { useAIState } from 'ai/rsc' +import { ShowStockPriceProps } from '@/lib/types' -interface Stock { - symbol: string - price: number - delta: number -} function scaleLinear(domain: [number, number], range: [number, number]) { const [d0, d1] = domain @@ -47,7 +43,7 @@ function useResizeObserver( return size } -export function Stock({ props: { symbol, price, delta } }: { props: Stock }) { +export function Stock({ symbol, price, delta }: ShowStockPriceProps ) { const [aiState, setAIState] = useAIState() const id = useId() diff --git a/components/stocks/stocks.tsx b/components/stocks/stocks.tsx index a5f78262b..171f62d45 100644 --- a/components/stocks/stocks.tsx +++ b/components/stocks/stocks.tsx @@ -3,14 +3,8 @@ import { useActions, useUIState } from 'ai/rsc' import type { AI } from '@/lib/chat/actions' - -interface Stock { - symbol: string - price: number - delta: number -} - -export function Stocks({ props: stocks }: { props: Stock[] }) { +import { ListStockProps } from '@/lib/types' +export function Stocks({ stocks }: ListStockProps) { const [, setMessages] = useUIState() const { submitUserMessage } = useActions() @@ -27,9 +21,8 @@ export function Stocks({ props: stocks }: { props: Stock[] }) { }} >
0 ? 'text-green-600' : 'text-red-600' - } flex w-11 flex-row justify-center rounded-md bg-white/10 p-2`} + 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 ? '↑' : '↓'}
@@ -41,16 +34,14 @@ export function Stocks({ props: stocks }: { props: Stock[] }) {
0 ? 'text-green-600' : 'text-red-600' - } bold text-right uppercase`} + className={`${stock.delta > 0 ? 'text-green-600' : 'text-red-600' + } bold text-right uppercase`} > {` ${((stock.delta / stock.price) * 100).toExponential(1)}%`}
0 ? 'text-green-700' : 'text-red-700' - } text-right text-base`} + className={`${stock.delta > 0 ? 'text-green-700' : 'text-red-700' + } text-right text-base`} > {stock.delta.toExponential(1)}
diff --git a/lib/chat/actions.tsx b/lib/chat/actions.tsx index 0aa4313b1..23356782c 100644 --- a/lib/chat/actions.tsx +++ b/lib/chat/actions.tsx @@ -33,8 +33,9 @@ import { } from '@/lib/utils' import { saveChat } from '@/app/actions' import { SpinnerMessage, UserMessage } from '@/components/stocks/message' -import { Chat, Message } from '@/lib/types' +import { Chat, GetEventProps, ListStockProps, Message, PurchaseProps, ShowStockPriceProps } from '@/lib/types' import { auth } from '@/auth' +import { getEventsSchema, listStockSchema, showStockPriceSchema, showStockPurchaseSchema } from '../schemas' async function confirmPurchase(symbol: string, price: number, amount: number) { 'use server' @@ -89,9 +90,8 @@ async function confirmPurchase(symbol: string, price: number, amount: number) { { id: nanoid(), role: 'system', - content: `[User has purchased ${amount} shares of ${symbol} at ${price}. Total cost = ${ - amount * price - }]` + content: `[User has purchased ${amount} shares of ${symbol} at ${price}. Total cost = ${amount * price + }]` } ] }) @@ -179,15 +179,7 @@ async function submitUserMessage(content: string) { tools: { 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') - }) - ) - }), + parameters: listStockSchema, generate: async function* ({ stocks }) { yield ( @@ -232,7 +224,7 @@ async function submitUserMessage(content: string) { return ( - + ) } @@ -240,15 +232,7 @@ async function submitUserMessage(content: string) { 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') - }), + parameters: showStockPriceSchema, generate: async function* ({ symbol, price, delta }) { yield ( @@ -293,7 +277,7 @@ async function submitUserMessage(content: string) { return ( - + ) } @@ -301,20 +285,7 @@ async function submitUserMessage(content: string) { 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() - .optional() - .describe( - 'The **number of shares** for a stock or currency to purchase. Can be optional if the user did not specify it.' - ) - }), + parameters: showStockPurchaseSchema, generate: async function* ({ symbol, price, numberOfShares = 100 }) { const toolCallId = nanoid() @@ -400,12 +371,10 @@ async function submitUserMessage(content: string) { return ( ) @@ -415,17 +384,7 @@ async function submitUserMessage(content: string) { 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') - }) - ) - }), + parameters: getEventsSchema, generate: async function* ({ events }) { yield ( @@ -470,7 +429,7 @@ async function submitUserMessage(content: string) { return ( - + ) } @@ -557,24 +516,19 @@ export const getUIStateFromAIState = (aiState: Chat) => { message.content.map(tool => { return tool.toolName === 'listStocks' ? ( - {/* TODO: Infer types based on the tool result*/} - {/* @ts-expect-error */} - + ) : tool.toolName === 'showStockPrice' ? ( - {/* @ts-expect-error */} - + ) : tool.toolName === 'showStockPurchase' ? ( - {/* @ts-expect-error */} - + ) : tool.toolName === 'getEvents' ? ( - {/* @ts-expect-error */} - + ) : null }) diff --git a/lib/schemas/index.ts b/lib/schemas/index.ts new file mode 100644 index 000000000..ffac8d4ea --- /dev/null +++ b/lib/schemas/index.ts @@ -0,0 +1,46 @@ +import { z } from "zod" +const getEventsSchema = 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') + }) + ) +}) + +const showStockPurchaseSchema = 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() + .optional() + .describe( + 'The **number of shares** for a stock or currency to purchase. Can be optional if the user did not specify it.' + ) +}) +const showStockPriceSchema = 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') +}) +const listStockSchema = 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') + }) + ) +}) +export { getEventsSchema, showStockPurchaseSchema, showStockPriceSchema, listStockSchema } \ No newline at end of file diff --git a/lib/types.ts b/lib/types.ts index d892a0c5a..e10fead00 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,4 +1,6 @@ import { CoreMessage } from 'ai' +import { z } from 'zod' +import { showStockPriceSchema, getEventsSchema, listStockSchema, showStockPurchaseSchema } from './schemas' export type Message = CoreMessage & { id: string @@ -17,8 +19,8 @@ export interface Chat extends Record { export type ServerActionResult = Promise< | Result | { - error: string - } + error: string + } > export interface Session { @@ -39,3 +41,11 @@ export interface User extends Record { password: string salt: string } +export type ShowStockPriceProps = z.infer +export type GetEventProps = z.infer +export type ListStockProps = z.infer +export type ShowStockPurchaseProps = z.infer + +export type PurchaseProps = { + status: 'requires_action' | 'completed' | 'expired' +} & ShowStockPurchaseProps \ No newline at end of file