diff --git a/app/page.tsx b/app/page.tsx index 8fa811a..ea4b9a4 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,6 +4,8 @@ import { useChat } from "@ai-sdk/react"; import ChatInput from "@/components/chat-input"; import type { MarketAgentUIMessage } from "@/agent/market-agent"; import MarketView from "@/components/market-view"; +import CompareView from "@/components/compare-view"; +import PriceHistoryView from "@/components/price-history-view"; import type { MarketUIToolInvocation } from "@/tool/market-tool"; import Image from "next/image"; import { Streamdown } from "streamdown"; @@ -253,6 +255,22 @@ export default function MarketChat() { /> ); } + case "tool-compare": { + const inv = part as { state: string; comparisonData?: string; toolCallId?: string; id?: string }; + return inv.state === "ready" && inv.comparisonData ? ( + + ) : ( +
Comparing tokens…
+ ); + } + case "tool-priceHistory": { + const inv = part as { state: string; historyData?: string; toolCallId?: string; id?: string }; + return inv.state === "ready" && inv.historyData ? ( + + ) : ( +
Fetching price history…
+ ); + } } })} diff --git a/components/compare-view.tsx b/components/compare-view.tsx new file mode 100644 index 0000000..d3d7aca --- /dev/null +++ b/components/compare-view.tsx @@ -0,0 +1,97 @@ +interface CompareToken { + id: string; + symbol: string; + name: string; + marketCapRank: number | null; + price?: number; + marketCap?: number; + volume24h?: number; + change24h?: number; +} + +interface CompareData { + provider: string; + vsCurrency: string; + tokens: CompareToken[]; +} + +function fmt(n: number | undefined, currency: string): string { + if (n === undefined) return "—"; + if (n >= 1_000_000_000_000) return `$${(n / 1_000_000_000_000).toFixed(2)}T`; + if (n >= 1_000_000_000) return `$${(n / 1_000_000_000).toFixed(2)}B`; + if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(2)}M`; + return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(n); +} + +function fmtChange(n: number | undefined): { text: string; positive: boolean } { + if (n === undefined) return { text: "—", positive: true }; + const positive = n >= 0; + return { text: `${positive ? "+" : ""}${n.toFixed(2)}%`, positive }; +} + +export default function CompareView({ data }: { data: string }) { + let parsed: CompareData | null = null; + try { + parsed = JSON.parse(data); + } catch { + return ( +
+ Failed to parse comparison data. +
+ ); + } + + if (!parsed) return null; + const { vsCurrency, tokens } = parsed; + + return ( +
+
+ + tool + + compare + {tokens.map((t) => t.symbol.toUpperCase()).join(" · ")} +
+ +
+ + + + + + + + + + + + {tokens.map((token) => { + const change = fmtChange(token.change24h); + return ( + + + + + + + + ); + })} + +
TokenPrice24hMarket CapVolume 24h
+
{token.symbol.toUpperCase()}
+
{token.name}
+
+ {fmt(token.price, vsCurrency)} + + {change.text} + + {fmt(token.marketCap, vsCurrency)} + + {fmt(token.volume24h, vsCurrency)} +
+
+
+ ); +} diff --git a/components/price-history-view.tsx b/components/price-history-view.tsx new file mode 100644 index 0000000..fb907e0 --- /dev/null +++ b/components/price-history-view.tsx @@ -0,0 +1,109 @@ +interface SparklinePoint { + t: number; + p: number; +} + +interface HistoryData { + provider: string; + coinId: string; + token: string; + vsCurrency: string; + days: number; + dataPoints: number; + startPrice: number; + endPrice: number; + high: number; + low: number; + changePercent: number; + startTime: string; + endTime: string; + sparkline: SparklinePoint[]; +} + +function fmt(n: number, currency: string): string { + if (n >= 1_000_000_000_000) return `$${(n / 1_000_000_000_000).toFixed(2)}T`; + if (n >= 1_000_000_000) return `$${(n / 1_000_000_000).toFixed(2)}B`; + if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(2)}M`; + return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(n); +} + +function Sparkline({ points, positive }: { points: SparklinePoint[]; positive: boolean }) { + if (points.length < 2) return null; + const prices = points.map((p) => p.p); + const min = Math.min(...prices); + const max = Math.max(...prices); + const range = max - min || 1; + const w = 200; + const h = 40; + const coords = points.map((p, i) => { + const x = (i / (points.length - 1)) * w; + const y = h - ((p.p - min) / range) * h; + return `${x},${y}`; + }); + const color = positive ? "#4ade80" : "#f87171"; + return ( + + + + ); +} + +export default function PriceHistoryView({ data }: { data: string }) { + let parsed: HistoryData | null = null; + try { + parsed = JSON.parse(data); + } catch { + return ( +
+ Failed to parse historical data. +
+ ); + } + + if (!parsed) return null; + const positive = parsed.changePercent >= 0; + const changeColor = positive ? "text-green-400" : "text-red-400"; + const changeText = `${positive ? "+" : ""}${parsed.changePercent.toFixed(2)}%`; + + return ( +
+
+
+ + tool + + priceHistory + + {parsed.token.toUpperCase()} · {parsed.days}d + +
+ {changeText} +
+ +
+ +
+ +
+ {[ + { label: "Start", value: fmt(parsed.startPrice, parsed.vsCurrency) }, + { label: "End", value: fmt(parsed.endPrice, parsed.vsCurrency) }, + { label: "High", value: fmt(parsed.high, parsed.vsCurrency) }, + { label: "Low", value: fmt(parsed.low, parsed.vsCurrency) }, + ].map(({ label, value }) => ( +
+
{label}
+
{value}
+
+ ))} +
+
+ ); +}