Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import pandas as pd | |
| import folium | |
| import numpy as np | |
| import os | |
| import re | |
| import json | |
| BASE = os.path.dirname(os.path.abspath(__file__)) if "__file__" in dir() else os.getcwd() | |
| STAY_POINTS = os.path.join(BASE, "data", "stay_points_inference_sample.csv") | |
| POI_PATH = os.path.join(BASE, "data", "poi_inference_sample.csv") | |
| DEMO_PATH = os.path.join(BASE, "data", "demographics_inference_sample.csv") | |
| COT_PATH = os.path.join(BASE, "data", "inference_results_sample.json") | |
| SEX_MAP = {1:"Male", 2:"Female", -8:"Unknown", -7:"Prefer not to answer"} | |
| EDU_MAP = {1:"Less than HS", 2:"HS Graduate/GED", 3:"Some College/Associate", | |
| 4:"Bachelor's Degree", 5:"Graduate/Professional Degree", | |
| -1:"N/A", -7:"Prefer not to answer", -8:"Unknown"} | |
| INC_MAP = {1:"<$10,000", 2:"$10,000β$14,999", 3:"$15,000β$24,999", | |
| 4:"$25,000β$34,999", 5:"$35,000β$49,999", 6:"$50,000β$74,999", | |
| 7:"$75,000β$99,999", 8:"$100,000β$124,999", 9:"$125,000β$149,999", | |
| 10:"$150,000β$199,999", 11:"$200,000+", | |
| -7:"Prefer not to answer", -8:"Unknown", -9:"Not ascertained"} | |
| RACE_MAP = {1:"White", 2:"Black or African American", 3:"Asian", | |
| 4:"American Indian or Alaska Native", | |
| 5:"Native Hawaiian or Other Pacific Islander", | |
| 6:"Multiple races", 97:"Other", | |
| -7:"Prefer not to answer", -8:"Unknown"} | |
| ACT_MAP = {0:"Transportation", 1:"Home", 2:"Work", 3:"School", 4:"ChildCare", | |
| 5:"BuyGoods", 6:"Services", 7:"EatOut", 8:"Errands", 9:"Recreation", | |
| 10:"Exercise", 11:"Visit", 12:"HealthCare", 13:"Religious", | |
| 14:"SomethingElse", 15:"DropOff"} | |
| print("Loading data...") | |
| sp = pd.read_csv(STAY_POINTS) | |
| poi = pd.read_csv(POI_PATH) | |
| demo = pd.read_csv(DEMO_PATH) | |
| sp = sp.merge(poi, on="poi_id", how="left") | |
| sp["start_datetime"] = pd.to_datetime(sp["start_datetime"], utc=True) | |
| sp["end_datetime"] = pd.to_datetime(sp["end_datetime"], utc=True) | |
| sp["duration_min"] = ((sp["end_datetime"] - sp["start_datetime"]).dt.total_seconds() / 60).round(1) | |
| def parse_act_types(x): | |
| try: | |
| codes = list(map(int, str(x).strip("[]").split())) | |
| return ", ".join(ACT_MAP.get(c, str(c)) for c in codes) | |
| except: | |
| return str(x) | |
| sp["act_label"] = sp["act_types"].apply(parse_act_types) | |
| # Load CoT JSON (optional) | |
| cot_by_agent = {} | |
| if os.path.exists(COT_PATH): | |
| with open(COT_PATH, "r") as f: | |
| cot_raw = json.load(f) | |
| records = cot_raw if isinstance(cot_raw, list) else cot_raw.get("inference_results", []) | |
| for result in records: | |
| cot_by_agent[int(result["agent_id"])] = result | |
| print(f"Loaded CoT for {len(cot_by_agent)} agents.") | |
| sample_agents = sorted(sp["agent_id"].unique().tolist()) | |
| print(f"Ready. {len(sample_agents)} agents loaded.") | |
| def get_cot(agent_id): | |
| result = cot_by_agent.get(int(agent_id), {}) | |
| s1 = result.get("step1_response", "") | |
| s2 = result.get("step2_response", "") | |
| s3 = result.get("step3_response", "") | |
| p1 = result.get("step1_prompt", "") | |
| p2 = result.get("step2_prompt", "") | |
| p3 = result.get("step3_prompt", "") | |
| return s1, s2, s3, p1, p2, p3 | |
| # ββ Mobility text builders ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_mobility_summary(agent_sp): | |
| top5 = (agent_sp.groupby("name")["duration_min"] | |
| .agg(visits="count", avg_dur="mean") | |
| .sort_values("visits", ascending=False) | |
| .head(5)) | |
| obs_start = agent_sp["start_datetime"].min().strftime("%Y-%m-%d") | |
| obs_end = agent_sp["end_datetime"].max().strftime("%Y-%m-%d") | |
| days = (agent_sp["end_datetime"].max() - agent_sp["start_datetime"].min()).days | |
| act_counts = agent_sp["act_label"].value_counts().head(3) | |
| top_acts = ", ".join(f"{a} ({n})" for a, n in act_counts.items()) | |
| agent_sp2 = agent_sp.copy() | |
| agent_sp2["hour"] = agent_sp2["start_datetime"].dt.hour | |
| def tod(h): | |
| if 5 <= h < 12: return "Morning" | |
| if 12 <= h < 17: return "Afternoon" | |
| if 17 <= h < 21: return "Evening" | |
| return "Night" | |
| agent_sp2["tod"] = agent_sp2["hour"].apply(tod) | |
| peak_tod = agent_sp2["tod"].value_counts().idxmax() | |
| agent_sp2["is_weekend"] = agent_sp2["start_datetime"].dt.dayofweek >= 5 | |
| wd_pct = int((~agent_sp2["is_weekend"]).mean() * 100) | |
| lines = [ | |
| f"Period: {obs_start} ~ {obs_end} ({days} days)", | |
| f"Stay points: {len(agent_sp)} | Unique locations: {agent_sp['name'].nunique()}", | |
| f"Weekday/Weekend: {wd_pct}% / {100-wd_pct}% | Peak time: {peak_tod}", | |
| f"Top activities: {top_acts}", | |
| "", | |
| "Top Locations:", | |
| ] | |
| for i, (name, row) in enumerate(top5.iterrows(), 1): | |
| lines.append(f" {i}. {name} β {int(row['visits'])} visits, avg {int(row['avg_dur'])} min") | |
| return "\n".join(lines) | |
| def build_weekly_checkin(agent_sp, max_days=None): | |
| agent_sp2 = agent_sp.copy() | |
| agent_sp2["date"] = agent_sp2["start_datetime"].dt.date | |
| all_dates = sorted(agent_sp2["date"].unique()) | |
| dates_to_show = all_dates[:max_days] if max_days else all_dates | |
| total_days = len(all_dates) | |
| lines = ["WEEKLY CHECK-IN SUMMARY", "======================="] | |
| for date in dates_to_show: | |
| grp = agent_sp2[agent_sp2["date"] == date] | |
| dow = grp["start_datetime"].iloc[0].strftime("%A") | |
| label = "Weekend" if grp["start_datetime"].iloc[0].dayofweek >= 5 else "Weekday" | |
| lines.append(f"\n--- {dow}, {date} ({label}) ---") | |
| lines.append(f"Total activities: {len(grp)}") | |
| for _, row in grp.iterrows(): | |
| lines.append( | |
| f"- {row['start_datetime'].strftime('%H:%M')}-" | |
| f"{row['end_datetime'].strftime('%H:%M')} " | |
| f"({int(row['duration_min'])} mins): " | |
| f"{row['name']} - {row['act_label']}" | |
| ) | |
| if max_days and total_days > max_days: | |
| lines.append(f"\n... ({total_days - max_days} more days)") | |
| return "\n".join(lines) | |
| # ββ HTML reasoning chain ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CHAIN_CSS = """ | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500;600&display=swap'); | |
| .hct-root { | |
| font-family: 'DM Sans', sans-serif; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0; | |
| padding: 4px 0 8px; | |
| } | |
| /* ββ Stage shell ββ */ | |
| .hct-stage { | |
| border-radius: 12px; | |
| overflow: hidden; | |
| transition: opacity 0.3s, filter 0.3s; | |
| } | |
| .hct-stage.dim { opacity: 0.28; filter: grayscale(0.6); pointer-events: none; } | |
| .hct-stage.active { opacity: 1; } | |
| /* ββ Stage header strip ββ */ | |
| .hct-head { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 9px 14px; | |
| } | |
| .hct-num { | |
| font-family: 'DM Mono', monospace; | |
| font-size: 12px; | |
| font-weight: 600; | |
| letter-spacing: 0.12em; | |
| padding: 3px 9px; | |
| border-radius: 4px; | |
| } | |
| .hct-title { | |
| font-size: 13px; | |
| font-weight: 700; | |
| letter-spacing: 0.05em; | |
| text-transform: uppercase; | |
| flex: 1; | |
| } | |
| /* Stage 1 colors */ | |
| .hct-s1 { background: #f4f6fb; border: 1.5px solid #d4daf0; } | |
| .hct-s1 .hct-head { background: #eaecf7; border-bottom: 1px solid #d4daf0; } | |
| .hct-s1 .hct-num { background: #dde2f3; color: #3a4a80; } | |
| .hct-s1 .hct-title { color: #3a4a80; } | |
| /* Stage 2 colors */ | |
| .hct-s2 { background: #fdf8f2; border: 1.5px solid #e8d5b8; } | |
| .hct-s2 .hct-head { background: #f7ede0; border-bottom: 1px solid #e8d5b8; } | |
| .hct-s2 .hct-num { background: #f0dcbf; color: #7a4a10; } | |
| .hct-s2 .hct-title { color: #7a4a10; } | |
| /* Stage 3 colors */ | |
| .hct-s3 { background: #fff6f5; border: 2px solid #d4453a; } | |
| .hct-s3 .hct-head { background: #fce8e7; border-bottom: 1px solid #d4453a; } | |
| .hct-s3 .hct-num { background: #d4453a; color: #fff; } | |
| .hct-s3 .hct-title { color: #b0302a; } | |
| /* ββ Prompt pill ββ */ | |
| /* ββ Paper-style prompt+response layout ββ */ | |
| .hct-paper-wrap { padding: 0 12px 10px; } | |
| .hct-paper-prompt { | |
| background: #fffef5; | |
| border: 1.5px dashed #c8b870; | |
| border-radius: 8px; | |
| padding: 8px 11px; | |
| margin-bottom: 0; | |
| position: relative; | |
| } | |
| .hct-paper-response { | |
| background: #f8fffe; | |
| border: 1.5px solid #8ab8a8; | |
| border-radius: 8px; | |
| padding: 8px 11px; | |
| margin-top: 6px; | |
| position: relative; | |
| } | |
| .hct-s2 .hct-paper-prompt { background: #fffbf0; border-color: #d4a840; } | |
| .hct-s2 .hct-paper-response { background: #fffbf0; border-color: #c89050; } | |
| .hct-s3 .hct-paper-prompt { background: #fff8f7; border-color: #d4453a; border-style: dashed; } | |
| .hct-s3 .hct-paper-response { background: #fff8f7; border-color: #c03030; } | |
| .hct-paper-tag { | |
| display: inline-block; | |
| font-family: 'DM Mono', monospace; font-size: 8.5px; font-weight: 600; | |
| letter-spacing: 0.1em; text-transform: uppercase; | |
| padding: 1px 6px; border-radius: 3px; margin-bottom: 5px; | |
| } | |
| .hct-paper-prompt .hct-paper-tag { background: #f0e48a; color: #7a6010; } | |
| .hct-s2 .hct-paper-prompt .hct-paper-tag { background: #f5d580; color: #7a5010; } | |
| .hct-s3 .hct-paper-prompt .hct-paper-tag { background: #fac8c4; color: #a03028; } | |
| .hct-paper-response .hct-paper-tag { background: #c8e8d8; color: #206048; } | |
| .hct-s2 .hct-paper-response .hct-paper-tag { background: #f0dcb0; color: #7a5010; } | |
| .hct-s3 .hct-paper-response .hct-paper-tag { background: #f8c8c4; color: #902828; } | |
| .hct-paper-text { | |
| font-size: 11px; line-height: 1.6; color: #333; | |
| white-space: pre-wrap; word-break: break-word; | |
| } | |
| .hct-paper-connector { | |
| display: flex; align-items: center; justify-content: center; | |
| height: 14px; margin: 0 20px; | |
| } | |
| .hct-paper-connector-line { | |
| width: 1px; height: 100%; background: #aaa; | |
| } | |
| /* ββ Body ββ */ | |
| .hct-body { padding: 12px 14px; } | |
| /* ββ Arrow connector ββ */ | |
| .hct-arrow { | |
| display: flex; align-items: center; gap: 8px; | |
| padding: 5px 18px; transition: opacity 0.3s; | |
| } | |
| .hct-arrow-line { flex: 1; height: 1px; background: #d8d4ce; } | |
| .hct-arrow-label { | |
| font-family: 'DM Mono', monospace; font-size: 11px; | |
| color: #6a6258; letter-spacing: 0.06em; text-transform: uppercase; | |
| white-space: nowrap; background: white; font-weight: 500; | |
| padding: 3px 12px; border: 1px solid #ccc8c0; border-radius: 20px; | |
| } | |
| /* ββ Stage 1: Location table ββ */ | |
| .hct-loc-table { | |
| width: 100%; border-collapse: collapse; | |
| font-size: 11.5px; margin-bottom: 10px; | |
| } | |
| .hct-loc-table th { | |
| font-family: 'DM Mono', monospace; font-size: 9px; font-weight: 500; | |
| letter-spacing: 0.1em; text-transform: uppercase; color: #8090b0; | |
| border-bottom: 1px solid #d4daf0; padding: 3px 6px 5px; text-align: left; | |
| } | |
| .hct-loc-table th:not(:first-child) { text-align: right; } | |
| .hct-loc-table td { | |
| padding: 5px 6px; color: #2a3050; | |
| border-bottom: 1px solid #eaecf5; line-height: 1.3; | |
| } | |
| .hct-loc-table td:not(:first-child) { | |
| text-align: right; font-family: 'DM Mono', monospace; | |
| font-size: 11px; color: #5060a0; | |
| } | |
| .hct-loc-table tr:last-child td { border-bottom: none; } | |
| .hct-loc-name { | |
| font-weight: 500; max-width: 170px; overflow: hidden; | |
| text-overflow: ellipsis; white-space: nowrap; display: block; | |
| } | |
| .hct-visit-bar-wrap { display: flex; align-items: center; gap: 6px; justify-content: flex-end; } | |
| .hct-visit-bar { height: 4px; border-radius: 2px; background: #6878c8; opacity: 0.55; } | |
| /* ββ Stage 1: Temporal panel ββ */ | |
| .hct-temporal { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } | |
| .hct-temp-block { background: #eef0fa; border-radius: 8px; padding: 8px 10px; } | |
| .hct-temp-label { | |
| font-family: 'DM Mono', monospace; font-size: 9px; font-weight: 500; | |
| letter-spacing: 0.1em; text-transform: uppercase; color: #7080b0; margin-bottom: 6px; | |
| } | |
| .hct-seg-row { display: flex; height: 10px; border-radius: 5px; overflow: hidden; margin-bottom: 5px; } | |
| .hct-seg { transition: width 0.5s; } | |
| .seg-morning { background: #fbbf24; } | |
| .seg-afternoon { background: #f97316; } | |
| .seg-evening { background: #8b5cf6; } | |
| .seg-night { background: #1e3a5f; } | |
| .seg-weekday { background: #6878c8; } | |
| .seg-weekend { background: #e8c080; } | |
| .hct-legend { display: flex; flex-wrap: wrap; gap: 4px 10px; } | |
| .hct-leg-item { display: flex; align-items: center; gap: 4px; font-size: 10px; color: #5a6080; } | |
| .hct-leg-dot { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; } | |
| .hct-dist-line { | |
| margin-top: 8px; font-size: 11px; color: #6070a0; | |
| font-family: 'DM Mono', monospace; padding: 5px 8px; | |
| background: #eef0fa; border-radius: 6px; | |
| display: flex; align-items: center; gap: 6px; | |
| } | |
| /* ββ Stage 2: 2x2 grid ββ */ | |
| .hct-dim-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } | |
| .hct-dim-card { | |
| background: #fff; border: 1px solid #e8d5b8; | |
| border-radius: 8px; padding: 9px 11px; | |
| } | |
| .hct-dim-head { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; } | |
| .hct-dim-icon { font-size: 13px; line-height: 1; } | |
| .hct-dim-name { | |
| font-family: 'DM Mono', monospace; font-size: 9px; font-weight: 500; | |
| letter-spacing: 0.1em; text-transform: uppercase; color: #a07040; | |
| } | |
| .hct-dim-text { font-size: 11px; color: #3a2a10; line-height: 1.55; } | |
| .hct-dim-empty { color: #ccc; font-style: italic; } | |
| /* ββ Stage 3 ββ */ | |
| .hct-pred-row { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 10px; } | |
| .hct-pred-badge { | |
| background: #d4453a; color: white; border-radius: 8px; | |
| padding: 8px 14px; text-align: center; flex-shrink: 0; | |
| } | |
| .hct-pred-val { font-size: 18px; font-weight: 600; line-height: 1.2; white-space: nowrap; } | |
| .hct-pred-sub { | |
| font-family: 'DM Mono', monospace; font-size: 9px; | |
| opacity: 0.8; letter-spacing: 0.08em; text-transform: uppercase; margin-top: 2px; | |
| } | |
| .hct-conf-col { flex: 1; padding-top: 4px; } | |
| .hct-conf-label { | |
| font-family: 'DM Mono', monospace; font-size: 9px; color: #a04040; | |
| letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 4px; | |
| } | |
| .hct-conf-track { height: 6px; background: #f0d0cf; border-radius: 3px; overflow: hidden; margin-bottom: 6px; } | |
| .hct-conf-fill { height: 100%; background: linear-gradient(90deg, #e74c3c, #8b0000); border-radius: 3px; } | |
| .hct-reasoning { | |
| font-size: 11.5px; color: #4a2020; line-height: 1.6; | |
| border-left: 3px solid #e8b0ae; padding-left: 10px; | |
| } | |
| /* ββ Idle / loading ββ */ | |
| .hct-idle { font-size: 12px; color: #b0bac8; padding: 6px 0; font-style: italic; } | |
| .hct-loading { font-size: 12px; padding: 6px 0; display: flex; align-items: center; gap: 8px; } | |
| .hct-dot { | |
| width: 6px; height: 6px; border-radius: 50%; display: inline-block; | |
| animation: hct-pulse 1.2s ease-in-out infinite; | |
| } | |
| .hct-dot:nth-child(2) { animation-delay: 0.2s; } | |
| .hct-dot:nth-child(3) { animation-delay: 0.4s; } | |
| @keyframes hct-pulse { | |
| 0%,100% { opacity: 0.2; transform: scale(0.8); } | |
| 50% { opacity: 1; transform: scale(1.1); } | |
| } | |
| .hct-s1 .hct-dot { background: #6878c8; } | |
| .hct-s2 .hct-dot { background: #c08040; } | |
| .hct-s3 .hct-dot { background: #d4453a; } | |
| /* ββ Data flow banner ββ */ | |
| .hct-flow-banner { | |
| background: #f8f9fc; border: 1px solid #dde0ee; | |
| border-radius: 10px; padding: 10px 14px; margin-bottom: 10px; | |
| font-size: 11.5px; color: #445; | |
| } | |
| .hct-flow-banner-title { | |
| font-family: 'DM Mono', monospace; font-size: 9.5px; font-weight: 600; | |
| letter-spacing: 0.1em; text-transform: uppercase; | |
| color: #7080a0; margin-bottom: 7px; | |
| } | |
| .hct-flow-steps { | |
| display: flex; align-items: center; gap: 0; flex-wrap: nowrap; | |
| } | |
| .hct-flow-step { | |
| flex: 1; background: white; border: 1px solid #d4daf0; | |
| border-radius: 7px; padding: 6px 8px; text-align: center; | |
| min-width: 0; | |
| } | |
| .hct-flow-step-label { | |
| font-family: 'DM Mono', monospace; font-size: 8.5px; | |
| color: #8090b0; letter-spacing: 0.08em; text-transform: uppercase; | |
| margin-bottom: 3px; | |
| } | |
| .hct-flow-step-desc { | |
| font-size: 10.5px; color: #334; line-height: 1.4; | |
| } | |
| .hct-flow-arrow { | |
| font-size: 14px; color: #a0aac0; padding: 0 5px; | |
| flex-shrink: 0; | |
| } | |
| /* ββ Prompt collapsible ββ */ | |
| details.hct-prompt-details { padding: 0 14px 10px; } | |
| details.hct-prompt-details summary { | |
| display: inline-flex; align-items: center; gap: 5px; list-style: none; | |
| font-family: 'DM Mono', monospace; font-size: 10.5px; font-weight: 600; | |
| letter-spacing: 0.06em; text-transform: uppercase; | |
| padding: 4px 13px; border-radius: 20px; cursor: pointer; | |
| border: 1.5px solid currentColor; opacity: 0.75; | |
| transition: opacity 0.2s, background 0.2s; background: rgba(255,255,255,0.6); | |
| user-select: none; | |
| } | |
| details.hct-prompt-details summary::-webkit-details-marker { display: none; } | |
| details.hct-prompt-details summary::before { content: 'βΌ View Prompt'; } | |
| details.hct-prompt-details[open] summary::before { content: 'β² Hide Prompt'; } | |
| details.hct-prompt-details summary:hover { opacity: 1; background: rgba(255,255,255,0.95); } | |
| .hct-s1 details.hct-prompt-details summary { color: #3a4a80; } | |
| .hct-s2 details.hct-prompt-details summary { color: #7a4a10; } | |
| .hct-s3 details.hct-prompt-details summary { color: #b0302a; } | |
| .hct-prompt-content { | |
| margin-top: 7px; background: rgba(0,0,0,0.025); | |
| border-radius: 7px; padding: 8px 12px 8px 10px; | |
| border-left: 2px solid #ccc; opacity: 0.85; | |
| } | |
| .hct-prompt-list { | |
| margin: 0; padding: 0 0 0 16px; list-style: disc; | |
| } | |
| .hct-prompt-list li { | |
| margin-bottom: 5px; color: #445; | |
| font-size: 11px; line-height: 1.6; | |
| } | |
| .hct-prompt-list li:last-child { margin-bottom: 0; } | |
| .hct-prompt-list code { | |
| font-family: 'DM Mono', monospace; font-size: 10px; | |
| background: rgba(0,0,0,0.07); padding: 1px 4px; border-radius: 3px; | |
| } | |
| </style> | |
| """ | |
| def _loading(msg): | |
| return (f'<div class="hct-loading">' | |
| f'<span class="hct-dot"></span><span class="hct-dot"></span><span class="hct-dot"></span>' | |
| f'<span style="color:#8090a0;font-size:12px">{msg}</span></div>') | |
| # ββ Parsers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _parse_s1(text): | |
| locations, dur_map, tod, wk, dist = [], {}, {}, {}, None | |
| for line in text.splitlines(): | |
| s = line.strip() | |
| # Locations: "- Name: N visits/times/time/times each" | |
| m = re.match(r'-\s+(.+?):\s+(\d+)\s+(?:visit|time)', s, re.IGNORECASE) | |
| if m: | |
| locations.append((m.group(1).strip(), int(m.group(2)))) | |
| continue | |
| # Duration β 4 formats | |
| m2 = re.match(r'-?\s*(.+?):\s+(?:Average duration of\s*)?([\d.]+)\s+min(?:utes?)?\s+on average', s, re.IGNORECASE) | |
| if not m2: | |
| m2 = re.match(r'-?\s*(.+?):\s+Average duration of ([\d.]+)\s+min', s, re.IGNORECASE) | |
| if not m2: | |
| m2 = re.match(r'-?\s*Average duration at (.+?):\s+([\d.]+)\s+min', s, re.IGNORECASE) | |
| if not m2: | |
| m2 = re.search(r'\bat ([A-Za-z][^(,]+?)\s*\(average ([\d.]+)\s*min', s, re.IGNORECASE) | |
| if m2: | |
| dur_map[m2.group(1).strip()] = float(m2.group(2)) | |
| # TOD format A: "65% morning, 23% afternoon, 6% evening, 5% night" | |
| if not tod: | |
| mA = re.search(r'(\d+)%\s*morning.*?(\d+)%\s*afternoon.*?(\d+)%\s*evening.*?(\d+)%\s*night', s, re.IGNORECASE) | |
| if mA: | |
| tod = {'Morning': int(mA.group(1)), 'Afternoon': int(mA.group(2)), | |
| 'Evening': int(mA.group(3)), 'Night': int(mA.group(4))} | |
| # TOD format B: "morning: 40%, afternoon: 36%, ..." | |
| if not tod: | |
| mB = re.search(r'morning[:\s]+(\d+)%.*?afternoon[:\s]+(\d+)%.*?evening[:\s]+(\d+)%.*?night[:\s]+(\d+)%', s, re.IGNORECASE) | |
| if mB: | |
| tod = {'Morning': int(mB.group(1)), 'Afternoon': int(mB.group(2)), | |
| 'Evening': int(mB.group(3)), 'Night': int(mB.group(4))} | |
| # TOD format C: "Afternoon (43%), morning (27%), ..." | |
| if not tod: | |
| parts = re.findall(r'(morning|afternoon|evening|night)\s*\(?(\d+)%\)?', s, re.IGNORECASE) | |
| if len(parts) >= 3: | |
| d = {k.capitalize(): int(v) for k, v in parts} | |
| if all(k in d for k in ['Morning', 'Afternoon', 'Evening']): | |
| d.setdefault('Night', 0) | |
| tod = d | |
| # Weekday/weekend | |
| if not wk: | |
| m4 = re.search(r'(\d+)%\s*weekday.*?(\d+)%\s*weekend', s, re.IGNORECASE) | |
| if m4: | |
| wk = {'Weekday': int(m4.group(1)), 'Weekend': int(m4.group(2))} | |
| # Distance | |
| if not dist: | |
| m5 = re.search(r'average distance of approximately ([\d.]+)\s*(?:km|miles?)', s, re.IGNORECASE) | |
| if m5: | |
| dist = float(m5.group(1)) | |
| return [(n, v, dur_map.get(n)) for n, v in locations[:7]], tod, wk, dist | |
| def _parse_s2(text): | |
| DIMS = { | |
| 'ROUTINE': ['ROUTINE', 'SCHEDULE'], | |
| 'ECONOMIC': ['ECONOMIC', 'SPENDING'], | |
| 'SOCIAL': ['SOCIAL', 'LIFESTYLE'], | |
| 'STABILITY': ['STABILITY', 'REGULARITY', 'CONSISTENCY', 'URBAN'], | |
| } | |
| sections, current_key, current_lines = {}, None, [] | |
| for line in text.splitlines(): | |
| s = line.strip() | |
| mA = re.match(r'^\d+\.\s+([A-Z][A-Z\s&]+?)(?:\s+ANALYSIS|\s+PATTERNS|\s+INDICATORS|\s+CHARACTERISTICS|\s+STABILITY)?:\s*$', s, re.IGNORECASE) | |
| mB = re.match(r'^STEP\s+\d+:\s+([A-Z][A-Z\s&]+?)(?:\s+ANALYSIS|\s+PATTERNS|\s+INDICATORS|\s+CHARACTERISTICS|\s+STABILITY)?\s*$', s, re.IGNORECASE) | |
| mm = mA or mB | |
| if mm: | |
| if current_key and current_lines: | |
| sections[current_key] = ' '.join(current_lines) | |
| current_key = mm.group(1).upper().strip() | |
| current_lines = [] | |
| elif current_key and s: | |
| if re.match(r'^\d+\.\d+', s): | |
| sub = re.sub(r'^\d+\.\d+[^:]*:\s*', '', s) | |
| if sub: current_lines.append(sub) | |
| elif s.startswith('-'): | |
| current_lines.append(s.lstrip('-').strip()) | |
| elif not re.match(r'^\d+\.', s): | |
| current_lines.append(s) | |
| if current_key and current_lines: | |
| sections[current_key] = ' '.join(current_lines) | |
| result = {} | |
| for dim, keywords in DIMS.items(): | |
| for k, txt in sections.items(): | |
| if any(kw in k for kw in keywords) and txt: | |
| sents = re.split(r'(?<=[.!?])\s+', txt.strip()) | |
| summary = ' '.join(sents[:2]) | |
| result[dim] = summary[:157] + 'β¦' if len(summary) > 160 else summary | |
| break | |
| return result | |
| def _parse_s3(text): | |
| pred, conf, r_lines, in_r = '', 0, [], False | |
| for line in text.splitlines(): | |
| s = line.strip() | |
| if s.startswith('INCOME_PREDICTION:'): | |
| pred = s.replace('INCOME_PREDICTION:', '').strip() | |
| elif s.startswith('INCOME_CONFIDENCE:'): | |
| try: conf = int(re.search(r'\d+', s).group()) | |
| except: pass | |
| elif s.startswith('INCOME_REASONING:'): | |
| in_r = True | |
| r_lines.append(s.replace('INCOME_REASONING:', '').strip()) | |
| elif in_r: | |
| if re.match(r'^2\.', s) or s.startswith('INCOME_'): break | |
| if s: r_lines.append(s) | |
| reasoning = ' '.join(r_lines).strip() | |
| sents = re.split(r'(?<=[.!?])\s+', reasoning) | |
| reasoning = ' '.join(sents[:3]) | |
| return pred, conf, (reasoning[:277] + 'β¦' if len(reasoning) > 280 else reasoning) | |
| PROMPT_BULLETS = { | |
| 1: [ | |
| "Extract objective factual features from the agent's mobility trajectory <b>without any interpretation</b>", | |
| "Location inventory: list all visited POIs with visit counts and apparent price tier (budget / mid-range / high-end)", | |
| "Temporal patterns: time-of-day distribution, weekday vs. weekend split, and regularity of routines", | |
| "Spatial characteristics: activity radius, average movement distance between locations", | |
| "Sequence observations: common location transitions and typical daily activity chains", | |
| ], | |
| 2: [ | |
| "Perform behavioral abstraction across four dimensions based on Step 1 features", | |
| "Routine & Schedule: infer work schedule type (fixed hours, flexible, shift work, etc.) and daily structure", | |
| "Economic Behavior: assess spending tier from venue choices, transportation costs, and lifestyle signals", | |
| "Social & Lifestyle: identify social engagement patterns, leisure activities, and community involvement", | |
| "Routine Stability: evaluate consistency and regularity of movement patterns over the observation period", | |
| ], | |
| 3: [ | |
| "Synthesize factual features (Step 1) and behavioral patterns (Step 2) to infer household income bracket", | |
| "Score location economic indicators: luxury / mid-range / budget venue distribution", | |
| "Consider transportation mode signals, activity diversity, and temporal flexibility as income proxies", | |
| "Output: <code>INCOME_PREDICTION</code> β a single income range with confidence rating (1β5)", | |
| "Output: <code>INCOME_REASONING</code> β evidence-grounded justification referencing specific mobility observations", | |
| ], | |
| } | |
| PROMPT_INPUTS = { | |
| 1: "β‘ Activity Chronicles + β’ Visiting Summaries β detailed daily visit logs and weekly behavioral statistics generated from raw stay points", | |
| 2: "Stage 1 response β factual features extracted from Activity Chronicles", | |
| 3: "Stage 1 + Stage 2 responses β feature extraction and behavioral abstraction combined", | |
| } | |
| _INPUT_TAG = ('<span style="font-family:\'DM Mono\',monospace;font-size:9px;font-weight:600;' | |
| 'letter-spacing:0.08em;text-transform:uppercase;color:#888;margin-right:6px;">Input</span>') | |
| def _extract_prompt_instruction(prompt_text, stage): | |
| bullets = PROMPT_BULLETS.get(stage, []) | |
| if not bullets: | |
| return '' | |
| inp = PROMPT_INPUTS.get(stage, '') | |
| input_block = ('<div style="margin-bottom:8px;padding:6px 10px;background:rgba(0,0,0,0.04);' | |
| 'border-radius:6px;font-size:10.5px;line-height:1.6;">' | |
| + _INPUT_TAG + inp + '</div>') | |
| items = ''.join('<li>' + b + '</li>' for b in bullets) | |
| return input_block + '<ul class="hct-prompt-list">' + items + '</ul>' | |
| # ββ Body renderers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _s1_body(text, active): | |
| if not active: | |
| return '<div class="hct-idle">Press βΆ to start</div>' | |
| if not text: | |
| return _loading('Extracting features') | |
| locs, tod, wk, dist = _parse_s1(text) | |
| max_v = max((v for _, v, _ in locs), default=1) | |
| rows = '' | |
| for name, visits, dur in locs: | |
| bar_w = int(60 * visits / max_v) | |
| dur_str = f'{int(dur)}m' if dur else 'β' | |
| rows += (f'<tr>' | |
| f'<td><span class="hct-loc-name" title="{name}">{name}</span></td>' | |
| f'<td><div class="hct-visit-bar-wrap">' | |
| f'<div class="hct-visit-bar" style="width:{bar_w}px"></div>{visits}</div></td>' | |
| f'<td>{dur_str}</td></tr>') | |
| table = (f'<table class="hct-loc-table">' | |
| f'<thead><tr><th>Location</th><th>Visits</th><th>Avg Stay</th></tr></thead>' | |
| f'<tbody>{rows}</tbody></table>') if rows else '' | |
| def seg_bar(data, seg_classes): | |
| total = sum(data.values()) or 1 | |
| segs = ''.join( | |
| f'<div class="hct-seg {cls}" style="width:{int(100*v/total)}%"></div>' | |
| for (label, v), cls in zip(data.items(), seg_classes)) | |
| legend = ''.join( | |
| f'<div class="hct-leg-item"><div class="hct-leg-dot {cls}"></div>{label} {v}%</div>' | |
| for (label, v), cls in zip(data.items(), seg_classes)) | |
| return f'<div class="hct-seg-row">{segs}</div><div class="hct-legend">{legend}</div>' | |
| tod_panel = (f'<div class="hct-temp-block"><div class="hct-temp-label">Time of Day</div>' | |
| f'{seg_bar(tod, ["seg-morning","seg-afternoon","seg-evening","seg-night"])}</div>') if tod else '' | |
| wk_panel = (f'<div class="hct-temp-block"><div class="hct-temp-label">Weekday / Weekend</div>' | |
| f'{seg_bar(wk, ["seg-weekday","seg-weekend"])}</div>') if wk else '' | |
| temporal = f'<div class="hct-temporal">{tod_panel}{wk_panel}</div>' if (tod_panel or wk_panel) else '' | |
| dist_line = f'<div class="hct-dist-line">π Avg trip distance {dist} mi</div>' if dist else '' | |
| return table + temporal + dist_line | |
| def _s2_body(text, active): | |
| if not active: | |
| return '<div class="hct-idle">Waitingβ¦</div>' | |
| if not text: | |
| return _loading('Analyzing behavior') | |
| dims = _parse_s2(text) | |
| DIM_META = [('ROUTINE','π','Schedule'), ('ECONOMIC','π°','Economic'), | |
| ('SOCIAL','π₯','Social'), ('STABILITY','π','Stability')] | |
| cards = '' | |
| for key, icon, label in DIM_META: | |
| txt = dims.get(key, '') | |
| content = (f'<div class="hct-dim-text">{txt}</div>' if txt | |
| else '<div class="hct-dim-text hct-dim-empty">β</div>') | |
| cards += (f'<div class="hct-dim-card">' | |
| f'<div class="hct-dim-head">' | |
| f'<span class="hct-dim-icon">{icon}</span>' | |
| f'<span class="hct-dim-name">{label}</span></div>' | |
| f'{content}</div>') | |
| return f'<div class="hct-dim-grid">{cards}</div>' | |
| def _s3_body(text, active): | |
| if not active: | |
| return '<div class="hct-idle">Waitingβ¦</div>' | |
| if not text: | |
| return _loading('Inferring demographics') | |
| pred, conf, reasoning = _parse_s3(text) | |
| return (f'<div class="hct-pred-row">' | |
| f'<div class="hct-pred-badge">' | |
| f'<div class="hct-pred-val">{pred or "β"}</div>' | |
| f'<div class="hct-pred-sub">Income</div></div>' | |
| f'</div>' | |
| f'<div class="hct-reasoning">{reasoning}</div>') | |
| # ββ Main renderer βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def render_chain(s1_text, s2_text, s3_text, status="idle", | |
| s1_prompt="", s2_prompt="", s3_prompt=""): | |
| s1_on = status in ("running1", "running2", "running3", "done") | |
| s2_on = status in ("running2", "running3", "done") | |
| s3_on = status in ("running3", "done") | |
| s1_body = _s1_body(s1_text if s1_on else '', s1_on) | |
| s2_body = _s2_body(s2_text if s2_on else '', s2_on) | |
| s3_body = _s3_body(s3_text if s3_on else '', s3_on) | |
| def prompt_pill(stage_num): | |
| bullets_html = _extract_prompt_instruction('', stage_num) | |
| if not bullets_html: | |
| return '' | |
| return (f'<details class="hct-prompt-details">' | |
| f'<summary></summary>' | |
| f'<div class="hct-prompt-content">{bullets_html}</div>' | |
| f'</details>') | |
| def stage(cls, num, title, body, on, stage_num): | |
| dim_cls = 'active' if on else 'dim' | |
| pill = prompt_pill(stage_num) if on else '' | |
| return (f'<div class="hct-stage hct-{cls} {dim_cls}">' | |
| f'<div class="hct-head">' | |
| f'<span class="hct-num">{num}</span>' | |
| f'<span class="hct-title">{title}</span>' | |
| f'</div>' | |
| f'{pill}' | |
| f'<div class="hct-body">{body}</div>' | |
| f'</div>') | |
| def arrow(label, on): | |
| op = '1' if on else '0.2' | |
| return (f'<div class="hct-arrow" style="opacity:{op}">' | |
| f'<div class="hct-arrow-line"></div>' | |
| f'<div class="hct-arrow-label">{label}</div>' | |
| f'<div class="hct-arrow-line"></div></div>') | |
| flow_banner = ( | |
| '<div class="hct-flow-banner">' | |
| '<div class="hct-flow-banner-title">Data Pipeline</div>' | |
| '<div class="hct-flow-steps">' | |
| '<div class="hct-flow-step">' | |
| '<div class="hct-flow-step-label">Raw Data</div>' | |
| '<div class="hct-flow-step-desc">Stay points + POI metadata<br><span style="color:#8090b0;font-size:10px">β Raw Stay Points tab</span></div>' | |
| '</div>' | |
| '<div class="hct-flow-arrow">β</div>' | |
| '<div class="hct-flow-step">' | |
| '<div class="hct-flow-step-label">Activity Chronicles</div>' | |
| '<div class="hct-flow-step-desc">Detailed Chronicles + Visiting Summaries<br><span style="color:#8090b0;font-size:10px">β‘ β’ tabs Β· micro + macro level</span></div>' | |
| '</div>' | |
| '<div class="hct-flow-arrow">β</div>' | |
| '<div class="hct-flow-step" style="border-color:#b0bce8;background:#f4f6fb;">' | |
| '<div class="hct-flow-step-label" style="color:#5060a0;">Prompt 1</div>' | |
| '<div class="hct-flow-step-desc" style="color:#3a4a80;">Factual feature extraction<br><span style="color:#8090b0;font-size:10px">no interpretation Β· pattern identification</span></div>' | |
| '</div>' | |
| '</div>' | |
| '</div>' | |
| ) | |
| html = CHAIN_CSS + '<div class="hct-root">' | |
| html += flow_banner | |
| html += stage('s1', 'Stage 01', 'Feature Extraction', s1_body, s1_on, 1) | |
| html += arrow('behavioral abstraction', s2_on) | |
| html += stage('s2', 'Stage 02', 'Behavioral Analysis', s2_body, s2_on, 2) | |
| html += arrow('demographic inference', s3_on) | |
| html += stage('s3', 'Stage 03', 'Demographic Inference', s3_body, s3_on, 3) | |
| html += '</div>' | |
| return html | |
| def build_map(agent_sp): | |
| agent_sp = agent_sp.reset_index(drop=True).copy() | |
| agent_sp["latitude"] += np.random.uniform(-0.0003, 0.0003, len(agent_sp)) | |
| agent_sp["longitude"] += np.random.uniform(-0.0003, 0.0003, len(agent_sp)) | |
| lat = agent_sp["latitude"].mean() | |
| lon = agent_sp["longitude"].mean() | |
| m = folium.Map(location=[lat, lon], zoom_start=12, tiles="CartoDB positron") | |
| coords = list(zip(agent_sp["latitude"], agent_sp["longitude"])) | |
| if len(coords) > 1: | |
| folium.PolyLine(coords, color="#cc000055", weight=1.5, opacity=0.4).add_to(m) | |
| n = len(agent_sp) | |
| for i, row in agent_sp.iterrows(): | |
| ratio = i / max(n - 1, 1) | |
| r = int(255 - ratio * (255 - 139)) | |
| g = int(204 * (1 - ratio) ** 2) | |
| b = 0 | |
| color = f"#{r:02x}{g:02x}{b:02x}" | |
| folium.CircleMarker( | |
| location=[row["latitude"], row["longitude"]], | |
| radius=7, color=color, fill=True, fill_color=color, fill_opacity=0.9, | |
| popup=folium.Popup( | |
| f"<b>#{i+1} {row['name']}</b><br>" | |
| f"{row['start_datetime'].strftime('%a %m/%d %H:%M')}<br>" | |
| f"{int(row['duration_min'])} min<br>{row['act_label']}", | |
| max_width=220 | |
| ) | |
| ).add_to(m) | |
| legend_html = """ | |
| <div style=" | |
| position:fixed; bottom:18px; left:18px; z-index:9999; | |
| background:rgba(255,255,255,0.92); border-radius:8px; | |
| padding:8px 12px; font-size:11px; font-family:sans-serif; | |
| box-shadow:0 1px 5px rgba(0,0,0,0.2); line-height:1.8; | |
| "> | |
| <div style="font-weight:600;margin-bottom:4px;">Stay Point Legend</div> | |
| <div style="display:flex;align-items:center;gap:6px;"> | |
| <svg width="60" height="10"> | |
| <defs><linearGradient id="lg" x1="0" x2="1" y1="0" y2="0"> | |
| <stop offset="0%" stop-color="#ffcc00"/> | |
| <stop offset="100%" stop-color="#8b0000"/> | |
| </linearGradient></defs> | |
| <rect width="60" height="10" rx="4" fill="url(#lg)"/> | |
| </svg> | |
| <span>Earlier → Later</span> | |
| </div> | |
| <div style="display:flex;align-items:center;gap:6px;margin-top:2px;"> | |
| <svg width="14" height="14"><circle cx="7" cy="7" r="5" fill="#cc4444" opacity="0.5"/></svg> | |
| <span>Movement path</span> | |
| </div> | |
| <div style="color:#999;font-size:10px;margin-top:2px;">Click dot for details</div> | |
| </div> | |
| """ | |
| m.get_root().html.add_child(folium.Element(legend_html)) | |
| m.get_root().width = "100%" | |
| m.get_root().height = "420px" | |
| return m._repr_html_() | |
| def build_demo_text(row): | |
| age = int(row["age"]) if row["age"] > 0 else "Unknown" | |
| return ( | |
| f"Age: {age} | " | |
| f"Sex: {SEX_MAP.get(int(row['sex']), row['sex'])} | " | |
| f"Race: {RACE_MAP.get(int(row['race']), row['race'])} | " | |
| f"Education: {EDU_MAP.get(int(row['education']), row['education'])} | " | |
| f"Income: {INC_MAP.get(int(row['hh_income']), row['hh_income'])}" | |
| ) | |
| def build_raw_staypoints(agent_sp, n=12): | |
| cols = ["start_datetime", "end_datetime", "duration_min", "latitude", "longitude", "name", "act_label"] | |
| df = agent_sp[cols].head(n).copy() | |
| df["start_datetime"] = df["start_datetime"].dt.strftime("%m/%d %H:%M") | |
| df["end_datetime"] = df["end_datetime"].dt.strftime("%H:%M") | |
| df["duration_min"] = df["duration_min"].astype(int).astype(str) + " min" | |
| df["latitude"] = df["latitude"].round(5).astype(str) | |
| df["longitude"] = df["longitude"].round(5).astype(str) | |
| df.columns = ["Start", "End", "Duration", "Lat", "Lon", "Venue", "Activity"] | |
| lines = ["Stay Points (raw input β first {} records)".format(n), ""] | |
| col_w = {"Start": 11, "End": 7, "Duration": 9, "Lat": 9, "Lon": 10, "Venue": 26, "Activity": 16} | |
| header = " ".join(k.ljust(v) for k, v in col_w.items()) | |
| lines.append(header) | |
| lines.append("-" * len(header)) | |
| for _, row in df.iterrows(): | |
| line = " ".join(str(row[k]).ljust(v)[:v] for k, v in col_w.items()) | |
| lines.append(line) | |
| lines.append("") | |
| lines.append("β These records are transformed into Activity Chronicles (Detailed + Visiting Summaries)") | |
| lines.append(" and fed into Prompt 1 for factual feature extraction.") | |
| return "\n".join(lines) | |
| # ββ Callbacks βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def on_select(agent_id): | |
| agent_id = int(agent_id) | |
| agent_sp = sp[sp["agent_id"] == agent_id].sort_values("start_datetime") | |
| agent_demo = demo[demo["agent_id"] == agent_id].iloc[0] | |
| map_html = build_map(agent_sp) | |
| demo_text = build_demo_text(agent_demo) | |
| raw_text = build_mobility_summary(agent_sp) + "\n\n" + build_weekly_checkin(agent_sp) | |
| chain_html = render_chain("", "", "", status="idle") | |
| return map_html, raw_text, demo_text, chain_html | |
| def run_step(agent_id, step): | |
| agent_id = int(agent_id) | |
| s1, s2, s3, p1, p2, p3 = get_cot(agent_id) | |
| next_step = step + 1 | |
| if next_step == 1: | |
| html = render_chain(s1, "", "", status="running2", s1_prompt=p1) | |
| return html, 1, gr.update(value="βΆ Stage 2: Behavioral Analysis") | |
| elif next_step == 2: | |
| html = render_chain(s1, s2, "", status="running3", s1_prompt=p1, s2_prompt=p2) | |
| return html, 2, gr.update(value="βΆ Stage 3: Demographic Inference") | |
| else: | |
| html = render_chain(s1, s2, s3, status="done", s1_prompt=p1, s2_prompt=p2, s3_prompt=p3) | |
| return html, -1, gr.update(value="βΊ Reset") | |
| def handle_btn(agent_id, step): | |
| if step == -1: | |
| html = render_chain("", "", "", status="idle") | |
| return html, 0, gr.update(value="βΆ Stage 1: Feature Extraction") | |
| return run_step(agent_id, step) | |
| def on_select_reset(agent_id): | |
| agent_id_int = int(agent_id) | |
| agent_sp = sp[sp["agent_id"] == agent_id_int].sort_values("start_datetime") | |
| agent_demo = demo[demo["agent_id"] == agent_id_int].iloc[0] | |
| map_html = build_map(agent_sp) | |
| demo_text = build_demo_text(agent_demo) | |
| cot_entry = cot_by_agent.get(agent_id_int, {}) | |
| summary = build_mobility_summary(agent_sp) | |
| raw_full = cot_entry.get("weekly_checkin") or build_weekly_checkin(agent_sp) | |
| sep = "\n\n--- " | |
| parts = raw_full.split(sep) | |
| extra = len(parts) - 1 | |
| raw_text = parts[0] + (sep.join([""] + parts[1:2]) + ("\n\n... ({} more days)".format(extra - 1) if extra > 1 else "")) if extra > 0 else raw_full | |
| chain_html = render_chain("", "", "", status="idle") | |
| raw_sp_text = build_raw_staypoints(agent_sp) | |
| return map_html, raw_sp_text, summary, raw_text, demo_text, chain_html, 0, gr.update(value="βΆ Stage 1: Feature Extraction") | |
| SHOWCASE_AGENTS = sample_agents[:6] | |
| def build_agent_cards(selected_id): | |
| selected_id = int(selected_id) | |
| parts = [] | |
| parts.append("<div style='display:grid;grid-template-columns:repeat(3,1fr);gap:10px;padding:4px 0;'>") | |
| for aid in SHOWCASE_AGENTS: | |
| row = demo[demo["agent_id"] == aid].iloc[0] | |
| age = int(row["age"]) if row["age"] > 0 else "?" | |
| sex = SEX_MAP.get(int(row["sex"]), "?") | |
| edu = EDU_MAP.get(int(row["education"]), "?") | |
| inc = INC_MAP.get(int(row["hh_income"]), "?") | |
| is_sel = (aid == selected_id) | |
| sel_style = "border:2px solid #c0392b;background:#fff8f8;box-shadow:0 2px 8px rgba(192,57,43,0.15);" | |
| nor_style = "border:1.5px solid #ddd;background:#fafafa;box-shadow:0 1px 3px rgba(0,0,0,0.06);" | |
| style = sel_style if is_sel else nor_style | |
| dot = "<span style='display:inline-block;width:8px;height:8px;border-radius:50%;background:#c0392b;margin-right:5px;'></span>" if is_sel else "" | |
| js = "var t=document.querySelector('#agent_hidden_input textarea');t.value='AID';t.dispatchEvent(new Event('input',{bubbles:true}));".replace("AID", str(aid)) | |
| parts.append( | |
| "<div onclick=\"" + js + "\" style=\"cursor:pointer;border-radius:10px;padding:10px 13px;transition:all 0.2s;" + style + "\">" | |
| "<div style='font-size:11px;font-weight:700;color:#c0392b;margin-bottom:3px;font-family:monospace;'>" + dot + "Agent #" + str(aid) + "</div>" | |
| "<div style='font-size:11px;color:#333;line-height:1.6;'>" | |
| "<b>Age:</b> " + str(age) + " <b>Sex:</b> " + sex + "<br>" | |
| "<b>Edu:</b> " + edu + "<br>" | |
| "<b>Income:</b> " + inc + "</div></div>" | |
| ) | |
| parts.append("</div>") | |
| return "".join(parts) | |
| # ββ UI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Blocks(title="HiCoTraj Demo") as app: | |
| gr.Markdown("## HiCoTraj β Trajectory Visualization & Hierarchical CoT Demo") | |
| gr.Markdown("*Zero-Shot Demographic Reasoning via Hierarchical Chain-of-Thought Prompting from Trajectory* Β· ACM SIGSPATIAL GeoGenAgent 2025") | |
| gr.HTML("<div style='display:inline-flex;align-items:center;gap:7px;background:#fffbe6;border:1px solid #f0d060;border-radius:8px;padding:6px 14px;font-size:12px;color:#7a6010;margin-bottom:4px;'>💻 <b>Best experienced on a laptop or desktop</b> — the side-by-side layout requires a wide screen.</div>") | |
| gr.HTML("<div style='display:inline-flex;align-items:center;gap:7px;background:#e8f4fd;border:1px solid #90c8e8;border-radius:8px;padding:6px 14px;font-size:12px;color:#1a5070;margin-bottom:8px;'>βοΈ <b>Use Light Mode</b> — dark mode will hide most UI elements. In your browser: View → Appearance → Light.</div>") | |
| gr.Markdown(""" | |
| **Dataset:** NUMOSIM[1] | |
| > [1]Stanford C, Adari S, Liao X, et al. *NUMoSim: A Synthetic Mobility Dataset with Anomaly Detection Benchmarks.* ACM SIGSPATIAL Workshop on Geospatial Anomaly Detection, 2024. | |
| """) | |
| gr.Markdown("### Select Agent") | |
| agent_cards = gr.HTML(value=build_agent_cards(SHOWCASE_AGENTS[0])) | |
| agent_hidden = gr.Textbox( | |
| value=str(SHOWCASE_AGENTS[0]), | |
| visible=True, | |
| elem_id="agent_hidden_input", | |
| elem_classes=["hidden-input"] | |
| ) | |
| gr.HTML("<style>.hidden-input { display:none !important; }</style>") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Trajectory Map") | |
| map_out = gr.HTML() | |
| gr.Markdown("### Mobility Data") | |
| with gr.Tabs(): | |
| with gr.Tab("β Raw Stay Points"): | |
| sp_out = gr.Textbox(lines=10, interactive=False, label="", show_label=False) | |
| with gr.Tab("β‘ Activity Chronicles"): | |
| raw_out = gr.Textbox(lines=10, interactive=False, label="", show_label=False) | |
| show_all_btn = gr.Button("Show All Days", size="sm", variant="secondary") | |
| with gr.Tab("β’ Visiting Summaries"): | |
| summary_out = gr.Textbox(lines=10, interactive=False, label="", show_label=False) | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Hierarchical Chain-of-Thought Reasoning") | |
| step_state = gr.State(value=0) | |
| run_btn = gr.Button("βΆ Stage 1: Feature Extraction", variant="primary") | |
| chain_out = gr.HTML(value=render_chain("", "", "", status="idle")) | |
| def on_agent_click(agent_id): | |
| cards_html = build_agent_cards(agent_id) | |
| map_html, raw_sp, summary, raw_text, _demo_text, chain_html, step, btn = on_select_reset(agent_id) | |
| return cards_html, map_html, raw_sp, summary, raw_text, chain_html, step, btn | |
| agent_hidden.change( | |
| fn=on_agent_click, inputs=agent_hidden, | |
| outputs=[agent_cards, map_out, sp_out, summary_out, raw_out, chain_out, step_state, run_btn] | |
| ) | |
| def on_load(agent_id): | |
| map_html, raw_sp, summary, raw_text, _demo_text, chain_html, step, btn = on_select_reset(agent_id) | |
| return map_html, raw_sp, summary, raw_text, chain_html, step, btn | |
| app.load( | |
| fn=on_load, inputs=agent_hidden, | |
| outputs=[map_out, sp_out, summary_out, raw_out, chain_out, step_state, run_btn] | |
| ) | |
| run_btn.click( | |
| fn=handle_btn, inputs=[agent_hidden, step_state], | |
| outputs=[chain_out, step_state, run_btn] | |
| ) | |
| def toggle_raw(agent_id, current_text): | |
| agent_id_int = int(agent_id) | |
| cot_entry = cot_by_agent.get(agent_id_int, {}) | |
| agent_sp = sp[sp["agent_id"] == agent_id_int].sort_values("start_datetime") | |
| raw_full = cot_entry.get("weekly_checkin") or build_weekly_checkin(agent_sp) | |
| if "more days" in current_text: | |
| return raw_full, gr.update(value="Show Less") | |
| else: | |
| sep = "\n\n--- " | |
| parts = raw_full.split(sep) | |
| extra = len(parts) - 1 | |
| short = parts[0] + (sep.join([""] + parts[1:2]) + ("\n\n... ({} more days)".format(extra - 1) if extra > 1 else "")) if extra > 0 else raw_full | |
| return short, gr.update(value="Show All Days") | |
| show_all_btn.click( | |
| fn=toggle_raw, inputs=[agent_hidden, raw_out], | |
| outputs=[raw_out, show_all_btn] | |
| ) | |
| app.launch(show_error=True, theme=gr.themes.Soft(), share=True, js="() => { document.body.classList.remove('dark'); }") |