HiCoTraj / app.py
ginnyxxxxxxx's picture
dark
de0cf31
Raw
History Blame Contribute Delete
46.9 kB
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 &amp; 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 &amp; 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 &nbsp;{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 &rarr; 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) + " &nbsp; <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;'>&#x1F4BB; <b>Best experienced on a laptop or desktop</b> &mdash; 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> &mdash; dark mode will hide most UI elements. In your browser: View &rarr; Appearance &rarr; 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'); }")