merge main and remove book resturant and accommodation tools/uis
This commit is contained in:
@@ -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", {
|
||||
toolCallId: buyStockToolCall.id ?? "",
|
||||
snapshot,
|
||||
quantity: buyStockToolCall.args.quantity,
|
||||
});
|
||||
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", {
|
||||
toolCallId: listAccommodationsToolCall.id ?? "",
|
||||
...getAccommodationsListProps(state.tripDetails),
|
||||
});
|
||||
}
|
||||
if (bookAccommodationToolCall && bookAccommodationToolCall.args.accommodationName) {
|
||||
ui.write("book-accommodation", {
|
||||
tripDetails: state.tripDetails,
|
||||
accommodationName: bookAccommodationToolCall.args.accommodationName,
|
||||
});
|
||||
ui.push(
|
||||
{
|
||||
name: "accommodations-list",
|
||||
content: {
|
||||
toolCallId: listAccommodationsToolCall.id ?? "",
|
||||
...getAccommodationsListProps(state.tripDetails),
|
||||
},
|
||||
},
|
||||
{ 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user