factoryflow / src /demo /components.py
oabolade23's picture
FactoryFlow demo — initial submission
8ad9950
"""Reusable Gradio components and small formatting helpers for the demo UI."""
from __future__ import annotations
from collections import deque
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
import pandas as pd
HISTORY_WINDOWS = 60 # last N polled points shown in the chart
@dataclass
class DemoState:
score_points: deque[tuple[int, float, float]] = field(
default_factory=lambda: deque(maxlen=HISTORY_WINDOWS)
) # (tick, score, rul_hours)
tick: int = 0
agent_log: list[str] = field(default_factory=list)
last_state_label: str = "normal"
last_dominant_hz: float = 0.0
procurement: dict[str, Any] | None = None
auth: dict[str, Any] | None = None
payment: dict[str, Any] | None = None
def append_score(self, score: float, rul_hours: float) -> None:
self.tick += 1
self.score_points.append((self.tick, score, rul_hours))
def score_dataframe(self) -> pd.DataFrame:
if not self.score_points:
return pd.DataFrame({"tick": [], "anomaly_score": []})
ticks, scores, _ = zip(*self.score_points)
return pd.DataFrame({"tick": list(ticks), "anomaly_score": list(scores)})
def log(self, msg: str) -> None:
ts = datetime.now(timezone.utc).strftime("%H:%M:%S")
self.agent_log.append(f"[{ts}] {msg}")
if len(self.agent_log) > 200:
del self.agent_log[: len(self.agent_log) - 200]
def log_text(self) -> str:
return "\n".join(self.agent_log[-40:])
def format_gauge(score: float, rul_hours: float, state_label: str, dominant_hz: float) -> str:
bar_len = 24
filled = int(round(score * bar_len))
bar = "█" * filled + "░" * (bar_len - filled)
severity = "NORMAL"
if score >= 0.85:
severity = "IMMINENT FAILURE"
elif score >= 0.75:
severity = "ACTION REQUIRED"
elif score >= 0.5:
severity = "DEGRADING"
return (
f"### Live Inference\n"
f"```\n"
f"score {score:0.3f} [{bar}]\n"
f"rul {rul_hours:0.1f} h\n"
f"state {state_label}\n"
f"dom_hz {dominant_hz:0.1f}\n"
f"status {severity}\n"
f"```"
)
def format_supplier_card(state: DemoState) -> str:
proc = state.procurement
auth = state.auth
pay = state.payment
if proc is None:
return "_No procurement cycle has been run yet._"
if not proc.get("selected_supplier"):
return f"**No procurement action.** Reason: `{proc.get('reason', 'not specified')}`"
lines = [
"### Procurement",
f"- **Supplier:** {proc.get('selected_supplier')}",
f"- **SKU:** `{proc.get('part_sku')}`",
f"- **Price:** ${float(proc.get('unit_price_usd', 0.0)):.2f}",
f"- **Delivery:** {proc.get('delivery_days')} days",
f"- **Stock:** {proc.get('stock_status')}",
f"- **URL:** {proc.get('purchase_url')}",
]
if auth:
lines.append("")
lines.append("### Authorization (Proxlock)")
lines.append(f"- **Authorized:** {auth.get('authorized')}")
lines.append(f"- **Approver:** `{auth.get('approver') or '—'}`")
lines.append(f"- **Reason:** {auth.get('reason')}")
if pay:
lines.append("")
lines.append("### Payment (X402)")
lines.append(f"- **Status:** {pay.get('status')}")
lines.append(f"- **Transaction:** `{pay.get('transaction_id')}`")
lines.append(f"- **Amount:** ${float(pay.get('amount_usd', 0.0)):.2f}")
if pay.get("receipt_url"):
lines.append(f"- **Receipt:** {pay.get('receipt_url')}")
return "\n".join(lines)