import { type AgentId, type DimensionKey, type LayerKey, type Neighborhood, neighborhoods, } from "../data/neighborhoods"; export interface ParsedPrompt { weights: Record; budget: number; cap: number; } export interface RankedNeighborhood extends Neighborhood { dims: Record; overall: number; rank: number; } export interface CityAgent { id: AgentId; name: string; color: string; tag: string; } export interface AgentState extends CityAgent { status: "queued" | "scanning" | "scoring" | "done"; lines: string[]; score: number | null; finding?: string; } const palette = { ink: "#243a5c", deep: "#304b78", strong: "#46648d", mid: "#5f7faa", soft: "#7f99ba", mist: "#9fb5ce", }; export const defaultPrompt = "I make $70k, work near Union, want under 45 min commute, safe area, near cafes, max rent $2,200."; export const cityAgents: CityAgent[] = [ { id: "affordability", name: "Affordability", color: palette.mid, tag: "Budget" }, { id: "commute", name: "Commute", color: palette.strong, tag: "Routes" }, { id: "safety", name: "Safety Signals", color: palette.deep, tag: "Signals" }, { id: "lifestyle", name: "Lifestyle", color: palette.soft, tag: "Amenities" }, { id: "growth", name: "Future Growth", color: palette.mist, tag: "Momentum" }, { id: "recommendation", name: "Recommendation", color: palette.ink, tag: "Consensus" }, ]; export function initialAgentStates(top: RankedNeighborhood): AgentState[] { return cityAgents.map((agent) => ({ ...agent, status: "done", lines: [], score: agent.id === "recommendation" ? top.overall : top.dims[agentDimension(agent.id)], })); } export function queuedAgentStates(): AgentState[] { return cityAgents.map((agent) => ({ ...agent, status: "queued", lines: [], score: null, })); } export function parsePrompt(prompt: string): ParsedPrompt { const source = prompt.toLowerCase(); const weights: Record = { affordability: 1, safety: 1, commute: 1, transit: 1, amenities: 1, lifestyle: 1, growth: 1, }; if (/safe|safety|secur|crime/.test(source)) weights.safety += 1.5; if (/afford|cheap|budget|max rent|under|\brent\b|\$\s?\d/.test(source)) { weights.affordability += 1.4; } if (/union|downtown|commut|\bmin\b|transit|ttc|subway|go train|\bgo\b/.test(source)) { weights.commute += 1.2; weights.transit += 0.6; } if (/caf|coffee|park|night|grocer|restaurant|amenit|walk|lifestyle|\bbar\b|food/.test(source)) { weights.lifestyle += 1; weights.amenities += 0.9; } if (/growth|invest|future|appreciat|up.?and.?coming|value|momentum|emerging/.test(source)) { weights.growth += 1.2; } let budget: number | null = null; const moneyMatches = source.match(/\$\s?([\d,]{3,6})/g); if (moneyMatches) { const values = moneyMatches .map((match) => Number.parseInt(match.replace(/[^\d]/g, ""), 10)) .filter((value) => value >= 1100 && value <= 6000); if (values.length) budget = Math.max(...values); } const commuteMatch = source.match(/(\d{2,3})\s*min/); return { weights, budget: budget ?? 2200, cap: commuteMatch ? Number.parseInt(commuteMatch[1], 10) : 45, }; } export function rankNeighborhoods( parsed: ParsedPrompt, rows: Neighborhood[] = neighborhoods, ): RankedNeighborhood[] { const weightSum = Object.values(parsed.weights).reduce((sum, value) => sum + value, 0); const ranked = rows.map((neighborhood) => { const dims = neighborhood.scores; let base = 0; for (const dimension of Object.keys(parsed.weights) as DimensionKey[]) { base += dims[dimension] * parsed.weights[dimension]; } base /= weightSum; let adjustment = 0; if (neighborhood.rentHi <= parsed.budget) adjustment += 4; else if (neighborhood.rentLo > parsed.budget) adjustment -= 11; else adjustment -= 2; const commuteMidpoint = (neighborhood.comLo + neighborhood.comHi) / 2; if (commuteMidpoint > parsed.cap) adjustment -= (commuteMidpoint - parsed.cap) * 0.6; return { ...neighborhood, dims, overall: clamp(Math.round(base + adjustment), 34, 99), rank: 0, }; }); ranked.sort((a, b) => b.overall - a.overall || b.scores.affordability - a.scores.affordability); return ranked.map((row, index) => ({ ...row, rank: index + 1 })); } export function agentDimension(id: AgentId): DimensionKey { if (id === "recommendation") return "growth"; if (id === "affordability") return "affordability"; if (id === "commute") return "commute"; if (id === "safety") return "safety"; if (id === "lifestyle") return "lifestyle"; return "growth"; } export function layerScore(neighborhood: RankedNeighborhood, layer: LayerKey): number { if (layer === "overall") return neighborhood.overall; if (layer === "rent") return neighborhood.dims.affordability; return neighborhood.dims[layer]; } export function consensus(overall: number) { if (overall >= 88) return { label: "Strong fit", color: palette.ink }; if (overall >= 80) return { label: "Solid fit", color: palette.deep }; if (overall >= 72) return { label: "Fair fit", color: palette.strong }; return { label: "Limited fit", color: palette.soft }; } export function tagFor(neighborhood: RankedNeighborhood) { const scores = Object.values(neighborhood.dims); const spread = Math.max(...scores) - Math.min(...scores); if ( neighborhood.dims.growth >= 84 && neighborhood.dims.growth >= neighborhood.dims.affordability ) { return { label: "Up and coming", color: palette.strong }; } if (neighborhood.dims.affordability >= 84) { return { label: neighborhood.overall >= 82 ? "Great value" : "Good value", color: palette.mid, }; } if (spread <= 16) { return { label: neighborhood.overall >= 82 ? "Great balance" : "Balanced", color: palette.deep, }; } if (neighborhood.dims.lifestyle >= 86) return { label: "Vibrant", color: palette.soft }; if (neighborhood.dims.commute >= 86) return { label: "Well connected", color: palette.strong }; return { label: "Solid pick", color: palette.ink }; } export function formatRentRange(neighborhood: Neighborhood): string { return `$${neighborhood.rentLo.toLocaleString()}-${neighborhood.rentHi.toLocaleString()}`; } export function getAgentThinking(id: AgentId, parsed: ParsedPrompt): string[] { const budget = parsed.budget.toLocaleString(); const cap = parsed.cap; const lines: Record = { affordability: [ "Scanning one-bedroom rent across 14 areas", `Comparing medians to your $${budget} cap`, "Scoring budget fit and flagging overpriced zones", ], commute: [ "Routing every area to Union Station", `Weighing GO, subway and streetcar vs ${cap} min`, "Scoring commute burden and transfers", ], safety: [ "Aggregating reported incident density", "Normalising by population and time of day", "Setting relative safety signals with data limits", ], lifestyle: [ "Mapping cafes, parks, groceries and nightlife", "Gauging walkability and local character", "Scoring lifestyle fit", ], growth: [ "Reading rent trajectories and permits", "Tracking transit expansion and development", "Projecting 12-month momentum", ], recommendation: [ "Gathering every agent finding", "Weighting by your stated priorities", "Forming consensus and ranking neighborhoods", ], }; return lines[id]; } export function getAgentFinding(id: AgentId, neighborhood: RankedNeighborhood, parsed: ParsedPrompt) { const tier = (score: number) => { if (score >= 85) return "excellent"; if (score >= 78) return "strong"; if (score >= 70) return "fair"; return "limited"; }; const commuteMidpoint = Math.round((neighborhood.comLo + neighborhood.comHi) / 2); if (id === "affordability") { const relation = neighborhood.rentHi <= parsed.budget ? "fits under" : neighborhood.rentLo > parsed.budget ? "runs above" : "brushes"; return `One-bed rent ${formatRentRange(neighborhood)} ${relation} your $${parsed.budget.toLocaleString()} cap. Budget fit is ${tier(neighborhood.dims.affordability)}.`; } if (id === "commute") { return `About ${commuteMidpoint} min to Union via ${neighborhood.comMode}; ${ commuteMidpoint <= parsed.cap ? "under" : "around" } your ${parsed.cap}-min target.`; } if (id === "safety") { return `Relative safety signals read ${tier(neighborhood.dims.safety)} here from reported incident density. Confidence is limited by public data quality.`; } if (id === "lifestyle") { return `${neighborhood.lifeHi}. Cafe, park and grocery density is ${tier(neighborhood.dims.amenities)}.`; } if (id === "growth") { return `${neighborhood.growthNote}. Rent is projected ${ neighborhood.trend > 0 ? "+" : "" }${neighborhood.trend}% over the next 12 months.`; } return `Agents converge on ${neighborhood.name}: ${consensus(neighborhood.overall).label}. ${neighborhood.short}`; } export function colorForScore(score: number): string { const t = clamp((score - 36) / 63, 0, 1); const lightness = 57 - t * 26; return `hsl(214 42% ${lightness.toFixed(0)}%)`; } export function clamp(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)); }