AutonomousAgent / agent /ui_theme.py
Mathias Heider
Claude Fable 5.1
Keep the agent running: cycle watchdog, Retry-After cap, boot at container start, self keep-alive
73849a1 unverified
Raw History Blame Contribute Delete
26.3 kB
"""Design system for the agent Space — tokens, CSS, altair theme, components.
Everything visual that is shared between pages lives here so a page only
composes: ``page_header``, ``status_pill``, ``kpi_row``, ``empty_state``,
``chip``, ``feed`` and the altair theme. Nothing in this module talks to the
database or changes behavior; it is presentation only.
Palette / chrome follow the project design system (colorblind-validated
categorical order, status colors reserved for state, one custom light theme
with a serviceable dark variant picked from ``st.context.theme``).
"""
from __future__ import annotations
import html as _html
from datetime import datetime, timezone
from typing import Iterable, Optional
import streamlit as st
# ---------------------------------------------------------------------------
# tokens
# ---------------------------------------------------------------------------
CATEGORICAL_LIGHT = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100",
"#e87ba4", "#008300", "#4a3aa7", "#e34948"]
CATEGORICAL_DARK = ["#3987e5", "#d95926", "#199e70", "#c98500",
"#d55181", "#008300", "#9085e9", "#e66767"]
SEQUENTIAL = ["#cde2fb", "#9ec5f4", "#6da7ec", "#3987e5",
"#256abf", "#184f95", "#0d366b"]
STATUS = { # reserved for state — never used as series colors
"good": "#0ca30c",
"warning": "#fab219",
"serious": "#ec835a",
"critical": "#d03b3b",
}
LIGHT = dict(
ink="#0b0b0b", ink2="#52514e", muted="#898781",
grid="#e1e0d9", baseline="#c3c2b7", hairline="rgba(11,11,11,0.10)",
surface="#fcfcfb", plane="#f9f9f7", success_text="#006300",
tint="rgba(11,11,11,0.035)", primary="#2a78d6",
)
DARK = dict(
ink="#f3f3f1", ink2="#c2c1bb", muted="#8f8e88",
grid="#33332f", baseline="#4a4a45", hairline="rgba(255,255,255,0.12)",
surface="#151515", plane="#0e0e0e", success_text="#5fd35f",
tint="rgba(255,255,255,0.05)", primary="#3987e5",
)
FONT = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
MONO = 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace'
def is_dark() -> bool:
"""True only when the *configured* Streamlit theme is dark.
Deliberately NOT st.context.theme.type: with a single custom light theme
that call reports the viewer's OS preference ("dark" on a dark desktop)
while Streamlit still renders the light theme — injecting dark tokens on
a light page. The config is the one source of truth for the whole app.
"""
try:
return (st.get_option("theme.base") or "light") == "dark"
except Exception:
return False
def tokens() -> dict:
return DARK if is_dark() else LIGHT
def series() -> list[str]:
"""Categorical series colors in fixed order (color follows the entity)."""
return CATEGORICAL_DARK if is_dark() else CATEGORICAL_LIGHT
def esc(x) -> str:
"""HTML-escape anything (None/NaN-proof)."""
if x is None:
return ""
try:
import pandas as pd
if isinstance(x, float) and pd.isna(x):
return ""
except Exception:
pass
return _html.escape(str(x), quote=True)
def fmt_int(n) -> str:
"""Thousands-separated integer, '—' for None/NaN."""
try:
if n is None:
return "—"
import pandas as pd
if pd.isna(n):
return "—"
return f"{int(n):,}"
except Exception:
return "—"
def rel_time(ts) -> str:
"""'3 min ago' style relative time; '' if unparsable."""
if ts is None:
return ""
try:
import pandas as pd
if pd.isna(ts):
return ""
t = pd.Timestamp(ts)
if t.tzinfo is None:
t = t.tz_localize("UTC")
now = pd.Timestamp.now(tz="UTC")
s = int((now - t).total_seconds())
except Exception:
return ""
if s < 0:
s = 0
if s < 45:
return "just now"
if s < 3600:
return f"{s // 60} min ago"
if s < 86400:
h = s // 3600
return f"{h} h ago"
d = s // 86400
return f"{d} day{'s' if d != 1 else ''} ago"
def fmt_ts(ts, fmt: str = "%b %d, %H:%M") -> str:
if ts is None:
return ""
try:
import pandas as pd
if pd.isna(ts):
return ""
return pd.Timestamp(ts).strftime(fmt)
except Exception:
return str(ts)
# ---------------------------------------------------------------------------
# CSS (single place)
# ---------------------------------------------------------------------------
def _css(t: dict) -> str:
good, warn, serious, crit = (STATUS["good"], STATUS["warning"],
STATUS["serious"], STATUS["critical"])
return f"""
<style>
:root {{
--aim-ink: {t['ink']}; --aim-ink2: {t['ink2']}; --aim-muted: {t['muted']};
--aim-grid: {t['grid']}; --aim-baseline: {t['baseline']};
--aim-hairline: {t['hairline']}; --aim-surface: {t['surface']};
--aim-plane: {t['plane']}; --aim-tint: {t['tint']}; --aim-primary: {t['primary']};
--aim-success-text: {t['success_text']};
--aim-good: {good}; --aim-warn: {warn}; --aim-serious: {serious}; --aim-crit: {crit};
--aim-font: {FONT}; --aim-mono: {MONO};
}}
/* ---- typography scale ------------------------------------------------ */
.stApp, .stApp * {{ font-variant-numeric: proportional-nums; }}
.stApp h1, .stApp h2, .stApp h3 {{ letter-spacing: -0.01em; }}
[data-testid="stMainBlockContainer"] {{ padding-top: 4.4rem; padding-bottom: 3rem; max-width: 1360px; }}
[data-testid="stHeading"] h2 {{ font-size: 1.15rem; font-weight: 650; padding-top: .9rem; padding-bottom: .2rem; }}
[data-testid="stHeading"] h3 {{ font-size: 1.0rem; font-weight: 650; }}
[data-testid="stCaptionContainer"] p {{ color: var(--aim-muted); }}
[data-testid="stExpander"] details {{ border: 1px solid var(--aim-hairline); border-radius: 10px; background: var(--aim-surface); }}
[data-testid="stExpander"] summary {{ padding: .55rem .9rem; }}
[data-testid="stExpander"] summary p {{ margin: 0; }}
[data-testid="stTabs"] [data-baseweb="tab-list"] {{ gap: 6px; }}
[data-testid="stTabs"] [data-baseweb="tab-highlight"] {{ background-color: var(--aim-primary); height: 2px; }}
[data-testid="stTabs"] [data-baseweb="tab-border"] {{ background-color: var(--aim-hairline); }}
[data-testid="stTabs"] [data-baseweb="tab"] p {{ font-weight: 600; }}
[data-testid="stDataFrame"] {{ border-radius: 8px; overflow: hidden; }}
[data-testid="stDataFrame"], [data-testid="stDataFrame"] * {{ font-variant-numeric: tabular-nums; }}
[data-testid="stSidebar"] {{ border-right: 1px solid var(--aim-hairline); }}
[data-testid="stHeader"] {{ border-bottom: 1px solid var(--aim-hairline); background: var(--aim-plane); }}
[data-testid="stTopNavLink"] p {{ font-weight: 560; }}
[data-testid="stTopNavLink"][aria-current="page"] p,
[data-testid="stTopNavLinkContainer"] a[aria-current="page"] p {{ font-weight: 700; }}
[data-testid="stAppDeployButton"] {{ display: none; }}
code, kbd, pre {{ font-family: var(--aim-mono) !important; }}
/* cards: every bordered container is a card */
[data-testid="stVerticalBlockBorderWrapper"] > div[data-testid="stVerticalBlock"] {{ gap: .6rem; }}
div[data-testid="stVerticalBlockBorderWrapper"]:has(> div > div[data-testid="stVerticalBlock"]) {{
border-color: var(--aim-hairline) !important; border-radius: 12px; background: var(--aim-surface);
padding: 1rem 1.15rem 1.05rem 1.15rem;
}}
.aim-card-title {{ font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
color: var(--aim-muted); margin: 0 0 .15rem 0; display: flex; align-items: center; justify-content: space-between; gap: 8px; }}
.aim-card-title .aim-card-sub {{ font-weight: 500; letter-spacing: 0; text-transform: none; font-size: 12px; }}
/* ---- brand row / footer ---------------------------------------------- */
.aim-brand {{ display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
padding: 2px 0 10px 0; margin-bottom: 4px; border-bottom: 1px solid var(--aim-hairline); }}
.aim-brand .aim-wordmark {{ font-size: 13px; font-weight: 700; letter-spacing: .02em; color: var(--aim-ink); }}
.aim-brand .aim-wordmark span {{ font-weight: 500; color: var(--aim-ink2); }}
.aim-brand .aim-env {{ font-size: 12px; color: var(--aim-muted); white-space: nowrap; }}
.aim-brand .aim-env code {{ font-size: 11.5px; background: var(--aim-tint); padding: 1px 6px; border-radius: 5px; color: var(--aim-ink2); }}
[data-testid="stLayoutWrapper"]:has(> .st-key-aim_footer), .st-key-aim_footer {{ order: 9999; }}
.st-key-aim_footer {{ margin-top: 2.5rem; }}
.aim-footer {{ border-top: 1px solid var(--aim-hairline); padding-top: 10px; font-size: 12px; color: var(--aim-muted);
display: flex; gap: 18px; flex-wrap: wrap; align-items: center; }}
.aim-footer a {{ color: var(--aim-ink2); text-decoration: none; border-bottom: 1px solid var(--aim-baseline); }}
.aim-footer a:hover {{ color: var(--aim-primary); border-color: var(--aim-primary); }}
/* ---- page header ------------------------------------------------------- */
.aim-ph {{ display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin: .35rem 0 1.1rem 0; }}
.aim-ph h1 {{ font-size: 1.85rem; font-weight: 700; margin: 0; padding: 0; line-height: 1.15; color: var(--aim-ink); }}
.aim-ph .aim-sub {{ font-size: 14px; color: var(--aim-ink2); margin-top: 4px; }}
.aim-ph .aim-right {{ display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }}
/* ---- pills / chips ----------------------------------------------------- */
.aim-pill {{ display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 600;
padding: 3px 10px 3px 8px; border-radius: 999px; border: 1px solid transparent; white-space: nowrap; line-height: 1.5; }}
.aim-pill .dot {{ width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex: none; }}
.aim-pill-ok {{ color: #0a6a0a; background: rgba(12,163,12,.12); border-color: rgba(12,163,12,.28); }}
.aim-pill-running {{ color: #1b5cab; background: rgba(42,120,214,.12); border-color: rgba(42,120,214,.30); }}
.aim-pill-running .dot {{ animation: aim-pulse 1.4s ease-in-out infinite; }}
.aim-pill-warn, .aim-pill-stale {{ color: #7a5200; background: rgba(250,178,25,.18); border-color: rgba(250,178,25,.45); }}
.aim-pill-crashed, .aim-pill-error {{ color: #9c1f1f; background: rgba(208,59,59,.12); border-color: rgba(208,59,59,.32); }}
.aim-pill-idle, .aim-pill-off {{ color: var(--aim-ink2); background: var(--aim-tint); border-color: var(--aim-hairline); }}
.aim-pill-info {{ color: var(--aim-ink2); background: var(--aim-tint); border-color: var(--aim-hairline); }}
.aim-pill-new {{ color: #1b5cab; background: rgba(42,120,214,.12); border-color: rgba(42,120,214,.30); }}
@keyframes aim-pulse {{ 0%,100% {{ opacity: 1; transform: scale(1);}} 50% {{ opacity: .35; transform: scale(.75);}} }}
.aim-chip {{ display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px; font-weight: 600; padding: 1px 8px;
border-radius: 6px; border: 1px solid var(--aim-hairline); background: var(--aim-tint); color: var(--aim-ink2); white-space: nowrap; }}
.aim-chip-muted {{ font-weight: 500; }}
.aim-chip-info {{ color: var(--aim-ink2); }}
.aim-chip-warn {{ color: #7a5200; background: rgba(250,178,25,.18); border-color: rgba(250,178,25,.45); }}
.aim-chip-error, .aim-chip-critical {{ color: #9c1f1f; background: rgba(208,59,59,.12); border-color: rgba(208,59,59,.32); }}
.aim-chip-good {{ color: #0a6a0a; background: rgba(12,163,12,.12); border-color: rgba(12,163,12,.28); }}
.aim-chip-blue {{ color: #1b5cab; background: rgba(42,120,214,.12); border-color: rgba(42,120,214,.30); }}
.aim-chips {{ display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 2px 0 8px 0; }}
.aim-chips .k {{ color: var(--aim-muted); font-weight: 500; margin-right: 2px; }}
/* ---- KPI tiles --------------------------------------------------------- */
.aim-kpis {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: .2rem 0 1rem 0; }}
.aim-kpi {{ border: 1px solid var(--aim-hairline); border-radius: 12px; background: var(--aim-surface); padding: 12px 14px 11px 14px; min-width: 0; }}
.aim-kpi-label {{ font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--aim-muted);
display: flex; align-items: center; gap: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
.aim-kpi-label .help {{ font-weight: 600; text-transform: none; letter-spacing: 0; cursor: help; color: var(--aim-baseline);
border: 1px solid var(--aim-baseline); border-radius: 50%; width: 13px; height: 13px; font-size: 9px; display: inline-flex; align-items: center; justify-content: center; }}
.aim-kpi-value {{ font-size: 30px; font-weight: 650; line-height: 1.15; color: var(--aim-ink); margin-top: 6px; letter-spacing: -0.015em; }}
.aim-kpi-value.small {{ font-size: 22px; }}
.aim-kpi-delta {{ font-size: 12px; margin-top: 4px; color: var(--aim-ink2); font-weight: 500; }}
.aim-kpi-delta.up {{ color: var(--aim-success-text); }}
.aim-kpi-delta.down {{ color: var(--aim-crit); }}
.aim-kpi-delta.warn {{ color: #7a5200; }}
/* ---- empty state ------------------------------------------------------- */
.aim-empty {{ text-align: center; padding: 34px 20px; border: 1px dashed var(--aim-baseline); border-radius: 12px; background: var(--aim-surface); margin: .5rem 0 1rem 0; }}
.aim-empty .ico {{ font-size: 28px; line-height: 1; margin-bottom: 8px; }}
.aim-empty .ttl {{ font-size: 15px; font-weight: 650; color: var(--aim-ink); }}
.aim-empty .hint {{ font-size: 13px; color: var(--aim-muted); margin-top: 4px; max-width: 520px; margin-left: auto; margin-right: auto; }}
/* ---- feed (events) ----------------------------------------------------- */
.aim-feed {{ border: 1px solid var(--aim-hairline); border-radius: 12px; background: var(--aim-surface); overflow: hidden; }}
.aim-feed-row {{ display: grid; grid-template-columns: 62px 78px minmax(0, 1fr) 90px; gap: 10px; align-items: start;
padding: 8px 14px; border-top: 1px solid var(--aim-hairline); font-size: 13px; color: var(--aim-ink); }}
.aim-feed-row:first-child {{ border-top: 0; }}
.aim-feed-row .lvl {{ padding-top: 1px; }}
.aim-feed-row .node {{ color: var(--aim-muted); font-size: 12px; padding-top: 2px; font-family: var(--aim-mono); }}
.aim-feed-row .msg {{ min-width: 0; overflow-wrap: anywhere; }}
.aim-feed-row .msg pre {{ margin: 6px 0 0 0; font-size: 11.5px; line-height: 1.4; padding: 8px 10px; background: var(--aim-tint);
border-radius: 8px; white-space: pre-wrap; color: var(--aim-ink2); }}
.aim-feed-row .ts {{ color: var(--aim-muted); font-size: 12px; text-align: right; white-space: nowrap; padding-top: 2px; font-variant-numeric: tabular-nums; }}
.aim-feed-row .ts .abs {{ display: block; font-size: 10.5px; color: var(--aim-baseline); }}
.aim-feed-empty {{ padding: 16px; font-size: 13px; color: var(--aim-muted); text-align: center; }}
/* ---- material expander label (Database page) --------------------------- */
.aim-mat {{ display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 14px; }}
.aim-mat .name {{ font-weight: 700; color: var(--aim-ink); }}
.aim-mat .meta {{ color: var(--aim-muted); font-size: 12.5px; }}
.aim-mat .meta b {{ color: var(--aim-ink2); font-weight: 600; }}
.aim-note {{ font-size: 12px; color: var(--aim-muted); margin: 6px 0 4px 0; }}
.aim-inline-count {{ font-size: 13px; color: var(--aim-ink2); }}
.aim-inline-count b {{ color: var(--aim-ink); }}
</style>
"""
def inject_css() -> None:
"""Inject the design-system CSS once per run (call from app.py)."""
st.html(_css(tokens()))
# ---------------------------------------------------------------------------
# altair theme
# ---------------------------------------------------------------------------
def _altair_config(t: dict, cats: list[str]) -> dict:
axis = dict(labelColor=t["muted"], titleColor=t["ink2"], gridColor=t["grid"],
domainColor=t["baseline"], tickColor=t["baseline"], labelFont=FONT,
titleFont=FONT, labelFontSize=11, titleFontSize=11, titleFontWeight=600,
labelPadding=6, titlePadding=8, gridDash=[1, 0], gridWidth=1, ticks=False,
labelFlush=True)
return {
"config": {
"background": t["surface"],
"view": {"stroke": None, "continuousWidth": 400, "continuousHeight": 220},
"font": FONT,
"padding": {"left": 4, "top": 6, "right": 8, "bottom": 4},
"title": {"font": FONT, "fontSize": 13, "fontWeight": 650, "color": t["ink"],
"anchor": "start", "offset": 10, "subtitleColor": t["muted"],
"subtitleFontSize": 11, "subtitleFont": FONT},
"axis": axis,
"axisX": {**axis, "grid": False, "domain": True},
"axisY": {**axis, "grid": True, "domain": False},
"legend": {"labelColor": t["ink2"], "titleColor": t["ink2"], "labelFont": FONT,
"titleFont": FONT, "labelFontSize": 11, "titleFontSize": 11,
"symbolSize": 70, "symbolType": "circle", "orient": "top",
"direction": "horizontal", "offset": 6, "padding": 0},
"range": {"category": cats, "ordinal": SEQUENTIAL, "ramp": SEQUENTIAL,
"heatmap": SEQUENTIAL},
"bar": {"color": cats[0], "cornerRadiusTopLeft": 4, "cornerRadiusTopRight": 4,
"binSpacing": 2, "stroke": None},
"line": {"color": cats[0], "strokeWidth": 2, "interpolate": "linear"},
"area": {"color": cats[0], "opacity": 0.18, "line": True},
"point": {"color": cats[0], "size": 46, "filled": True},
"rule": {"color": t["baseline"]},
"text": {"color": t["ink2"], "font": FONT, "fontSize": 11},
"mark": {"tooltip": True},
}
}
def register_altair_theme() -> None:
"""Register + enable the 'aim' altair theme (works with altair 4/5/6)."""
try:
import altair as alt
except Exception:
return
t = tokens()
cats = series()
cfg = _altair_config(t, cats)
def _aim_theme():
return cfg
try: # altair >= 5.5 / 6
alt.theme.register("aim", enable=True)(_aim_theme)
return
except Exception:
pass
try: # altair 4 / 5.0-5.4
alt.themes.register("aim", _aim_theme)
alt.themes.enable("aim")
except Exception:
pass
def chart(c, key: Optional[str] = None, height: Optional[int] = None) -> None:
"""Render an altair chart with the AIM theme (not Streamlit's built-in one)."""
kwargs = {"theme": None}
if key:
kwargs["key"] = key
try:
st.altair_chart(c, width="stretch", **kwargs)
except TypeError: # older Streamlit: use_container_width
st.altair_chart(c, use_container_width=True, **kwargs)
# ---------------------------------------------------------------------------
# components
# ---------------------------------------------------------------------------
_PILL_ICON = {"ok": "●", "running": None, "stale": "▲", "warn": "▲", "crashed": "■",
"error": "■", "idle": "○", "off": "○", "info": "○", "new": "✦"}
def pill_html(kind: str, text: str) -> str:
kind = (kind or "idle").lower()
if kind not in _PILL_ICON:
kind = {"ok": "ok", "success": "ok", "warning": "warn", "critical": "crashed",
"failed": "crashed", "on": "ok"}.get(kind, "idle")
icon = _PILL_ICON.get(kind)
lead = '<span class="dot"></span>' if icon is None else f'<span aria-hidden="true">{icon}</span>'
return f'<span class="aim-pill aim-pill-{kind}">{lead}<span>{esc(text)}</span></span>'
def status_pill(kind: str, text: str) -> None:
st.html(pill_html(kind, text))
def chip_html(text: str, kind: str = "muted", title: str = "") -> str:
kind = (kind or "muted").lower()
t = f' title="{esc(title)}"' if title else ""
return f'<span class="aim-chip aim-chip-{kind}"{t}>{esc(text)}</span>'
def chips_html(items: Iterable[tuple], gap_label: bool = True) -> str:
"""items: (text, kind) or (label, text, kind) → one flex row of chips."""
parts = []
for it in items:
if len(it) == 2:
text, kind = it
parts.append(chip_html(text, kind))
else:
label, text, kind = it
parts.append(f'<span class="aim-chip aim-chip-{esc(kind)}"><span class="k">{esc(label)}</span>{esc(text)}</span>')
return '<div class="aim-chips">' + "".join(parts) + "</div>"
def level_chip_html(level: str) -> str:
lv = (level or "info").lower()
kind = {"info": "info", "warn": "warn", "warning": "warn", "error": "error",
"critical": "error"}.get(lv, "info")
return chip_html(lv, kind)
def page_header(title: str, subtitle: str = "", right: Optional[str] = None) -> None:
"""H1 + one-line muted subtitle + optional right-aligned pill HTML."""
right_html = f'<div class="aim-right">{right}</div>' if right else ""
st.html(
f'<div class="aim-ph"><div><h1>{esc(title)}</h1>'
f'<div class="aim-sub">{esc(subtitle)}</div></div>{right_html}</div>')
def kpi_row(items: list[dict], small: bool = False) -> None:
"""items: [{label, value, delta?, delta_kind?('up'|'down'|'warn'|'flat'), help?}]"""
tiles = []
for it in items:
label = esc(it.get("label", ""))
val = it.get("value", "—")
val = fmt_int(val) if isinstance(val, (int, float)) and not isinstance(val, bool) else esc(val)
help_ = it.get("help")
help_html = f'<span class="help" title="{esc(help_)}">?</span>' if help_ else ""
delta = it.get("delta")
dk = it.get("delta_kind", "flat")
delta_html = f'<div class="aim-kpi-delta {esc(dk)}">{esc(delta)}</div>' if delta else ""
cls = "aim-kpi-value small" if small else "aim-kpi-value"
tiles.append(f'<div class="aim-kpi"><div class="aim-kpi-label"><span>{label}</span>{help_html}</div>'
f'<div class="{cls}">{val}</div>{delta_html}</div>')
st.html('<div class="aim-kpis">' + "".join(tiles) + "</div>")
def empty_state(icon: str, title: str, hint: str = "") -> None:
st.html(f'<div class="aim-empty"><div class="ico">{esc(icon)}</div>'
f'<div class="ttl">{esc(title)}</div><div class="hint">{esc(hint)}</div></div>')
def card_title(title: str, sub: str = "", right_html: str = "") -> None:
"""Small uppercase card heading (use inside st.container(border=True))."""
sub_html = f'<span class="aim-card-sub">{esc(sub)}</span>' if sub else ""
st.html(f'<div class="aim-card-title"><span>{esc(title)} {sub_html}</span>{right_html}</div>')
def _is_traceback(msg: str) -> bool:
return ("Traceback" in msg) or ("\n" in msg.strip())
def feed_html(rows: Iterable[dict], empty: str = "No events yet.") -> str:
"""rows: dicts with ts, level, node, message → compact event feed."""
out = []
for r in rows:
msg = str(r.get("message") or "")
if _is_traceback(msg):
head, _, tail = msg.partition("\n")
body = esc(head) + (f"<pre>{esc(tail)}</pre>" if tail.strip() else "")
else:
body = esc(msg)
ts = r.get("ts")
out.append(
f'<div class="aim-feed-row"><div class="lvl">{level_chip_html(r.get("level"))}</div>'
f'<div class="node">{esc(r.get("node") or "")}</div>'
f'<div class="msg">{body}</div>'
f'<div class="ts" title="{esc(fmt_ts(ts, "%Y-%m-%d %H:%M:%S UTC"))}">{esc(rel_time(ts))}'
f'<span class="abs">{esc(fmt_ts(ts, "%b %d, %H:%M"))}</span></div></div>')
if not out:
return f'<div class="aim-feed"><div class="aim-feed-empty">{esc(empty)}</div></div>'
return '<div class="aim-feed">' + "".join(out) + "</div>"
def feed(rows: Iterable[dict], empty: str = "No events yet.") -> None:
st.html(feed_html(rows, empty))
def run_status_kind(status) -> str:
s = str(status or "").lower()
return {"ok": "ok", "running": "running", "stale": "stale", "crashed": "crashed",
"error": "error", "timeout": "crashed"}.get(s, "idle")
def status_cell_style(status) -> str:
"""pandas Styler CSS for a run/level status cell (word + color, never color alone)."""
s = str(status or "").lower()
if s in ("ok", "ingested", "info", "good"):
return "background-color: rgba(12,163,12,.14); color: #0a6a0a; font-weight: 600;"
if s in ("running",):
return "background-color: rgba(42,120,214,.14); color: #1b5cab; font-weight: 600;"
if s in ("stale", "warn", "warning") or s.startswith("skipped"):
return "background-color: rgba(250,178,25,.22); color: #7a5200; font-weight: 600;"
if s in ("crashed", "error", "failed", "critical", "timeout"):
return "background-color: rgba(208,59,59,.14); color: #9c1f1f; font-weight: 600;"
if s.startswith("flag"):
return "background-color: rgba(250,178,25,.22); color: #7a5200; font-weight: 600;"
return ""
def brand_row(db_name: str, extra: str = "") -> None:
st.html(
'<div class="aim-brand"><div class="aim-wordmark">AIM for Composites '
'<span>· Autonomous Ingestion Agent</span></div>'
f'<div class="aim-env">database: <code>{esc(db_name or "?")}</code> · read-only for visitors{esc(extra)}</div></div>')
def footer() -> None:
st.html(
'<div class="aim-footer">'
'<span>AIM for Composites · Autonomous Ingestion Agent</span>'
'<a href="https://huggingface.co/spaces/aim4composites/MaterialsDatabase" target="_blank" rel="noopener">MaterialsDatabase Space</a>'
'<a href="https://github.com/AIM-for-Composites/InDesDatabaseTool" target="_blank" rel="noopener">GitHub · AIM-for-Composites/InDesDatabaseTool</a>'
'<span>Postgres · Streamlit · LangGraph · Gemini</span>'
'</div>')
def now_utc() -> datetime:
return datetime.now(timezone.utc)