merge main and remove book resturant and accommodation tools/uis
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.env
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -6,9 +6,13 @@ import { stockbrokerGraph } from "./stockbroker";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { tripPlannerGraph } from "./trip-planner";
|
||||
import { formatMessages } from "./utils/format-messages";
|
||||
import { graph as openCodeGraph } from "./open-code";
|
||||
import { graph as orderPizzaGraph } from "./pizza-orderer";
|
||||
|
||||
const allToolDescriptions = `- stockbroker: can fetch the price of a ticker, purchase/sell a ticker, or get the user's portfolio
|
||||
- tripPlanner: helps the user plan their trip. it can suggest restaurants, and places to stay in any given location.`;
|
||||
- tripPlanner: helps the user plan their trip. it can suggest restaurants, and places to stay in any given location.
|
||||
- openCode: can write code for the user. call this tool when the user asks you to write code
|
||||
- orderPizza: can order a pizza for the user`;
|
||||
|
||||
async function router(
|
||||
state: GenerativeUIState,
|
||||
@@ -19,7 +23,13 @@ ${allToolDescriptions}
|
||||
`;
|
||||
const routerSchema = z.object({
|
||||
route: z
|
||||
.enum(["stockbroker", "tripPlanner", "generalInput"])
|
||||
.enum([
|
||||
"stockbroker",
|
||||
"tripPlanner",
|
||||
"openCode",
|
||||
"orderPizza",
|
||||
"generalInput",
|
||||
])
|
||||
.describe(routerDescription),
|
||||
});
|
||||
const routerTool = {
|
||||
@@ -73,7 +83,7 @@ Please pick the proper route based on the most recent message, in the context of
|
||||
|
||||
function handleRoute(
|
||||
state: GenerativeUIState,
|
||||
): "stockbroker" | "tripPlanner" | "generalInput" {
|
||||
): "stockbroker" | "tripPlanner" | "openCode" | "orderPizza" | "generalInput" {
|
||||
return state.next;
|
||||
}
|
||||
|
||||
@@ -104,16 +114,22 @@ const builder = new StateGraph(GenerativeUIAnnotation)
|
||||
.addNode("router", router)
|
||||
.addNode("stockbroker", stockbrokerGraph)
|
||||
.addNode("tripPlanner", tripPlannerGraph)
|
||||
.addNode("openCode", openCodeGraph)
|
||||
.addNode("orderPizza", orderPizzaGraph)
|
||||
.addNode("generalInput", handleGeneralInput)
|
||||
|
||||
.addConditionalEdges("router", handleRoute, [
|
||||
"stockbroker",
|
||||
"tripPlanner",
|
||||
"openCode",
|
||||
"orderPizza",
|
||||
"generalInput",
|
||||
])
|
||||
.addEdge(START, "router")
|
||||
.addEdge("stockbroker", END)
|
||||
.addEdge("tripPlanner", END)
|
||||
.addEdge("openCode", END)
|
||||
.addEdge("orderPizza", END)
|
||||
.addEdge("generalInput", END);
|
||||
|
||||
export const graph = builder.compile();
|
||||
45
agent/open-code/index.ts
Normal file
45
agent/open-code/index.ts
Normal 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";
|
||||
127
agent/open-code/nodes/executor.ts
Normal file
127
agent/open-code/nodes/executor.ts
Normal 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(),
|
||||
};
|
||||
}
|
||||
5
agent/open-code/nodes/plan-code/step-1.txt
Normal file
5
agent/open-code/nodes/plan-code/step-1.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
```bash
|
||||
npx create-react-app todo-app --template typescript
|
||||
cd todo-app
|
||||
mkdir -p src/{components,styles,utils}
|
||||
```
|
||||
21
agent/open-code/nodes/plan-code/step-2.txt
Normal file
21
agent/open-code/nodes/plan-code/step-2.txt
Normal 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>
|
||||
);
|
||||
```
|
||||
22
agent/open-code/nodes/plan-code/step-3.txt
Normal file
22
agent/open-code/nodes/plan-code/step-3.txt
Normal 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>;
|
||||
};
|
||||
```
|
||||
33
agent/open-code/nodes/plan-code/step-4.txt
Normal file
33
agent/open-code/nodes/plan-code/step-4.txt
Normal 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>
|
||||
);
|
||||
};
|
||||
```
|
||||
22
agent/open-code/nodes/plan-code/step-5.txt
Normal file
22
agent/open-code/nodes/plan-code/step-5.txt
Normal 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>
|
||||
);
|
||||
```
|
||||
13
agent/open-code/nodes/plan-code/step-6.txt
Normal file
13
agent/open-code/nodes/plan-code/step-6.txt
Normal 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) : [];
|
||||
};
|
||||
```
|
||||
114
agent/open-code/nodes/planner.ts
Normal file
114
agent/open-code/nodes/planner.ts
Normal 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
11
agent/open-code/types.ts
Normal 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;
|
||||
113
agent/pizza-orderer/index.ts
Normal file
113
agent/pizza-orderer/index.ts
Normal 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";
|
||||
@@ -8,6 +8,23 @@ import { findToolCall } from "../../find-tool-call";
|
||||
import { format, subDays } from "date-fns";
|
||||
import { Price, Snapshot } from "../../types";
|
||||
|
||||
async function getNextPageData(url: string) {
|
||||
if (!process.env.FINANCIAL_DATASETS_API_KEY) {
|
||||
throw new Error("Financial datasets API key not set");
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: "GET",
|
||||
headers: { "X-API-KEY": process.env.FINANCIAL_DATASETS_API_KEY },
|
||||
};
|
||||
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch prices");
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async function getPricesForTicker(ticker: string): Promise<{
|
||||
oneDayPrices: Price[];
|
||||
thirtyDayPrices: Price[];
|
||||
@@ -54,7 +71,21 @@ async function getPricesForTicker(ticker: string): Promise<{
|
||||
}
|
||||
|
||||
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 {
|
||||
oneDayPrices: pricesOneDay,
|
||||
@@ -145,28 +176,37 @@ export async function callTools(
|
||||
|
||||
if (stockbrokerToolCall) {
|
||||
const prices = await getPricesForTicker(stockbrokerToolCall.args.ticker);
|
||||
ui.write("stock-price", {
|
||||
ticker: stockbrokerToolCall.args.ticker,
|
||||
...prices,
|
||||
});
|
||||
ui.push(
|
||||
{
|
||||
name: "stock-price",
|
||||
content: { ticker: stockbrokerToolCall.args.ticker, ...prices },
|
||||
},
|
||||
{ message },
|
||||
);
|
||||
}
|
||||
if (portfolioToolCall) {
|
||||
ui.write("portfolio", {});
|
||||
ui.push({ name: "portfolio", content: {} }, { message });
|
||||
}
|
||||
if (buyStockToolCall) {
|
||||
const snapshot = await getPriceSnapshotForTicker(
|
||||
buyStockToolCall.args.ticker,
|
||||
);
|
||||
ui.write("buy-stock", {
|
||||
ui.push(
|
||||
{
|
||||
name: "buy-stock",
|
||||
content: {
|
||||
toolCallId: buyStockToolCall.id ?? "",
|
||||
snapshot,
|
||||
quantity: buyStockToolCall.args.quantity,
|
||||
});
|
||||
},
|
||||
},
|
||||
{ message },
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
messages: [message],
|
||||
ui: ui.collect as StockbrokerUpdate["ui"],
|
||||
ui: ui.items,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,7 +63,9 @@ export async function extraction(
|
||||
.describe("The end date of the trip. Should be in YYYY-MM-DD format"),
|
||||
numberOfGuests: z
|
||||
.number()
|
||||
.describe("The number of guests for the trip. Should default to 2 if not specified"),
|
||||
.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([
|
||||
@@ -126,6 +128,6 @@ Extract only what is specified by the user. It is okay to leave fields blank if
|
||||
|
||||
return {
|
||||
tripDetails: extractionDetailsWithDefaults,
|
||||
messages: [response, extractToolResponse]
|
||||
messages: [response, extractToolResponse],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,14 +7,12 @@ import { LangGraphRunnableConfig } from "@langchain/langgraph";
|
||||
import { getAccommodationsListProps } from "../utils/get-accommodations";
|
||||
import { findToolCall } from "../../find-tool-call";
|
||||
|
||||
const listAccommodationsSchema = z.object({}).describe("A tool to list accommodations for the user")
|
||||
const bookAccommodationSchema = z.object({
|
||||
accommodationName: z.string().describe("The name of the accommodation to book a reservation for"),
|
||||
}).describe("A tool to book a reservation for an accommodation");
|
||||
const listRestaurantsSchema = z.object({}).describe("A tool to list restaurants for the user");
|
||||
const bookRestaurantSchema = z.object({
|
||||
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 listAccommodationsSchema = z
|
||||
.object({})
|
||||
.describe("A tool to list accommodations for the user");
|
||||
const listRestaurantsSchema = z
|
||||
.object({})
|
||||
.describe("A tool to list restaurants for the user");
|
||||
|
||||
const ACCOMMODATIONS_TOOLS = [
|
||||
{
|
||||
@@ -22,21 +20,11 @@ const ACCOMMODATIONS_TOOLS = [
|
||||
description: "A tool to list accommodations for the user",
|
||||
schema: listAccommodationsSchema,
|
||||
},
|
||||
{
|
||||
name: "book-accommodation",
|
||||
description: "A tool to book a reservation for an accommodation",
|
||||
schema: bookAccommodationSchema,
|
||||
},
|
||||
{
|
||||
name: "list-restaurants",
|
||||
description: "A tool to list restaurants for the user",
|
||||
schema: listRestaurantsSchema,
|
||||
},
|
||||
{
|
||||
name: "book-restaurant",
|
||||
description: "A tool to book a reservation for a restaurant",
|
||||
schema: bookRestaurantSchema,
|
||||
},
|
||||
];
|
||||
|
||||
export async function callTools(
|
||||
@@ -49,7 +37,9 @@ export async function callTools(
|
||||
|
||||
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([
|
||||
{
|
||||
@@ -63,47 +53,40 @@ export async function callTools(
|
||||
const listAccommodationsToolCall = response.tool_calls?.find(
|
||||
findToolCall("list-accommodations")<typeof listAccommodationsSchema>,
|
||||
);
|
||||
const bookAccommodationToolCall = response.tool_calls?.find(
|
||||
findToolCall("book-accommodation")<typeof bookAccommodationSchema>,
|
||||
);
|
||||
const listRestaurantsToolCall = response.tool_calls?.find(
|
||||
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");
|
||||
}
|
||||
|
||||
if (listAccommodationsToolCall) {
|
||||
ui.write("accommodations-list", {
|
||||
ui.push(
|
||||
{
|
||||
name: "accommodations-list",
|
||||
content: {
|
||||
toolCallId: listAccommodationsToolCall.id ?? "",
|
||||
...getAccommodationsListProps(state.tripDetails),
|
||||
});
|
||||
}
|
||||
if (bookAccommodationToolCall && bookAccommodationToolCall.args.accommodationName) {
|
||||
ui.write("book-accommodation", {
|
||||
tripDetails: state.tripDetails,
|
||||
accommodationName: bookAccommodationToolCall.args.accommodationName,
|
||||
});
|
||||
},
|
||||
},
|
||||
{ message: response },
|
||||
);
|
||||
}
|
||||
|
||||
if (listRestaurantsToolCall) {
|
||||
ui.write("restaurants-list", { tripDetails: state.tripDetails });
|
||||
}
|
||||
|
||||
if (bookRestaurantToolCall && bookRestaurantToolCall.args.restaurantName) {
|
||||
ui.write("book-restaurant", {
|
||||
tripDetails: state.tripDetails,
|
||||
restaurantName: bookRestaurantToolCall.args.restaurantName,
|
||||
});
|
||||
ui.push(
|
||||
{
|
||||
name: "restaurants-list",
|
||||
content: { tripDetails: state.tripDetails },
|
||||
},
|
||||
{ message: response },
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
messages: [response],
|
||||
ui: ui.collect as TripPlannerUpdate["ui"],
|
||||
ui: ui.items,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ export const GenerativeUIAnnotation = Annotation.Root({
|
||||
UIMessage | RemoveUIMessage | (UIMessage | RemoveUIMessage)[]
|
||||
>({ default: () => [], reducer: uiMessageReducer }),
|
||||
timestamp: Annotation<number>,
|
||||
next: Annotation<"stockbroker" | "tripPlanner" | "generalInput">(),
|
||||
next: Annotation<
|
||||
"stockbroker" | "tripPlanner" | "openCode" | "orderPizza" | "generalInput"
|
||||
>(),
|
||||
});
|
||||
|
||||
export type GenerativeUIState = typeof GenerativeUIAnnotation.State;
|
||||
|
||||
@@ -5,6 +5,8 @@ import BookAccommodation from "./trip-planner/book-accommodation";
|
||||
import RestaurantsList from "./trip-planner/restaurants-list";
|
||||
import BookRestaurant from "./trip-planner/book-restaurant";
|
||||
import BuyStock from "./stockbroker/buy-stock";
|
||||
import Plan from "./open-code/plan";
|
||||
import ProposedChange from "./open-code/proposed-change";
|
||||
|
||||
const ComponentMap = {
|
||||
"stock-price": StockPrice,
|
||||
@@ -14,5 +16,7 @@ const ComponentMap = {
|
||||
"restaurants-list": RestaurantsList,
|
||||
"book-restaurant": BookRestaurant,
|
||||
"buy-stock": BuyStock,
|
||||
"code-plan": Plan,
|
||||
"proposed-change": ProposedChange,
|
||||
} as const;
|
||||
export default ComponentMap;
|
||||
|
||||
76
agent/uis/open-code/plan/index.tsx
Normal file
76
agent/uis/open-code/plan/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
122
agent/uis/open-code/proposed-change/index.css
Normal file
122
agent/uis/open-code/proposed-change/index.css
Normal 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;
|
||||
}
|
||||
}
|
||||
189
agent/uis/open-code/proposed-change/index.tsx
Normal file
189
agent/uis/open-code/proposed-change/index.tsx
Normal 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't ask again
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -160,7 +160,7 @@ export default function PortfolioView() {
|
||||
const chartData = selectedHolding ? generateChartData(selectedHolding) : [];
|
||||
|
||||
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="flex justify-between items-center">
|
||||
<h2 className="text-white font-bold text-xl tracking-tight flex items-center">
|
||||
|
||||
@@ -138,8 +138,18 @@ export default function StockPrice(props: {
|
||||
};
|
||||
}, [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 (
|
||||
<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">
|
||||
<p>{ticker}</p>
|
||||
<p>${currentPrice}</p>
|
||||
@@ -180,7 +190,7 @@ export default function StockPrice(props: {
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => format(value, "h:mm a")}
|
||||
tickFormatter={(v) => formatDateByDisplayRange(v)}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[lowPrice - 2, highPrice + 2]}
|
||||
@@ -191,10 +201,11 @@ export default function StockPrice(props: {
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
wrapperStyle={{ backgroundColor: "white" }}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
hideLabel={false}
|
||||
labelFormatter={(value) => format(value, "h:mm a")}
|
||||
labelFormatter={(v) => formatDateByDisplayRange(v, true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -320,7 +320,7 @@ export default function AccommodationsList({
|
||||
align: "start",
|
||||
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>
|
||||
{accommodations.map((accommodation) => (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<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" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": {
|
||||
"agent": "./agent/agent.tsx:graph"
|
||||
"agent": "./agent/agent.ts:graph"
|
||||
},
|
||||
"ui": {
|
||||
"agent": "./agent/uis/index.tsx"
|
||||
},
|
||||
"_INTERNAL_docker_tag": "20",
|
||||
"env": ".env"
|
||||
"env": ".env",
|
||||
"dependencies": ["."]
|
||||
}
|
||||
|
||||
12
package.json
12
package.json
@@ -16,12 +16,13 @@
|
||||
"@assistant-ui/react-markdown": "^0.8.0",
|
||||
"@assistant-ui/react-syntax-highlighter": "^0.7.2",
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@langchain/anthropic": "^0.3.15",
|
||||
"@langchain/core": "^0.3.41",
|
||||
"@langchain/google-genai": "^0.1.10",
|
||||
"@langchain/langgraph": "^0.2.49",
|
||||
"@langchain/langgraph-api": "*",
|
||||
"@langchain/langgraph-cli": "*",
|
||||
"@langchain/langgraph-sdk": "*",
|
||||
"@langchain/langgraph-api": "^0.0.14",
|
||||
"@langchain/langgraph-cli": "^0.0.14",
|
||||
"@langchain/langgraph-sdk": "^0.0.52",
|
||||
"@langchain/openai": "^0.4.4",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
@@ -58,11 +59,6 @@
|
||||
"uuid": "^11.0.5",
|
||||
"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": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@types/node": "^22.13.5",
|
||||
|
||||
423
pnpm-lock.yaml
generated
423
pnpm-lock.yaml
generated
@@ -4,11 +4,6 @@ settings:
|
||||
autoInstallPeers: true
|
||||
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:
|
||||
.:
|
||||
dependencies:
|
||||
@@ -24,6 +19,9 @@ importers:
|
||||
"@faker-js/faker":
|
||||
specifier: ^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":
|
||||
specifier: ^0.3.41
|
||||
version: 0.3.41(openai@4.85.4(zod@3.24.2))
|
||||
@@ -34,14 +32,14 @@ importers:
|
||||
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)
|
||||
"@langchain/langgraph-api":
|
||||
specifier: 0.0.14-experimental.1
|
||||
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)
|
||||
specifier: ^0.0.14
|
||||
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":
|
||||
specifier: 0.0.14-experimental.1
|
||||
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)
|
||||
specifier: ^0.0.14
|
||||
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":
|
||||
specifier: 0.0.47-experimental.0
|
||||
version: 0.0.47-experimental.0(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0)
|
||||
specifier: ^0.0.52
|
||||
version: 0.0.52(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0)
|
||||
"@langchain/openai":
|
||||
specifier: ^0.4.4
|
||||
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" }
|
||||
|
||||
"@anthropic-ai/sdk@0.37.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw==,
|
||||
}
|
||||
|
||||
"@assistant-ui/react-markdown@0.8.0":
|
||||
resolution:
|
||||
{
|
||||
@@ -437,15 +441,6 @@ packages:
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -455,15 +450,6 @@ packages:
|
||||
cpu: [ppc64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -473,15 +459,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -491,15 +468,6 @@ packages:
|
||||
cpu: [arm]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -509,15 +477,6 @@ packages:
|
||||
cpu: [x64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -527,15 +486,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -545,15 +495,6 @@ packages:
|
||||
cpu: [x64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -563,15 +504,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -581,15 +513,6 @@ packages:
|
||||
cpu: [x64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -599,15 +522,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -617,15 +531,6 @@ packages:
|
||||
cpu: [arm]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -635,15 +540,6 @@ packages:
|
||||
cpu: [ia32]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -653,15 +549,6 @@ packages:
|
||||
cpu: [loong64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -671,15 +558,6 @@ packages:
|
||||
cpu: [mips64el]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -689,15 +567,6 @@ packages:
|
||||
cpu: [ppc64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -707,15 +576,6 @@ packages:
|
||||
cpu: [riscv64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -725,15 +585,6 @@ packages:
|
||||
cpu: [s390x]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -752,15 +603,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -770,15 +612,6 @@ packages:
|
||||
cpu: [x64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -788,15 +621,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -806,15 +630,6 @@ packages:
|
||||
cpu: [x64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -824,15 +639,6 @@ packages:
|
||||
cpu: [x64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -842,15 +648,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -860,15 +657,6 @@ packages:
|
||||
cpu: [ia32]
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -1077,6 +865,15 @@ packages:
|
||||
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":
|
||||
resolution:
|
||||
{
|
||||
@@ -1093,10 +890,10 @@ packages:
|
||||
peerDependencies:
|
||||
"@langchain/core": ">=0.3.17 <0.4.0"
|
||||
|
||||
"@langchain/langgraph-api@0.0.14-experimental.1":
|
||||
"@langchain/langgraph-api@0.0.14":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-gSQzZZk9tIrxXMQjudQbYHXPeK7l3Y/YbzCtnH6hWHvETQOZApUn0G18O5hWT9iYaAzZfSS8ExG7y6YM0MsFTQ==,
|
||||
integrity: sha512-/lh6ug9kXBhL5zrX56MA4xxNt99kzLQqNuYqQRd2PWflVNATMRJNMfWhLjh91Hbn0yf3CWQoIX/6mPQiwCfrKg==,
|
||||
}
|
||||
engines: { node: ^18.19.0 || >=20.16.0 }
|
||||
peerDependencies:
|
||||
@@ -1114,18 +911,18 @@ packages:
|
||||
peerDependencies:
|
||||
"@langchain/core": ">=0.2.31 <0.4.0"
|
||||
|
||||
"@langchain/langgraph-cli@0.0.14-experimental.1":
|
||||
"@langchain/langgraph-cli@0.0.14":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-S8Y7WrBPsNZR7wUyWj3De0sEdTTf+ipJf1lCrJho+moL9TVXUXUE+oFoMb1G/uHvt8Q/FCSE9BfadEg4JUb5MQ==,
|
||||
integrity: sha512-wB6Q1VjAspGUXfbZnNuq56lXQNHHedqN09nfpGxNQnfnCf8VW/8veSkhCaNV5gdvRV9mDAWhJ0i78gxLxPhbRw==,
|
||||
}
|
||||
engines: { node: ^18.19.0 || >=20.16.0 }
|
||||
hasBin: true
|
||||
|
||||
"@langchain/langgraph-sdk@0.0.47-experimental.0":
|
||||
"@langchain/langgraph-sdk@0.0.52":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-di60Pi2knQbe/sjOB3gNbNQNuTIhj0Yjls0SfEYeWDHirSN9heumPB/oxvwaxLBA8JKhuHg2h5lKUxAIT4b+aA==,
|
||||
integrity: sha512-nPHm9trQJnRxUDWVl0LCZ0FrQu22RtnamTkrlNibTxpcpI8E3d6KxGxzwYqLgs+hQVyJXjCb6pTNSgahaPaR5g==,
|
||||
}
|
||||
peerDependencies:
|
||||
"@langchain/core": ">=0.2.31 <0.4.0"
|
||||
@@ -2871,14 +2668,6 @@ packages:
|
||||
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:
|
||||
resolution:
|
||||
{
|
||||
@@ -3073,6 +2862,13 @@ packages:
|
||||
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:
|
||||
resolution:
|
||||
{
|
||||
@@ -5169,6 +4965,12 @@ packages:
|
||||
}
|
||||
engines: { node: ">=8" }
|
||||
|
||||
strnum@1.1.2:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==,
|
||||
}
|
||||
|
||||
style-to-object@1.0.8:
|
||||
resolution:
|
||||
{
|
||||
@@ -5774,6 +5576,18 @@ snapshots:
|
||||
"@jridgewell/gen-mapping": 0.3.8
|
||||
"@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)":
|
||||
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)
|
||||
@@ -5956,150 +5770,78 @@ snapshots:
|
||||
enabled: 2.0.0
|
||||
kuler: 2.0.0
|
||||
|
||||
"@esbuild/aix-ppc64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/aix-ppc64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/android-arm64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/android-arm64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/android-arm@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/android-arm@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/android-x64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/android-x64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/darwin-arm64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/darwin-arm64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/darwin-x64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/darwin-x64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/freebsd-arm64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/freebsd-arm64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/freebsd-x64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/freebsd-x64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-arm64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-arm64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-arm@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-arm@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-ia32@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-ia32@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-loong64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-loong64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-mips64el@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-mips64el@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-ppc64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-ppc64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-riscv64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-riscv64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-s390x@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-s390x@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-x64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/linux-x64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/netbsd-arm64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/netbsd-x64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/netbsd-x64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/openbsd-arm64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/openbsd-arm64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/openbsd-x64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/openbsd-x64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/sunos-x64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/sunos-x64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/win32-arm64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/win32-arm64@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/win32-ia32@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/win32-ia32@0.25.0":
|
||||
optional: true
|
||||
|
||||
"@esbuild/win32-x64@0.23.1":
|
||||
optional: true
|
||||
|
||||
"@esbuild/win32-x64@0.25.0":
|
||||
optional: true
|
||||
|
||||
@@ -6218,6 +5960,16 @@ snapshots:
|
||||
"@jridgewell/resolve-uri": 3.1.2
|
||||
"@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))":
|
||||
dependencies:
|
||||
"@cfworker/json-schema": 4.1.1
|
||||
@@ -6243,7 +5995,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
"@babel/code-frame": 7.26.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)
|
||||
dedent: 1.5.3
|
||||
dotenv: 16.4.7
|
||||
esbuild: 0.23.1
|
||||
esbuild: 0.25.0
|
||||
esbuild-plugin-tailwindcss: 2.0.1
|
||||
exit-hook: 4.0.0
|
||||
hono: 4.7.2
|
||||
@@ -6279,11 +6031,11 @@ snapshots:
|
||||
"@langchain/core": 0.3.41(openai@4.85.4(zod@3.24.2))
|
||||
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:
|
||||
"@babel/code-frame": 7.26.2
|
||||
"@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
|
||||
commander: 13.1.0
|
||||
dedent: 1.5.3
|
||||
@@ -6308,7 +6060,7 @@ snapshots:
|
||||
- supports-color
|
||||
- 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:
|
||||
"@types/json-schema": 7.0.15
|
||||
p-queue: 6.6.2
|
||||
@@ -6322,7 +6074,7 @@ snapshots:
|
||||
dependencies:
|
||||
"@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
|
||||
zod: 3.24.2
|
||||
transitivePeerDependencies:
|
||||
@@ -7296,33 +7048,6 @@ snapshots:
|
||||
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:
|
||||
optionalDependencies:
|
||||
"@esbuild/aix-ppc64": 0.25.0
|
||||
@@ -7484,6 +7209,10 @@ snapshots:
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fast-xml-parser@4.5.3:
|
||||
dependencies:
|
||||
strnum: 1.1.2
|
||||
|
||||
fastq@1.19.1:
|
||||
dependencies:
|
||||
reusify: 1.1.0
|
||||
@@ -8939,6 +8668,8 @@ snapshots:
|
||||
|
||||
strip-json-comments@3.1.1: {}
|
||||
|
||||
strnum@1.1.2: {}
|
||||
|
||||
style-to-object@1.0.8:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.4
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { useThreads } from "@/providers/Thread";
|
||||
import { Thread } from "@langchain/langgraph-sdk";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { getContentString } from "../utils";
|
||||
import { useQueryParam, StringParam, BooleanParam } from "use-query-params";
|
||||
import {
|
||||
@@ -11,6 +12,8 @@ import {
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { PanelRightOpen } from "lucide-react";
|
||||
import { useMediaQuery } from "@/hooks/useMediaQuery";
|
||||
|
||||
function ThreadList({
|
||||
threads,
|
||||
@@ -22,7 +25,7 @@ function ThreadList({
|
||||
const [threadId, setThreadId] = useQueryParam("threadId", StringParam);
|
||||
|
||||
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) => {
|
||||
let itemText = t.thread_id;
|
||||
if (
|
||||
@@ -36,10 +39,10 @@ function ThreadList({
|
||||
itemText = getContentString(firstMessage.content);
|
||||
}
|
||||
return (
|
||||
<div key={t.thread_id} className="w-full">
|
||||
<div key={t.thread_id} className="w-full px-1">
|
||||
<Button
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
onThreadClick?.(t.thread_id);
|
||||
@@ -47,7 +50,7 @@ function ThreadList({
|
||||
setThreadId(t.thread_id);
|
||||
}}
|
||||
>
|
||||
{itemText}
|
||||
<p className="truncate text-ellipsis">{itemText}</p>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -58,15 +61,16 @@ function ThreadList({
|
||||
|
||||
function ThreadHistoryLoading() {
|
||||
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) => (
|
||||
<Skeleton key={`skeleton-${i}`} className="w-[264px] h-10" />
|
||||
<Skeleton key={`skeleton-${i}`} className="w-[280px] h-10" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ThreadHistory() {
|
||||
const isLargeScreen = useMediaQuery("(min-width: 1024px)");
|
||||
const [chatHistoryOpen, setChatHistoryOpen] = useQueryParam(
|
||||
"chatHistoryOpen",
|
||||
BooleanParam,
|
||||
@@ -86,15 +90,33 @@ export default function ThreadHistory() {
|
||||
|
||||
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">
|
||||
<h1 className="text-2xl font-medium pl-4">Thread History</h1>
|
||||
<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">
|
||||
<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 ? (
|
||||
<ThreadHistoryLoading />
|
||||
) : (
|
||||
<ThreadList threads={threads} />
|
||||
)}
|
||||
</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">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Thread History</SheetTitle>
|
||||
@@ -105,6 +127,7 @@ export default function ThreadHistory() {
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { ReactNode, useEffect, useRef } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useStreamContext } from "@/providers/Stream";
|
||||
import { useState, FormEvent } from "react";
|
||||
import { Input } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
import { Checkpoint, Message } from "@langchain/langgraph-sdk";
|
||||
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 ThreadHistory from "./history";
|
||||
import { toast } from "sonner";
|
||||
import { useMediaQuery } from "@/hooks/useMediaQuery";
|
||||
|
||||
function StickyToBottomContent(props: {
|
||||
content: ReactNode;
|
||||
@@ -65,12 +66,13 @@ function ScrollToBottom(props: { className?: string }) {
|
||||
|
||||
export function Thread() {
|
||||
const [threadId, setThreadId] = useQueryParam("threadId", StringParam);
|
||||
const [_, setChatHistoryOpen] = useQueryParam(
|
||||
const [chatHistoryOpen, setChatHistoryOpen] = useQueryParam(
|
||||
"chatHistoryOpen",
|
||||
BooleanParam,
|
||||
);
|
||||
const [input, setInput] = useState("");
|
||||
const [firstTokenReceived, setFirstTokenReceived] = useState(false);
|
||||
const isLargeScreen = useMediaQuery("(min-width: 1024px)");
|
||||
|
||||
const stream = useStreamContext();
|
||||
const messages = stream.messages;
|
||||
@@ -166,32 +168,91 @@ export function Thread() {
|
||||
|
||||
return (
|
||||
<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 />
|
||||
<div
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
<motion.div
|
||||
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]",
|
||||
)}
|
||||
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 && (
|
||||
<div className="flex items-center justify-between gap-3 p-2 pl-4 z-10 relative">
|
||||
<div className="flex gap-2 items-center justify-start">
|
||||
<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>
|
||||
{!chatStarted && (
|
||||
<div className="absolute top-0 left-0 w-full flex items-center justify-between gap-3 p-2 pl-4 z-10">
|
||||
{(!chatHistoryOpen || !isLargeScreen) && (
|
||||
<Button
|
||||
className="flex lg:hidden"
|
||||
className="hover:bg-gray-100"
|
||||
variant="ghost"
|
||||
onClick={() => setChatHistoryOpen((p) => !p)}
|
||||
>
|
||||
<PanelRightOpen />
|
||||
<PanelRightOpen className="size-5" />
|
||||
</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>
|
||||
|
||||
<TooltipIconButton
|
||||
@@ -215,7 +276,7 @@ export function Thread() {
|
||||
!chatStarted && "flex flex-col items-stretch mt-[25vh]",
|
||||
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={
|
||||
<>
|
||||
{messages
|
||||
@@ -242,29 +303,36 @@ export function Thread() {
|
||||
</>
|
||||
}
|
||||
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 && (
|
||||
<div className="flex gap-3 items-center">
|
||||
<LangGraphLogoSVG className="flex-shrink-0 h-8" />
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
LangGraph Chat
|
||||
Chat LangGraph
|
||||
</h1>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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
|
||||
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
|
||||
type="text"
|
||||
<textarea
|
||||
value={input}
|
||||
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..."
|
||||
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">
|
||||
@@ -276,6 +344,7 @@ export function Thread() {
|
||||
) : (
|
||||
<Button
|
||||
type="submit"
|
||||
className="transition-all shadow-md"
|
||||
disabled={isLoading || !input.trim()}
|
||||
>
|
||||
Send
|
||||
@@ -288,7 +357,7 @@ export function Thread() {
|
||||
}
|
||||
/>
|
||||
</StickToBottom>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { parsePartialJson } from "@langchain/core/output_parsers";
|
||||
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 { BranchSwitcher, CommandBar } from "./shared";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { MarkdownText } from "../markdown-text";
|
||||
import { LoadExternalComponent } from "@langchain/langgraph-sdk/react-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ToolCalls, ToolResult } from "./tool-calls";
|
||||
import { MessageContentComplex } from "@langchain/core/messages";
|
||||
import { Fragment } from "react/jsx-runtime";
|
||||
|
||||
function CustomComponent({
|
||||
message,
|
||||
@@ -17,32 +19,53 @@ function CustomComponent({
|
||||
}) {
|
||||
const meta = thread.getMessagesMetadata(message);
|
||||
const seenState = meta?.firstSeenState;
|
||||
const customComponent = seenState?.values.ui
|
||||
const customComponents = seenState?.values.ui
|
||||
?.slice()
|
||||
.reverse()
|
||||
.find(
|
||||
({ additional_kwargs }) =>
|
||||
additional_kwargs.run_id === seenState.metadata?.run_id,
|
||||
.filter(({ additional_kwargs }) =>
|
||||
!additional_kwargs.message_id
|
||||
? additional_kwargs.run_id === seenState.metadata?.run_id
|
||||
: additional_kwargs.message_id === message.id,
|
||||
);
|
||||
|
||||
if (!customComponent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!customComponents?.length) return null;
|
||||
return (
|
||||
<div key={message.id}>
|
||||
{customComponent && (
|
||||
<Fragment key={message.id}>
|
||||
{customComponents.map((customComponent) => (
|
||||
<LoadExternalComponent
|
||||
assistantId="agent"
|
||||
key={customComponent.id}
|
||||
stream={thread}
|
||||
message={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({
|
||||
message,
|
||||
isLoading,
|
||||
@@ -57,29 +80,41 @@ export function AssistantMessage({
|
||||
const thread = useStreamContext();
|
||||
const meta = thread.getMessagesMetadata(message);
|
||||
const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
|
||||
const anthropicStreamedToolCalls = Array.isArray(message.content)
|
||||
? parseAnthropicStreamedToolCalls(message.content)
|
||||
: undefined;
|
||||
|
||||
const hasToolCalls =
|
||||
"tool_calls" in message &&
|
||||
message.tool_calls &&
|
||||
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";
|
||||
|
||||
return (
|
||||
<div className="flex items-start mr-auto gap-2 group">
|
||||
<Avatar>
|
||||
<AvatarFallback>A</AvatarFallback>
|
||||
</Avatar>
|
||||
{isToolResult ? (
|
||||
<ToolResult message={message} />
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{hasToolCalls && <ToolCalls toolCalls={message.tool_calls} />}
|
||||
<CustomComponent message={message} thread={thread} />
|
||||
{contentString.length > 0 && (
|
||||
<div className="rounded-2xl bg-muted px-4 py-2">
|
||||
<div className="py-1">
|
||||
<MarkdownText>{contentString}</MarkdownText>
|
||||
</div>
|
||||
)}
|
||||
{(hasToolCalls && toolCallsHaveContents && (
|
||||
<ToolCalls toolCalls={message.tool_calls} />
|
||||
)) ||
|
||||
(hasAnthropicToolCalls && (
|
||||
<ToolCalls toolCalls={anthropicStreamedToolCalls} />
|
||||
)) ||
|
||||
(hasToolCalls && <ToolCalls toolCalls={message.tool_calls} />)}
|
||||
<CustomComponent message={message} thread={thread} />
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-2 items-center mr-auto transition-opacity",
|
||||
@@ -108,9 +143,6 @@ export function AssistantMessage({
|
||||
export function AssistantMessageLoading() {
|
||||
return (
|
||||
<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="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>
|
||||
|
||||
@@ -84,7 +84,9 @@ export function HumanMessage({
|
||||
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
|
||||
|
||||
@@ -15,7 +15,7 @@ export function ToolCalls({
|
||||
if (!toolCalls || toolCalls.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 w-full max-w-4xl">
|
||||
{toolCalls.map((tc, idx) => {
|
||||
const args = tc.args as Record<string, any>;
|
||||
const hasArgs = Object.keys(args).length > 0;
|
||||
|
||||
16
src/hooks/useMediaQuery.tsx
Normal file
16
src/hooks/useMediaQuery.tsx
Normal 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;
|
||||
}
|
||||
@@ -118,9 +118,11 @@
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
:root {
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { createContext, useContext, ReactNode, useState } from "react";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
import { type Message } from "@langchain/langgraph-sdk";
|
||||
import type {
|
||||
UIMessage,
|
||||
RemoveUIMessage,
|
||||
import {
|
||||
uiMessageReducer,
|
||||
type UIMessage,
|
||||
type RemoveUIMessage,
|
||||
} from "@langchain/langgraph-sdk/react-ui";
|
||||
import { useQueryParam, StringParam } from "use-query-params";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -24,7 +25,7 @@ const useTypedStream = useStream<
|
||||
messages?: Message[] | Message | string;
|
||||
ui?: (UIMessage | RemoveUIMessage)[] | UIMessage | RemoveUIMessage;
|
||||
};
|
||||
CustomUpdateType: UIMessage | RemoveUIMessage;
|
||||
CustomEventType: UIMessage | RemoveUIMessage;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -53,6 +54,12 @@ const StreamSession = ({
|
||||
apiKey: apiKey ?? undefined,
|
||||
assistantId,
|
||||
threadId: threadId ?? null,
|
||||
onCustomEvent: (event, options) => {
|
||||
options.mutate((prev) => {
|
||||
const ui = uiMessageReducer(prev.ui ?? [], event);
|
||||
return { ...prev, ui };
|
||||
});
|
||||
},
|
||||
onThreadId: (id) => {
|
||||
setThreadId(id);
|
||||
// Refetch threads list when thread ID changes.
|
||||
@@ -89,16 +96,16 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
|
||||
if (!apiUrl || !assistantId) {
|
||||
return (
|
||||
<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 items-start flex-col gap-2">
|
||||
<LangGraphLogoSVG className="h-7" />
|
||||
<h1 className="text-xl font-semibold tracking-tight">
|
||||
LangGraph Chat
|
||||
Chat LangGraph
|
||||
</h1>
|
||||
</div>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"],
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{ts,tsx,js,jsx}",
|
||||
"./agent/**/*.{ts,tsx,js,jsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
borderRadius: {
|
||||
|
||||
Reference in New Issue
Block a user