import { useStream } from "@langchain/langgraph-sdk/react"; import type { AIMessage, Message } from "@langchain/langgraph-sdk"; import { useState, useEffect, useCallback } from "react"; export default function StockPrice(props: { instruction: string; logo: string; }) { 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[] }>({ assistantId: "assistant_123", apiUrl: "http://localhost:3123", }); const messagesCopy = thread.messages; const aiTool = messagesCopy .slice() .reverse() .find( (message): message is AIMessage => message.type === "ai" && !!message.tool_calls?.length, ); const toolCallId = aiTool?.tool_calls?.[0]?.id; // Simulated price history generation on component mount useEffect(() => { generatePriceHistory(); }, []); const generatePriceHistory = () => { const now = new Date(); const history = []; for (let i = 30; i >= 0; i--) { const time = new Date(now.getTime() - i * 15 * 60000); // 15-minute intervals const basePrice = 187.32; // Make the price movement more interesting with some trends const trend = Math.sin(i / 5) * 1.5; const randomFactor = (Math.random() - 0.5) * 1.5; const price = basePrice + trend + randomFactor; history.push({ time: time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }), price: parseFloat(price.toFixed(2)), }); } setPriceHistory(history); }; // Simulate live price updates useEffect(() => { let interval: NodeJS.Timeout; if (isLiveUpdating) { interval = setInterval(() => { setStockData((prev) => { // Random small price movement const priceChange = (Math.random() - 0.5) * 0.3; const newPrice = parseFloat((prev.price + priceChange).toFixed(2)); // Update price history setPriceHistory((history) => { const now = new Date(); const newHistory = [...history]; if (newHistory.length > 30) { newHistory.shift(); // Remove oldest entry to keep array length consistent } newHistory.push({ time: now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }), price: newPrice, }); 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 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)}
)}
); }