diff --git a/agent/.gitignore b/agent/.gitignore deleted file mode 100644 index e1f451b..0000000 --- a/agent/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# LangGraph API -.langgraph_api -dist diff --git a/agent/agent.ts b/agent/agent.ts deleted file mode 100644 index 484b232..0000000 --- a/agent/agent.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { StateGraph, START, END } from "@langchain/langgraph"; -import { ChatGoogleGenerativeAI } from "@langchain/google-genai"; -import { z } from "zod"; -import { GenerativeUIAnnotation, GenerativeUIState } from "./types"; -import { stockbrokerGraph } from "./stockbroker"; -import { ChatOpenAI } from "@langchain/openai"; -import { tripPlannerGraph } from "./trip-planner"; -import { formatMessages } from "./utils/format-messages"; -import { graph as openCodeGraph } from "./open-code"; -import { graph as orderPizzaGraph } from "./pizza-orderer"; - -const allToolDescriptions = `- stockbroker: can fetch the price of a ticker, purchase/sell a ticker, or get the user's portfolio -- tripPlanner: helps the user plan their trip. it can suggest restaurants, and places to stay in any given location. -- openCode: can write code for the user. call this tool when the user asks you to write code -- orderPizza: can order a pizza for the user`; - -async function router( - state: GenerativeUIState, -): Promise> { - const routerDescription = `The route to take based on the user's input. -${allToolDescriptions} -- generalInput: handles all other cases where the above tools don't apply -`; - const routerSchema = z.object({ - route: z - .enum([ - "stockbroker", - "tripPlanner", - "openCode", - "orderPizza", - "generalInput", - ]) - .describe(routerDescription), - }); - const routerTool = { - name: "router", - description: "A tool to route the user's query to the appropriate tool.", - schema: routerSchema, - }; - - const llm = new ChatGoogleGenerativeAI({ - model: "gemini-2.0-flash", - temperature: 0, - }) - .bindTools([routerTool], { tool_choice: "router" }) - .withConfig({ tags: ["langsmith:nostream"] }); - - const prompt = `You're a highly helpful AI assistant, tasked with routing the user's query to the appropriate tool. -You should analyze the user's input, and choose the appropriate tool to use.`; - - const allMessagesButLast = state.messages.slice(0, -1); - const lastMessage = state.messages.at(-1); - - const formattedPreviousMessages = formatMessages(allMessagesButLast); - const formattedLastMessage = lastMessage ? formatMessages([lastMessage]) : ""; - - const humanMessage = `Here is the full conversation, excluding the most recent message: - -${formattedPreviousMessages} - -Here is the most recent message: - -${formattedLastMessage} - -Please pick the proper route based on the most recent message, in the context of the entire conversation.`; - - const response = await llm.invoke([ - { role: "system", content: prompt }, - { role: "user", content: humanMessage }, - ]); - - const toolCall = response.tool_calls?.[0]?.args as - | z.infer - | undefined; - if (!toolCall) { - throw new Error("No tool call found in response"); - } - - return { - next: toolCall.route, - }; -} - -function handleRoute( - state: GenerativeUIState, -): "stockbroker" | "tripPlanner" | "openCode" | "orderPizza" | "generalInput" { - return state.next; -} - -const GENERAL_INPUT_SYSTEM_PROMPT = `You are an AI assistant. -If the user asks what you can do, describe these tools. -${allToolDescriptions} - -If the last message is a tool result, describe what the action was, congratulate the user, or send a friendly followup in response to the tool action. Ensure this is a clear and concise message. - -Otherwise, just answer as normal.`; - -async function handleGeneralInput(state: GenerativeUIState) { - const llm = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 }); - const response = await llm.invoke([ - { - role: "system", - content: GENERAL_INPUT_SYSTEM_PROMPT, - }, - ...state.messages, - ]); - - return { - messages: [response], - }; -} - -const builder = new StateGraph(GenerativeUIAnnotation) - .addNode("router", router) - .addNode("stockbroker", stockbrokerGraph) - .addNode("tripPlanner", tripPlannerGraph) - .addNode("openCode", openCodeGraph) - .addNode("orderPizza", orderPizzaGraph) - .addNode("generalInput", handleGeneralInput) - - .addConditionalEdges("router", handleRoute, [ - "stockbroker", - "tripPlanner", - "openCode", - "orderPizza", - "generalInput", - ]) - .addEdge(START, "router") - .addEdge("stockbroker", END) - .addEdge("tripPlanner", END) - .addEdge("openCode", END) - .addEdge("orderPizza", END) - .addEdge("generalInput", END); - -export const graph = builder.compile(); -graph.name = "Generative UI Agent"; diff --git a/agent/find-tool-call.ts b/agent/find-tool-call.ts deleted file mode 100644 index 7c9cc40..0000000 --- a/agent/find-tool-call.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z, ZodTypeAny } from "zod"; - -interface ToolCall { - name: string; - args: Record; - id?: string; - type?: "tool_call"; -} - -export function findToolCall(name: Name) { - return ( - x: ToolCall, - ): x is { name: Name; args: z.infer; id?: string } => x.name === name; -} diff --git a/agent/open-code/index.ts b/agent/open-code/index.ts deleted file mode 100644 index 2707cff..0000000 --- a/agent/open-code/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - END, - LangGraphRunnableConfig, - START, - StateGraph, -} from "@langchain/langgraph"; -import { OpenCodeAnnotation, OpenCodeState } from "./types"; -import { planner } from "./nodes/planner"; -import { - executor, - SUCCESSFULLY_COMPLETED_STEPS_CONTENT, -} from "./nodes/executor"; -import { AIMessage } from "@langchain/langgraph-sdk"; - -function conditionallyEnd( - state: OpenCodeState, - config: LangGraphRunnableConfig, -): typeof END | "planner" { - const fullWriteAccess = !!config.configurable?.permissions?.full_write_access; - const lastAiMessage = state.messages.findLast( - (m) => m.getType() === "ai", - ) as unknown as AIMessage; - - // If the user did not grant full write access, or the last AI message is the success message, end - // otherwise, loop back to the start. - if ( - (typeof lastAiMessage.content === "string" && - lastAiMessage.content === SUCCESSFULLY_COMPLETED_STEPS_CONTENT) || - !fullWriteAccess - ) { - return END; - } - - return "planner"; -} - -const workflow = new StateGraph(OpenCodeAnnotation) - .addNode("planner", planner) - .addNode("executor", executor) - .addEdge(START, "planner") - .addEdge("planner", "executor") - .addConditionalEdges("executor", conditionallyEnd, ["planner", END]); - -export const graph = workflow.compile(); -graph.name = "Open Code Graph"; diff --git a/agent/open-code/nodes/executor.ts b/agent/open-code/nodes/executor.ts deleted file mode 100644 index 9158f96..0000000 --- a/agent/open-code/nodes/executor.ts +++ /dev/null @@ -1,127 +0,0 @@ -import fs from "fs/promises"; -import { v4 as uuidv4 } from "uuid"; -import { AIMessage } from "@langchain/langgraph-sdk"; -import { OpenCodeState, OpenCodeUpdate } from "../types"; -import { LangGraphRunnableConfig } from "@langchain/langgraph"; -import ComponentMap from "../../uis"; -import { typedUi } from "@langchain/langgraph-sdk/react-ui/server"; - -export const SUCCESSFULLY_COMPLETED_STEPS_CONTENT = - "Successfully completed all the steps in the plan. Please let me know if you need anything else!"; - -export async function executor( - state: OpenCodeState, - config: LangGraphRunnableConfig, -): Promise { - const ui = typedUi(config); - - const lastPlanToolCall = state.messages.findLast( - (m) => - m.getType() === "ai" && - (m as unknown as AIMessage).tool_calls?.some((tc) => tc.name === "plan"), - ) as AIMessage | undefined; - const planToolCallArgs = lastPlanToolCall?.tool_calls?.[0]?.args; - const nextPlanItem = planToolCallArgs?.remainingPlans?.[0] as - | string - | undefined; - const numSeenPlans = - [ - ...(planToolCallArgs?.executedPlans ?? []), - ...(planToolCallArgs?.rejectedPlans ?? []), - ]?.length ?? 0; - - if (!nextPlanItem) { - // All plans have been executed - const successfullyFinishedMsg: AIMessage = { - type: "ai", - id: uuidv4(), - content: SUCCESSFULLY_COMPLETED_STEPS_CONTENT, - }; - return { messages: [successfullyFinishedMsg] }; - } - - let updateFileContents = ""; - switch (numSeenPlans) { - case 0: - updateFileContents = await fs.readFile( - "agent/open-code/nodes/plan-code/step-1.txt", - "utf-8", - ); - break; - case 1: - updateFileContents = await fs.readFile( - "agent/open-code/nodes/plan-code/step-2.txt", - "utf-8", - ); - break; - case 2: - updateFileContents = await fs.readFile( - "agent/open-code/nodes/plan-code/step-3.txt", - "utf-8", - ); - break; - case 3: - updateFileContents = await fs.readFile( - "agent/open-code/nodes/plan-code/step-4.txt", - "utf-8", - ); - break; - case 4: - updateFileContents = await fs.readFile( - "agent/open-code/nodes/plan-code/step-5.txt", - "utf-8", - ); - break; - case 5: - updateFileContents = await fs.readFile( - "agent/open-code/nodes/plan-code/step-6.txt", - "utf-8", - ); - break; - default: - updateFileContents = ""; - } - - if (!updateFileContents) { - throw new Error("No file updates found!"); - } - - const toolCallId = uuidv4(); - const aiMessage: AIMessage = { - type: "ai", - id: uuidv4(), - content: "", - tool_calls: [ - { - name: "update_file", - args: { - new_file_content: updateFileContents, - executed_plan_item: nextPlanItem, - }, - id: toolCallId, - type: "tool_call", - }, - ], - }; - - const fullWriteAccess = !!config.configurable?.permissions?.full_write_access; - - ui.push( - { - name: "proposed-change", - content: { - toolCallId, - change: updateFileContents, - planItem: nextPlanItem, - fullWriteAccess, - }, - }, - { message: aiMessage }, - ); - - return { - messages: [aiMessage], - ui: ui.items, - timestamp: Date.now(), - }; -} diff --git a/agent/open-code/nodes/plan-code/step-1.txt b/agent/open-code/nodes/plan-code/step-1.txt deleted file mode 100644 index e4776f4..0000000 --- a/agent/open-code/nodes/plan-code/step-1.txt +++ /dev/null @@ -1,5 +0,0 @@ -```bash -npx create-react-app todo-app --template typescript -cd todo-app -mkdir -p src/{components,styles,utils} -``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-2.txt b/agent/open-code/nodes/plan-code/step-2.txt deleted file mode 100644 index 93c51c9..0000000 --- a/agent/open-code/nodes/plan-code/step-2.txt +++ /dev/null @@ -1,21 +0,0 @@ -```tsx -// src/components/TodoItem.tsx -import React from 'react'; -import styles from '../styles/TodoItem.module.css'; - -interface TodoItemProps { - id: string; - text: string; - completed: boolean; - onToggle: (id: string) => void; - onDelete: (id: string) => void; -} - -export const TodoItem: React.FC = ({ id, text, completed, onToggle, onDelete }) => ( -
- onToggle(id)} /> - {text} - -
-); -``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-3.txt b/agent/open-code/nodes/plan-code/step-3.txt deleted file mode 100644 index 3e6d746..0000000 --- a/agent/open-code/nodes/plan-code/step-3.txt +++ /dev/null @@ -1,22 +0,0 @@ -```tsx -// src/context/TodoContext.tsx -import React, { createContext, useContext, useReducer } from 'react'; - -type Todo = { id: string; text: string; completed: boolean; }; - -type TodoState = { todos: Todo[]; }; -type TodoAction = - | { type: 'ADD_TODO'; payload: string } - | { type: 'TOGGLE_TODO'; payload: string } - | { type: 'DELETE_TODO'; payload: string }; - -const TodoContext = createContext<{ - state: TodoState; - dispatch: React.Dispatch; -} | undefined>(undefined); - -export const TodoProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [state, dispatch] = useReducer(todoReducer, { todos: [] }); - return {children}; -}; -``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-4.txt b/agent/open-code/nodes/plan-code/step-4.txt deleted file mode 100644 index b3c2a37..0000000 --- a/agent/open-code/nodes/plan-code/step-4.txt +++ /dev/null @@ -1,33 +0,0 @@ -```tsx -// src/components/AddTodo.tsx -import React, { useState } from 'react'; -import styles from '../styles/AddTodo.module.css'; - -export const AddTodo: React.FC<{ onAdd: (text: string) => void }> = ({ onAdd }) => { - const [text, setText] = useState(''); - const [error, setError] = useState(''); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!text.trim()) { - setError('Todo text cannot be empty'); - return; - } - onAdd(text.trim()); - setText(''); - setError(''); - }; - - return ( -
- setText(e.target.value)} - placeholder='Add a new todo' - /> - {error &&
{error}
} - -
- ); -}; -``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-5.txt b/agent/open-code/nodes/plan-code/step-5.txt deleted file mode 100644 index 2082d9d..0000000 --- a/agent/open-code/nodes/plan-code/step-5.txt +++ /dev/null @@ -1,22 +0,0 @@ -```tsx -// src/components/TodoFilters.tsx -import React from 'react'; - -type FilterType = 'all' | 'active' | 'completed'; - -export const TodoFilters: React.FC<{ - currentFilter: FilterType; - onFilterChange: (filter: FilterType) => void; - onSortChange: (ascending: boolean) => void; -}> = ({ currentFilter, onFilterChange, onSortChange }) => ( -
- - - -
-); -``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-6.txt b/agent/open-code/nodes/plan-code/step-6.txt deleted file mode 100644 index 84d96fa..0000000 --- a/agent/open-code/nodes/plan-code/step-6.txt +++ /dev/null @@ -1,13 +0,0 @@ -```tsx -// src/utils/storage.ts -const STORAGE_KEY = 'todos'; - -export const saveTodos = (todos: Todo[]) => { - localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); -}; - -export const loadTodos = (): Todo[] => { - const stored = localStorage.getItem(STORAGE_KEY); - return stored ? JSON.parse(stored) : []; -}; -``` \ No newline at end of file diff --git a/agent/open-code/nodes/planner.ts b/agent/open-code/nodes/planner.ts deleted file mode 100644 index b7f2615..0000000 --- a/agent/open-code/nodes/planner.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { v4 as uuidv4 } from "uuid"; -import { AIMessage, ToolMessage } from "@langchain/langgraph-sdk"; -import { OpenCodeState, OpenCodeUpdate } from "../types"; -import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses"; -import { LangGraphRunnableConfig } from "@langchain/langgraph"; -import ComponentMap from "../../uis"; -import { typedUi } from "@langchain/langgraph-sdk/react-ui/server"; - -const PLAN = [ - "Set up project scaffolding using Create React App and implement basic folder structure for components, styles, and utilities.", - "Create reusable UI components for TodoItem, including styling with CSS modules.", - "Implement state management using React Context to handle todo items, including actions for adding, updating, and deleting todos.", - "Add form functionality for creating new todos with input validation and error handling.", - "Create filtering and sorting capabilities to allow users to view completed, active, or all todos.", - "Implement local storage integration to persist todo items between page refreshes.", -]; - -export async function planner( - state: OpenCodeState, - config: LangGraphRunnableConfig, -): Promise { - const ui = typedUi(config); - - const lastUpdateCodeToolCall = state.messages.findLast( - (m) => - m.getType() === "ai" && - (m as unknown as AIMessage).tool_calls?.some( - (tc) => tc.name === "update_file", - ), - ) as AIMessage | undefined; - const lastUpdateToolCallResponse = state.messages.findLast( - (m) => - m.getType() === "tool" && - (m as unknown as ToolMessage).tool_call_id === - lastUpdateCodeToolCall?.tool_calls?.[0]?.id, - ) as ToolMessage | undefined; - const lastPlanToolCall = state.messages.findLast( - (m) => - m.getType() === "ai" && - (m as unknown as AIMessage).tool_calls?.some((tc) => tc.name === "plan"), - ) as AIMessage | undefined; - - const wasPlanRejected = ( - lastUpdateToolCallResponse?.content as string | undefined - ) - ?.toLowerCase() - .includes("rejected"); - - const planToolCallArgs = lastPlanToolCall?.tool_calls?.[0]?.args; - const executedPlans: string[] = planToolCallArgs?.executedPlans ?? []; - const rejectedPlans: string[] = planToolCallArgs?.rejectedPlans ?? []; - let remainingPlans: string[] = planToolCallArgs?.remainingPlans ?? PLAN; - - const proposedChangePlanItem: string | undefined = - lastUpdateCodeToolCall?.tool_calls?.[0]?.args?.executed_plan_item; - if (proposedChangePlanItem) { - if (wasPlanRejected) { - rejectedPlans.push(proposedChangePlanItem); - } else { - executedPlans.push(proposedChangePlanItem); - } - - remainingPlans = remainingPlans.filter((p) => p !== proposedChangePlanItem); - } - - const content = proposedChangePlanItem - ? `I've updated the plan list based on the last proposed change.` - : `I've come up with a detailed plan for building the todo app.`; - - const toolCallId = uuidv4(); - const aiMessage: AIMessage = { - type: "ai", - id: uuidv4(), - content, - tool_calls: [ - { - name: "plan", - args: { - executedPlans, - rejectedPlans, - remainingPlans, - }, - id: toolCallId, - type: "tool_call", - }, - ], - }; - - ui.push( - { - name: "code-plan", - content: { - toolCallId, - executedPlans, - rejectedPlans, - remainingPlans, - }, - }, - { message: aiMessage }, - ); - - const toolMessage: ToolMessage = { - type: "tool", - id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`, - tool_call_id: toolCallId, - content: "User has approved the plan.", - }; - - return { - messages: [aiMessage, toolMessage], - ui: ui.items, - timestamp: Date.now(), - }; -} diff --git a/agent/open-code/types.ts b/agent/open-code/types.ts deleted file mode 100644 index 3a32290..0000000 --- a/agent/open-code/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Annotation } from "@langchain/langgraph"; -import { GenerativeUIAnnotation } from "../types"; - -export const OpenCodeAnnotation = Annotation.Root({ - messages: GenerativeUIAnnotation.spec.messages, - ui: GenerativeUIAnnotation.spec.ui, - timestamp: GenerativeUIAnnotation.spec.timestamp, -}); - -export type OpenCodeState = typeof OpenCodeAnnotation.State; -export type OpenCodeUpdate = typeof OpenCodeAnnotation.Update; diff --git a/agent/pizza-orderer/index.ts b/agent/pizza-orderer/index.ts deleted file mode 100644 index 948cbfe..0000000 --- a/agent/pizza-orderer/index.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { ChatAnthropic } from "@langchain/anthropic"; -import { Annotation, END, START, StateGraph } from "@langchain/langgraph"; -import { GenerativeUIAnnotation } from "../types"; -import { z } from "zod"; -import { AIMessage, ToolMessage } from "@langchain/langgraph-sdk"; -import { v4 as uuidv4 } from "uuid"; - -const PizzaOrdererAnnotation = Annotation.Root({ - messages: GenerativeUIAnnotation.spec.messages, -}); - -async function sleep(ms = 5000) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -const workflow = new StateGraph(PizzaOrdererAnnotation) - .addNode("findStore", async (state) => { - const findShopSchema = z - .object({ - location: z - .string() - .describe( - "The location the user is in. E.g. 'San Francisco' or 'New York'", - ), - pizza_company: z - .string() - .optional() - .describe( - "The name of the pizza company. E.g. 'Dominos' or 'Papa John's'. Optional, if not defined it will search for all pizza shops", - ), - }) - .describe("The schema for finding a pizza shop for the user"); - const model = new ChatAnthropic({ - model: "claude-3-5-sonnet-latest", - temperature: 0, - }).withStructuredOutput(findShopSchema, { - name: "find_pizza_shop", - includeRaw: true, - }); - - const response = await model.invoke([ - { - role: "system", - content: - "You are a helpful AI assistant, tasked with extracting information from the conversation between you, and the user, in order to find a pizza shop for them.", - }, - ...state.messages, - ]); - - await sleep(); - - const toolResponse: ToolMessage = { - type: "tool", - id: uuidv4(), - content: - "I've found a pizza shop at 1119 19th St, San Francisco, CA 94107. The phone number for the shop is 415-555-1234.", - tool_call_id: - (response.raw as unknown as AIMessage).tool_calls?.[0].id ?? "", - }; - - return { - messages: [response.raw, toolResponse], - }; - }) - .addNode("orderPizza", async (state) => { - await sleep(1500); - - const placeOrderSchema = z - .object({ - address: z - .string() - .describe("The address of the store to order the pizza from"), - phone_number: z - .string() - .describe("The phone number of the store to order the pizza from"), - order: z.string().describe("The full pizza order for the user"), - }) - .describe("The schema for ordering a pizza for the user"); - const model = new ChatAnthropic({ - model: "claude-3-5-sonnet-latest", - temperature: 0, - }).withStructuredOutput(placeOrderSchema, { - name: "place_pizza_order", - includeRaw: true, - }); - - const response = await model.invoke([ - { - role: "system", - content: - "You are a helpful AI assistant, tasked with placing an order for a pizza for the user.", - }, - ...state.messages, - ]); - - const toolResponse: ToolMessage = { - type: "tool", - id: uuidv4(), - content: "Pizza order successfully placed.", - tool_call_id: - (response.raw as unknown as AIMessage).tool_calls?.[0].id ?? "", - }; - - return { - messages: [response.raw, toolResponse], - }; - }) - .addEdge(START, "findStore") - .addEdge("findStore", "orderPizza") - .addEdge("orderPizza", END); - -export const graph = workflow.compile(); -graph.name = "Order Pizza Graph"; diff --git a/agent/stockbroker/index.tsx b/agent/stockbroker/index.tsx deleted file mode 100644 index 7315838..0000000 --- a/agent/stockbroker/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { StateGraph, START } from "@langchain/langgraph"; -import { StockbrokerAnnotation } from "./types"; -import { callTools } from "./nodes/tools"; - -const builder = new StateGraph(StockbrokerAnnotation) - .addNode("agent", callTools) - .addEdge(START, "agent"); - -export const stockbrokerGraph = builder.compile(); -stockbrokerGraph.name = "Stockbroker"; diff --git a/agent/stockbroker/nodes/tools.tsx b/agent/stockbroker/nodes/tools.tsx deleted file mode 100644 index 4fca505..0000000 --- a/agent/stockbroker/nodes/tools.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { StockbrokerState, StockbrokerUpdate } from "../types"; -import { ChatOpenAI } from "@langchain/openai"; -import { typedUi } from "@langchain/langgraph-sdk/react-ui/server"; -import type ComponentMap from "../../uis/index"; -import { z } from "zod"; -import { LangGraphRunnableConfig } from "@langchain/langgraph"; -import { findToolCall } from "../../find-tool-call"; -import { format, subDays } from "date-fns"; -import { Price, Snapshot } from "../../types"; - -async function getNextPageData(url: string) { - if (!process.env.FINANCIAL_DATASETS_API_KEY) { - throw new Error("Financial datasets API key not set"); - } - - const options = { - method: "GET", - headers: { "X-API-KEY": process.env.FINANCIAL_DATASETS_API_KEY }, - }; - - const response = await fetch(url, options); - if (!response.ok) { - throw new Error("Failed to fetch prices"); - } - return await response.json(); -} - -async function getPricesForTicker(ticker: string): Promise<{ - oneDayPrices: Price[]; - thirtyDayPrices: Price[]; -}> { - if (!process.env.FINANCIAL_DATASETS_API_KEY) { - throw new Error("Financial datasets API key not set"); - } - - const options = { - method: "GET", - headers: { "X-API-KEY": process.env.FINANCIAL_DATASETS_API_KEY }, - }; - - const url = "https://api.financialdatasets.ai/prices"; - - const oneMonthAgo = format(subDays(new Date(), 30), "yyyy-MM-dd"); - const now = format(new Date(), "yyyy-MM-dd"); - - const queryParamsOneDay = new URLSearchParams({ - ticker, - interval: "minute", - interval_multiplier: "5", - start_date: now, - end_date: now, - limit: "5000", - }); - - const queryParamsThirtyDays = new URLSearchParams({ - ticker, - interval: "minute", - interval_multiplier: "30", - start_date: oneMonthAgo, - end_date: now, - limit: "5000", - }); - - const [resOneDay, resThirtyDays] = await Promise.all([ - fetch(`${url}?${queryParamsOneDay.toString()}`, options), - fetch(`${url}?${queryParamsThirtyDays.toString()}`, options), - ]); - - if (!resOneDay.ok || !resThirtyDays.ok) { - throw new Error("Failed to fetch prices"); - } - - const { prices: pricesOneDay } = await resOneDay.json(); - const { prices: pricesThirtyDays, next_page_url } = - await resThirtyDays.json(); - - let nextPageUrlThirtyDays = next_page_url; - - let iters = 0; - while (nextPageUrlThirtyDays) { - if (iters > 10) { - throw new Error("MAX ITERS REACHED"); - } - const nextPageData = await getNextPageData(nextPageUrlThirtyDays); - pricesThirtyDays.push(...nextPageData.prices); - nextPageUrlThirtyDays = nextPageData.next_page_url; - iters += 1; - } - - return { - oneDayPrices: pricesOneDay, - thirtyDayPrices: pricesThirtyDays, - }; -} - -async function getPriceSnapshotForTicker(ticker: string): Promise { - if (!process.env.FINANCIAL_DATASETS_API_KEY) { - throw new Error("Financial datasets API key not set"); - } - - const options = { - method: "GET", - headers: { "X-API-KEY": process.env.FINANCIAL_DATASETS_API_KEY }, - }; - const url = "https://api.financialdatasets.ai/prices/snapshot"; - - const queryParams = new URLSearchParams({ - ticker, - }); - - const response = await fetch(`${url}?${queryParams.toString()}`, options); - if (!response.ok) { - throw new Error("Failed to fetch price snapshot"); - } - - const { snapshot } = await response.json(); - return snapshot; -} - -const llm = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 }); - -const getStockPriceSchema = z.object({ - ticker: z.string().describe("The ticker symbol of the company"), -}); -const getPortfolioSchema = z.object({ - get_portfolio: z.boolean().describe("Should be true."), -}); -const buyStockSchema = z.object({ - ticker: z.string().describe("The ticker symbol of the company"), - quantity: z.number().describe("The quantity of the stock to buy"), -}); - -const STOCKBROKER_TOOLS = [ - { - name: "stock-price", - description: "A tool to get the stock price of a company", - schema: getStockPriceSchema, - }, - { - name: "portfolio", - description: - "A tool to get the user's portfolio details. Only call this tool if the user requests their portfolio details.", - schema: getPortfolioSchema, - }, - { - name: "buy-stock", - description: "A tool to buy a stock", - schema: buyStockSchema, - }, -]; - -export async function callTools( - state: StockbrokerState, - config: LangGraphRunnableConfig, -): Promise { - const ui = typedUi(config); - - const message = await llm.bindTools(STOCKBROKER_TOOLS).invoke([ - { - role: "system", - content: - "You are a stockbroker agent that uses tools to get the stock price of a company", - }, - ...state.messages, - ]); - - const stockbrokerToolCall = message.tool_calls?.find( - findToolCall("stock-price"), - ); - const portfolioToolCall = message.tool_calls?.find( - findToolCall("portfolio"), - ); - const buyStockToolCall = message.tool_calls?.find( - findToolCall("buy-stock"), - ); - - if (stockbrokerToolCall) { - const prices = await getPricesForTicker(stockbrokerToolCall.args.ticker); - ui.push( - { - name: "stock-price", - content: { ticker: stockbrokerToolCall.args.ticker, ...prices }, - }, - { message }, - ); - } - if (portfolioToolCall) { - ui.push({ name: "portfolio", content: {} }, { message }); - } - if (buyStockToolCall) { - const snapshot = await getPriceSnapshotForTicker( - buyStockToolCall.args.ticker, - ); - ui.push( - { - name: "buy-stock", - content: { - toolCallId: buyStockToolCall.id ?? "", - snapshot, - quantity: buyStockToolCall.args.quantity, - }, - }, - { message }, - ); - } - - return { - messages: [message], - ui: ui.items, - timestamp: Date.now(), - }; -} diff --git a/agent/stockbroker/types.ts b/agent/stockbroker/types.ts deleted file mode 100644 index ab8f43b..0000000 --- a/agent/stockbroker/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Annotation } from "@langchain/langgraph"; -import { GenerativeUIAnnotation } from "../types"; - -export const StockbrokerAnnotation = Annotation.Root({ - messages: GenerativeUIAnnotation.spec.messages, - ui: GenerativeUIAnnotation.spec.ui, - timestamp: GenerativeUIAnnotation.spec.timestamp, -}); - -export type StockbrokerState = typeof StockbrokerAnnotation.State; -export type StockbrokerUpdate = typeof StockbrokerAnnotation.Update; diff --git a/agent/trip-planner/index.tsx b/agent/trip-planner/index.tsx deleted file mode 100644 index 60f8f25..0000000 --- a/agent/trip-planner/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { StateGraph, START, END } from "@langchain/langgraph"; -import { TripPlannerAnnotation, TripPlannerState } from "./types"; -import { extraction } from "./nodes/extraction"; -import { callTools } from "./nodes/tools"; -import { classify } from "./nodes/classify"; - -function routeStart(state: TripPlannerState): "classify" | "extraction" { - if (!state.tripDetails) { - return "extraction"; - } - - return "classify"; -} - -function routeAfterClassifying( - state: TripPlannerState, -): "callTools" | "extraction" { - // if `tripDetails` is undefined, this means they are not relevant to the conversation - if (!state.tripDetails) { - return "extraction"; - } - - // otherwise, they are relevant, and we should route to callTools - return "callTools"; -} - -function routeAfterExtraction( - state: TripPlannerState, -): "callTools" | typeof END { - // if `tripDetails` is undefined, this means they're missing some fields. - if (!state.tripDetails) { - return END; - } - - return "callTools"; -} - -const builder = new StateGraph(TripPlannerAnnotation) - .addNode("classify", classify) - .addNode("extraction", extraction) - .addNode("callTools", callTools) - .addConditionalEdges(START, routeStart, ["classify", "extraction"]) - .addConditionalEdges("classify", routeAfterClassifying, [ - "callTools", - "extraction", - ]) - .addConditionalEdges("extraction", routeAfterExtraction, ["callTools", END]) - .addEdge("callTools", END); - -export const tripPlannerGraph = builder.compile(); -tripPlannerGraph.name = "Trip Planner"; diff --git a/agent/trip-planner/nodes/classify.ts b/agent/trip-planner/nodes/classify.ts deleted file mode 100644 index ee82a7f..0000000 --- a/agent/trip-planner/nodes/classify.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { ChatOpenAI } from "@langchain/openai"; -import { TripPlannerState, TripPlannerUpdate } from "../types"; -import { z } from "zod"; -import { formatMessages } from "agent/utils/format-messages"; - -export async function classify( - state: TripPlannerState, -): Promise { - if (!state.tripDetails) { - // Can not classify if tripDetails are undefined - return {}; - } - - const schema = z.object({ - isRelevant: z - .boolean() - .describe( - "Whether the trip details are still relevant to the user's request.", - ), - }); - - const model = new ChatOpenAI({ model: "gpt-4o", temperature: 0 }).bindTools( - [ - { - name: "classify", - description: - "A tool to classify whether or not the trip details are still relevant to the user's request.", - schema, - }, - ], - { - tool_choice: "classify", - }, - ); - - const prompt = `You're an AI assistant for planning trips. The user has already specified the following details for their trip: -- location - ${state.tripDetails.location} -- startDate - ${state.tripDetails.startDate} -- endDate - ${state.tripDetails.endDate} -- numberOfGuests - ${state.tripDetails.numberOfGuests} - -Your task is to carefully read over the user's conversation, and determine if their trip details are still relevant to their most recent request. -You should set is relevant to false if they are now asking about a new location, trip duration, or number of guests. -If they do NOT change their request details (or they never specified them), please set is relevant to true. -`; - - const humanMessage = `Here is the entire conversation so far:\n${formatMessages(state.messages)}`; - - const response = await model.invoke( - [ - { role: "system", content: prompt }, - { role: "human", content: humanMessage }, - ], - { tags: ["langsmith:nostream"] }, - ); - - const classificationDetails = response.tool_calls?.[0]?.args as - | z.infer - | undefined; - - if (!classificationDetails) { - throw new Error("Could not classify trip details"); - } - - if (!classificationDetails.isRelevant) { - return { - tripDetails: undefined, - }; - } - - // If it is relevant, return the state unchanged - return {}; -} diff --git a/agent/trip-planner/nodes/extraction.tsx b/agent/trip-planner/nodes/extraction.tsx deleted file mode 100644 index 8493d10..0000000 --- a/agent/trip-planner/nodes/extraction.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { v4 as uuidv4 } from "uuid"; -import { ChatOpenAI } from "@langchain/openai"; -import { TripDetails, TripPlannerState, TripPlannerUpdate } from "../types"; -import { z } from "zod"; -import { formatMessages } from "agent/utils/format-messages"; -import { ToolMessage } from "@langchain/langgraph-sdk"; -import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses"; - -function calculateDates( - startDate: string | undefined, - endDate: string | undefined, -): { startDate: Date; endDate: Date } { - const now = new Date(); - - if (!startDate && !endDate) { - // Both undefined: 4 and 5 weeks in future - const start = new Date(now); - start.setDate(start.getDate() + 28); // 4 weeks - const end = new Date(now); - end.setDate(end.getDate() + 35); // 5 weeks - return { startDate: start, endDate: end }; - } - - if (startDate && !endDate) { - // Only start defined: end is 1 week after - const start = new Date(startDate); - const end = new Date(start); - end.setDate(end.getDate() + 7); - return { startDate: start, endDate: end }; - } - - if (!startDate && endDate) { - // Only end defined: start is 1 week before - const end = new Date(endDate); - const start = new Date(end); - start.setDate(start.getDate() - 7); - return { startDate: start, endDate: end }; - } - - // Both defined: use as is - return { - startDate: new Date(startDate!), - endDate: new Date(endDate!), - }; -} - -export async function extraction( - state: TripPlannerState, -): Promise { - const schema = z.object({ - location: z - .string() - .describe( - "The location to plan the trip for. Can be a city, state, or country.", - ), - startDate: z - .string() - .optional() - .describe("The start date of the trip. Should be in YYYY-MM-DD format"), - endDate: z - .string() - .optional() - .describe("The end date of the trip. Should be in YYYY-MM-DD format"), - numberOfGuests: z - .number() - .describe( - "The number of guests for the trip. Should default to 2 if not specified", - ), - }); - - const model = new ChatOpenAI({ model: "gpt-4o", temperature: 0 }).bindTools([ - { - name: "extract", - description: "A tool to extract information from a user's request.", - schema: schema, - }, - ]); - - const prompt = `You're an AI assistant for planning trips. The user has requested information about a trip they want to go on. -Before you can help them, you need to extract the following information from their request: -- location - The location to plan the trip for. Can be a city, state, or country. -- startDate - The start date of the trip. Should be in YYYY-MM-DD format. Optional -- endDate - The end date of the trip. Should be in YYYY-MM-DD format. Optional -- numberOfGuests - The number of guests for the trip. Optional - -You are provided with the ENTIRE conversation history between you, and the user. Use these messages to extract the necessary information. - -Do NOT guess, or make up any information. If the user did NOT specify a location, please respond with a request for them to specify the location. -You should ONLY send a clarification message if the user did not provide the location. You do NOT need any of the other fields, so if they're missing, proceed without them. -It should be a single sentence, along the lines of "Please specify the location for the trip you want to go on". - -Extract only what is specified by the user. It is okay to leave fields blank if the user did not specify them. -`; - - const humanMessage = `Here is the entire conversation so far:\n${formatMessages(state.messages)}`; - - const response = await model.invoke([ - { role: "system", content: prompt }, - { role: "human", content: humanMessage }, - ]); - - const toolCall = response.tool_calls?.[0]; - if (!toolCall) { - return { - messages: [response], - }; - } - const extractedDetails = toolCall.args as z.infer; - - const { startDate, endDate } = calculateDates( - extractedDetails.startDate, - extractedDetails.endDate, - ); - - const extractionDetailsWithDefaults: TripDetails = { - startDate, - endDate, - numberOfGuests: extractedDetails.numberOfGuests ?? 2, - location: extractedDetails.location, - }; - - const extractToolResponse: ToolMessage = { - type: "tool", - id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`, - tool_call_id: toolCall.id ?? "", - content: "Successfully extracted trip details", - }; - - return { - tripDetails: extractionDetailsWithDefaults, - messages: [response, extractToolResponse], - }; -} diff --git a/agent/trip-planner/nodes/tools.tsx b/agent/trip-planner/nodes/tools.tsx deleted file mode 100644 index a704776..0000000 --- a/agent/trip-planner/nodes/tools.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { TripPlannerState, TripPlannerUpdate } from "../types"; -import { ChatOpenAI } from "@langchain/openai"; -import { typedUi } from "@langchain/langgraph-sdk/react-ui/server"; -import type ComponentMap from "../../uis/index"; -import { z } from "zod"; -import { LangGraphRunnableConfig } from "@langchain/langgraph"; -import { getAccommodationsListProps } from "../utils/get-accommodations"; -import { findToolCall } from "../../find-tool-call"; - -const listAccommodationsSchema = z - .object({}) - .describe("A tool to list accommodations for the user"); -const listRestaurantsSchema = z - .object({}) - .describe("A tool to list restaurants for the user"); - -const ACCOMMODATIONS_TOOLS = [ - { - name: "list-accommodations", - description: "A tool to list accommodations for the user", - schema: listAccommodationsSchema, - }, - { - name: "list-restaurants", - description: "A tool to list restaurants for the user", - schema: listRestaurantsSchema, - }, -]; - -export async function callTools( - state: TripPlannerState, - config: LangGraphRunnableConfig, -): Promise { - if (!state.tripDetails) { - throw new Error("No trip details found"); - } - - const ui = typedUi(config); - - const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 }).bindTools( - ACCOMMODATIONS_TOOLS, - ); - - const response = await llm.invoke([ - { - role: "system", - content: - "You are an AI assistant who helps users book trips. Use the user's most recent message(s) to contextually generate a response.", - }, - ...state.messages, - ]); - - const listAccommodationsToolCall = response.tool_calls?.find( - findToolCall("list-accommodations"), - ); - const listRestaurantsToolCall = response.tool_calls?.find( - findToolCall("list-restaurants"), - ); - - if (!listAccommodationsToolCall && !listRestaurantsToolCall) { - throw new Error("No tool calls found"); - } - - if (listAccommodationsToolCall) { - ui.push( - { - name: "accommodations-list", - content: { - toolCallId: listAccommodationsToolCall.id ?? "", - ...getAccommodationsListProps(state.tripDetails), - }, - }, - { message: response }, - ); - } - - if (listRestaurantsToolCall) { - ui.push( - { - name: "restaurants-list", - content: { tripDetails: state.tripDetails }, - }, - { message: response }, - ); - } - - return { - messages: [response], - ui: ui.items, - timestamp: Date.now(), - }; -} diff --git a/agent/trip-planner/types.ts b/agent/trip-planner/types.ts deleted file mode 100644 index 391aa0d..0000000 --- a/agent/trip-planner/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Annotation } from "@langchain/langgraph"; -import { GenerativeUIAnnotation } from "../types"; - -export type TripDetails = { - location: string; - startDate: Date; - endDate: Date; - numberOfGuests: number; -}; - -export const TripPlannerAnnotation = Annotation.Root({ - messages: GenerativeUIAnnotation.spec.messages, - ui: GenerativeUIAnnotation.spec.ui, - timestamp: GenerativeUIAnnotation.spec.timestamp, - tripDetails: Annotation(), -}); - -export type TripPlannerState = typeof TripPlannerAnnotation.State; -export type TripPlannerUpdate = typeof TripPlannerAnnotation.Update; diff --git a/agent/trip-planner/utils/get-accommodations.ts b/agent/trip-planner/utils/get-accommodations.ts deleted file mode 100644 index eb923c8..0000000 --- a/agent/trip-planner/utils/get-accommodations.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { faker } from "@faker-js/faker"; -import { Accommodation } from "../../types"; -import { TripDetails } from "../types"; - -export function getAccommodationsListProps(tripDetails: TripDetails) { - const IMAGE_URLS = [ - "https://a0.muscache.com/im/pictures/c88d4356-9e33-4277-83fd-3053e5695333.jpg?im_w=1200&im_format=avif", - "https://a0.muscache.com/im/pictures/miso/Hosting-999231834211657440/original/fa140513-cc51-48a6-83c9-ef4e11e69bc2.jpeg?im_w=1200&im_format=avif", - "https://a0.muscache.com/im/pictures/miso/Hosting-5264493/original/10d2c21f-84c2-46c5-b20b-b51d1c2c971a.jpeg?im_w=1200&im_format=avif", - "https://a0.muscache.com/im/pictures/d0e3bb05-a96a-45cf-af92-980269168096.jpg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/miso/Hosting-50597302/original/eb1bb383-4b70-45ae-b3ce-596f83436e6f.jpeg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/miso/Hosting-900891950206269231/original/7cc71402-9430-48b4-b4f1-e8cac69fd7d3.jpeg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/460efdcd-1286-431d-b4e5-e316d6427707.jpg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/prohost-api/Hosting-51234810/original/5231025a-4c39-4a96-ac9c-b088fceb5531.jpeg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/miso/Hosting-14886949/original/a9d72542-cd1f-418d-b070-a73035f94fe4.jpeg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/2011683a-c045-4b5a-97a8-37bca4b98079.jpg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/11bcbeec-749c-4897-8593-1ec6f6dc04ad.jpg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/prohost-api/Hosting-18327626/original/fba2e4e8-9d68-47a8-838e-dab5353e5209.jpeg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/miso/Hosting-813949239894880001/original/b2abe806-b60f-4c0b-b4e6-46808024e5b6.jpeg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/prohost-api/Hosting-894877242638354447/original/29e50d48-1733-4c5b-9068-da4443dd7757.jpeg?im_w=720&im_format=avif", - , - "https://a0.muscache.com/im/pictures/hosting/Hosting-1079897686805296552/original/b24bd803-52f2-4ca7-9389-f73c9d9b3c64.jpeg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/miso/Hosting-43730011/original/29f90186-4f83-408a-89ce-a82e520b4e36.png?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/300ae0e1-fc7e-4a05-93a4-26809311ef19.jpg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/0c7b03c9-8907-437f-8874-628e89e00679.jpg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/prohost-api/Hosting-1040593515802997386/original/0c910b31-03d3-450f-8dc3-2d7f7902b93e.jpeg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/d336587a-a4bf-44c9-b4a6-68b71c359be0.jpg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/prohost-api/Hosting-50345540/original/f8e911bb-8021-4edd-aca4-913d6f41fc6f.jpeg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/prohost-api/Hosting-46122096/original/1bd27f94-cf00-4864-8ad9-bc1cd6c5e10d.jpeg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/574424e1-4935-45f5-a5f0-e960b16a3fcc.jpg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/181d4be2-6cb2-4306-94bf-89aa45c5de66.jpg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/miso/Hosting-50545526/original/af14ce0b-481e-41be-88d1-b84758f578e5.jpeg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/10d8309a-8ae6-492b-b1d5-20a543242c68.jpg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/miso/Hosting-813727499556203528/original/12c1b750-4bea-40d9-9a10-66804df0530a.jpeg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/83e4c0a0-65ce-4c5d-967e-d378ed1bfe15.jpg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/852f2d4d-6786-47b5-a3ca-ff7f21bcac2d.jpg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/92534e36-d67a-4346-b3cf-7371b1985aca.jpg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/ecbfed18-29d0-4f86-b6aa-4325b076dfb3.jpg?im_w=720&im_format=avif", - "https://a0.muscache.com/im/pictures/prohost-api/Hosting-52443635/original/05f084c6-60d0-4945-81ff-d23dfb89c3ca.jpeg?im_w=720&im_format=avif", - ]; - - const getAccommodations = (city: string): Accommodation[] => { - // Shuffle the image URLs array and take the first 6 - const shuffledImages = [...IMAGE_URLS] - .sort(() => Math.random() - 0.5) - .slice(0, 6) - .filter((i): i is string => typeof i === "string"); - - return Array.from({ length: 6 }, (_, index) => ({ - id: faker.string.uuid(), - name: faker.location.streetAddress(), - price: faker.number.int({ min: 100, max: 1000 }), - rating: Number( - faker.number - .float({ min: 4.0, max: 5.0, fractionDigits: 2 }) - .toFixed(2), - ), - city: city, - image: shuffledImages[index], - })); - }; - - return { - tripDetails, - accommodations: getAccommodations(tripDetails.location), - }; -} diff --git a/agent/types.ts b/agent/types.ts deleted file mode 100644 index 8002509..0000000 --- a/agent/types.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { MessagesAnnotation, Annotation } from "@langchain/langgraph"; -import { - RemoveUIMessage, - UIMessage, - uiMessageReducer, -} from "@langchain/langgraph-sdk/react-ui/server"; - -export const GenerativeUIAnnotation = Annotation.Root({ - messages: MessagesAnnotation.spec["messages"], - ui: Annotation< - UIMessage[], - UIMessage | RemoveUIMessage | (UIMessage | RemoveUIMessage)[] - >({ default: () => [], reducer: uiMessageReducer }), - timestamp: Annotation, - next: Annotation< - "stockbroker" | "tripPlanner" | "openCode" | "orderPizza" | "generalInput" - >(), -}); - -export type GenerativeUIState = typeof GenerativeUIAnnotation.State; - -export type Accommodation = { - id: string; - name: string; - price: number; - rating: number; - city: string; - image: string; -}; - -export type Price = { - ticker: string; - open: number; - close: number; - high: number; - low: number; - volume: number; - time: string; -}; - -export type Snapshot = { - price: number; - ticker: string; - day_change: number; - day_change_percent: number; - market_cap: number; - time: string; -}; diff --git a/agent/uis/index.tsx b/agent/uis/index.tsx deleted file mode 100644 index 11b2163..0000000 --- a/agent/uis/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import StockPrice from "./stockbroker/stock-price"; -import PortfolioView from "./stockbroker/portfolio-view"; -import AccommodationsList from "./trip-planner/accommodations-list"; -import RestaurantsList from "./trip-planner/restaurants-list"; -import BuyStock from "./stockbroker/buy-stock"; -import Plan from "./open-code/plan"; -import ProposedChange from "./open-code/proposed-change"; - -const ComponentMap = { - "stock-price": StockPrice, - portfolio: PortfolioView, - "accommodations-list": AccommodationsList, - "restaurants-list": RestaurantsList, - "buy-stock": BuyStock, - "code-plan": Plan, - "proposed-change": ProposedChange, -} as const; -export default ComponentMap; diff --git a/agent/uis/open-code/plan/index.css b/agent/uis/open-code/plan/index.css deleted file mode 100644 index f1d8c73..0000000 --- a/agent/uis/open-code/plan/index.css +++ /dev/null @@ -1 +0,0 @@ -@import "tailwindcss"; diff --git a/agent/uis/open-code/plan/index.tsx b/agent/uis/open-code/plan/index.tsx deleted file mode 100644 index 2b524ee..0000000 --- a/agent/uis/open-code/plan/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import "./index.css"; -import { motion } from "framer-motion"; -import { ChevronDown } from "lucide-react"; -import { useState } from "react"; - -interface PlanProps { - toolCallId: string; - executedPlans: string[]; - rejectedPlans: string[]; - remainingPlans: string[]; -} - -export default function Plan(props: PlanProps) { - const [isExpanded, setIsExpanded] = useState(false); - - return ( -
-
-

Code Plan

-
- -
-
-

- Remaining Plans -

- {props.remainingPlans.map((step, index) => ( -

- {index + 1}. {step} -

- ))} -
-
-

- Executed Plans -

- {props.executedPlans.map((step, index) => ( -

- {step} -

- ))} -
-
-

- Rejected Plans -

- {props.rejectedPlans.map((step, index) => ( -

- {step} -

- ))} -
-
-
- setIsExpanded(!isExpanded)} - animate={{ rotate: isExpanded ? 180 : 0 }} - transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }} - > - - -
- ); -} diff --git a/agent/uis/open-code/proposed-change/index.css b/agent/uis/open-code/proposed-change/index.css deleted file mode 100644 index e69504a..0000000 --- a/agent/uis/open-code/proposed-change/index.css +++ /dev/null @@ -1,122 +0,0 @@ -@import "tailwindcss"; - -@custom-variant dark (&:is(.dark *)); - -:root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.145 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.145 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.985 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.396 0.141 25.723); - --destructive-foreground: oklch(0.637 0.237 25.331); - --border: oklch(0.269 0 0); - --input: oklch(0.269 0 0); - --ring: oklch(0.439 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(0.269 0 0); - --sidebar-ring: oklch(0.439 0 0); -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/agent/uis/open-code/proposed-change/index.tsx b/agent/uis/open-code/proposed-change/index.tsx deleted file mode 100644 index 402c00c..0000000 --- a/agent/uis/open-code/proposed-change/index.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import "./index.css"; -import { v4 as uuidv4 } from "uuid"; -import { Button } from "@/components/ui/button"; -import ReactMarkdown from "react-markdown"; -import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; -import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; -import { UIMessage, useStreamContext } from "@langchain/langgraph-sdk/react-ui"; -import { Message } from "@langchain/langgraph-sdk"; -import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses"; -import { useEffect, useState } from "react"; -import { getToolResponse } from "../../utils/get-tool-response"; -import { cn } from "@/lib/utils"; - -interface ProposedChangeProps { - toolCallId: string; - change: string; - planItem: string; - /** - * Whether or not to show the "Accept"/"Reject" buttons - * If true, this means the user selected the "Accept, don't ask again" - * button for this session. - */ - fullWriteAccess: boolean; -} - -const ACCEPTED_CHANGE_CONTENT = - "User accepted the proposed change. Please continue."; -const REJECTED_CHANGE_CONTENT = - "User rejected the proposed change. Please continue."; - -export default function ProposedChange(props: ProposedChangeProps) { - const [isAccepted, setIsAccepted] = useState(false); - const [isRejected, setIsRejected] = useState(false); - - const thread = useStreamContext< - { messages: Message[]; ui: UIMessage[] }, - { MetaType: { ui: UIMessage | undefined } } - >(); - - const handleReject = () => { - thread.submit({ - messages: [ - { - type: "tool", - tool_call_id: props.toolCallId, - id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`, - name: "update_file", - content: REJECTED_CHANGE_CONTENT, - }, - { - type: "human", - content: `Rejected change.`, - }, - ], - }); - - setIsRejected(true); - }; - - const handleAccept = (shouldGrantFullWriteAccess = false) => { - const humanMessageContent = `Accepted change. ${shouldGrantFullWriteAccess ? "Granted full write access." : ""}`; - thread.submit( - { - messages: [ - { - type: "tool", - tool_call_id: props.toolCallId, - id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`, - name: "update_file", - content: ACCEPTED_CHANGE_CONTENT, - }, - { - type: "human", - content: humanMessageContent, - }, - ], - }, - { - config: { - configurable: { - permissions: { - full_write_access: shouldGrantFullWriteAccess, - }, - }, - }, - }, - ); - - setIsAccepted(true); - }; - - useEffect(() => { - if (typeof window === "undefined" || isAccepted) return; - const toolResponse = getToolResponse(props.toolCallId, thread); - if (toolResponse) { - if (toolResponse.content === ACCEPTED_CHANGE_CONTENT) { - setIsAccepted(true); - } else if (toolResponse.content === REJECTED_CHANGE_CONTENT) { - setIsRejected(true); - } - } - }, []); - - if (isAccepted || isRejected) { - return ( -
-
-

- {isAccepted ? "Accepted" : "Rejected"} Change -

-

{props.planItem}

-
- - ) : ( - {children} - ); - }, - }} - /> -
- ); - } - - return ( -
-
-

Proposed Change

-

{props.planItem}

-
- - ) : ( - {children} - ); - }, - }} - /> - {!props.fullWriteAccess && ( -
- - - -
- )} -
- ); -} diff --git a/agent/uis/stockbroker/buy-stock/index.css b/agent/uis/stockbroker/buy-stock/index.css deleted file mode 100644 index f1d8c73..0000000 --- a/agent/uis/stockbroker/buy-stock/index.css +++ /dev/null @@ -1 +0,0 @@ -@import "tailwindcss"; diff --git a/agent/uis/stockbroker/buy-stock/index.tsx b/agent/uis/stockbroker/buy-stock/index.tsx deleted file mode 100644 index 9a727bc..0000000 --- a/agent/uis/stockbroker/buy-stock/index.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import "./index.css"; -import { v4 as uuidv4 } from "uuid"; -import { Snapshot } from "../../../types"; -import { Button } from "@/components/ui/button"; -import { useEffect, useState } from "react"; -import { Input } from "@/components/ui/input"; -import { UIMessage, useStreamContext } from "@langchain/langgraph-sdk/react-ui"; -import { Message } from "@langchain/langgraph-sdk"; -import { getToolResponse } from "agent/uis/utils/get-tool-response"; -import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses"; - -function Purchased({ - ticker, - quantity, - price, -}: { - ticker: string; - quantity: number; - price: number; -}) { - return ( -
-

Purchase Executed - {ticker}

-
-
-

Number of Shares

-

Market Price

-

Total Cost

-
-
-

{quantity}

-

${price}

-

${(quantity * price).toFixed(2)}

-
-
-
- ); -} - -export default function BuyStock(props: { - toolCallId: string; - snapshot: Snapshot; - quantity: number; -}) { - const { snapshot, toolCallId } = props; - const [quantity, setQuantity] = useState(props.quantity); - const [finalPurchase, setFinalPurchase] = useState<{ - ticker: string; - quantity: number; - price: number; - }>(); - - const thread = useStreamContext< - { messages: Message[]; ui: UIMessage[] }, - { MetaType: { ui: UIMessage | undefined } } - >(); - - useEffect(() => { - if (typeof window === "undefined" || finalPurchase) return; - const toolResponse = getToolResponse(toolCallId, thread); - if (toolResponse) { - try { - const parsedContent: { - purchaseDetails: { - ticker: string; - quantity: number; - price: number; - }; - } = JSON.parse(toolResponse.content as string); - setFinalPurchase(parsedContent.purchaseDetails); - } catch { - console.error("Failed to parse tool response content."); - } - } - }, []); - - function handleBuyStock() { - const orderDetails = { - message: "Successfully purchased stock", - purchaseDetails: { - ticker: snapshot.ticker, - quantity: quantity, - price: snapshot.price, - }, - }; - - thread.submit({ - messages: [ - { - type: "tool", - tool_call_id: toolCallId, - id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`, - name: "buy-stock", - content: JSON.stringify(orderDetails), - }, - { - type: "human", - content: `Purchased ${quantity} shares of ${snapshot.ticker} at ${snapshot.price} per share`, - }, - ], - }); - - setFinalPurchase(orderDetails.purchaseDetails); - } - - if (finalPurchase) { - return ; - } - - return ( -
-

Buy {snapshot.ticker}

-
-
-

Number of Shares

-

Market Price

-

Total Cost

-
-
- setQuantity(Number(e.target.value))} - min={1} - /> -

${snapshot.price}

-

${(quantity * snapshot.price).toFixed(2)}

-
-
- -
- ); -} diff --git a/agent/uis/stockbroker/portfolio-view/index.css b/agent/uis/stockbroker/portfolio-view/index.css deleted file mode 100644 index f1d8c73..0000000 --- a/agent/uis/stockbroker/portfolio-view/index.css +++ /dev/null @@ -1 +0,0 @@ -@import "tailwindcss"; diff --git a/agent/uis/stockbroker/portfolio-view/index.tsx b/agent/uis/stockbroker/portfolio-view/index.tsx deleted file mode 100644 index 2eff0be..0000000 --- a/agent/uis/stockbroker/portfolio-view/index.tsx +++ /dev/null @@ -1,959 +0,0 @@ -import "./index.css"; -import { useState } from "react"; - -export default function PortfolioView() { - // Placeholder portfolio data - ideally would come from props - const [portfolio] = useState({ - totalValue: 156842.75, - cashBalance: 12467.32, - performance: { - daily: 1.24, - weekly: -0.52, - monthly: 3.87, - yearly: 14.28, - }, - holdings: [ - { - symbol: "AAPL", - name: "Apple Inc.", - shares: 45, - price: 187.32, - value: 8429.4, - change: 1.2, - allocation: 5.8, - avgCost: 162.5, - }, - { - symbol: "MSFT", - name: "Microsoft Corporation", - shares: 30, - price: 403.78, - value: 12113.4, - change: 0.5, - allocation: 8.4, - avgCost: 340.25, - }, - { - symbol: "AMZN", - name: "Amazon.com Inc.", - shares: 25, - price: 178.75, - value: 4468.75, - change: -0.8, - allocation: 3.1, - avgCost: 145.3, - }, - { - symbol: "GOOGL", - name: "Alphabet Inc.", - shares: 20, - price: 164.85, - value: 3297.0, - change: 2.1, - allocation: 2.3, - avgCost: 125.75, - }, - { - symbol: "NVDA", - name: "NVIDIA Corporation", - shares: 35, - price: 875.28, - value: 30634.8, - change: 3.4, - allocation: 21.3, - avgCost: 520.4, - }, - { - symbol: "TSLA", - name: "Tesla, Inc.", - shares: 40, - price: 175.9, - value: 7036.0, - change: -1.2, - allocation: 4.9, - avgCost: 190.75, - }, - ], - }); - - const [activeTab, setActiveTab] = useState<"holdings" | "performance">( - "holdings", - ); - const [sortConfig, setSortConfig] = useState<{ - key: string; - direction: "asc" | "desc"; - }>({ - key: "allocation", - direction: "desc", - }); - const [selectedHolding, setSelectedHolding] = useState(null); - - const sortedHoldings = [...portfolio.holdings].sort((a, b) => { - if ( - a[sortConfig.key as keyof typeof a] < b[sortConfig.key as keyof typeof b] - ) { - return sortConfig.direction === "asc" ? -1 : 1; - } - if ( - a[sortConfig.key as keyof typeof a] > b[sortConfig.key as keyof typeof b] - ) { - return sortConfig.direction === "asc" ? 1 : -1; - } - return 0; - }); - - const requestSort = (key: string) => { - let direction: "asc" | "desc" = "asc"; - if (sortConfig.key === key && sortConfig.direction === "asc") { - direction = "desc"; - } - setSortConfig({ key, direction }); - }; - - const formatCurrency = (value: number) => { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(value); - }; - - const formatPercent = (value: number) => { - return `${value > 0 ? "+" : ""}${value.toFixed(2)}%`; - }; - - // Faux chart data for selected holding - const generateChartData = (symbol: string) => { - const data = []; - const basePrice = - portfolio.holdings.find((h) => h.symbol === symbol)?.price || 100; - - for (let i = 0; i < 30; i++) { - const date = new Date(); - date.setDate(date.getDate() - 30 + i); - - const randomFactor = (Math.sin(i / 5) + Math.random() - 0.5) * 0.05; - const price = basePrice * (1 + randomFactor * (i / 3)); - - data.push({ - date: date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }), - price: parseFloat(price.toFixed(2)), - }); - } - - return data; - }; - - // Calculate total value and percent change for display - const totalChange = portfolio.holdings.reduce( - (acc, curr) => acc + (curr.price - curr.avgCost) * curr.shares, - 0, - ); - const totalPercentChange = - (totalChange / (portfolio.totalValue - totalChange)) * 100; - - const selectedStock = selectedHolding - ? portfolio.holdings.find((h) => h.symbol === selectedHolding) - : null; - const chartData = selectedHolding ? generateChartData(selectedHolding) : []; - - return ( -
-
-
-

- - - - - Portfolio Summary -

-
- - - - Updated: {new Date().toLocaleString()} -
-
-
- -
-
-
-
-

Total Value

- - - -
-

- {formatCurrency(portfolio.totalValue)} -

-

= 0 ? "text-green-600" : "text-red-600"}`} - > - {totalPercentChange >= 0 ? ( - - - - ) : ( - - - - )} - {formatPercent(totalPercentChange)} All Time -

-
-
-
-

Cash Balance

- - - - -
-

- {formatCurrency(portfolio.cashBalance)} -

-

- {((portfolio.cashBalance / portfolio.totalValue) * 100).toFixed( - 1, - )} - % of portfolio -

-
-
-
-

Daily Change

- - - -
-

= 0 ? "text-green-600" : "text-red-600"}`} - > - {formatPercent(portfolio.performance.daily)} -

-

= 0 ? "text-green-600" : "text-red-600"}`} - > - {formatCurrency( - (portfolio.totalValue * portfolio.performance.daily) / 100, - )} -

-
-
- -
-
- - -
-
- - {activeTab === "holdings" && !selectedHolding && ( -
- - - - - - - - - - - - - - {sortedHoldings.map((holding) => ( - setSelectedHolding(holding.symbol)} - > - - - - - - - - - ))} - -
requestSort("symbol")} - className="group px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" - > -
- Symbol - - {sortConfig.key === "symbol" - ? sortConfig.direction === "asc" - ? "↑" - : "↓" - : "↕"} - -
-
- Company - requestSort("shares")} - className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" - > -
- Shares - - {sortConfig.key === "shares" - ? sortConfig.direction === "asc" - ? "↑" - : "↓" - : "↕"} - -
-
requestSort("price")} - className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" - > -
- Price - - {sortConfig.key === "price" - ? sortConfig.direction === "asc" - ? "↑" - : "↓" - : "↕"} - -
-
requestSort("change")} - className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" - > -
- Change - - {sortConfig.key === "change" - ? sortConfig.direction === "asc" - ? "↑" - : "↓" - : "↕"} - -
-
requestSort("value")} - className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" - > -
- Value - - {sortConfig.key === "value" - ? sortConfig.direction === "asc" - ? "↑" - : "↓" - : "↕"} - -
-
requestSort("allocation")} - className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" - > -
- Allocation - - {sortConfig.key === "allocation" - ? sortConfig.direction === "asc" - ? "↑" - : "↓" - : "↕"} - -
-
- {holding.symbol} - - {holding.name} - - {holding.shares.toLocaleString()} - - {formatCurrency(holding.price)} - = 0 ? "text-green-600" : "text-red-600"}`} - > - {holding.change >= 0 ? ( - - - - ) : ( - - - - )} - {formatPercent(holding.change)} - - {formatCurrency(holding.value)} - -
-
-
= 0 ? "bg-green-500" : "bg-red-500"}`} - style={{ - width: `${Math.min(100, holding.allocation * 3)}%`, - }} - >
-
- - {holding.allocation.toFixed(1)}% - -
-
-
- )} - - {activeTab === "holdings" && selectedHolding && selectedStock && ( -
-
-
-
-

- {selectedStock.symbol} -

- - {selectedStock.name} - -
-
- - {formatCurrency(selectedStock.price)} - - = 0 ? "text-green-600" : "text-red-600"}`} - > - {selectedStock.change >= 0 ? "▲" : "▼"}{" "} - {formatPercent(selectedStock.change)} - -
-
- -
- -
-
-
- {chartData.map((point, index) => { - const maxPrice = Math.max(...chartData.map((d) => d.price)); - const minPrice = Math.min(...chartData.map((d) => d.price)); - const range = maxPrice - minPrice; - const heightPercent = - range === 0 - ? 50 - : ((point.price - minPrice) / range) * 80 + 10; - - return ( -
-
= chartData[Math.max(0, index - 1)].price ? "bg-green-500" : "bg-red-500"}`} - style={{ height: `${heightPercent}%` }} - >
- {index % 5 === 0 && ( - - {point.date} - - )} -
- ); - })} -
-
-
- -
-
-
-

Shares Owned

-

- {selectedStock.shares.toLocaleString()} -

-
-
-

Market Value

-

- {formatCurrency(selectedStock.value)} -

-
-
-

Avg. Cost

-

- {formatCurrency(selectedStock.avgCost)} -

-
-
-

Cost Basis

-

- {formatCurrency( - selectedStock.avgCost * selectedStock.shares, - )} -

-
-
-

Gain/Loss

-

= 0 ? "text-green-600" : "text-red-600"}`} - > - {formatCurrency( - (selectedStock.price - selectedStock.avgCost) * - selectedStock.shares, - )} -

-
-
-

Allocation

-

- {selectedStock.allocation.toFixed(2)}% -

-
-
-
- -
- - - -
-
- )} - - {activeTab === "performance" && ( -
-
-

- - - - Performance Overview -

-
-
-

Daily

-

= 0 ? "text-green-600" : "text-red-600"}`} - > - {portfolio.performance.daily >= 0 ? ( - - - - ) : ( - - - - )} - {formatPercent(portfolio.performance.daily)} -

-
-
-

Weekly

-

= 0 ? "text-green-600" : "text-red-600"}`} - > - {portfolio.performance.weekly >= 0 ? ( - - - - ) : ( - - - - )} - {formatPercent(portfolio.performance.weekly)} -

-
-
-

Monthly

-

= 0 ? "text-green-600" : "text-red-600"}`} - > - {portfolio.performance.monthly >= 0 ? ( - - - - ) : ( - - - - )} - {formatPercent(portfolio.performance.monthly)} -

-
-
-

Yearly

-

= 0 ? "text-green-600" : "text-red-600"}`} - > - {portfolio.performance.yearly >= 0 ? ( - - - - ) : ( - - - - )} - {formatPercent(portfolio.performance.yearly)} -

-
-
-
- -
-

- - - - - Portfolio Allocation -

-
- {sortedHoldings.map((holding) => ( -
-
-
= 0 ? "bg-green-500" : "bg-red-500"}`} - >
- {holding.symbol} -
-
-
-
-
-
-
- {holding.allocation.toFixed(1)}% -
-
- -
-
- ))} -
- -
-

- Portfolio Diversification -

-
- {[ - "Technology", - "Consumer Cyclical", - "Communication Services", - "Financial", - "Other", - ].map((sector, index) => { - const widths = [42, 23, 18, 10, 7]; // example percentages - const colors = [ - "bg-indigo-600", - "bg-blue-500", - "bg-green-500", - "bg-yellow-500", - "bg-red-500", - ]; - return ( -
- ); - })} -
-
- {[ - "Technology", - "Consumer Cyclical", - "Communication Services", - "Financial", - "Other", - ].map((sector, index) => { - const widths = [42, 23, 18, 10, 7]; // example percentages - const colors = [ - "text-indigo-600", - "text-blue-500", - "text-green-500", - "text-yellow-500", - "text-red-500", - ]; - return ( -
-
- - {sector} {widths[index]}% - -
- ); - })} -
-
-
- -
- - -
-
- )} -
-
- ); -} diff --git a/agent/uis/stockbroker/stock-price/index.css b/agent/uis/stockbroker/stock-price/index.css deleted file mode 100644 index f1d8c73..0000000 --- a/agent/uis/stockbroker/stock-price/index.css +++ /dev/null @@ -1 +0,0 @@ -@import "tailwindcss"; diff --git a/agent/uis/stockbroker/stock-price/index.tsx b/agent/uis/stockbroker/stock-price/index.tsx deleted file mode 100644 index 8bcceca..0000000 --- a/agent/uis/stockbroker/stock-price/index.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import "./index.css"; -import { useState, useMemo } from "react"; -import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from "@/components/ui/chart"; -import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"; -import { Price } from "../../../types"; -import { format } from "date-fns"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; - -const chartConfig = { - price: { - label: "Price", - color: "hsl(var(--chart-1))", - }, -} satisfies ChartConfig; - -type DisplayRange = "1d" | "5d" | "1m"; - -function DisplayRangeSelector({ - displayRange, - setDisplayRange, -}: { - displayRange: DisplayRange; - setDisplayRange: (range: DisplayRange) => void; -}) { - const sharedClass = - " bg-transparent text-gray-500 hover:bg-gray-50 transition-colors ease-in-out duration-200 p-2 cursor-pointer"; - const selectedClass = `text-black bg-gray-100 hover:bg-gray-50`; - return ( -
- -

|

- -

|

- -
- ); -} - -function getPropsForDisplayRange( - displayRange: DisplayRange, - oneDayPrices: Price[], - thirtyDayPrices: Price[], -) { - const now = new Date(); - const fiveDays = 5 * 24 * 60 * 60 * 1000; // 5 days in milliseconds - - switch (displayRange) { - case "1d": - return oneDayPrices; - case "5d": - return thirtyDayPrices.filter( - (p) => new Date(p.time).getTime() >= now.getTime() - fiveDays, - ); - case "1m": - return thirtyDayPrices; - default: - return []; - } -} -export default function StockPrice(props: { - ticker: string; - oneDayPrices: Price[]; - thirtyDayPrices: Price[]; -}) { - const { ticker } = props; - const { oneDayPrices, thirtyDayPrices } = props; - const [displayRange, setDisplayRange] = useState("1d"); - - const { - currentPrice, - openPrice, - dollarChange, - percentChange, - highPrice, - lowPrice, - chartData, - change, - } = useMemo(() => { - const prices = getPropsForDisplayRange( - displayRange, - oneDayPrices, - thirtyDayPrices, - ); - - const firstPrice = prices[0]; - const lastPrice = prices[prices.length - 1]; - - const currentPrice = lastPrice?.close; - const openPrice = firstPrice?.open; - const dollarChange = currentPrice - openPrice; - const percentChange = ((currentPrice - openPrice) / openPrice) * 100; - - const highPrice = prices.reduce( - (acc, p) => Math.max(acc, p.high), - -Infinity, - ); - const lowPrice = prices.reduce((acc, p) => Math.min(acc, p.low), Infinity); - - const chartData = prices.map((p) => ({ - time: p.time, - price: p.close, - })); - - const change: "up" | "down" = dollarChange > 0 ? "up" : "down"; - return { - currentPrice, - openPrice, - dollarChange, - percentChange, - highPrice, - lowPrice, - chartData, - change, - }; - }, [oneDayPrices, thirtyDayPrices, displayRange]); - - const formatDateByDisplayRange = (value: string, isTooltip?: boolean) => { - if (displayRange === "1d") { - return format(value, "h:mm a"); - } - if (isTooltip) { - return format(value, "LLL do h:mm a"); - } - return format(value, "LLL do"); - }; - - return ( -
-
-

{ticker}

-

${currentPrice}

-
-
-

- ${dollarChange.toFixed(2)} (${percentChange.toFixed(2)}%) -

-
-
-
-

Open

-

High

-

Low

-
-
-

${openPrice}

-

${highPrice}

-

${lowPrice}

-
-
- - - - - formatDateByDisplayRange(v)} - /> - `${value.toFixed(2)}`} - /> - formatDateByDisplayRange(v, true)} - /> - } - /> - - - -
- ); -} diff --git a/agent/uis/trip-planner/accommodations-list/index.css b/agent/uis/trip-planner/accommodations-list/index.css deleted file mode 100644 index f1d8c73..0000000 --- a/agent/uis/trip-planner/accommodations-list/index.css +++ /dev/null @@ -1 +0,0 @@ -@import "tailwindcss"; diff --git a/agent/uis/trip-planner/accommodations-list/index.tsx b/agent/uis/trip-planner/accommodations-list/index.tsx deleted file mode 100644 index 39d979b..0000000 --- a/agent/uis/trip-planner/accommodations-list/index.tsx +++ /dev/null @@ -1,341 +0,0 @@ -import "./index.css"; -import { v4 as uuidv4 } from "uuid"; -import { - useStreamContext, - type UIMessage, -} from "@langchain/langgraph-sdk/react-ui"; -import { useEffect, useState } from "react"; -import { X } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { TripDetails } from "../../../trip-planner/types"; -import { - Carousel, - CarouselContent, - CarouselItem, - CarouselNext, - CarouselPrevious, -} from "@/components/ui/carousel"; -import { format } from "date-fns"; -import { Accommodation } from "agent/types"; -import { capitalizeSentence } from "../../../utils/capitalize"; -import { Message } from "@langchain/langgraph-sdk"; -import { getToolResponse } from "../../utils/get-tool-response"; -import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses"; - -const StarSVG = ({ fill = "white" }: { fill?: string }) => ( - - - -); - -function AccommodationCard({ - accommodation, -}: { - accommodation: Accommodation; -}) { - return ( -
-
-

{accommodation.name}

-
-

- - {accommodation.rating} -

-

·

-

{accommodation.price}

-
-

{capitalizeSentence(accommodation.city)}

-
-
- ); -} - -function SelectedAccommodation({ - accommodation, - onHide, - tripDetails, - onBook, -}: { - accommodation: Accommodation; - onHide: () => void; - tripDetails: TripDetails; - onBook: (accommodation: Accommodation) => void; -}) { - const startDate = new Date(tripDetails.startDate); - const endDate = new Date(tripDetails.endDate); - const totalTripDurationDays = Math.max( - startDate.getDate() - endDate.getDate(), - 1, - ); - const totalPrice = totalTripDurationDays * accommodation.price; - - return ( -
-
- {accommodation.name} -
-
-
-

{accommodation.name}

- -
-
-
- - - {accommodation.rating} - -

- {capitalizeSentence(accommodation.city)} -

-
-
-
- Check-in - {format(startDate, "MMM d, yyyy")} -
-
- Check-out - {format(endDate, "MMM d, yyyy")} -
-
- Guests - {tripDetails.numberOfGuests} -
-
- Total Price - ${totalPrice.toLocaleString()} -
-
-
- -
-
- ); -} - -function BookedAccommodation({ - accommodation, - tripDetails, -}: { - accommodation: Accommodation; - tripDetails: TripDetails; -}) { - const startDate = new Date(tripDetails.startDate); - const endDate = new Date(tripDetails.endDate); - const totalTripDurationDays = Math.max( - startDate.getDate() - endDate.getDate(), - 1, - ); - const totalPrice = totalTripDurationDays * accommodation.price; - - return ( -
-
-

Booked Accommodation

- -
-

-
-
-
- Address: -
-
- - {accommodation.name}, {capitalizeSentence(accommodation.city)} - -
- -
- Rating: -
-
- - - {accommodation.rating} - -
- -
- Dates: -
-
- - {format(startDate, "MMM d, yyyy")} -{" "} - {format(endDate, "MMM d, yyyy")} - -
- -
- Guests: -
-
- {tripDetails.numberOfGuests} -
- -
- Total Price: -
-
- ${totalPrice.toLocaleString()} -
-
-
-
- ); -} - -export default function AccommodationsList({ - toolCallId, - tripDetails, - accommodations, -}: { - toolCallId: string; - tripDetails: TripDetails; - accommodations: Accommodation[]; -}) { - const thread = useStreamContext< - { messages: Message[]; ui: UIMessage[] }, - { MetaType: { ui: UIMessage | undefined } } - >(); - - const [selectedAccommodation, setSelectedAccommodation] = useState< - Accommodation | undefined - >(); - const [accommodationBooked, setAccommodationBooked] = useState(false); - - useEffect(() => { - if (typeof window === "undefined" || accommodationBooked) return; - const toolResponse = getToolResponse(toolCallId, thread); - if (toolResponse) { - setAccommodationBooked(true); - try { - const parsedContent: { - accommodation: Accommodation; - tripDetails: TripDetails; - } = JSON.parse(toolResponse.content as string); - setSelectedAccommodation(parsedContent.accommodation); - } catch { - console.error("Failed to parse tool response content."); - } - } - }, []); - - function handleBookAccommodation(accommodation: Accommodation) { - const orderDetails = { - accommodation, - tripDetails, - }; - - thread.submit({ - messages: [ - { - type: "tool", - tool_call_id: toolCallId, - id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`, - name: "book-accommodation", - content: JSON.stringify(orderDetails), - }, - { - type: "human", - content: `Booked ${accommodation.name} for ${tripDetails.numberOfGuests}.`, - }, - ], - }); - - setAccommodationBooked(true); - if (selectedAccommodation?.id !== accommodation.id) { - setSelectedAccommodation(accommodation); - } - } - - if (accommodationBooked && selectedAccommodation) { - return ( - - ); - } else if (accommodationBooked) { - return
Successfully booked accommodation!
; - } - - if (selectedAccommodation) { - return ( - setSelectedAccommodation(undefined)} - accommodation={selectedAccommodation} - onBook={handleBookAccommodation} - /> - ); - } - - return ( -
- - - {accommodations.map((accommodation) => ( - setSelectedAccommodation(accommodation)} - > - - - ))} - - - - -
- ); -} diff --git a/agent/uis/trip-planner/restaurants-list/index.css b/agent/uis/trip-planner/restaurants-list/index.css deleted file mode 100644 index f1d8c73..0000000 --- a/agent/uis/trip-planner/restaurants-list/index.css +++ /dev/null @@ -1 +0,0 @@ -@import "tailwindcss"; diff --git a/agent/uis/trip-planner/restaurants-list/index.tsx b/agent/uis/trip-planner/restaurants-list/index.tsx deleted file mode 100644 index af6bc16..0000000 --- a/agent/uis/trip-planner/restaurants-list/index.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import "./index.css"; -import { TripDetails } from "../../../trip-planner/types"; -import { useState } from "react"; - -export default function RestaurantsList({ - tripDetails, -}: { - tripDetails: TripDetails; -}) { - // Placeholder data - ideally would come from props - const [restaurants] = useState([ - { - id: "1", - name: "The Local Grill", - cuisine: "Steakhouse", - priceRange: "$$", - rating: 4.7, - distance: "0.5 miles from center", - image: "https://placehold.co/300x200?text=Restaurant1", - openingHours: "5:00 PM - 10:00 PM", - popular: true, - }, - { - id: "2", - name: "Ocean Breeze", - cuisine: "Seafood", - priceRange: "$$$", - rating: 4.9, - distance: "0.8 miles from center", - image: "https://placehold.co/300x200?text=Restaurant2", - openingHours: "12:00 PM - 11:00 PM", - popular: true, - }, - { - id: "3", - name: "Pasta Paradise", - cuisine: "Italian", - priceRange: "$$", - rating: 4.5, - distance: "1.2 miles from center", - image: "https://placehold.co/300x200?text=Restaurant3", - openingHours: "11:30 AM - 9:30 PM", - popular: false, - }, - { - id: "4", - name: "Spice Garden", - cuisine: "Indian", - priceRange: "$$", - rating: 4.6, - distance: "0.7 miles from center", - image: "https://placehold.co/300x200?text=Restaurant4", - openingHours: "12:00 PM - 10:00 PM", - popular: false, - }, - ]); - - const [selectedId, setSelectedId] = useState(null); - const [filter, setFilter] = useState(null); - - const selectedRestaurant = restaurants.find((r) => r.id === selectedId); - - const filteredRestaurants = filter - ? restaurants.filter((r) => r.cuisine === filter) - : restaurants; - - const cuisines = Array.from(new Set(restaurants.map((r) => r.cuisine))); - - return ( -
-
-
-

- Restaurants in {tripDetails.location} -

- {selectedId && ( - - )} -
-

- For your trip {new Date(tripDetails.startDate).toLocaleDateString()} -{" "} - {new Date(tripDetails.endDate).toLocaleDateString()} -

-
- - {!selectedId ? ( -
-
-
- - {cuisines.map((cuisine) => ( - - ))} -
-

- Showing {filteredRestaurants.length} restaurants{" "} - {filter ? `in ${filter}` : ""} -

-
- -
- {filteredRestaurants.map((restaurant) => ( -
setSelectedId(restaurant.id)} - className="border rounded-lg p-3 cursor-pointer hover:border-orange-300 hover:shadow-md transition-all" - > -
-
- {restaurant.name} -
-
-
-
-

- {restaurant.name} -

-

- {restaurant.cuisine} -

-
- - {restaurant.priceRange} - -
-
- - - - - {restaurant.rating} - -
-
- - {restaurant.distance} - - {restaurant.popular && ( - - Popular - - )} -
-
-
-
- ))} -
-
- ) : ( -
- {selectedRestaurant && ( -
-
- {selectedRestaurant.name} -
- -
-
-
-

- {selectedRestaurant.name} -

-

- {selectedRestaurant.cuisine} -

-
- - {selectedRestaurant.priceRange} - -
- -
- - - - - {selectedRestaurant.rating} rating - -
- -
- {selectedRestaurant.distance} - - {selectedRestaurant.openingHours} -
- -

- {selectedRestaurant.name} offers a wonderful dining experience - in {tripDetails.location}. Perfect for a group of{" "} - {tripDetails.numberOfGuests} guests. Enjoy authentic{" "} - {selectedRestaurant.cuisine} cuisine in a relaxed atmosphere. -

- -
- - -
-
-
- )} -
- )} -
- ); -} diff --git a/agent/uis/utils/get-tool-response.ts b/agent/uis/utils/get-tool-response.ts deleted file mode 100644 index be5191c..0000000 --- a/agent/uis/utils/get-tool-response.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { - useStreamContext, - type UIMessage, -} from "@langchain/langgraph-sdk/react-ui"; -import { Message, ToolMessage } from "@langchain/langgraph-sdk"; - -type StreamContextType = ReturnType< - typeof useStreamContext< - { messages: Message[]; ui: UIMessage[] }, - { MetaType: { ui: UIMessage | undefined } } - > ->; - -export function getToolResponse( - toolCallId: string, - thread: StreamContextType, -): ToolMessage | undefined { - const toolResponse = thread.messages.findLast( - (message): message is ToolMessage => - message.type === "tool" && message.tool_call_id === toolCallId, - ); - return toolResponse; -} diff --git a/agent/utils/capitalize.ts b/agent/utils/capitalize.ts deleted file mode 100644 index 1f2d619..0000000 --- a/agent/utils/capitalize.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Capitalizes the first letter of each word in a string. - */ -export function capitalizeSentence(string: string): string { - return string - .split(" ") - .map((word) => { - return word.charAt(0).toUpperCase() + word.slice(1); - }) - .join(" "); -} - -/** - * Capitalizes the first letter of a string. - */ -export function capitalize(string: string): string { - return string.charAt(0).toUpperCase() + string.slice(1); -} diff --git a/agent/utils/format-messages.ts b/agent/utils/format-messages.ts deleted file mode 100644 index 91f3bf4..0000000 --- a/agent/utils/format-messages.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { BaseMessage } from "@langchain/core/messages"; - -export function formatMessages(messages: BaseMessage[]): string { - return messages - .map((m, i) => { - const role = m.getType(); - const contentString = - typeof m.content === "string" ? m.content : JSON.stringify(m.content); - return `<${role} index="${i}">\n${contentString}\n`; - }) - .join("\n"); -} diff --git a/langgraph.json b/langgraph.json deleted file mode 100644 index 0856fb4..0000000 --- a/langgraph.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "node_version": "20", - "graphs": { - "agent": "./agent/agent.ts:graph" - }, - "ui": { - "agent": "./agent/uis/index.tsx" - }, - "env": ".env", - "dependencies": ["."] -} diff --git a/package.json b/package.json index cf19114..cb3c1f9 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { - "name": "agent-ui-client", + "name": "chat-langgraph", + "homepage": "https://github.com/langchain-ai/chat-langgraph/blob/main/README.md", + "repository": "https://github.com/langchain-ai/chat-langgraph", "private": true, "version": "0.0.0", "type": "module", "scripts": { - "agent": "langgraphjs dev --no-browser", "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", @@ -15,15 +16,11 @@ "@assistant-ui/react": "^0.8.0", "@assistant-ui/react-markdown": "^0.8.0", "@assistant-ui/react-syntax-highlighter": "^0.7.2", - "@faker-js/faker": "^9.5.1", - "@langchain/anthropic": "^0.3.15", "@langchain/core": "^0.3.41", - "@langchain/google-genai": "^0.1.10", "@langchain/langgraph": "^0.2.49", "@langchain/langgraph-api": "^0.0.14", "@langchain/langgraph-cli": "^0.0.14", "@langchain/langgraph-sdk": "^0.0.52", - "@langchain/openai": "^0.4.4", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-label": "^2.1.2", @@ -34,7 +31,6 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", - "embla-carousel-react": "^8.5.2", "esbuild": "^0.25.0", "esbuild-plugin-tailwindcss": "^2.0.1", "framer-motion": "^12.4.9", diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx deleted file mode 100644 index 09cf122..0000000 --- a/src/components/ui/carousel.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import * as React from "react"; -import useEmblaCarousel, { - type UseEmblaCarouselType, -} from "embla-carousel-react"; -import { ArrowLeft, ArrowRight } from "lucide-react"; - -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; - -type CarouselApi = UseEmblaCarouselType[1]; -type UseCarouselParameters = Parameters; -type CarouselOptions = UseCarouselParameters[0]; -type CarouselPlugin = UseCarouselParameters[1]; - -type CarouselProps = { - opts?: CarouselOptions; - plugins?: CarouselPlugin; - orientation?: "horizontal" | "vertical"; - setApi?: (api: CarouselApi) => void; -}; - -type CarouselContextProps = { - carouselRef: ReturnType[0]; - api: ReturnType[1]; - scrollPrev: () => void; - scrollNext: () => void; - canScrollPrev: boolean; - canScrollNext: boolean; -} & CarouselProps; - -const CarouselContext = React.createContext(null); - -function useCarousel() { - const context = React.useContext(CarouselContext); - - if (!context) { - throw new Error("useCarousel must be used within a "); - } - - return context; -} - -function Carousel({ - orientation = "horizontal", - opts, - setApi, - plugins, - className, - children, - ...props -}: React.ComponentProps<"div"> & CarouselProps) { - const [carouselRef, api] = useEmblaCarousel( - { - ...opts, - axis: orientation === "horizontal" ? "x" : "y", - }, - plugins, - ); - const [canScrollPrev, setCanScrollPrev] = React.useState(false); - const [canScrollNext, setCanScrollNext] = React.useState(false); - - const onSelect = React.useCallback((api: CarouselApi) => { - if (!api) return; - setCanScrollPrev(api.canScrollPrev()); - setCanScrollNext(api.canScrollNext()); - }, []); - - const scrollPrev = React.useCallback(() => { - api?.scrollPrev(); - }, [api]); - - const scrollNext = React.useCallback(() => { - api?.scrollNext(); - }, [api]); - - const handleKeyDown = React.useCallback( - (event: React.KeyboardEvent) => { - if (event.key === "ArrowLeft") { - event.preventDefault(); - scrollPrev(); - } else if (event.key === "ArrowRight") { - event.preventDefault(); - scrollNext(); - } - }, - [scrollPrev, scrollNext], - ); - - React.useEffect(() => { - if (!api || !setApi) return; - setApi(api); - }, [api, setApi]); - - React.useEffect(() => { - if (!api) return; - onSelect(api); - api.on("reInit", onSelect); - api.on("select", onSelect); - - return () => { - api?.off("select", onSelect); - }; - }, [api, onSelect]); - - return ( - -
- {children} -
-
- ); -} - -function CarouselContent({ className, ...props }: React.ComponentProps<"div">) { - const { carouselRef, orientation } = useCarousel(); - - return ( -
-
-
- ); -} - -function CarouselItem({ className, ...props }: React.ComponentProps<"div">) { - const { orientation } = useCarousel(); - - return ( -
- ); -} - -function CarouselPrevious({ - className, - variant = "outline", - size = "icon", - ...props -}: React.ComponentProps) { - const { orientation, scrollPrev, canScrollPrev } = useCarousel(); - - return ( - - ); -} - -function CarouselNext({ - className, - variant = "outline", - size = "icon", - ...props -}: React.ComponentProps) { - const { orientation, scrollNext, canScrollNext } = useCarousel(); - - return ( - - ); -} - -export { - type CarouselApi, - Carousel, - CarouselContent, - CarouselItem, - CarouselPrevious, - CarouselNext, -}; diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx deleted file mode 100644 index 86503e7..0000000 --- a/src/components/ui/chart.tsx +++ /dev/null @@ -1,353 +0,0 @@ -import * as React from "react"; -import * as RechartsPrimitive from "recharts"; - -import { cn } from "@/lib/utils"; - -// Format: { THEME_NAME: CSS_SELECTOR } -const THEMES = { light: "", dark: ".dark" } as const; - -export type ChartConfig = { - [k in string]: { - label?: React.ReactNode; - icon?: React.ComponentType; - } & ( - | { color?: string; theme?: never } - | { color?: never; theme: Record } - ); -}; - -type ChartContextProps = { - config: ChartConfig; -}; - -const ChartContext = React.createContext(null); - -function useChart() { - const context = React.useContext(ChartContext); - - if (!context) { - throw new Error("useChart must be used within a "); - } - - return context; -} - -function ChartContainer({ - id, - className, - children, - config, - ...props -}: React.ComponentProps<"div"> & { - config: ChartConfig; - children: React.ComponentProps< - typeof RechartsPrimitive.ResponsiveContainer - >["children"]; -}) { - const uniqueId = React.useId(); - const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; - - return ( - -
- - - {children} - -
-
- ); -} - -const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { - const colorConfig = Object.entries(config).filter( - ([, config]) => config.theme || config.color, - ); - - if (!colorConfig.length) { - return null; - } - - return ( -