From 6b382879c5a26345babaf79701aa6f50ffd271d4 Mon Sep 17 00:00:00 2001 From: bracesproul Date: Mon, 3 Mar 2025 17:53:00 -0800 Subject: [PATCH 1/2] feat: Add basic styling --- agent/trip-planner/nodes/extraction.tsx | 1 + .../uis/stockbroker/portfolio-view/index.tsx | 956 +++++++++++++++++- agent/uis/stockbroker/stock-price/index.tsx | 955 ++++++++++++++++- .../accommodations-list/index.tsx | 188 +++- .../trip-planner/book-accommodation/index.tsx | 391 ++++++- .../trip-planner/book-restaurant/index.tsx | 338 ++++++- .../trip-planner/restaurants-list/index.tsx | 242 ++++- src/components/thread/messages/ai.tsx | 4 +- 8 files changed, 3035 insertions(+), 40 deletions(-) diff --git a/agent/trip-planner/nodes/extraction.tsx b/agent/trip-planner/nodes/extraction.tsx index 5019027..264e992 100644 --- a/agent/trip-planner/nodes/extraction.tsx +++ b/agent/trip-planner/nodes/extraction.tsx @@ -82,6 +82,7 @@ Before you can help them, you need to extract the following information from the You are provided with the ENTIRE conversation history between you, and the user. Use these messages to extract the necessary information. Do NOT guess, or make up any information. If the user did NOT specify a location, please respond with a request for them to specify the location. +You should ONLY send a clarification message if the user did not provide the location. You do NOT need any of the other fields, so if they're missing, proceed without them. It should be a single sentence, along the lines of "Please specify the location for the trip you want to go on". Extract only what is specified by the user. It is okay to leave fields blank if the user did not specify them. diff --git a/agent/uis/stockbroker/portfolio-view/index.tsx b/agent/uis/stockbroker/portfolio-view/index.tsx index 26e240a..3ac5635 100644 --- a/agent/uis/stockbroker/portfolio-view/index.tsx +++ b/agent/uis/stockbroker/portfolio-view/index.tsx @@ -1,9 +1,959 @@ -import "./index.css"; +import { useState } from "react"; export default function PortfolioView() { + // Placeholder portfolio data - ideally would come from props + const [portfolio] = useState({ + totalValue: 156842.75, + cashBalance: 12467.32, + performance: { + daily: 1.24, + weekly: -0.52, + monthly: 3.87, + yearly: 14.28, + }, + holdings: [ + { + symbol: "AAPL", + name: "Apple Inc.", + shares: 45, + price: 187.32, + value: 8429.4, + change: 1.2, + allocation: 5.8, + avgCost: 162.5, + }, + { + symbol: "MSFT", + name: "Microsoft Corporation", + shares: 30, + price: 403.78, + value: 12113.4, + change: 0.5, + allocation: 8.4, + avgCost: 340.25, + }, + { + symbol: "AMZN", + name: "Amazon.com Inc.", + shares: 25, + price: 178.75, + value: 4468.75, + change: -0.8, + allocation: 3.1, + avgCost: 145.3, + }, + { + symbol: "GOOGL", + name: "Alphabet Inc.", + shares: 20, + price: 164.85, + value: 3297.0, + change: 2.1, + allocation: 2.3, + avgCost: 125.75, + }, + { + symbol: "NVDA", + name: "NVIDIA Corporation", + shares: 35, + price: 875.28, + value: 30634.8, + change: 3.4, + allocation: 21.3, + avgCost: 520.4, + }, + { + symbol: "TSLA", + name: "Tesla, Inc.", + shares: 40, + price: 175.9, + value: 7036.0, + change: -1.2, + allocation: 4.9, + avgCost: 190.75, + }, + ], + }); + + const [activeTab, setActiveTab] = useState<"holdings" | "performance">( + "holdings", + ); + const [sortConfig, setSortConfig] = useState<{ + key: string; + direction: "asc" | "desc"; + }>({ + key: "allocation", + direction: "desc", + }); + const [selectedHolding, setSelectedHolding] = useState(null); + + const sortedHoldings = [...portfolio.holdings].sort((a, b) => { + if ( + a[sortConfig.key as keyof typeof a] < b[sortConfig.key as keyof typeof b] + ) { + return sortConfig.direction === "asc" ? -1 : 1; + } + if ( + a[sortConfig.key as keyof typeof a] > b[sortConfig.key as keyof typeof b] + ) { + return sortConfig.direction === "asc" ? 1 : -1; + } + return 0; + }); + + const requestSort = (key: string) => { + let direction: "asc" | "desc" = "asc"; + if (sortConfig.key === key && sortConfig.direction === "asc") { + direction = "desc"; + } + setSortConfig({ key, direction }); + }; + + const formatCurrency = (value: number) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(value); + }; + + const formatPercent = (value: number) => { + return `${value > 0 ? "+" : ""}${value.toFixed(2)}%`; + }; + + // Faux chart data for selected holding + const generateChartData = (symbol: string) => { + const data = []; + const basePrice = + portfolio.holdings.find((h) => h.symbol === symbol)?.price || 100; + + for (let i = 0; i < 30; i++) { + const date = new Date(); + date.setDate(date.getDate() - 30 + i); + + const randomFactor = (Math.sin(i / 5) + Math.random() - 0.5) * 0.05; + const price = basePrice * (1 + randomFactor * (i / 3)); + + data.push({ + date: date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }), + price: parseFloat(price.toFixed(2)), + }); + } + + return data; + }; + + // Calculate total value and percent change for display + const totalValue = portfolio.totalValue; + const totalChange = portfolio.holdings.reduce( + (acc, curr) => acc + (curr.price - curr.avgCost) * curr.shares, + 0, + ); + const totalPercentChange = + (totalChange / (portfolio.totalValue - totalChange)) * 100; + + const selectedStock = selectedHolding + ? portfolio.holdings.find((h) => h.symbol === selectedHolding) + : null; + const chartData = selectedHolding ? generateChartData(selectedHolding) : []; + return ( -
- Portfolio View +
+
+
+

+ + + + + Portfolio Summary +

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

Total Value

+ + + +
+

+ {formatCurrency(portfolio.totalValue)} +

+

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

+
+
+
+

Cash Balance

+ + + + +
+

+ {formatCurrency(portfolio.cashBalance)} +

+

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

+
+
+
+

Daily Change

+ + + +
+

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

+

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

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

+ {selectedStock.symbol} +

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

Shares Owned

+

+ {selectedStock.shares.toLocaleString()} +

+
+
+

Market Value

+

+ {formatCurrency(selectedStock.value)} +

+
+
+

Avg. Cost

+

+ {formatCurrency(selectedStock.avgCost)} +

+
+
+

Cost Basis

+

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

+
+
+

Gain/Loss

+

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

+
+
+

Allocation

+

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

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

+ + + + Performance Overview +

+
+
+

Daily

+

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

+
+
+

Weekly

+

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

+
+
+

Monthly

+

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

+
+
+

Yearly

+

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

+
+
+
+ +
+

+ + + + + Portfolio Allocation +

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

+ Portfolio Diversification +

+
+ {[ + "Technology", + "Consumer Cyclical", + "Communication Services", + "Financial", + "Other", + ].map((sector, index) => { + const widths = [42, 23, 18, 10, 7]; // example percentages + const colors = [ + "bg-indigo-600", + "bg-blue-500", + "bg-green-500", + "bg-yellow-500", + "bg-red-500", + ]; + return ( +
+ ); + })} +
+
+ {[ + "Technology", + "Consumer Cyclical", + "Communication Services", + "Financial", + "Other", + ].map((sector, index) => { + const widths = [42, 23, 18, 10, 7]; // example percentages + const colors = [ + "text-indigo-600", + "text-blue-500", + "text-green-500", + "text-yellow-500", + "text-red-500", + ]; + return ( +
+
+ + {sector} {widths[index]}% + +
+ ); + })} +
+
+
+ +
+ + +
+
+ )} +
); } diff --git a/agent/uis/stockbroker/stock-price/index.tsx b/agent/uis/stockbroker/stock-price/index.tsx index f012780..62e37c1 100644 --- a/agent/uis/stockbroker/stock-price/index.tsx +++ b/agent/uis/stockbroker/stock-price/index.tsx @@ -1,13 +1,54 @@ -import "./index.css"; import { useStream } from "@langchain/langgraph-sdk/react"; import type { AIMessage, Message } from "@langchain/langgraph-sdk"; -import { useState } from "react"; +import { useState, useEffect, useCallback } from "react"; export default function StockPrice(props: { instruction: string; logo: string; }) { - const [counter, setCounter] = useState(0); + const [quantity, setQuantity] = useState(1); + const [stockData, setStockData] = useState({ + symbol: "AAPL", + name: "Apple Inc.", + price: 187.32, + change: 1.24, + changePercent: 0.67, + previousClose: 186.08, + open: 186.75, + dayHigh: 188.45, + dayLow: 186.2, + volume: 54320000, + marketCap: "2.92T", + peRatio: 29.13, + dividend: 0.96, + dividendYield: 0.51, + moving50Day: 182.46, + moving200Day: 175.8, + fiftyTwoWeekHigh: 201.48, + fiftyTwoWeekLow: 143.9, + analystRating: "Buy", + analystCount: 32, + priceTarget: 210.5, + }); + + const [priceHistory, setPriceHistory] = useState< + { time: string; price: number }[] + >([]); + const [orderType, setOrderType] = useState<"buy" | "sell">("buy"); + const [showOrder, setShowOrder] = useState(false); + const [activeTab, setActiveTab] = useState<"chart" | "details" | "analysis">( + "chart", + ); + const [isLiveUpdating, setIsLiveUpdating] = useState(false); + const [orderTypeOptions] = useState([ + { value: "market", label: "Market" }, + { value: "limit", label: "Limit" }, + { value: "stop", label: "Stop" }, + ]); + const [selectedOrderTypeOption, setSelectedOrderTypeOption] = + useState("market"); + const [limitPrice, setLimitPrice] = useState(stockData.price.toFixed(2)); + const [showOrderSuccess, setShowOrderSuccess] = useState(false); // useStream should be able to be infered from context const thread = useStream<{ messages: Message[] }>({ @@ -27,33 +68,889 @@ export default function StockPrice(props: { const toolCallId = aiTool?.tool_calls?.[0]?.id; - return ( -
- Request: {props.instruction} - -

Counter: {counter}

- {toolCallId && ( - - )} + return newHistory; + }); + + return { + ...prev, + price: newPrice, + change: parseFloat((newPrice - prev.previousClose).toFixed(2)), + changePercent: parseFloat( + ( + ((newPrice - prev.previousClose) / prev.previousClose) * + 100 + ).toFixed(2), + ), + dayHigh: Math.max(prev.dayHigh, newPrice), + dayLow: Math.min(prev.dayLow, newPrice), + }; + }); + }, 3000); + } + + return () => { + if (interval) clearInterval(interval); + }; + }, [isLiveUpdating]); + + const handleQuantityChange = (e: React.ChangeEvent) => { + const value = parseInt(e.target.value); + if (value > 0 && value <= 1000) { + setQuantity(value); + } + }; + + const handleLimitPriceChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (/^\d*\.?\d*$/.test(value)) { + setLimitPrice(value); + } + }; + + const handleOrderTypeChange = (type: "buy" | "sell") => { + setOrderType(type); + }; + + const toggleLiveUpdates = () => { + setIsLiveUpdating((prev) => !prev); + }; + + const handleOrder = () => { + // Submit the order through the thread + if (toolCallId) { + const orderDetails = { + action: orderType, + quantity, + symbol: stockData.symbol, + orderType: selectedOrderTypeOption, + ...(selectedOrderTypeOption !== "market" && { + limitPrice: parseFloat(limitPrice), + }), + }; + + thread.submit({ + messages: [ + { + type: "tool", + tool_call_id: toolCallId, + name: "stockbroker", + content: JSON.stringify(orderDetails), + }, + { + type: "human", + content: `${orderType === "buy" ? "Bought" : "Sold"} ${quantity} shares of ${stockData.symbol} at ${ + selectedOrderTypeOption === "market" + ? formatCurrency(stockData.price) + : formatCurrency(parseFloat(limitPrice)) + }`, + }, + ], + }); + + setShowOrderSuccess(true); + setTimeout(() => { + setShowOrderSuccess(false); + setShowOrder(false); + }, 2000); + } + }; + + const formatCurrency = (value: number) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(value); + }; + + const formatPercent = (value: number) => { + return `${value > 0 ? "+" : ""}${value.toFixed(2)}%`; + }; + + const getMinMax = () => { + if (priceHistory.length === 0) return { min: 0, max: 0 }; + const prices = priceHistory.map((item) => item.price); + return { + min: Math.min(...prices), + max: Math.max(...prices), + }; + }; + + // Generate a simple price chart with the last price as endpoint + const generateChartPath = useCallback(() => { + if (priceHistory.length === 0) return ""; + + const { min, max } = getMinMax(); + const range = max - min || 1; + const width = 100; // % of container width + const height = 100; // % of container height + + const points = priceHistory.map((point, i) => { + const x = (i / (priceHistory.length - 1)) * width; + const y = height - ((point.price - min) / range) * height; + return `${x},${y}`; + }); + + return `M ${points.join(" L ")}`; + }, [priceHistory]); + + const { min, max } = getMinMax(); + const range = max - min || 1; + const chartPath = generateChartPath(); + + return ( +
+
= 0 ? "bg-gradient-to-r from-green-600 to-green-500" : "bg-gradient-to-r from-red-600 to-red-500"}`} + > +
+
+ {props.logo && ( + Logo + )} +

+ {stockData.symbol}{" "} + + {stockData.name} + +

+
+
+ + {formatCurrency(stockData.price)} + +
= 0 ? "bg-green-700/50" : "bg-red-700/50"} backdrop-blur-sm border ${stockData.change >= 0 ? "border-green-400/30" : "border-red-400/30"}`} + > + {stockData.change >= 0 ? "▲" : "▼"}{" "} + {formatCurrency(Math.abs(stockData.change))} ( + {formatPercent(stockData.changePercent)}) +
+
+
+
+ {props.instruction} + +
+
+ +
+
+ + + +
+
+ +
+ {activeTab === "chart" && ( + <> +
+
+
+ Price Chart +
+ + + + +
+
+ +
+
+ {/* Chart grid lines */} +
+ {[0, 1, 2, 3].map((line) => ( +
+ ))} +
+ + {/* SVG Line Chart */} + + {/* Chart path */} + = 0 ? "#10b981" : "#ef4444"} + strokeWidth="1.5" + fill="none" + strokeLinecap="round" + /> + + {/* Gradient area under the chart */} + = 0 + ? "url(#greenGradient)" + : "url(#redGradient)" + } + fillOpacity="0.2" + /> + + {/* Gradient definitions */} + + + + + + + + + + + + + {/* Price labels on Y-axis */} +
+ {formatCurrency(max)} + {formatCurrency(min + range * 0.66)} + {formatCurrency(min + range * 0.33)} + {formatCurrency(min)} +
+
+
+ +
+
+ + {priceHistory[0]?.time || "9:30 AM"} +
+
+ Vol: {stockData.volume.toLocaleString()} +
+
+ + {priceHistory[priceHistory.length - 1]?.time || "4:00 PM"} +
+
+
+
+ + {/* Key Stock Information */} +
+
+

Day Range

+
+
+
= 0 ? "bg-green-500" : "bg-red-500"}`} + style={{ + width: `${((stockData.price - stockData.dayLow) / (stockData.dayHigh - stockData.dayLow)) * 100}%`, + }} + >
+
+
+ {formatCurrency(stockData.dayLow)} + {formatCurrency(stockData.dayHigh)} +
+
+
+
+

+ 52-Week Range +

+
+
+
+
+
+ {formatCurrency(stockData.fiftyTwoWeekLow)} + {formatCurrency(stockData.fiftyTwoWeekHigh)} +
+
+
+
+ + )} + + {activeTab === "details" && ( +
+
+
+

Open

+

{formatCurrency(stockData.open)}

+
+
+

+ Previous Close +

+

+ {formatCurrency(stockData.previousClose)} +

+
+
+

+ Market Cap +

+

{stockData.marketCap}

+
+
+

Volume

+

+ {stockData.volume.toLocaleString()} +

+
+
+

+ P/E Ratio +

+

{stockData.peRatio}

+
+
+

+ Dividend Yield +

+

{stockData.dividendYield}%

+
+
+

+ 50-Day Avg +

+

+ {formatCurrency(stockData.moving50Day)} +

+
+
+

+ 200-Day Avg +

+

+ {formatCurrency(stockData.moving200Day)} +

+
+
+ +
+

+ Company Overview +

+

+ {stockData.name} is a leading technology company that designs, + manufactures, and markets consumer electronics, computer + software, and online services. The company has a strong global + presence and is known for its innovation in the industry. +

+
+
+ )} + + {activeTab === "analysis" && ( +
+
+
+

Analyst Consensus

+
+ {stockData.analystRating} +
+
+ +
+
Strong Buy
+
+
+
+
+
+
65%
+
+
+
Buy
+
+
+
+
+
+
20%
+
+
+
Hold
+
+
+
+
+
+
10%
+
+
+
Sell
+
+
+
+
+
+
5%
+
+ +
+ Based on {stockData.analystCount} analyst ratings +
+
+ +
+
+

Price Target

+ + {formatCurrency(stockData.priceTarget)} + +
+ +
+
+
+ + + + {( + (stockData.priceTarget / stockData.price - 1) * + 100 + ).toFixed(2)} + % + {" "} + Upside +
+
+
+
+
+
+ Current: {formatCurrency(stockData.price)} + Target: {formatCurrency(stockData.priceTarget)} +
+
+
+ +
+

Recent News

+
+
+

+ {stockData.name} Reports Strong Quarterly Earnings +

+

2 days ago

+
+
+

+ New Product Launch Expected Next Month +

+

5 days ago

+
+
+
+
+ )} + + {/* Order Interface */} + {!showOrder && !showOrderSuccess ? ( +
+ + +
+ ) : showOrderSuccess ? ( +
+ + + + Order submitted successfully! +
+ ) : ( +
+
+

+ {orderType === "buy" ? "Buy" : "Sell"} {stockData.symbol} +

+ +
+ +
+
+ {orderTypeOptions.map((option) => ( + + ))} +
+ +
+
+ +
+ + + +
+
+ + {selectedOrderTypeOption !== "market" && ( +
+ +
+ + $ + + +
+
+ )} +
+ + {selectedOrderTypeOption !== "market" && ( +
+
+
+ + + +
+
+

+ {selectedOrderTypeOption === "limit" + ? `Your ${orderType} order will execute only at ${formatCurrency(parseFloat(limitPrice))} or better.` + : `Your ${orderType} order will execute when the price reaches ${formatCurrency(parseFloat(limitPrice))}.`} +

+
+
+
+ )} + +
+
+ Market Price + + {formatCurrency(stockData.price)} + +
+ +
+ + {orderType === "buy" ? "Cost" : "Credit"} ({quantity}{" "} + {quantity === 1 ? "share" : "shares"}) + + {formatCurrency(stockData.price * quantity)} +
+ +
+ Commission + $0.00 +
+ +
+ Estimated Total + + {formatCurrency(stockData.price * quantity)} + +
+
+
+ +
+ + +
+
+ )} +
); } diff --git a/agent/uis/trip-planner/accommodations-list/index.tsx b/agent/uis/trip-planner/accommodations-list/index.tsx index da97aad..2368c49 100644 --- a/agent/uis/trip-planner/accommodations-list/index.tsx +++ b/agent/uis/trip-planner/accommodations-list/index.tsx @@ -1,9 +1,195 @@ import { TripDetails } from "../../../trip-planner/types"; +import { useState } from "react"; export default function AccommodationsList({ tripDetails, }: { tripDetails: TripDetails; }) { - return
Accommodations list for {JSON.stringify(tripDetails)}
; + console.log("tripDetails", tripDetails); + // Placeholder data - ideally would come from props + const [accommodations] = useState([ + { + id: "1", + name: "Grand Hotel", + type: "Hotel", + price: "$150/night", + rating: 4.8, + amenities: ["WiFi", "Pool", "Breakfast"], + image: "https://placehold.co/300x200?text=Hotel", + available: true, + }, + { + id: "2", + name: "Cozy Apartment", + type: "Apartment", + price: "$120/night", + rating: 4.5, + amenities: ["WiFi", "Kitchen", "Washing Machine"], + image: "https://placehold.co/300x200?text=Apartment", + available: true, + }, + { + id: "3", + name: "Beachside Villa", + type: "Villa", + price: "$300/night", + rating: 4.9, + amenities: ["WiFi", "Private Pool", "Ocean View"], + image: "https://placehold.co/300x200?text=Villa", + available: false, + }, + ]); + + const [selectedId, setSelectedId] = useState(null); + + const selectedAccommodation = accommodations.find( + (acc) => acc.id === selectedId, + ); + + return ( +
+
+
+

+ Accommodations in {tripDetails.location} +

+ {selectedId && ( + + )} +
+

+ {new Date(tripDetails.startDate).toLocaleDateString()} -{" "} + {new Date(tripDetails.endDate).toLocaleDateString()} ·{" "} + {tripDetails.numberOfGuests} guests +

+
+ +
+ {!selectedId ? ( +
+ {accommodations.map((accommodation) => ( +
setSelectedId(accommodation.id)} + className={`flex border rounded-lg p-3 cursor-pointer transition-all ${ + accommodation.available + ? "hover:border-blue-300 hover:shadow-md" + : "opacity-60" + }`} + > +
+ {accommodation.name} +
+
+
+

+ {accommodation.name} +

+ + {accommodation.price} + +
+

{accommodation.type}

+
+ + + + + {accommodation.rating} + +
+ {!accommodation.available && ( + + Unavailable for your dates + + )} +
+
+ ))} +
+ ) : ( +
+ {selectedAccommodation && ( + <> +
+ {selectedAccommodation.name} +
+
+
+

+ {selectedAccommodation.name} +

+ + {selectedAccommodation.price} + +
+
+ + + + + {selectedAccommodation.rating} + +
+

+ Perfect accommodation in {tripDetails.location} for your{" "} + {tripDetails.numberOfGuests} guests. +

+
+

+ Amenities: +

+
+ {selectedAccommodation.amenities.map((amenity) => ( + + {amenity} + + ))} +
+
+ +
+ + )} +
+ )} +
+
+ ); } diff --git a/agent/uis/trip-planner/book-accommodation/index.tsx b/agent/uis/trip-planner/book-accommodation/index.tsx index 9c98a5e..0e749bc 100644 --- a/agent/uis/trip-planner/book-accommodation/index.tsx +++ b/agent/uis/trip-planner/book-accommodation/index.tsx @@ -1,4 +1,5 @@ import { TripDetails } from "../../../trip-planner/types"; +import { useState } from "react"; export default function BookAccommodation({ tripDetails, @@ -7,9 +8,395 @@ export default function BookAccommodation({ 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, + ) => { + 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 ( -
- Book accommodation {accommodationName} for {JSON.stringify(tripDetails)} +
+
+

Book {accommodation.name}

+

+ {new Date(tripDetails.startDate).toLocaleDateString()} -{" "} + {new Date(tripDetails.endDate).toLocaleDateString()} ·{" "} + {tripDetails.numberOfGuests} guests +

+
+ +
+ {bookingStep === "details" && ( + <> +
+
+ {accommodation.name} +
+
+

+ {accommodation.name} +

+
+ + + + + {accommodation.rating} + +
+
+ + {accommodation.type} + + + {accommodation.price} + +
+
+
+ +
+
+ Check-in + + {new Date(tripDetails.startDate).toLocaleDateString()} ( + {accommodation.checkInTime}) + +
+
+ Check-out + + {new Date(tripDetails.endDate).toLocaleDateString()} ( + {accommodation.checkOutTime}) + +
+
+ Guests + + {tripDetails.numberOfGuests} + +
+
+ +
+ +
+ {accommodation.roomTypes.map((room) => ( + + ))} +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +