merge main and remove book resturant and accommodation tools/uis

This commit is contained in:
bracesproul
2025-03-10 10:41:27 -07:00
42 changed files with 1370 additions and 1263 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
.next
.git
.env

View File

@@ -1,4 +1,7 @@
# agent-ui demo # Chat LangGraph
> [!WARNING]
> This repo is still a work in progress and is not intended for use. Estimated launch date 03/11. Thank you for your patience.
## Setup ## Setup

View File

@@ -6,9 +6,13 @@ import { stockbrokerGraph } from "./stockbroker";
import { ChatOpenAI } from "@langchain/openai"; import { ChatOpenAI } from "@langchain/openai";
import { tripPlannerGraph } from "./trip-planner"; import { tripPlannerGraph } from "./trip-planner";
import { formatMessages } from "./utils/format-messages"; 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 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.`; - 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( async function router(
state: GenerativeUIState, state: GenerativeUIState,
@@ -19,7 +23,13 @@ ${allToolDescriptions}
`; `;
const routerSchema = z.object({ const routerSchema = z.object({
route: z route: z
.enum(["stockbroker", "tripPlanner", "generalInput"]) .enum([
"stockbroker",
"tripPlanner",
"openCode",
"orderPizza",
"generalInput",
])
.describe(routerDescription), .describe(routerDescription),
}); });
const routerTool = { const routerTool = {
@@ -73,7 +83,7 @@ Please pick the proper route based on the most recent message, in the context of
function handleRoute( function handleRoute(
state: GenerativeUIState, state: GenerativeUIState,
): "stockbroker" | "tripPlanner" | "generalInput" { ): "stockbroker" | "tripPlanner" | "openCode" | "orderPizza" | "generalInput" {
return state.next; return state.next;
} }
@@ -104,16 +114,22 @@ const builder = new StateGraph(GenerativeUIAnnotation)
.addNode("router", router) .addNode("router", router)
.addNode("stockbroker", stockbrokerGraph) .addNode("stockbroker", stockbrokerGraph)
.addNode("tripPlanner", tripPlannerGraph) .addNode("tripPlanner", tripPlannerGraph)
.addNode("openCode", openCodeGraph)
.addNode("orderPizza", orderPizzaGraph)
.addNode("generalInput", handleGeneralInput) .addNode("generalInput", handleGeneralInput)
.addConditionalEdges("router", handleRoute, [ .addConditionalEdges("router", handleRoute, [
"stockbroker", "stockbroker",
"tripPlanner", "tripPlanner",
"openCode",
"orderPizza",
"generalInput", "generalInput",
]) ])
.addEdge(START, "router") .addEdge(START, "router")
.addEdge("stockbroker", END) .addEdge("stockbroker", END)
.addEdge("tripPlanner", END) .addEdge("tripPlanner", END)
.addEdge("openCode", END)
.addEdge("orderPizza", END)
.addEdge("generalInput", END); .addEdge("generalInput", END);
export const graph = builder.compile(); export const graph = builder.compile();

45
agent/open-code/index.ts Normal file
View File

@@ -0,0 +1,45 @@
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";

View File

@@ -0,0 +1,127 @@
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<OpenCodeUpdate> {
const ui = typedUi<typeof ComponentMap>(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(),
};
}

View File

@@ -0,0 +1,5 @@
```bash
npx create-react-app todo-app --template typescript
cd todo-app
mkdir -p src/{components,styles,utils}
```

View File

@@ -0,0 +1,21 @@
```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<TodoItemProps> = ({ id, text, completed, onToggle, onDelete }) => (
<div className={styles.todoItem}>
<input type='checkbox' checked={completed} onChange={() => onToggle(id)} />
<span className={completed ? styles.completed : ''}>{text}</span>
<button onClick={() => onDelete(id)}>Delete</button>
</div>
);
```

View File

@@ -0,0 +1,22 @@
```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<TodoAction>;
} | undefined>(undefined);
export const TodoProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(todoReducer, { todos: [] });
return <TodoContext.Provider value={{ state, dispatch }}>{children}</TodoContext.Provider>;
};
```

View File

@@ -0,0 +1,33 @@
```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 (
<form onSubmit={handleSubmit} className={styles.form}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder='Add a new todo'
/>
{error && <div className={styles.error}>{error}</div>}
<button type='submit'>Add Todo</button>
</form>
);
};
```

View File

@@ -0,0 +1,22 @@
```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 }) => (
<div>
<select value={currentFilter} onChange={(e) => onFilterChange(e.target.value as FilterType)}>
<option value='all'>All</option>
<option value='active'>Active</option>
<option value='completed'>Completed</option>
</select>
<button onClick={() => onSortChange(true)}>Sort A-Z</button>
<button onClick={() => onSortChange(false)}>Sort Z-A</button>
</div>
);
```

View File

@@ -0,0 +1,13 @@
```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) : [];
};
```

View File

@@ -0,0 +1,114 @@
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<OpenCodeUpdate> {
const ui = typedUi<typeof ComponentMap>(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(),
};
}

11
agent/open-code/types.ts Normal file
View File

@@ -0,0 +1,11 @@
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;

View File

@@ -0,0 +1,113 @@
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";

View File

@@ -8,6 +8,23 @@ import { findToolCall } from "../../find-tool-call";
import { format, subDays } from "date-fns"; import { format, subDays } from "date-fns";
import { Price, Snapshot } from "../../types"; 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<{ async function getPricesForTicker(ticker: string): Promise<{
oneDayPrices: Price[]; oneDayPrices: Price[];
thirtyDayPrices: Price[]; thirtyDayPrices: Price[];
@@ -54,7 +71,21 @@ async function getPricesForTicker(ticker: string): Promise<{
} }
const { prices: pricesOneDay } = await resOneDay.json(); const { prices: pricesOneDay } = await resOneDay.json();
const { prices: pricesThirtyDays } = await resThirtyDays.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 { return {
oneDayPrices: pricesOneDay, oneDayPrices: pricesOneDay,
@@ -145,28 +176,37 @@ export async function callTools(
if (stockbrokerToolCall) { if (stockbrokerToolCall) {
const prices = await getPricesForTicker(stockbrokerToolCall.args.ticker); const prices = await getPricesForTicker(stockbrokerToolCall.args.ticker);
ui.write("stock-price", { ui.push(
ticker: stockbrokerToolCall.args.ticker, {
...prices, name: "stock-price",
}); content: { ticker: stockbrokerToolCall.args.ticker, ...prices },
},
{ message },
);
} }
if (portfolioToolCall) { if (portfolioToolCall) {
ui.write("portfolio", {}); ui.push({ name: "portfolio", content: {} }, { message });
} }
if (buyStockToolCall) { if (buyStockToolCall) {
const snapshot = await getPriceSnapshotForTicker( const snapshot = await getPriceSnapshotForTicker(
buyStockToolCall.args.ticker, buyStockToolCall.args.ticker,
); );
ui.write("buy-stock", { ui.push(
{
name: "buy-stock",
content: {
toolCallId: buyStockToolCall.id ?? "", toolCallId: buyStockToolCall.id ?? "",
snapshot, snapshot,
quantity: buyStockToolCall.args.quantity, quantity: buyStockToolCall.args.quantity,
}); },
},
{ message },
);
} }
return { return {
messages: [message], messages: [message],
ui: ui.collect as StockbrokerUpdate["ui"], ui: ui.items,
timestamp: Date.now(), timestamp: Date.now(),
}; };
} }

View File

@@ -63,7 +63,9 @@ export async function extraction(
.describe("The end date of the trip. Should be in YYYY-MM-DD format"), .describe("The end date of the trip. Should be in YYYY-MM-DD format"),
numberOfGuests: z numberOfGuests: z
.number() .number()
.describe("The number of guests for the trip. Should default to 2 if not specified"), .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([ const model = new ChatOpenAI({ model: "gpt-4o", temperature: 0 }).bindTools([
@@ -126,6 +128,6 @@ Extract only what is specified by the user. It is okay to leave fields blank if
return { return {
tripDetails: extractionDetailsWithDefaults, tripDetails: extractionDetailsWithDefaults,
messages: [response, extractToolResponse] messages: [response, extractToolResponse],
}; };
} }

View File

@@ -7,14 +7,12 @@ import { LangGraphRunnableConfig } from "@langchain/langgraph";
import { getAccommodationsListProps } from "../utils/get-accommodations"; import { getAccommodationsListProps } from "../utils/get-accommodations";
import { findToolCall } from "../../find-tool-call"; import { findToolCall } from "../../find-tool-call";
const listAccommodationsSchema = z.object({}).describe("A tool to list accommodations for the user") const listAccommodationsSchema = z
const bookAccommodationSchema = z.object({ .object({})
accommodationName: z.string().describe("The name of the accommodation to book a reservation for"), .describe("A tool to list accommodations for the user");
}).describe("A tool to book a reservation for an accommodation"); const listRestaurantsSchema = z
const listRestaurantsSchema = z.object({}).describe("A tool to list restaurants for the user"); .object({})
const bookRestaurantSchema = z.object({ .describe("A tool to list restaurants for the user");
restaurantName: z.string().describe("The name of the restaurant to book a reservation for"),
}).describe("A tool to book a reservation for a restaurant");
const ACCOMMODATIONS_TOOLS = [ const ACCOMMODATIONS_TOOLS = [
{ {
@@ -22,21 +20,11 @@ const ACCOMMODATIONS_TOOLS = [
description: "A tool to list accommodations for the user", description: "A tool to list accommodations for the user",
schema: listAccommodationsSchema, schema: listAccommodationsSchema,
}, },
{
name: "book-accommodation",
description: "A tool to book a reservation for an accommodation",
schema: bookAccommodationSchema,
},
{ {
name: "list-restaurants", name: "list-restaurants",
description: "A tool to list restaurants for the user", description: "A tool to list restaurants for the user",
schema: listRestaurantsSchema, schema: listRestaurantsSchema,
}, },
{
name: "book-restaurant",
description: "A tool to book a reservation for a restaurant",
schema: bookRestaurantSchema,
},
]; ];
export async function callTools( export async function callTools(
@@ -49,7 +37,9 @@ export async function callTools(
const ui = typedUi<typeof ComponentMap>(config); const ui = typedUi<typeof ComponentMap>(config);
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 }).bindTools(ACCOMMODATIONS_TOOLS); const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 }).bindTools(
ACCOMMODATIONS_TOOLS,
);
const response = await llm.invoke([ const response = await llm.invoke([
{ {
@@ -63,47 +53,40 @@ export async function callTools(
const listAccommodationsToolCall = response.tool_calls?.find( const listAccommodationsToolCall = response.tool_calls?.find(
findToolCall("list-accommodations")<typeof listAccommodationsSchema>, findToolCall("list-accommodations")<typeof listAccommodationsSchema>,
); );
const bookAccommodationToolCall = response.tool_calls?.find(
findToolCall("book-accommodation")<typeof bookAccommodationSchema>,
);
const listRestaurantsToolCall = response.tool_calls?.find( const listRestaurantsToolCall = response.tool_calls?.find(
findToolCall("list-restaurants")<typeof listRestaurantsSchema>, findToolCall("list-restaurants")<typeof listRestaurantsSchema>,
); );
const bookRestaurantToolCall = response.tool_calls?.find(
findToolCall("book-restaurant")<typeof bookRestaurantSchema>,
);
if (!listAccommodationsToolCall && !bookAccommodationToolCall && !listRestaurantsToolCall && !bookRestaurantToolCall) { if (!listAccommodationsToolCall && !listRestaurantsToolCall) {
throw new Error("No tool calls found"); throw new Error("No tool calls found");
} }
if (listAccommodationsToolCall) { if (listAccommodationsToolCall) {
ui.write("accommodations-list", { ui.push(
{
name: "accommodations-list",
content: {
toolCallId: listAccommodationsToolCall.id ?? "", toolCallId: listAccommodationsToolCall.id ?? "",
...getAccommodationsListProps(state.tripDetails), ...getAccommodationsListProps(state.tripDetails),
}); },
} },
if (bookAccommodationToolCall && bookAccommodationToolCall.args.accommodationName) { { message: response },
ui.write("book-accommodation", { );
tripDetails: state.tripDetails,
accommodationName: bookAccommodationToolCall.args.accommodationName,
});
} }
if (listRestaurantsToolCall) { if (listRestaurantsToolCall) {
ui.write("restaurants-list", { tripDetails: state.tripDetails }); ui.push(
} {
name: "restaurants-list",
if (bookRestaurantToolCall && bookRestaurantToolCall.args.restaurantName) { content: { tripDetails: state.tripDetails },
ui.write("book-restaurant", { },
tripDetails: state.tripDetails, { message: response },
restaurantName: bookRestaurantToolCall.args.restaurantName, );
});
} }
return { return {
messages: [response], messages: [response],
ui: ui.collect as TripPlannerUpdate["ui"], ui: ui.items,
timestamp: Date.now(), timestamp: Date.now(),
}; };
} }

View File

@@ -12,7 +12,9 @@ export const GenerativeUIAnnotation = Annotation.Root({
UIMessage | RemoveUIMessage | (UIMessage | RemoveUIMessage)[] UIMessage | RemoveUIMessage | (UIMessage | RemoveUIMessage)[]
>({ default: () => [], reducer: uiMessageReducer }), >({ default: () => [], reducer: uiMessageReducer }),
timestamp: Annotation<number>, timestamp: Annotation<number>,
next: Annotation<"stockbroker" | "tripPlanner" | "generalInput">(), next: Annotation<
"stockbroker" | "tripPlanner" | "openCode" | "orderPizza" | "generalInput"
>(),
}); });
export type GenerativeUIState = typeof GenerativeUIAnnotation.State; export type GenerativeUIState = typeof GenerativeUIAnnotation.State;

View File

@@ -5,6 +5,8 @@ import BookAccommodation from "./trip-planner/book-accommodation";
import RestaurantsList from "./trip-planner/restaurants-list"; import RestaurantsList from "./trip-planner/restaurants-list";
import BookRestaurant from "./trip-planner/book-restaurant"; import BookRestaurant from "./trip-planner/book-restaurant";
import BuyStock from "./stockbroker/buy-stock"; import BuyStock from "./stockbroker/buy-stock";
import Plan from "./open-code/plan";
import ProposedChange from "./open-code/proposed-change";
const ComponentMap = { const ComponentMap = {
"stock-price": StockPrice, "stock-price": StockPrice,
@@ -14,5 +16,7 @@ const ComponentMap = {
"restaurants-list": RestaurantsList, "restaurants-list": RestaurantsList,
"book-restaurant": BookRestaurant, "book-restaurant": BookRestaurant,
"buy-stock": BuyStock, "buy-stock": BuyStock,
"code-plan": Plan,
"proposed-change": ProposedChange,
} as const; } as const;
export default ComponentMap; export default ComponentMap;

View File

@@ -0,0 +1,76 @@
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 (
<div className="flex flex-col w-full max-w-4xl border-[1px] rounded-xl border-slate-200 overflow-hidden">
<div className="p-6">
<h2 className="text-2xl font-semibold text-left">Code Plan</h2>
</div>
<motion.div
className="relative overflow-hidden"
animate={{
height: isExpanded ? "auto" : "200px",
opacity: isExpanded ? 1 : 0.7,
}}
transition={{
height: { duration: 0.3, ease: [0.4, 0, 0.2, 1] },
opacity: { duration: 0.2 },
}}
initial={false}
>
<div className="grid grid-cols-3 divide-x divide-slate-300 w-full border-t border-slate-200 px-6 pt-4 pb-4">
<div className="flex flex-col gap-2">
<h3 className="text-lg font-medium mb-4 text-slate-700">
Remaining Plans
</h3>
{props.remainingPlans.map((step, index) => (
<p key={index} className="font-mono text-sm">
{index + 1}. {step}
</p>
))}
</div>
<div className="flex flex-col gap-2 px-6">
<h3 className="text-lg font-medium mb-4 text-slate-700">
Executed Plans
</h3>
{props.executedPlans.map((step, index) => (
<p key={index} className="font-mono text-sm">
{step}
</p>
))}
</div>
<div className="flex flex-col gap-2 px-6">
<h3 className="text-lg font-medium mb-4 text-slate-700">
Rejected Plans
</h3>
{props.rejectedPlans.map((step, index) => (
<p key={index} className="font-mono text-sm">
{step}
</p>
))}
</div>
</div>
</motion.div>
<motion.button
className="w-full py-2 border-t border-slate-200 flex items-center justify-center hover:bg-slate-50 transition-colors"
onClick={() => setIsExpanded(!isExpanded)}
animate={{ rotate: isExpanded ? 180 : 0 }}
transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
>
<ChevronDown className="w-5 h-5 text-slate-600" />
</motion.button>
</div>
);
}

View File

@@ -0,0 +1,122 @@
@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;
}
}

View File

@@ -0,0 +1,189 @@
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 (
<div
className={cn(
"flex flex-col gap-4 w-full max-w-4xl p-4 border-[1px] rounded-xl",
isAccepted ? "border-green-300" : "border-red-300",
)}
>
<div className="flex flex-col items-start justify-start gap-2">
<p className="text-lg font-medium">
{isAccepted ? "Accepted" : "Rejected"} Change
</p>
<p className="text-sm font-mono">{props.planItem}</p>
</div>
<ReactMarkdown
children={props.change}
components={{
code(props) {
const { children, className, node: _node } = props;
const match = /language-(\w+)/.exec(className || "");
return match ? (
<SyntaxHighlighter
children={String(children).replace(/\n$/, "")}
language={match[1]}
style={coldarkDark}
/>
) : (
<code className={className}>{children}</code>
);
},
}}
/>
</div>
);
}
return (
<div className="flex flex-col gap-4 w-full max-w-4xl p-4 border-[1px] rounded-xl border-slate-200">
<div className="flex flex-col items-start justify-start gap-2">
<p className="text-lg font-medium">Proposed Change</p>
<p className="text-sm font-mono">{props.planItem}</p>
</div>
<ReactMarkdown
children={props.change}
components={{
code(props) {
const { children, className, node: _node } = props;
const match = /language-(\w+)/.exec(className || "");
return match ? (
<SyntaxHighlighter
children={String(children).replace(/\n$/, "")}
language={match[1]}
style={coldarkDark}
/>
) : (
<code className={className}>{children}</code>
);
},
}}
/>
{!props.fullWriteAccess && (
<div className="flex gap-2 items-center w-full">
<Button
className="cursor-pointer w-full"
variant="destructive"
onClick={handleReject}
>
Reject
</Button>
<Button
className="cursor-pointer w-full"
onClick={() => handleAccept()}
>
Accept
</Button>
<Button
className="cursor-pointer w-full bg-blue-500 hover:bg-blue-500/90"
onClick={() => handleAccept(true)}
>
Accept, don&apos;t ask again
</Button>
</div>
)}
</div>
);
}

View File

@@ -160,7 +160,7 @@ export default function PortfolioView() {
const chartData = selectedHolding ? generateChartData(selectedHolding) : []; const chartData = selectedHolding ? generateChartData(selectedHolding) : [];
return ( return (
<div className="w-full max-w-4xl bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200"> <div className="w-full max-w-3xl bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200">
<div className="bg-gradient-to-r from-indigo-700 to-indigo-500 px-6 py-4"> <div className="bg-gradient-to-r from-indigo-700 to-indigo-500 px-6 py-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-white font-bold text-xl tracking-tight flex items-center"> <h2 className="text-white font-bold text-xl tracking-tight flex items-center">

View File

@@ -138,8 +138,18 @@ export default function StockPrice(props: {
}; };
}, [oneDayPrices, thirtyDayPrices, displayRange]); }, [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 ( return (
<div className="w-full max-w-4xl rounded-xl shadow-md overflow-hidden border border-gray-200 flex flex-col gap-4 p-3"> <div className="w-full max-w-3xl rounded-xl shadow-md overflow-hidden border border-gray-200 flex flex-col gap-4 p-3">
<div className="flex items-center justify-start gap-4 mb-2 text-lg font-medium text-gray-700"> <div className="flex items-center justify-start gap-4 mb-2 text-lg font-medium text-gray-700">
<p>{ticker}</p> <p>{ticker}</p>
<p>${currentPrice}</p> <p>${currentPrice}</p>
@@ -180,7 +190,7 @@ export default function StockPrice(props: {
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
tickMargin={8} tickMargin={8}
tickFormatter={(value) => format(value, "h:mm a")} tickFormatter={(v) => formatDateByDisplayRange(v)}
/> />
<YAxis <YAxis
domain={[lowPrice - 2, highPrice + 2]} domain={[lowPrice - 2, highPrice + 2]}
@@ -191,10 +201,11 @@ export default function StockPrice(props: {
/> />
<ChartTooltip <ChartTooltip
cursor={false} cursor={false}
wrapperStyle={{ backgroundColor: "white" }}
content={ content={
<ChartTooltipContent <ChartTooltipContent
hideLabel={false} hideLabel={false}
labelFormatter={(value) => format(value, "h:mm a")} labelFormatter={(v) => formatDateByDisplayRange(v, true)}
/> />
} }
/> />

View File

@@ -320,7 +320,7 @@ export default function AccommodationsList({
align: "start", align: "start",
loop: true, loop: true,
}} }}
className="w-full sm:max-w-sm md:max-w-2xl lg:max-w-3xl" className="w-full sm:max-w-sm md:max-w-3xl lg:max-w-3xl"
> >
<CarouselContent> <CarouselContent>
{accommodations.map((accommodation) => ( {accommodations.map((accommodation) => (

View File

@@ -1,403 +0,0 @@
import "./index.css";
import { TripDetails } from "../../../trip-planner/types";
import { useState } from "react";
export default function BookAccommodation({
tripDetails,
accommodationName,
}: {
tripDetails: TripDetails;
accommodationName: string;
}) {
// Placeholder data - ideally would come from props
const [accommodation] = useState({
name: accommodationName,
type: "Hotel",
price: "$150/night",
rating: 4.8,
totalPrice:
"$" +
150 *
Math.ceil(
(new Date(tripDetails.endDate).getTime() -
new Date(tripDetails.startDate).getTime()) /
(1000 * 60 * 60 * 24),
),
image: "https://placehold.co/300x200?text=Accommodation",
roomTypes: ["Standard", "Deluxe", "Suite"],
checkInTime: "3:00 PM",
checkOutTime: "11:00 AM",
});
const [selectedRoom, setSelectedRoom] = useState("Standard");
const [bookingStep, setBookingStep] = useState<
"details" | "payment" | "confirmed"
>("details");
const [formData, setFormData] = useState({
name: "",
email: "",
phone: "",
specialRequests: "",
});
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setBookingStep("payment");
};
const handlePayment = (e: React.FormEvent) => {
e.preventDefault();
setBookingStep("confirmed");
};
return (
<div className="w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden">
<div className="bg-blue-600 px-4 py-3">
<h3 className="text-white font-medium">Book {accommodation.name}</h3>
<p className="text-blue-100 text-xs">
{new Date(tripDetails.startDate).toLocaleDateString()} -{" "}
{new Date(tripDetails.endDate).toLocaleDateString()} ·{" "}
{tripDetails.numberOfGuests} guests
</p>
</div>
<div className="p-4">
{bookingStep === "details" && (
<>
<div className="flex items-center space-x-3 mb-4">
<div className="flex-shrink-0 w-16 h-16 bg-gray-200 rounded-md overflow-hidden">
<img
src={accommodation.image}
alt={accommodation.name}
className="w-full h-full object-cover"
/>
</div>
<div>
<h4 className="font-medium text-gray-900">
{accommodation.name}
</h4>
<div className="flex items-center mt-1">
<svg
className="w-4 h-4 text-yellow-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
</svg>
<span className="text-xs text-gray-500 ml-1">
{accommodation.rating}
</span>
</div>
<div className="flex items-center justify-between mt-1">
<span className="text-sm text-gray-500">
{accommodation.type}
</span>
<span className="text-sm font-semibold text-blue-600">
{accommodation.price}
</span>
</div>
</div>
</div>
<div className="border-t border-b py-3 mb-4">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Check-in</span>
<span className="font-medium">
{new Date(tripDetails.startDate).toLocaleDateString()} (
{accommodation.checkInTime})
</span>
</div>
<div className="flex justify-between text-sm mt-2">
<span className="text-gray-600">Check-out</span>
<span className="font-medium">
{new Date(tripDetails.endDate).toLocaleDateString()} (
{accommodation.checkOutTime})
</span>
</div>
<div className="flex justify-between text-sm mt-2">
<span className="text-gray-600">Guests</span>
<span className="font-medium">
{tripDetails.numberOfGuests}
</span>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Room Type
</label>
<div className="grid grid-cols-3 gap-2">
{accommodation.roomTypes.map((room) => (
<button
key={room}
type="button"
onClick={() => setSelectedRoom(room)}
className={`text-sm py-2 px-3 rounded-md border transition-colors ${
selectedRoom === room
? "border-blue-500 bg-blue-50 text-blue-700"
: "border-gray-300 text-gray-700 hover:border-gray-400"
}`}
>
{room}
</button>
))}
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-3">
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Full Name
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
/>
</div>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1"
>
Email
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
/>
</div>
<div>
<label
htmlFor="phone"
className="block text-sm font-medium text-gray-700 mb-1"
>
Phone
</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
/>
</div>
<div>
<label
htmlFor="specialRequests"
className="block text-sm font-medium text-gray-700 mb-1"
>
Special Requests
</label>
<textarea
id="specialRequests"
name="specialRequests"
value={formData.specialRequests}
onChange={handleInputChange}
rows={2}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
/>
</div>
<div className="border-t pt-3 mt-4">
<div className="flex justify-between items-center mb-3">
<span className="text-gray-600 text-sm">Total Price:</span>
<span className="font-semibold text-lg">
{accommodation.totalPrice}
</span>
</div>
<button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
>
Continue to Payment
</button>
</div>
</form>
</>
)}
{bookingStep === "payment" && (
<form onSubmit={handlePayment} className="space-y-3">
<h4 className="font-medium text-lg text-gray-900 mb-3">
Payment Details
</h4>
<div>
<label
htmlFor="cardName"
className="block text-sm font-medium text-gray-700 mb-1"
>
Name on Card
</label>
<input
type="text"
id="cardName"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
required
/>
</div>
<div>
<label
htmlFor="cardNumber"
className="block text-sm font-medium text-gray-700 mb-1"
>
Card Number
</label>
<input
type="text"
id="cardNumber"
placeholder="XXXX XXXX XXXX XXXX"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
required
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label
htmlFor="expiry"
className="block text-sm font-medium text-gray-700 mb-1"
>
Expiry Date
</label>
<input
type="text"
id="expiry"
placeholder="MM/YY"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
required
/>
</div>
<div>
<label
htmlFor="cvc"
className="block text-sm font-medium text-gray-700 mb-1"
>
CVC
</label>
<input
type="text"
id="cvc"
placeholder="XXX"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
required
/>
</div>
</div>
<div className="border-t pt-3 mt-4">
<div className="flex justify-between items-center mb-3">
<span className="text-gray-600 text-sm">Total Amount:</span>
<span className="font-semibold text-lg">
{accommodation.totalPrice}
</span>
</div>
<button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
>
Complete Booking
</button>
<button
type="button"
onClick={() => setBookingStep("details")}
className="w-full mt-2 bg-white border border-gray-300 text-gray-700 font-medium py-2 px-4 rounded-md hover:bg-gray-50 transition-colors"
>
Back
</button>
</div>
</form>
)}
{bookingStep === "confirmed" && (
<div className="text-center py-6">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-3">
<svg
className="h-6 w-6 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900">
Booking Confirmed!
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Your booking at {accommodation.name} has been confirmed. You'll
receive a confirmation email shortly at {formData.email}.
</p>
</div>
<div className="mt-4 p-3 bg-gray-50 rounded-lg text-left">
<h4 className="font-medium text-sm text-gray-700">
Booking Summary
</h4>
<ul className="mt-2 space-y-1 text-xs text-gray-600">
<li className="flex justify-between">
<span>Check-in:</span>
<span className="font-medium">
{new Date(tripDetails.startDate).toLocaleDateString()}
</span>
</li>
<li className="flex justify-between">
<span>Check-out:</span>
<span className="font-medium">
{new Date(tripDetails.endDate).toLocaleDateString()}
</span>
</li>
<li className="flex justify-between">
<span>Room type:</span>
<span className="font-medium">{selectedRoom}</span>
</li>
<li className="flex justify-between">
<span>Guests:</span>
<span className="font-medium">
{tripDetails.numberOfGuests}
</span>
</li>
<li className="flex justify-between pt-1 mt-1 border-t">
<span>Total paid:</span>
<span className="font-medium">
{accommodation.totalPrice}
</span>
</li>
</ul>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -1 +0,0 @@
@import "tailwindcss";

View File

@@ -1,350 +0,0 @@
import "./index.css";
import { TripDetails } from "../../../trip-planner/types";
import { useState } from "react";
export default function BookRestaurant({
tripDetails,
restaurantName,
}: {
tripDetails: TripDetails;
restaurantName: string;
}) {
// Placeholder data - ideally would come from props
const [restaurant] = useState({
name: restaurantName,
cuisine: "Contemporary",
priceRange: "$$",
rating: 4.7,
image: "https://placehold.co/300x200?text=Restaurant",
openingHours: "5:00 PM - 10:00 PM",
address: "123 Main St, " + tripDetails.location,
availableTimes: ["6:00 PM", "7:00 PM", "8:00 PM", "9:00 PM"],
});
const [reservationStep, setReservationStep] = useState<
"selection" | "details" | "confirmed"
>("selection");
const [selectedDate, setSelectedDate] = useState<Date>(
new Date(tripDetails.startDate),
);
const [selectedTime, setSelectedTime] = useState<string | null>(null);
const [guests, setGuests] = useState(Math.min(tripDetails.numberOfGuests, 8));
const [formData, setFormData] = useState({
name: "",
email: "",
phone: "",
specialRequests: "",
});
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const date = new Date(e.target.value);
setSelectedDate(date);
};
const handleGuestsChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setGuests(Number(e.target.value));
};
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleTimeSelection = (time: string) => {
setSelectedTime(time);
};
const handleContinue = () => {
if (selectedTime) {
setReservationStep("details");
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setReservationStep("confirmed");
};
const formatDate = (date: Date) => {
return date.toISOString().split("T")[0];
};
return (
<div className="w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden">
<div className="bg-orange-600 px-4 py-3">
<h3 className="text-white font-medium">Reserve at {restaurant.name}</h3>
<p className="text-orange-100 text-xs">
{restaurant.cuisine} {restaurant.priceRange} {restaurant.rating}
</p>
</div>
<div className="p-4">
{reservationStep === "selection" && (
<div className="space-y-4">
<div className="flex items-center space-x-3 mb-4">
<div className="flex-shrink-0 w-16 h-16 bg-gray-200 rounded-md overflow-hidden">
<img
src={restaurant.image}
alt={restaurant.name}
className="w-full h-full object-cover"
/>
</div>
<div>
<h4 className="font-medium text-gray-900">{restaurant.name}</h4>
<p className="text-sm text-gray-500">{restaurant.address}</p>
<p className="text-sm text-gray-500">
{restaurant.openingHours}
</p>
</div>
</div>
<div>
<label
htmlFor="date"
className="block text-sm font-medium text-gray-700 mb-1"
>
Date
</label>
<input
type="date"
id="date"
min={formatDate(new Date(tripDetails.startDate))}
max={formatDate(new Date(tripDetails.endDate))}
value={formatDate(selectedDate)}
onChange={handleDateChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500 text-sm"
/>
</div>
<div>
<label
htmlFor="guests"
className="block text-sm font-medium text-gray-700 mb-1"
>
Guests
</label>
<select
id="guests"
value={guests}
onChange={handleGuestsChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500 text-sm"
>
{Array.from({ length: 8 }, (_, i) => i + 1).map((num) => (
<option key={num} value={num}>
{num} {num === 1 ? "person" : "people"}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Available Times
</label>
<div className="grid grid-cols-3 gap-2">
{restaurant.availableTimes.map((time) => (
<button
key={time}
type="button"
onClick={() => handleTimeSelection(time)}
className={`text-sm py-2 px-3 rounded-md border transition-colors ${
selectedTime === time
? "border-orange-500 bg-orange-50 text-orange-700"
: "border-gray-300 text-gray-700 hover:border-gray-400"
}`}
>
{time}
</button>
))}
</div>
</div>
<button
onClick={handleContinue}
disabled={!selectedTime}
className={`w-full py-2 rounded-md text-white font-medium ${
selectedTime
? "bg-orange-600 hover:bg-orange-700"
: "bg-gray-400 cursor-not-allowed"
}`}
>
Continue
</button>
</div>
)}
{reservationStep === "details" && (
<form onSubmit={handleSubmit} className="space-y-3">
<div className="border-b pb-3 mb-1">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Date & Time</span>
<span className="font-medium">
{selectedDate.toLocaleDateString()} at {selectedTime}
</span>
</div>
<div className="flex justify-between text-sm mt-1">
<span className="text-gray-600">Party Size</span>
<span className="font-medium">
{guests} {guests === 1 ? "person" : "people"}
</span>
</div>
<button
type="button"
onClick={() => setReservationStep("selection")}
className="text-orange-600 text-xs hover:underline mt-2"
>
Change
</button>
</div>
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Full Name
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500 text-sm"
/>
</div>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1"
>
Email
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500 text-sm"
/>
</div>
<div>
<label
htmlFor="phone"
className="block text-sm font-medium text-gray-700 mb-1"
>
Phone
</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500 text-sm"
/>
</div>
<div>
<label
htmlFor="specialRequests"
className="block text-sm font-medium text-gray-700 mb-1"
>
Special Requests
</label>
<textarea
id="specialRequests"
name="specialRequests"
value={formData.specialRequests}
onChange={handleInputChange}
rows={2}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500 text-sm"
placeholder="Allergies, special occasions, seating preferences..."
/>
</div>
<div className="pt-2">
<button
type="submit"
className="w-full bg-orange-600 hover:bg-orange-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
>
Confirm Reservation
</button>
</div>
</form>
)}
{reservationStep === "confirmed" && (
<div className="text-center py-6">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-3">
<svg
className="h-6 w-6 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900">
Reservation Confirmed!
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Your table at {restaurant.name} has been reserved. You'll
receive a confirmation email shortly at {formData.email}.
</p>
</div>
<div className="mt-4 p-3 bg-gray-50 rounded-lg text-left">
<h4 className="font-medium text-sm text-gray-700">
Reservation Details
</h4>
<ul className="mt-2 space-y-1 text-xs text-gray-600">
<li className="flex justify-between">
<span>Restaurant:</span>
<span className="font-medium">{restaurant.name}</span>
</li>
<li className="flex justify-between">
<span>Date:</span>
<span className="font-medium">
{selectedDate.toLocaleDateString()}
</span>
</li>
<li className="flex justify-between">
<span>Time:</span>
<span className="font-medium">{selectedTime}</span>
</li>
<li className="flex justify-between">
<span>Party Size:</span>
<span className="font-medium">
{guests} {guests === 1 ? "person" : "people"}
</span>
</li>
<li className="flex justify-between">
<span>Reservation Name:</span>
<span className="font-medium">{formData.name}</span>
</li>
</ul>
<p className="mt-3 text-xs text-gray-500">
Need to cancel or modify? Please call the restaurant directly at
(123) 456-7890.
</p>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LangGraph Chat</title> <title>Chat LangGraph</title>
<link href="/src/styles.css" rel="stylesheet" /> <link href="/src/styles.css" rel="stylesheet" />
</head> </head>
<body> <body>

View File

@@ -1,10 +1,11 @@
{ {
"node_version": "20",
"graphs": { "graphs": {
"agent": "./agent/agent.tsx:graph" "agent": "./agent/agent.ts:graph"
}, },
"ui": { "ui": {
"agent": "./agent/uis/index.tsx" "agent": "./agent/uis/index.tsx"
}, },
"_INTERNAL_docker_tag": "20", "env": ".env",
"env": ".env" "dependencies": ["."]
} }

View File

@@ -16,12 +16,13 @@
"@assistant-ui/react-markdown": "^0.8.0", "@assistant-ui/react-markdown": "^0.8.0",
"@assistant-ui/react-syntax-highlighter": "^0.7.2", "@assistant-ui/react-syntax-highlighter": "^0.7.2",
"@faker-js/faker": "^9.5.1", "@faker-js/faker": "^9.5.1",
"@langchain/anthropic": "^0.3.15",
"@langchain/core": "^0.3.41", "@langchain/core": "^0.3.41",
"@langchain/google-genai": "^0.1.10", "@langchain/google-genai": "^0.1.10",
"@langchain/langgraph": "^0.2.49", "@langchain/langgraph": "^0.2.49",
"@langchain/langgraph-api": "*", "@langchain/langgraph-api": "^0.0.14",
"@langchain/langgraph-cli": "*", "@langchain/langgraph-cli": "^0.0.14",
"@langchain/langgraph-sdk": "*", "@langchain/langgraph-sdk": "^0.0.52",
"@langchain/openai": "^0.4.4", "@langchain/openai": "^0.4.4",
"@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dialog": "^1.1.6",
@@ -58,11 +59,6 @@
"uuid": "^11.0.5", "uuid": "^11.0.5",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"resolutions": {
"@langchain/langgraph-api": "0.0.14-experimental.1",
"@langchain/langgraph-cli": "0.0.14-experimental.1",
"@langchain/langgraph-sdk": "0.0.47-experimental.0"
},
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0", "@eslint/js": "^9.19.0",
"@types/node": "^22.13.5", "@types/node": "^22.13.5",

423
pnpm-lock.yaml generated
View File

@@ -4,11 +4,6 @@ settings:
autoInstallPeers: true autoInstallPeers: true
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
overrides:
"@langchain/langgraph-api": 0.0.14-experimental.1
"@langchain/langgraph-cli": 0.0.14-experimental.1
"@langchain/langgraph-sdk": 0.0.47-experimental.0
importers: importers:
.: .:
dependencies: dependencies:
@@ -24,6 +19,9 @@ importers:
"@faker-js/faker": "@faker-js/faker":
specifier: ^9.5.1 specifier: ^9.5.1
version: 9.5.1 version: 9.5.1
"@langchain/anthropic":
specifier: ^0.3.15
version: 0.3.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))
"@langchain/core": "@langchain/core":
specifier: ^0.3.41 specifier: ^0.3.41
version: 0.3.41(openai@4.85.4(zod@3.24.2)) version: 0.3.41(openai@4.85.4(zod@3.24.2))
@@ -34,14 +32,14 @@ importers:
specifier: ^0.2.49 specifier: ^0.2.49
version: 0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0) version: 0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0)
"@langchain/langgraph-api": "@langchain/langgraph-api":
specifier: 0.0.14-experimental.1 specifier: ^0.0.14
version: 0.0.14-experimental.1(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3) version: 0.0.14(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)
"@langchain/langgraph-cli": "@langchain/langgraph-cli":
specifier: 0.0.14-experimental.1 specifier: ^0.0.14
version: 0.0.14-experimental.1(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3) version: 0.0.14(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)
"@langchain/langgraph-sdk": "@langchain/langgraph-sdk":
specifier: 0.0.47-experimental.0 specifier: ^0.0.52
version: 0.0.47-experimental.0(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0) version: 0.0.52(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0)
"@langchain/openai": "@langchain/openai":
specifier: ^0.4.4 specifier: ^0.4.4
version: 0.4.4(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))) version: 0.4.4(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))
@@ -222,6 +220,12 @@ packages:
} }
engines: { node: ">=6.0.0" } engines: { node: ">=6.0.0" }
"@anthropic-ai/sdk@0.37.0":
resolution:
{
integrity: sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw==,
}
"@assistant-ui/react-markdown@0.8.0": "@assistant-ui/react-markdown@0.8.0":
resolution: resolution:
{ {
@@ -437,15 +441,6 @@ packages:
integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==, integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==,
} }
"@esbuild/aix-ppc64@0.23.1":
resolution:
{
integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==,
}
engines: { node: ">=18" }
cpu: [ppc64]
os: [aix]
"@esbuild/aix-ppc64@0.25.0": "@esbuild/aix-ppc64@0.25.0":
resolution: resolution:
{ {
@@ -455,15 +450,6 @@ packages:
cpu: [ppc64] cpu: [ppc64]
os: [aix] os: [aix]
"@esbuild/android-arm64@0.23.1":
resolution:
{
integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==,
}
engines: { node: ">=18" }
cpu: [arm64]
os: [android]
"@esbuild/android-arm64@0.25.0": "@esbuild/android-arm64@0.25.0":
resolution: resolution:
{ {
@@ -473,15 +459,6 @@ packages:
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
"@esbuild/android-arm@0.23.1":
resolution:
{
integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==,
}
engines: { node: ">=18" }
cpu: [arm]
os: [android]
"@esbuild/android-arm@0.25.0": "@esbuild/android-arm@0.25.0":
resolution: resolution:
{ {
@@ -491,15 +468,6 @@ packages:
cpu: [arm] cpu: [arm]
os: [android] os: [android]
"@esbuild/android-x64@0.23.1":
resolution:
{
integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [android]
"@esbuild/android-x64@0.25.0": "@esbuild/android-x64@0.25.0":
resolution: resolution:
{ {
@@ -509,15 +477,6 @@ packages:
cpu: [x64] cpu: [x64]
os: [android] os: [android]
"@esbuild/darwin-arm64@0.23.1":
resolution:
{
integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==,
}
engines: { node: ">=18" }
cpu: [arm64]
os: [darwin]
"@esbuild/darwin-arm64@0.25.0": "@esbuild/darwin-arm64@0.25.0":
resolution: resolution:
{ {
@@ -527,15 +486,6 @@ packages:
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
"@esbuild/darwin-x64@0.23.1":
resolution:
{
integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [darwin]
"@esbuild/darwin-x64@0.25.0": "@esbuild/darwin-x64@0.25.0":
resolution: resolution:
{ {
@@ -545,15 +495,6 @@ packages:
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
"@esbuild/freebsd-arm64@0.23.1":
resolution:
{
integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==,
}
engines: { node: ">=18" }
cpu: [arm64]
os: [freebsd]
"@esbuild/freebsd-arm64@0.25.0": "@esbuild/freebsd-arm64@0.25.0":
resolution: resolution:
{ {
@@ -563,15 +504,6 @@ packages:
cpu: [arm64] cpu: [arm64]
os: [freebsd] os: [freebsd]
"@esbuild/freebsd-x64@0.23.1":
resolution:
{
integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [freebsd]
"@esbuild/freebsd-x64@0.25.0": "@esbuild/freebsd-x64@0.25.0":
resolution: resolution:
{ {
@@ -581,15 +513,6 @@ packages:
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
"@esbuild/linux-arm64@0.23.1":
resolution:
{
integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==,
}
engines: { node: ">=18" }
cpu: [arm64]
os: [linux]
"@esbuild/linux-arm64@0.25.0": "@esbuild/linux-arm64@0.25.0":
resolution: resolution:
{ {
@@ -599,15 +522,6 @@ packages:
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
"@esbuild/linux-arm@0.23.1":
resolution:
{
integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==,
}
engines: { node: ">=18" }
cpu: [arm]
os: [linux]
"@esbuild/linux-arm@0.25.0": "@esbuild/linux-arm@0.25.0":
resolution: resolution:
{ {
@@ -617,15 +531,6 @@ packages:
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
"@esbuild/linux-ia32@0.23.1":
resolution:
{
integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==,
}
engines: { node: ">=18" }
cpu: [ia32]
os: [linux]
"@esbuild/linux-ia32@0.25.0": "@esbuild/linux-ia32@0.25.0":
resolution: resolution:
{ {
@@ -635,15 +540,6 @@ packages:
cpu: [ia32] cpu: [ia32]
os: [linux] os: [linux]
"@esbuild/linux-loong64@0.23.1":
resolution:
{
integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==,
}
engines: { node: ">=18" }
cpu: [loong64]
os: [linux]
"@esbuild/linux-loong64@0.25.0": "@esbuild/linux-loong64@0.25.0":
resolution: resolution:
{ {
@@ -653,15 +549,6 @@ packages:
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
"@esbuild/linux-mips64el@0.23.1":
resolution:
{
integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==,
}
engines: { node: ">=18" }
cpu: [mips64el]
os: [linux]
"@esbuild/linux-mips64el@0.25.0": "@esbuild/linux-mips64el@0.25.0":
resolution: resolution:
{ {
@@ -671,15 +558,6 @@ packages:
cpu: [mips64el] cpu: [mips64el]
os: [linux] os: [linux]
"@esbuild/linux-ppc64@0.23.1":
resolution:
{
integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==,
}
engines: { node: ">=18" }
cpu: [ppc64]
os: [linux]
"@esbuild/linux-ppc64@0.25.0": "@esbuild/linux-ppc64@0.25.0":
resolution: resolution:
{ {
@@ -689,15 +567,6 @@ packages:
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
"@esbuild/linux-riscv64@0.23.1":
resolution:
{
integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==,
}
engines: { node: ">=18" }
cpu: [riscv64]
os: [linux]
"@esbuild/linux-riscv64@0.25.0": "@esbuild/linux-riscv64@0.25.0":
resolution: resolution:
{ {
@@ -707,15 +576,6 @@ packages:
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
"@esbuild/linux-s390x@0.23.1":
resolution:
{
integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==,
}
engines: { node: ">=18" }
cpu: [s390x]
os: [linux]
"@esbuild/linux-s390x@0.25.0": "@esbuild/linux-s390x@0.25.0":
resolution: resolution:
{ {
@@ -725,15 +585,6 @@ packages:
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
"@esbuild/linux-x64@0.23.1":
resolution:
{
integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [linux]
"@esbuild/linux-x64@0.25.0": "@esbuild/linux-x64@0.25.0":
resolution: resolution:
{ {
@@ -752,15 +603,6 @@ packages:
cpu: [arm64] cpu: [arm64]
os: [netbsd] os: [netbsd]
"@esbuild/netbsd-x64@0.23.1":
resolution:
{
integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [netbsd]
"@esbuild/netbsd-x64@0.25.0": "@esbuild/netbsd-x64@0.25.0":
resolution: resolution:
{ {
@@ -770,15 +612,6 @@ packages:
cpu: [x64] cpu: [x64]
os: [netbsd] os: [netbsd]
"@esbuild/openbsd-arm64@0.23.1":
resolution:
{
integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==,
}
engines: { node: ">=18" }
cpu: [arm64]
os: [openbsd]
"@esbuild/openbsd-arm64@0.25.0": "@esbuild/openbsd-arm64@0.25.0":
resolution: resolution:
{ {
@@ -788,15 +621,6 @@ packages:
cpu: [arm64] cpu: [arm64]
os: [openbsd] os: [openbsd]
"@esbuild/openbsd-x64@0.23.1":
resolution:
{
integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [openbsd]
"@esbuild/openbsd-x64@0.25.0": "@esbuild/openbsd-x64@0.25.0":
resolution: resolution:
{ {
@@ -806,15 +630,6 @@ packages:
cpu: [x64] cpu: [x64]
os: [openbsd] os: [openbsd]
"@esbuild/sunos-x64@0.23.1":
resolution:
{
integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [sunos]
"@esbuild/sunos-x64@0.25.0": "@esbuild/sunos-x64@0.25.0":
resolution: resolution:
{ {
@@ -824,15 +639,6 @@ packages:
cpu: [x64] cpu: [x64]
os: [sunos] os: [sunos]
"@esbuild/win32-arm64@0.23.1":
resolution:
{
integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==,
}
engines: { node: ">=18" }
cpu: [arm64]
os: [win32]
"@esbuild/win32-arm64@0.25.0": "@esbuild/win32-arm64@0.25.0":
resolution: resolution:
{ {
@@ -842,15 +648,6 @@ packages:
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
"@esbuild/win32-ia32@0.23.1":
resolution:
{
integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==,
}
engines: { node: ">=18" }
cpu: [ia32]
os: [win32]
"@esbuild/win32-ia32@0.25.0": "@esbuild/win32-ia32@0.25.0":
resolution: resolution:
{ {
@@ -860,15 +657,6 @@ packages:
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
"@esbuild/win32-x64@0.23.1":
resolution:
{
integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [win32]
"@esbuild/win32-x64@0.25.0": "@esbuild/win32-x64@0.25.0":
resolution: resolution:
{ {
@@ -1077,6 +865,15 @@ packages:
integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==, integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==,
} }
"@langchain/anthropic@0.3.15":
resolution:
{
integrity: sha512-Ar2viYcZ64idgV7EtCBCb36tIkNtPAhQRxSaMTWPHGspFgMfvwRoleVri9e90sCpjpS9xhlHsIQ0LlUS/Atsrw==,
}
engines: { node: ">=18" }
peerDependencies:
"@langchain/core": ">=0.2.21 <0.4.0"
"@langchain/core@0.3.41": "@langchain/core@0.3.41":
resolution: resolution:
{ {
@@ -1093,10 +890,10 @@ packages:
peerDependencies: peerDependencies:
"@langchain/core": ">=0.3.17 <0.4.0" "@langchain/core": ">=0.3.17 <0.4.0"
"@langchain/langgraph-api@0.0.14-experimental.1": "@langchain/langgraph-api@0.0.14":
resolution: resolution:
{ {
integrity: sha512-gSQzZZk9tIrxXMQjudQbYHXPeK7l3Y/YbzCtnH6hWHvETQOZApUn0G18O5hWT9iYaAzZfSS8ExG7y6YM0MsFTQ==, integrity: sha512-/lh6ug9kXBhL5zrX56MA4xxNt99kzLQqNuYqQRd2PWflVNATMRJNMfWhLjh91Hbn0yf3CWQoIX/6mPQiwCfrKg==,
} }
engines: { node: ^18.19.0 || >=20.16.0 } engines: { node: ^18.19.0 || >=20.16.0 }
peerDependencies: peerDependencies:
@@ -1114,18 +911,18 @@ packages:
peerDependencies: peerDependencies:
"@langchain/core": ">=0.2.31 <0.4.0" "@langchain/core": ">=0.2.31 <0.4.0"
"@langchain/langgraph-cli@0.0.14-experimental.1": "@langchain/langgraph-cli@0.0.14":
resolution: resolution:
{ {
integrity: sha512-S8Y7WrBPsNZR7wUyWj3De0sEdTTf+ipJf1lCrJho+moL9TVXUXUE+oFoMb1G/uHvt8Q/FCSE9BfadEg4JUb5MQ==, integrity: sha512-wB6Q1VjAspGUXfbZnNuq56lXQNHHedqN09nfpGxNQnfnCf8VW/8veSkhCaNV5gdvRV9mDAWhJ0i78gxLxPhbRw==,
} }
engines: { node: ^18.19.0 || >=20.16.0 } engines: { node: ^18.19.0 || >=20.16.0 }
hasBin: true hasBin: true
"@langchain/langgraph-sdk@0.0.47-experimental.0": "@langchain/langgraph-sdk@0.0.52":
resolution: resolution:
{ {
integrity: sha512-di60Pi2knQbe/sjOB3gNbNQNuTIhj0Yjls0SfEYeWDHirSN9heumPB/oxvwaxLBA8JKhuHg2h5lKUxAIT4b+aA==, integrity: sha512-nPHm9trQJnRxUDWVl0LCZ0FrQu22RtnamTkrlNibTxpcpI8E3d6KxGxzwYqLgs+hQVyJXjCb6pTNSgahaPaR5g==,
} }
peerDependencies: peerDependencies:
"@langchain/core": ">=0.2.31 <0.4.0" "@langchain/core": ">=0.2.31 <0.4.0"
@@ -2871,14 +2668,6 @@ packages:
integrity: sha512-62CPYzyfcRE7OowGmWGKs9sz43QhCa/dZ5h6ruZhDg65B5Zsn++4EA4NKIwEMbAio9JV8+FJZNXzejNX/RjSkg==, integrity: sha512-62CPYzyfcRE7OowGmWGKs9sz43QhCa/dZ5h6ruZhDg65B5Zsn++4EA4NKIwEMbAio9JV8+FJZNXzejNX/RjSkg==,
} }
esbuild@0.23.1:
resolution:
{
integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==,
}
engines: { node: ">=18" }
hasBin: true
esbuild@0.25.0: esbuild@0.25.0:
resolution: resolution:
{ {
@@ -3073,6 +2862,13 @@ packages:
integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==,
} }
fast-xml-parser@4.5.3:
resolution:
{
integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==,
}
hasBin: true
fastq@1.19.1: fastq@1.19.1:
resolution: resolution:
{ {
@@ -5169,6 +4965,12 @@ packages:
} }
engines: { node: ">=8" } engines: { node: ">=8" }
strnum@1.1.2:
resolution:
{
integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==,
}
style-to-object@1.0.8: style-to-object@1.0.8:
resolution: resolution:
{ {
@@ -5774,6 +5576,18 @@ snapshots:
"@jridgewell/gen-mapping": 0.3.8 "@jridgewell/gen-mapping": 0.3.8
"@jridgewell/trace-mapping": 0.3.25 "@jridgewell/trace-mapping": 0.3.25
"@anthropic-ai/sdk@0.37.0":
dependencies:
"@types/node": 18.19.76
"@types/node-fetch": 2.6.12
abort-controller: 3.0.0
agentkeepalive: 4.6.0
form-data-encoder: 1.7.2
formdata-node: 4.4.1
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
"@assistant-ui/react-markdown@0.8.0(@assistant-ui/react@0.8.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)": "@assistant-ui/react-markdown@0.8.0(@assistant-ui/react@0.8.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)":
dependencies: dependencies:
"@assistant-ui/react": 0.8.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) "@assistant-ui/react": 0.8.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -5956,150 +5770,78 @@ snapshots:
enabled: 2.0.0 enabled: 2.0.0
kuler: 2.0.0 kuler: 2.0.0
"@esbuild/aix-ppc64@0.23.1":
optional: true
"@esbuild/aix-ppc64@0.25.0": "@esbuild/aix-ppc64@0.25.0":
optional: true optional: true
"@esbuild/android-arm64@0.23.1":
optional: true
"@esbuild/android-arm64@0.25.0": "@esbuild/android-arm64@0.25.0":
optional: true optional: true
"@esbuild/android-arm@0.23.1":
optional: true
"@esbuild/android-arm@0.25.0": "@esbuild/android-arm@0.25.0":
optional: true optional: true
"@esbuild/android-x64@0.23.1":
optional: true
"@esbuild/android-x64@0.25.0": "@esbuild/android-x64@0.25.0":
optional: true optional: true
"@esbuild/darwin-arm64@0.23.1":
optional: true
"@esbuild/darwin-arm64@0.25.0": "@esbuild/darwin-arm64@0.25.0":
optional: true optional: true
"@esbuild/darwin-x64@0.23.1":
optional: true
"@esbuild/darwin-x64@0.25.0": "@esbuild/darwin-x64@0.25.0":
optional: true optional: true
"@esbuild/freebsd-arm64@0.23.1":
optional: true
"@esbuild/freebsd-arm64@0.25.0": "@esbuild/freebsd-arm64@0.25.0":
optional: true optional: true
"@esbuild/freebsd-x64@0.23.1":
optional: true
"@esbuild/freebsd-x64@0.25.0": "@esbuild/freebsd-x64@0.25.0":
optional: true optional: true
"@esbuild/linux-arm64@0.23.1":
optional: true
"@esbuild/linux-arm64@0.25.0": "@esbuild/linux-arm64@0.25.0":
optional: true optional: true
"@esbuild/linux-arm@0.23.1":
optional: true
"@esbuild/linux-arm@0.25.0": "@esbuild/linux-arm@0.25.0":
optional: true optional: true
"@esbuild/linux-ia32@0.23.1":
optional: true
"@esbuild/linux-ia32@0.25.0": "@esbuild/linux-ia32@0.25.0":
optional: true optional: true
"@esbuild/linux-loong64@0.23.1":
optional: true
"@esbuild/linux-loong64@0.25.0": "@esbuild/linux-loong64@0.25.0":
optional: true optional: true
"@esbuild/linux-mips64el@0.23.1":
optional: true
"@esbuild/linux-mips64el@0.25.0": "@esbuild/linux-mips64el@0.25.0":
optional: true optional: true
"@esbuild/linux-ppc64@0.23.1":
optional: true
"@esbuild/linux-ppc64@0.25.0": "@esbuild/linux-ppc64@0.25.0":
optional: true optional: true
"@esbuild/linux-riscv64@0.23.1":
optional: true
"@esbuild/linux-riscv64@0.25.0": "@esbuild/linux-riscv64@0.25.0":
optional: true optional: true
"@esbuild/linux-s390x@0.23.1":
optional: true
"@esbuild/linux-s390x@0.25.0": "@esbuild/linux-s390x@0.25.0":
optional: true optional: true
"@esbuild/linux-x64@0.23.1":
optional: true
"@esbuild/linux-x64@0.25.0": "@esbuild/linux-x64@0.25.0":
optional: true optional: true
"@esbuild/netbsd-arm64@0.25.0": "@esbuild/netbsd-arm64@0.25.0":
optional: true optional: true
"@esbuild/netbsd-x64@0.23.1":
optional: true
"@esbuild/netbsd-x64@0.25.0": "@esbuild/netbsd-x64@0.25.0":
optional: true optional: true
"@esbuild/openbsd-arm64@0.23.1":
optional: true
"@esbuild/openbsd-arm64@0.25.0": "@esbuild/openbsd-arm64@0.25.0":
optional: true optional: true
"@esbuild/openbsd-x64@0.23.1":
optional: true
"@esbuild/openbsd-x64@0.25.0": "@esbuild/openbsd-x64@0.25.0":
optional: true optional: true
"@esbuild/sunos-x64@0.23.1":
optional: true
"@esbuild/sunos-x64@0.25.0": "@esbuild/sunos-x64@0.25.0":
optional: true optional: true
"@esbuild/win32-arm64@0.23.1":
optional: true
"@esbuild/win32-arm64@0.25.0": "@esbuild/win32-arm64@0.25.0":
optional: true optional: true
"@esbuild/win32-ia32@0.23.1":
optional: true
"@esbuild/win32-ia32@0.25.0": "@esbuild/win32-ia32@0.25.0":
optional: true optional: true
"@esbuild/win32-x64@0.23.1":
optional: true
"@esbuild/win32-x64@0.25.0": "@esbuild/win32-x64@0.25.0":
optional: true optional: true
@@ -6218,6 +5960,16 @@ snapshots:
"@jridgewell/resolve-uri": 3.1.2 "@jridgewell/resolve-uri": 3.1.2
"@jridgewell/sourcemap-codec": 1.5.0 "@jridgewell/sourcemap-codec": 1.5.0
"@langchain/anthropic@0.3.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))":
dependencies:
"@anthropic-ai/sdk": 0.37.0
"@langchain/core": 0.3.41(openai@4.85.4(zod@3.24.2))
fast-xml-parser: 4.5.3
zod: 3.24.2
zod-to-json-schema: 3.24.3(zod@3.24.2)
transitivePeerDependencies:
- encoding
"@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))": "@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))":
dependencies: dependencies:
"@cfworker/json-schema": 4.1.1 "@cfworker/json-schema": 4.1.1
@@ -6243,7 +5995,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- zod - zod
"@langchain/langgraph-api@0.0.14-experimental.1(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)": "@langchain/langgraph-api@0.0.14(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)":
dependencies: dependencies:
"@babel/code-frame": 7.26.2 "@babel/code-frame": 7.26.2
"@hono/node-server": 1.13.8(hono@4.7.2) "@hono/node-server": 1.13.8(hono@4.7.2)
@@ -6255,7 +6007,7 @@ snapshots:
"@typescript/vfs": 1.6.1(typescript@5.7.3) "@typescript/vfs": 1.6.1(typescript@5.7.3)
dedent: 1.5.3 dedent: 1.5.3
dotenv: 16.4.7 dotenv: 16.4.7
esbuild: 0.23.1 esbuild: 0.25.0
esbuild-plugin-tailwindcss: 2.0.1 esbuild-plugin-tailwindcss: 2.0.1
exit-hook: 4.0.0 exit-hook: 4.0.0
hono: 4.7.2 hono: 4.7.2
@@ -6279,11 +6031,11 @@ snapshots:
"@langchain/core": 0.3.41(openai@4.85.4(zod@3.24.2)) "@langchain/core": 0.3.41(openai@4.85.4(zod@3.24.2))
uuid: 10.0.0 uuid: 10.0.0
"@langchain/langgraph-cli@0.0.14-experimental.1(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)": "@langchain/langgraph-cli@0.0.14(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)":
dependencies: dependencies:
"@babel/code-frame": 7.26.2 "@babel/code-frame": 7.26.2
"@commander-js/extra-typings": 13.1.0(commander@13.1.0) "@commander-js/extra-typings": 13.1.0(commander@13.1.0)
"@langchain/langgraph-api": 0.0.14-experimental.1(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3) "@langchain/langgraph-api": 0.0.14(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)
chokidar: 4.0.3 chokidar: 4.0.3
commander: 13.1.0 commander: 13.1.0
dedent: 1.5.3 dedent: 1.5.3
@@ -6308,7 +6060,7 @@ snapshots:
- supports-color - supports-color
- typescript - typescript
"@langchain/langgraph-sdk@0.0.47-experimental.0(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0)": "@langchain/langgraph-sdk@0.0.52(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0)":
dependencies: dependencies:
"@types/json-schema": 7.0.15 "@types/json-schema": 7.0.15
p-queue: 6.6.2 p-queue: 6.6.2
@@ -6322,7 +6074,7 @@ snapshots:
dependencies: dependencies:
"@langchain/core": 0.3.41(openai@4.85.4(zod@3.24.2)) "@langchain/core": 0.3.41(openai@4.85.4(zod@3.24.2))
"@langchain/langgraph-checkpoint": 0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))) "@langchain/langgraph-checkpoint": 0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))
"@langchain/langgraph-sdk": 0.0.47-experimental.0(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0) "@langchain/langgraph-sdk": 0.0.52(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0)
uuid: 10.0.0 uuid: 10.0.0
zod: 3.24.2 zod: 3.24.2
transitivePeerDependencies: transitivePeerDependencies:
@@ -7296,33 +7048,6 @@ snapshots:
postcss: 8.5.3 postcss: 8.5.3
postcss-modules: 6.0.1(postcss@8.5.3) postcss-modules: 6.0.1(postcss@8.5.3)
esbuild@0.23.1:
optionalDependencies:
"@esbuild/aix-ppc64": 0.23.1
"@esbuild/android-arm": 0.23.1
"@esbuild/android-arm64": 0.23.1
"@esbuild/android-x64": 0.23.1
"@esbuild/darwin-arm64": 0.23.1
"@esbuild/darwin-x64": 0.23.1
"@esbuild/freebsd-arm64": 0.23.1
"@esbuild/freebsd-x64": 0.23.1
"@esbuild/linux-arm": 0.23.1
"@esbuild/linux-arm64": 0.23.1
"@esbuild/linux-ia32": 0.23.1
"@esbuild/linux-loong64": 0.23.1
"@esbuild/linux-mips64el": 0.23.1
"@esbuild/linux-ppc64": 0.23.1
"@esbuild/linux-riscv64": 0.23.1
"@esbuild/linux-s390x": 0.23.1
"@esbuild/linux-x64": 0.23.1
"@esbuild/netbsd-x64": 0.23.1
"@esbuild/openbsd-arm64": 0.23.1
"@esbuild/openbsd-x64": 0.23.1
"@esbuild/sunos-x64": 0.23.1
"@esbuild/win32-arm64": 0.23.1
"@esbuild/win32-ia32": 0.23.1
"@esbuild/win32-x64": 0.23.1
esbuild@0.25.0: esbuild@0.25.0:
optionalDependencies: optionalDependencies:
"@esbuild/aix-ppc64": 0.25.0 "@esbuild/aix-ppc64": 0.25.0
@@ -7484,6 +7209,10 @@ snapshots:
fast-levenshtein@2.0.6: {} fast-levenshtein@2.0.6: {}
fast-xml-parser@4.5.3:
dependencies:
strnum: 1.1.2
fastq@1.19.1: fastq@1.19.1:
dependencies: dependencies:
reusify: 1.1.0 reusify: 1.1.0
@@ -8939,6 +8668,8 @@ snapshots:
strip-json-comments@3.1.1: {} strip-json-comments@3.1.1: {}
strnum@1.1.2: {}
style-to-object@1.0.8: style-to-object@1.0.8:
dependencies: dependencies:
inline-style-parser: 0.2.4 inline-style-parser: 0.2.4

View File

@@ -2,6 +2,7 @@ import { Button } from "@/components/ui/button";
import { useThreads } from "@/providers/Thread"; import { useThreads } from "@/providers/Thread";
import { Thread } from "@langchain/langgraph-sdk"; import { Thread } from "@langchain/langgraph-sdk";
import { useEffect } from "react"; import { useEffect } from "react";
import { getContentString } from "../utils"; import { getContentString } from "../utils";
import { useQueryParam, StringParam, BooleanParam } from "use-query-params"; import { useQueryParam, StringParam, BooleanParam } from "use-query-params";
import { import {
@@ -11,6 +12,8 @@ import {
SheetTitle, SheetTitle,
} from "@/components/ui/sheet"; } from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { PanelRightOpen } from "lucide-react";
import { useMediaQuery } from "@/hooks/useMediaQuery";
function ThreadList({ function ThreadList({
threads, threads,
@@ -22,7 +25,7 @@ function ThreadList({
const [threadId, setThreadId] = useQueryParam("threadId", StringParam); const [threadId, setThreadId] = useQueryParam("threadId", StringParam);
return ( return (
<div className="h-full flex flex-col gap-2 items-start justify-start overflow-y-scroll [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent"> <div className="h-full flex flex-col w-full gap-2 items-start justify-start overflow-y-scroll [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent">
{threads.map((t) => { {threads.map((t) => {
let itemText = t.thread_id; let itemText = t.thread_id;
if ( if (
@@ -36,10 +39,10 @@ function ThreadList({
itemText = getContentString(firstMessage.content); itemText = getContentString(firstMessage.content);
} }
return ( return (
<div key={t.thread_id} className="w-full"> <div key={t.thread_id} className="w-full px-1">
<Button <Button
variant="ghost" variant="ghost"
className="truncate text-left items-start justify-start w-[264px]" className="text-left items-start justify-start font-normal w-[280px]"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
onThreadClick?.(t.thread_id); onThreadClick?.(t.thread_id);
@@ -47,7 +50,7 @@ function ThreadList({
setThreadId(t.thread_id); setThreadId(t.thread_id);
}} }}
> >
{itemText} <p className="truncate text-ellipsis">{itemText}</p>
</Button> </Button>
</div> </div>
); );
@@ -58,15 +61,16 @@ function ThreadList({
function ThreadHistoryLoading() { function ThreadHistoryLoading() {
return ( return (
<div className="h-full flex flex-col gap-2 items-start justify-start overflow-y-scroll [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent"> <div className="h-full flex flex-col w-full gap-2 items-start justify-start overflow-y-scroll [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent">
{Array.from({ length: 30 }).map((_, i) => ( {Array.from({ length: 30 }).map((_, i) => (
<Skeleton key={`skeleton-${i}`} className="w-[264px] h-10" /> <Skeleton key={`skeleton-${i}`} className="w-[280px] h-10" />
))} ))}
</div> </div>
); );
} }
export default function ThreadHistory() { export default function ThreadHistory() {
const isLargeScreen = useMediaQuery("(min-width: 1024px)");
const [chatHistoryOpen, setChatHistoryOpen] = useQueryParam( const [chatHistoryOpen, setChatHistoryOpen] = useQueryParam(
"chatHistoryOpen", "chatHistoryOpen",
BooleanParam, BooleanParam,
@@ -86,15 +90,33 @@ export default function ThreadHistory() {
return ( return (
<> <>
<div className="hidden lg:flex flex-col border-r-[1px] border-slate-300 items-start justify-start gap-6 h-screen w-[300px] shrink-0 px-2 py-4 shadow-inner-right"> <div className="hidden lg:flex flex-col border-r-[1px] border-slate-300 items-start justify-start gap-6 h-screen w-[300px] shrink-0 shadow-inner-right">
<h1 className="text-2xl font-medium pl-4">Thread History</h1> <div className="flex items-center justify-between w-full pt-1.5 px-4">
<Button
className="hover:bg-gray-100"
variant="ghost"
onClick={() => setChatHistoryOpen((p) => !p)}
>
<PanelRightOpen className="size-5" />
</Button>
<h1 className="text-xl font-semibold tracking-tight">
Thread History
</h1>
</div>
{threadsLoading ? ( {threadsLoading ? (
<ThreadHistoryLoading /> <ThreadHistoryLoading />
) : ( ) : (
<ThreadList threads={threads} /> <ThreadList threads={threads} />
)} )}
</div> </div>
<Sheet open={!!chatHistoryOpen} onOpenChange={setChatHistoryOpen}> <div className="lg:hidden">
<Sheet
open={!!chatHistoryOpen && !isLargeScreen}
onOpenChange={(open) => {
if (isLargeScreen) return;
setChatHistoryOpen(open);
}}
>
<SheetContent side="left" className="lg:hidden flex"> <SheetContent side="left" className="lg:hidden flex">
<SheetHeader> <SheetHeader>
<SheetTitle>Thread History</SheetTitle> <SheetTitle>Thread History</SheetTitle>
@@ -105,6 +127,7 @@ export default function ThreadHistory() {
/> />
</SheetContent> </SheetContent>
</Sheet> </Sheet>
</div>
</> </>
); );
} }

View File

@@ -1,9 +1,9 @@
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { ReactNode, useEffect, useRef } from "react"; import { ReactNode, useEffect, useRef } from "react";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useStreamContext } from "@/providers/Stream"; import { useStreamContext } from "@/providers/Stream";
import { useState, FormEvent } from "react"; import { useState, FormEvent } from "react";
import { Input } from "../ui/input";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Checkpoint, Message } from "@langchain/langgraph-sdk"; import { Checkpoint, Message } from "@langchain/langgraph-sdk";
import { AssistantMessage, AssistantMessageLoading } from "./messages/ai"; import { AssistantMessage, AssistantMessageLoading } from "./messages/ai";
@@ -24,6 +24,7 @@ import { BooleanParam, StringParam, useQueryParam } from "use-query-params";
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"; import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
import ThreadHistory from "./history"; import ThreadHistory from "./history";
import { toast } from "sonner"; import { toast } from "sonner";
import { useMediaQuery } from "@/hooks/useMediaQuery";
function StickyToBottomContent(props: { function StickyToBottomContent(props: {
content: ReactNode; content: ReactNode;
@@ -65,12 +66,13 @@ function ScrollToBottom(props: { className?: string }) {
export function Thread() { export function Thread() {
const [threadId, setThreadId] = useQueryParam("threadId", StringParam); const [threadId, setThreadId] = useQueryParam("threadId", StringParam);
const [_, setChatHistoryOpen] = useQueryParam( const [chatHistoryOpen, setChatHistoryOpen] = useQueryParam(
"chatHistoryOpen", "chatHistoryOpen",
BooleanParam, BooleanParam,
); );
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const [firstTokenReceived, setFirstTokenReceived] = useState(false); const [firstTokenReceived, setFirstTokenReceived] = useState(false);
const isLargeScreen = useMediaQuery("(min-width: 1024px)");
const stream = useStreamContext(); const stream = useStreamContext();
const messages = stream.messages; const messages = stream.messages;
@@ -166,32 +168,91 @@ export function Thread() {
return ( return (
<div className="flex w-full h-screen overflow-hidden"> <div className="flex w-full h-screen overflow-hidden">
<div className="relative lg:flex hidden">
<motion.div
className="absolute h-full border-r bg-white overflow-hidden z-20"
style={{ width: 300 }}
animate={
isLargeScreen
? { x: chatHistoryOpen ? 0 : -300 }
: { x: chatHistoryOpen ? 0 : -300 }
}
initial={{ x: -300 }}
transition={
isLargeScreen
? { type: "spring", stiffness: 300, damping: 30 }
: { duration: 0 }
}
>
<div className="relative h-full" style={{ width: 300 }}>
<ThreadHistory /> <ThreadHistory />
<div </div>
</motion.div>
</div>
<motion.div
className={cn( className={cn(
"flex-1 flex flex-col min-w-0 overflow-hidden", "flex-1 flex flex-col min-w-0 overflow-hidden relative",
!chatStarted && "grid-rows-[1fr]", !chatStarted && "grid-rows-[1fr]",
)} )}
layout={isLargeScreen}
animate={{
marginLeft: chatHistoryOpen ? (isLargeScreen ? 300 : 0) : 0,
width: chatHistoryOpen
? isLargeScreen
? "calc(100% - 300px)"
: "100%"
: "100%",
}}
transition={
isLargeScreen
? { type: "spring", stiffness: 300, damping: 30 }
: { duration: 0 }
}
> >
{chatStarted && ( {!chatStarted && (
<div className="flex items-center justify-between gap-3 p-2 pl-4 z-10 relative"> <div className="absolute top-0 left-0 w-full flex items-center justify-between gap-3 p-2 pl-4 z-10">
<div className="flex gap-2 items-center justify-start"> {(!chatHistoryOpen || !isLargeScreen) && (
<button
className="flex gap-2 items-center cursor-pointer"
onClick={() => setThreadId(null)}
>
<LangGraphLogoSVG width={32} height={32} />
<span className="text-xl font-semibold tracking-tight">
LangGraph Chat
</span>
</button>
<Button <Button
className="flex lg:hidden" className="hover:bg-gray-100"
variant="ghost" variant="ghost"
onClick={() => setChatHistoryOpen((p) => !p)} onClick={() => setChatHistoryOpen((p) => !p)}
> >
<PanelRightOpen /> <PanelRightOpen className="size-5" />
</Button> </Button>
)}
</div>
)}
{chatStarted && (
<div className="flex items-center justify-between gap-3 p-2 pl-4 z-10 relative">
<div className="flex items-center justify-start gap-2 relative">
<div className="absolute left-0 z-10">
{(!chatHistoryOpen || !isLargeScreen) && (
<Button
className="hover:bg-gray-100"
variant="ghost"
onClick={() => setChatHistoryOpen((p) => !p)}
>
<PanelRightOpen className="size-5" />
</Button>
)}
</div>
<motion.button
className="flex gap-2 items-center cursor-pointer"
onClick={() => setThreadId(null)}
animate={{
marginLeft: !chatHistoryOpen ? 48 : 0,
}}
transition={{
type: "spring",
stiffness: 300,
damping: 30,
}}
>
<LangGraphLogoSVG width={32} height={32} />
<span className="text-xl font-semibold tracking-tight">
Chat LangGraph
</span>
</motion.button>
</div> </div>
<TooltipIconButton <TooltipIconButton
@@ -215,7 +276,7 @@ export function Thread() {
!chatStarted && "flex flex-col items-stretch mt-[25vh]", !chatStarted && "flex flex-col items-stretch mt-[25vh]",
chatStarted && "grid grid-rows-[1fr_auto]", chatStarted && "grid grid-rows-[1fr_auto]",
)} )}
contentClassName="pt-8 pb-16 px-4 max-w-4xl mx-auto flex flex-col gap-4 w-full" contentClassName="pt-8 pb-16 max-w-3xl mx-auto flex flex-col gap-4 w-full"
content={ content={
<> <>
{messages {messages
@@ -242,29 +303,36 @@ export function Thread() {
</> </>
} }
footer={ footer={
<div className="sticky flex flex-col items-center gap-8 bottom-8 px-4"> <div className="sticky flex flex-col items-center gap-8 bottom-0 px-4 bg-white">
{!chatStarted && ( {!chatStarted && (
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<LangGraphLogoSVG className="flex-shrink-0 h-8" /> <LangGraphLogoSVG className="flex-shrink-0 h-8" />
<h1 className="text-2xl font-semibold tracking-tight"> <h1 className="text-2xl font-semibold tracking-tight">
LangGraph Chat Chat LangGraph
</h1> </h1>
</div> </div>
)} )}
<ScrollToBottom className="absolute bottom-full left-1/2 -translate-x-1/2 mb-4 animate-in fade-in-0 zoom-in-95" /> <ScrollToBottom className="absolute bottom-full left-1/2 -translate-x-1/2 mb-4 animate-in fade-in-0 zoom-in-95" />
<div className="bg-background rounded-2xl border shadow-md mx-auto w-full max-w-4xl"> <div className="bg-muted rounded-2xl border shadow-xs mx-auto mb-8 w-full max-w-3xl relative z-10">
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="grid grid-rows-[1fr_auto] gap-2 max-w-4xl mx-auto" className="grid grid-rows-[1fr_auto] gap-2 max-w-3xl mx-auto"
> >
<Input <textarea
type="text"
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey && !e.metaKey) {
e.preventDefault();
const el = e.target as HTMLElement | undefined;
const form = el?.closest("form");
form?.requestSubmit();
}
}}
placeholder="Type your message..." placeholder="Type your message..."
className="px-4 py-6 border-none bg-transparent shadow-none ring-0 outline-none focus:outline-none focus:ring-0" className="p-3.5 pb-0 border-none bg-transparent field-sizing-content shadow-none ring-0 outline-none focus:outline-none focus:ring-0 resize-none"
/> />
<div className="flex items-center justify-end p-2 pt-0"> <div className="flex items-center justify-end p-2 pt-0">
@@ -276,6 +344,7 @@ export function Thread() {
) : ( ) : (
<Button <Button
type="submit" type="submit"
className="transition-all shadow-md"
disabled={isLoading || !input.trim()} disabled={isLoading || !input.trim()}
> >
Send Send
@@ -288,7 +357,7 @@ export function Thread() {
} }
/> />
</StickToBottom> </StickToBottom>
</div> </motion.div>
</div> </div>
); );
} }

View File

@@ -1,12 +1,14 @@
import { parsePartialJson } from "@langchain/core/output_parsers";
import { useStreamContext } from "@/providers/Stream"; import { useStreamContext } from "@/providers/Stream";
import { Checkpoint, Message } from "@langchain/langgraph-sdk"; import { AIMessage, Checkpoint, Message } from "@langchain/langgraph-sdk";
import { getContentString } from "../utils"; import { getContentString } from "../utils";
import { BranchSwitcher, CommandBar } from "./shared"; import { BranchSwitcher, CommandBar } from "./shared";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { MarkdownText } from "../markdown-text"; import { MarkdownText } from "../markdown-text";
import { LoadExternalComponent } from "@langchain/langgraph-sdk/react-ui"; import { LoadExternalComponent } from "@langchain/langgraph-sdk/react-ui";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ToolCalls, ToolResult } from "./tool-calls"; import { ToolCalls, ToolResult } from "./tool-calls";
import { MessageContentComplex } from "@langchain/core/messages";
import { Fragment } from "react/jsx-runtime";
function CustomComponent({ function CustomComponent({
message, message,
@@ -17,32 +19,53 @@ function CustomComponent({
}) { }) {
const meta = thread.getMessagesMetadata(message); const meta = thread.getMessagesMetadata(message);
const seenState = meta?.firstSeenState; const seenState = meta?.firstSeenState;
const customComponent = seenState?.values.ui const customComponents = seenState?.values.ui
?.slice() ?.slice()
.reverse() .filter(({ additional_kwargs }) =>
.find( !additional_kwargs.message_id
({ additional_kwargs }) => ? additional_kwargs.run_id === seenState.metadata?.run_id
additional_kwargs.run_id === seenState.metadata?.run_id, : additional_kwargs.message_id === message.id,
); );
if (!customComponent) { if (!customComponents?.length) return null;
return null;
}
return ( return (
<div key={message.id}> <Fragment key={message.id}>
{customComponent && ( {customComponents.map((customComponent) => (
<LoadExternalComponent <LoadExternalComponent
assistantId="agent" key={customComponent.id}
stream={thread} stream={thread}
message={customComponent} message={customComponent}
meta={{ ui: customComponent }} meta={{ ui: customComponent }}
/> />
)} ))}
</div> </Fragment>
); );
} }
function parseAnthropicStreamedToolCalls(
content: MessageContentComplex[],
): AIMessage["tool_calls"] {
const toolCallContents = content.filter((c) => c.type === "tool_use" && c.id);
return toolCallContents.map((tc) => {
const toolCall = tc as Record<string, any>;
let json: Record<string, any> = {};
if (toolCall?.input) {
try {
json = parsePartialJson(toolCall.input) ?? {};
} catch {
// Pass
}
}
return {
name: toolCall.name ?? "",
id: toolCall.id ?? "",
args: json,
type: "tool_call",
};
});
}
export function AssistantMessage({ export function AssistantMessage({
message, message,
isLoading, isLoading,
@@ -57,29 +80,41 @@ export function AssistantMessage({
const thread = useStreamContext(); const thread = useStreamContext();
const meta = thread.getMessagesMetadata(message); const meta = thread.getMessagesMetadata(message);
const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint; const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
const anthropicStreamedToolCalls = Array.isArray(message.content)
? parseAnthropicStreamedToolCalls(message.content)
: undefined;
const hasToolCalls = const hasToolCalls =
"tool_calls" in message && "tool_calls" in message &&
message.tool_calls && message.tool_calls &&
message.tool_calls.length > 0; message.tool_calls.length > 0;
const toolCallsHaveContents =
hasToolCalls &&
message.tool_calls?.some(
(tc) => tc.args && Object.keys(tc.args).length > 0,
);
const hasAnthropicToolCalls = !!anthropicStreamedToolCalls?.length;
const isToolResult = message.type === "tool"; const isToolResult = message.type === "tool";
return ( return (
<div className="flex items-start mr-auto gap-2 group"> <div className="flex items-start mr-auto gap-2 group">
<Avatar>
<AvatarFallback>A</AvatarFallback>
</Avatar>
{isToolResult ? ( {isToolResult ? (
<ToolResult message={message} /> <ToolResult message={message} />
) : ( ) : (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{hasToolCalls && <ToolCalls toolCalls={message.tool_calls} />}
<CustomComponent message={message} thread={thread} />
{contentString.length > 0 && ( {contentString.length > 0 && (
<div className="rounded-2xl bg-muted px-4 py-2"> <div className="py-1">
<MarkdownText>{contentString}</MarkdownText> <MarkdownText>{contentString}</MarkdownText>
</div> </div>
)} )}
{(hasToolCalls && toolCallsHaveContents && (
<ToolCalls toolCalls={message.tool_calls} />
)) ||
(hasAnthropicToolCalls && (
<ToolCalls toolCalls={anthropicStreamedToolCalls} />
)) ||
(hasToolCalls && <ToolCalls toolCalls={message.tool_calls} />)}
<CustomComponent message={message} thread={thread} />
<div <div
className={cn( className={cn(
"flex gap-2 items-center mr-auto transition-opacity", "flex gap-2 items-center mr-auto transition-opacity",
@@ -108,9 +143,6 @@ export function AssistantMessage({
export function AssistantMessageLoading() { export function AssistantMessageLoading() {
return ( return (
<div className="flex items-start mr-auto gap-2"> <div className="flex items-start mr-auto gap-2">
<Avatar>
<AvatarFallback>A</AvatarFallback>
</Avatar>
<div className="flex items-center gap-1 rounded-2xl bg-muted px-4 py-2 h-8"> <div className="flex items-center gap-1 rounded-2xl bg-muted px-4 py-2 h-8">
<div className="w-1.5 h-1.5 rounded-full bg-foreground/50 animate-[pulse_1.5s_ease-in-out_infinite]"></div> <div className="w-1.5 h-1.5 rounded-full bg-foreground/50 animate-[pulse_1.5s_ease-in-out_infinite]"></div>
<div className="w-1.5 h-1.5 rounded-full bg-foreground/50 animate-[pulse_1.5s_ease-in-out_0.5s_infinite]"></div> <div className="w-1.5 h-1.5 rounded-full bg-foreground/50 animate-[pulse_1.5s_ease-in-out_0.5s_infinite]"></div>

View File

@@ -84,7 +84,9 @@ export function HumanMessage({
onSubmit={handleSubmitEdit} onSubmit={handleSubmitEdit}
/> />
) : ( ) : (
<p className="text-right py-1">{contentString}</p> <p className="text-right px-4 py-2 rounded-3xl bg-muted">
{contentString}
</p>
)} )}
<div <div

View File

@@ -15,7 +15,7 @@ export function ToolCalls({
if (!toolCalls || toolCalls.length === 0) return null; if (!toolCalls || toolCalls.length === 0) return null;
return ( return (
<div className="space-y-4"> <div className="space-y-4 w-full max-w-4xl">
{toolCalls.map((tc, idx) => { {toolCalls.map((tc, idx) => {
const args = tc.args as Record<string, any>; const args = tc.args as Record<string, any>;
const hasArgs = Object.keys(args).length > 0; const hasArgs = Object.keys(args).length > 0;

View File

@@ -0,0 +1,16 @@
import { useEffect, useState } from "react";
export function useMediaQuery(query: string) {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
setMatches(media.matches);
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
media.addEventListener("change", listener);
return () => media.removeEventListener("change", listener);
}, [query]);
return matches;
}

View File

@@ -118,9 +118,11 @@
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
:root { :root {
--chart-1: 12 76% 61%; --chart-1: 12 76% 61%;
--chart-2: 173 58% 39%; --chart-2: 173 58% 39%;

View File

@@ -1,9 +1,10 @@
import React, { createContext, useContext, ReactNode, useState } from "react"; import React, { createContext, useContext, ReactNode, useState } from "react";
import { useStream } from "@langchain/langgraph-sdk/react"; import { useStream } from "@langchain/langgraph-sdk/react";
import { type Message } from "@langchain/langgraph-sdk"; import { type Message } from "@langchain/langgraph-sdk";
import type { import {
UIMessage, uiMessageReducer,
RemoveUIMessage, type UIMessage,
type RemoveUIMessage,
} from "@langchain/langgraph-sdk/react-ui"; } from "@langchain/langgraph-sdk/react-ui";
import { useQueryParam, StringParam } from "use-query-params"; import { useQueryParam, StringParam } from "use-query-params";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -24,7 +25,7 @@ const useTypedStream = useStream<
messages?: Message[] | Message | string; messages?: Message[] | Message | string;
ui?: (UIMessage | RemoveUIMessage)[] | UIMessage | RemoveUIMessage; ui?: (UIMessage | RemoveUIMessage)[] | UIMessage | RemoveUIMessage;
}; };
CustomUpdateType: UIMessage | RemoveUIMessage; CustomEventType: UIMessage | RemoveUIMessage;
} }
>; >;
@@ -53,6 +54,12 @@ const StreamSession = ({
apiKey: apiKey ?? undefined, apiKey: apiKey ?? undefined,
assistantId, assistantId,
threadId: threadId ?? null, threadId: threadId ?? null,
onCustomEvent: (event, options) => {
options.mutate((prev) => {
const ui = uiMessageReducer(prev.ui ?? [], event);
return { ...prev, ui };
});
},
onThreadId: (id) => { onThreadId: (id) => {
setThreadId(id); setThreadId(id);
// Refetch threads list when thread ID changes. // Refetch threads list when thread ID changes.
@@ -89,16 +96,16 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
if (!apiUrl || !assistantId) { if (!apiUrl || !assistantId) {
return ( return (
<div className="flex items-center justify-center min-h-screen w-full p-4"> <div className="flex items-center justify-center min-h-screen w-full p-4">
<div className="animate-in fade-in-0 zoom-in-95 flex flex-col border bg-background shadow-lg rounded-lg max-w-2xl"> <div className="animate-in fade-in-0 zoom-in-95 flex flex-col border bg-background shadow-lg rounded-lg max-w-3xl">
<div className="flex flex-col gap-2 mt-14 p-6 border-b"> <div className="flex flex-col gap-2 mt-14 p-6 border-b">
<div className="flex items-start flex-col gap-2"> <div className="flex items-start flex-col gap-2">
<LangGraphLogoSVG className="h-7" /> <LangGraphLogoSVG className="h-7" />
<h1 className="text-xl font-semibold tracking-tight"> <h1 className="text-xl font-semibold tracking-tight">
LangGraph Chat Chat LangGraph
</h1> </h1>
</div> </div>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Welcome to LangGraph Chat! Before you get started, you need to Welcome to Chat LangGraph! Before you get started, you need to
enter the URL of the deployment and the assistant / graph ID. enter the URL of the deployment and the assistant / graph ID.
</p> </p>
</div> </div>

View File

@@ -1,7 +1,11 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
darkMode: ["class"], darkMode: ["class"],
content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"], content: [
"./index.html",
"./src/**/*.{ts,tsx,js,jsx}",
"./agent/**/*.{ts,tsx,js,jsx}",
],
theme: { theme: {
extend: { extend: {
borderRadius: { borderRadius: {