cep-terminal / app.py
Bitsage
fix: standalone download bar below report (outside Gradio control), gradient button
4d38797
import json
import math
import time
import hashlib
from pathlib import Path
from datetime import datetime, timezone
from typing import Any, Dict, List, Tuple
import gradio as gr
import requests
from huggingface_hub import HfApi, hf_hub_download
# -----------------------------------------------------------------------------
# CROVIA — CEP TERMINAL v2: EVIDENCE MACHINE
# World's first AI forensic evidence console
# Temporal proof + cryptographic anchoring + regulatory mapping + citation
# -----------------------------------------------------------------------------
CEP_DATASET_ID = "Crovia/cep-capsules"
REGISTRY_URL = "https://registry.croviatrust.com"
OPEN_EVIDENCE_MODE = True
# --- Caches ---
_CACHE = {
"tpa": {"ts": 0.0, "data": None},
"lineage": {"ts": 0.0, "data": None},
"outreach": {"ts": 0.0, "data": None},
"capsules": {"ts": 0.0, "data": []},
}
_TTL = 300 # 5 min
def _now():
return time.time()
def _nowz():
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def _sha256_hex(b: bytes) -> str:
return hashlib.sha256(b).hexdigest()
def _canonical_json(obj):
return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def _canonical_hash(obj):
return _sha256_hex(_canonical_json(obj).encode("utf-8"))
# --- Registry Data Fetchers ---
def _fetch_cached(key, url):
now = _now()
if now - _CACHE[key]["ts"] < _TTL and _CACHE[key]["data"] is not None:
return _CACHE[key]["data"]
try:
resp = requests.get(url, timeout=8)
data = resp.json()
_CACHE[key]["ts"] = now
_CACHE[key]["data"] = data
return data
except Exception:
return _CACHE[key]["data"] or {}
def fetch_tpa():
return _fetch_cached("tpa", f"{REGISTRY_URL}/registry/data/tpa_latest.json")
def fetch_lineage():
return _fetch_cached("lineage", f"{REGISTRY_URL}/registry/data/lineage_graph.json")
def fetch_outreach():
return _fetch_cached("outreach", f"{REGISTRY_URL}/registry/data/outreach_status.json")
def _list_capsules() -> List[str]:
now = _now()
if (now - _CACHE["capsules"]["ts"]) < _TTL and _CACHE["capsules"]["data"]:
return _CACHE["capsules"]["data"]
items = []
try:
files = HfApi().list_repo_files(repo_id=CEP_DATASET_ID, repo_type="dataset")
for f in files:
if f.endswith(".json") and f.startswith("CEP-") and not f.lower().endswith("index.json"):
items.append(Path(f).stem)
items = sorted(set(items))[:350]
except Exception:
items = []
_CACHE["capsules"]["ts"] = now
_CACHE["capsules"]["data"] = items
return items
# --- Evidence Computation ---
def get_model_tpa(model_id: str) -> dict:
"""Get TPA data for a specific model."""
tpa = fetch_tpa()
tpas = tpa.get("tpas", [])
for t in tpas:
if t.get("model_id", "").lower() == model_id.lower():
return t
return {}
def get_model_lineage(model_id: str) -> dict:
"""Get lineage node for a model."""
lg = fetch_lineage()
for node in lg.get("nodes", []):
if node.get("id", "").lower() == model_id.lower():
return node
return {}
def get_model_outreach(model_id: str) -> dict:
"""Get outreach status for a model's org."""
org = model_id.split("/")[0] if "/" in model_id else ""
if not org:
return {}
outreach = fetch_outreach()
entries = outreach if isinstance(outreach, list) else outreach.get("entries", outreach.get("organizations", []))
if isinstance(entries, list):
for e in entries:
oid = e.get("org", e.get("organization", ""))
if oid.lower() == org.lower():
return e
return {}
def compute_evidence_strength(tpa_entry: dict) -> dict:
"""Compute evidence strength from NEC# observations."""
obs = tpa_entry.get("observations", [])
if not obs:
return {"score": 0, "total": 0, "present": 0, "absent": 0, "critical_gaps": 0}
total = len(obs)
present = sum(1 for o in obs if o.get("is_present"))
absent = total - present
critical = sum(1 for o in obs if not o.get("is_present") and o.get("severity_label") == "CRITICAL")
score = round((present / total) * 100, 1) if total > 0 else 0
return {
"score": score,
"total": total,
"present": present,
"absent": absent,
"critical_gaps": critical,
}
def compute_peer_context(model_id: str) -> dict:
"""Compute peer comparison context."""
tpa = fetch_tpa()
tpas = tpa.get("tpas", [])
org = model_id.split("/")[0] if "/" in model_id else ""
all_scores = []
org_scores = []
for t in tpas:
obs = t.get("observations", [])
if not obs:
continue
s = sum(1 for o in obs if o.get("is_present")) / len(obs) * 100
all_scores.append(s)
tid = t.get("model_id", "")
if "/" in tid and tid.split("/")[0].lower() == org.lower():
org_scores.append(s)
return {
"industry_avg": round(sum(all_scores) / len(all_scores), 1) if all_scores else 0,
"org_avg": round(sum(org_scores) / len(org_scores), 1) if org_scores else 0,
"total_models": len(all_scores),
"org_models": len(org_scores),
}
# --- Main Evidence Function ---
def generate_evidence(model_id: str) -> str:
"""Generate complete forensic evidence package for a model."""
model_id = (model_id or "").split("|")[0].strip()
if not model_id:
return ""
tpa_entry = get_model_tpa(model_id)
lineage = get_model_lineage(model_id)
outreach = get_model_outreach(model_id)
strength = compute_evidence_strength(tpa_entry)
peer = compute_peer_context(model_id)
tpa_data = fetch_tpa()
chain_height = tpa_data.get("chain_height", 0)
# Build observations detail
obs_detail = []
jurisdictions = set()
for o in tpa_entry.get("observations", []):
obs_detail.append({
"nec_id": o.get("necessity_id", ""),
"name": o.get("necessity_name", ""),
"present": o.get("is_present", False),
"severity": o.get("severity_label", ""),
"jurisdictions": o.get("jurisdictions_affected", 0),
"jurisdiction_hints": o.get("jurisdictions_hint", []),
"commitment_x": o.get("commitment_x", ""),
"commitment_y": o.get("commitment_y", ""),
})
for j in o.get("jurisdictions_hint", []):
jurisdictions.add(j)
# Trust level
if strength["score"] >= 80:
trust = "GREEN"
elif strength["score"] >= 40:
trust = "YELLOW"
else:
trust = "RED"
org = model_id.split("/")[0] if "/" in model_id else "unknown"
# Citation text
citation = (
f"As of {_nowz()}, model {model_id} published by {org} "
f"has been monitored by the Crovia Temporal Proof Registry. "
f"{strength['absent']}/{strength['total']} NEC# documentation requirements "
f"remain absent ({strength['critical_gaps']} critical). "
f"Cryptographic anchor: chain height {chain_height}, "
f"TPA-ID {tpa_entry.get('tpa_id', 'N/A')}. "
f"Source: registry.croviatrust.com"
)
payload = {
"model_id": model_id,
"org": org,
"timestamp": _nowz(),
"found": bool(tpa_entry),
"trust_level": trust if tpa_entry else "UNKNOWN",
"tpa_id": tpa_entry.get("tpa_id", ""),
"chain_height": chain_height,
"strength": strength,
"peer": peer,
"observations": obs_detail,
"jurisdictions": sorted(jurisdictions),
"lineage": {
"compliance_score": lineage.get("compliance_score"),
"severity": lineage.get("severity"),
"nec_absent": lineage.get("nec_absent"),
"card_length": lineage.get("card_length"),
} if lineage else None,
"outreach": {
"status": outreach.get("status", outreach.get("outreach_status", "unknown")),
"contacted": outreach.get("contacted", outreach.get("discussion_sent", False)),
"response": outreach.get("response", outreach.get("response_received", False)),
} if outreach else None,
"citation": citation,
}
return json.dumps(payload, ensure_ascii=False)
# --- Startup data ---
def get_targets_list() -> str:
"""Get list of all monitored targets for autocomplete."""
tpa = fetch_tpa()
tpas = tpa.get("tpas", [])
targets = []
for t in tpas:
mid = t.get("model_id", "")
if mid:
obs = t.get("observations", [])
absent = sum(1 for o in obs if not o.get("is_present"))
targets.append({"id": mid, "gaps": absent})
targets.sort(key=lambda x: -x["gaps"])
return json.dumps(targets, ensure_ascii=False)
def get_registry_stats() -> str:
"""Get registry-wide stats for the header."""
tpa = fetch_tpa()
tpas = tpa.get("tpas", [])
lg = fetch_lineage()
models = set()
orgs = set()
total_gaps = 0
for t in tpas:
mid = t.get("model_id", "")
models.add(mid)
if "/" in mid:
orgs.add(mid.split("/")[0])
for o in t.get("observations", []):
if not o.get("is_present"):
total_gaps += 1
return json.dumps({
"models": len(models),
"orgs": len(orgs),
"chain_height": tpa.get("chain_height", 0),
"total_gaps": total_gaps,
"lineage_nodes": len(lg.get("nodes", [])),
}, ensure_ascii=False)
# --- Live Capsule Generator ---
def generate_live_capsule(model_id: str) -> str:
"""Generate a live evidence capsule from TPA registry data."""
try:
model_id = (model_id or "").split("|")[0].strip()
if not model_id:
return ""
tpa_entry = get_model_tpa(model_id)
if not tpa_entry:
return json.dumps({"error": f"No TPA data found for {model_id}"})
lineage = get_model_lineage(model_id)
outreach = get_model_outreach(model_id)
strength = compute_evidence_strength(tpa_entry)
tpa_data = fetch_tpa()
chain_height = tpa_data.get("chain_height", 0)
org = model_id.split("/")[0] if "/" in model_id else "unknown"
ts = _nowz()
# Build observations with full commitment data
observations = []
for o in tpa_entry.get("observations", []):
observations.append({
"necessity_id": o.get("necessity_id", ""),
"necessity_name": o.get("necessity_name", ""),
"is_present": o.get("is_present", False),
"severity_label": o.get("severity_label", ""),
"severity_weight": o.get("severity_weight", 0),
"jurisdictions_affected": o.get("jurisdictions_affected", 0),
"commitment": {
"x": o.get("commitment_x", ""),
"y": o.get("commitment_y", ""),
},
})
# Compute capsule content hash
capsule_content = {
"model_id": model_id,
"tpa_id": tpa_entry.get("tpa_id", ""),
"observations": observations,
"chain_height": chain_height,
"generated": ts,
}
content_hash = _canonical_hash(capsule_content)
# Build CRC-1-like capsule
capsule = {
"type": "live_capsule",
"schema": "crovia_evidence_capsule.v2",
"capsule_id": f"CEP-LIVE-{content_hash[:8].upper()}",
"generated": ts,
"mode": "OPEN_EVIDENCE",
"model": {
"model_id": model_id,
"organization": org,
"source": "huggingface.co",
},
"evidence": {
"tpa_id": tpa_entry.get("tpa_id", ""),
"chain_height": chain_height,
"anchor_hash": tpa_entry.get("anchor_hash", ""),
"epoch": tpa_entry.get("epoch", ""),
"observation_count": len(observations),
"present_count": strength["present"],
"absent_count": strength["absent"],
"critical_gaps": strength["critical_gaps"],
"evidence_strength": strength["score"],
"observations": observations,
},
"cryptographic_anchors": {
"type": "pedersen_secp256k1",
"commitment_count": sum(1 for o in observations if o["commitment"]["x"]),
"chain_type": "sha256_temporal",
"chain_height": chain_height,
"anchor_hash": tpa_entry.get("anchor_hash", ""),
"merkle_root": tpa_entry.get("merkle_root", ""),
},
"lineage": {
"compliance_score": lineage.get("compliance_score"),
"severity": lineage.get("severity"),
"card_length": lineage.get("card_length"),
} if lineage else None,
"outreach": {
"status": outreach.get("status", outreach.get("outreach_status", "unknown")),
"contacted": outreach.get("contacted", outreach.get("discussion_sent", False)),
} if outreach else None,
"content_hash": content_hash,
"trust_level": "GREEN" if strength["score"] >= 80 else "YELLOW" if strength["score"] >= 40 else "RED",
}
return json.dumps(capsule, ensure_ascii=False)
except Exception as e:
return json.dumps({"error": f"capsule_gen: {str(e)}"})
# --- Capsule Inspector (backward compat) ---
def fetch_capsule(cep_id: str) -> Dict[str, Any]:
path = hf_hub_download(repo_id=CEP_DATASET_ID, filename=f"{cep_id}.json", repo_type="dataset")
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def inspect_capsule(cep_id: str) -> str:
cep_id = (cep_id or "").strip()
if not cep_id:
return json.dumps({"error": "empty"})
try:
cap = fetch_capsule(cep_id)
schema = cap.get("schema", "unknown")
model = cap.get("model", {})
model_id = model.get("model_id", "unknown") if isinstance(model, dict) else "unknown"
evidence = cap.get("evidence", {}) if isinstance(cap.get("evidence"), dict) else {}
meta = cap.get("meta", {}) if isinstance(cap.get("meta"), dict) else {}
hashchain_root = meta.get("hashchain_sha256", "")
sig_present = "signature" in cap
cap_sha = _sha256_hex(_canonical_json(cap).encode("utf-8"))
return json.dumps({
"type": "capsule",
"cep_id": cep_id,
"schema": schema,
"model_id": model_id,
"evidence_nodes": len(evidence),
"signature": sig_present,
"hashchain": bool(hashchain_root),
"hashchain_short": hashchain_root[:16] if hashchain_root else "",
"capsule_sha256": cap_sha,
"evidence_keys": list(evidence.keys())[:20],
}, ensure_ascii=False)
except Exception as e:
return json.dumps({"error": f"{type(e).__name__}: {e}"})
# =============================================================================
# CSS
# =============================================================================
CSS = """
<style>
:root {
--bg: #030712;
--bg1: #0a0f1a;
--bg2: #111827;
--surface: #1f2937;
--border: rgba(255,255,255,0.08);
--border-h: rgba(255,255,255,0.16);
--text: #f9fafb;
--text2: #d1d5db;
--text3: #9ca3af;
--cyan: #22d3ee;
--blue: #3b82f6;
--violet: #8b5cf6;
--green: #22c55e;
--amber: #f59e0b;
--red: #ef4444;
--rose: #f43f5e;
--glow-cyan: 0 0 30px rgba(34,211,238,0.3);
--glow-violet: 0 0 30px rgba(139,92,246,0.3);
--radius: 16px;
}
html, body, #root, .gradio-container {
background: var(--bg) !important;
}
.gradio-container {
max-width: 1400px !important;
margin: 0 auto !important;
padding: 0 16px 60px !important;
font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
color: var(--text) !important;
}
footer { display: none !important; }
.gradio-container .wrap { border: 0 !important; }
.gradio-container .prose { max-width: none !important; }
.gradio-container, .gradio-container * {
opacity: 1 !important;
filter: none !important;
mix-blend-mode: normal !important;
visibility: visible !important;
}
/* ── TOPBAR ── */
.ev-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 0;
border-bottom: 1px solid var(--border);
margin-bottom: 20px;
}
.ev-brand {
display: flex;
align-items: center;
gap: 12px;
}
.ev-logo {
width: 38px; height: 38px;
border-radius: 10px;
background: linear-gradient(135deg, var(--cyan), var(--violet));
display: flex; align-items: center; justify-content: center;
font-weight: 900; font-size: 16px; color: #fff;
box-shadow: var(--glow-cyan);
}
.ev-brand-text {
font-size: 13px;
letter-spacing: 0.25em;
text-transform: uppercase;
font-weight: 700;
color: var(--text) !important;
}
.ev-brand-sub {
font-size: 10px;
letter-spacing: 0.15em;
color: var(--text3) !important;
text-transform: uppercase;
}
.ev-live {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
color: var(--green) !important;
letter-spacing: 0.1em;
}
.ev-live-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 12px rgba(34,197,94,0.6);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* ── COMMAND BAR ── */
.ev-command {
position: relative;
margin-bottom: 20px;
}
.ev-command-input {
width: 100%;
height: 56px;
background: var(--bg1);
border: 1px solid var(--border-h);
border-radius: 14px;
padding: 0 20px 0 52px;
font-size: 16px;
color: var(--text) !important;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
font-family: 'JetBrains Mono', ui-monospace, monospace;
}
.ev-command-input:focus {
border-color: var(--cyan);
box-shadow: var(--glow-cyan);
}
.ev-command-input::placeholder {
color: var(--text3);
font-family: 'Inter', sans-serif;
}
.ev-command-icon {
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
opacity: 0.5;
}
.ev-command-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: linear-gradient(135deg, var(--cyan), var(--blue));
border: none;
border-radius: 10px;
padding: 10px 24px;
color: #fff;
font-weight: 700;
font-size: 13px;
cursor: pointer;
letter-spacing: 0.08em;
transition: transform 0.15s;
}
.ev-command-btn:hover { transform: translateY(-50%) scale(1.03); }
.ev-command-btn:active { transform: translateY(-50%) scale(0.97); }
/* Quick targets */
.ev-quick {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-top: 10px;
}
.ev-quick-btn {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 5px 12px;
color: var(--text3) !important;
font-size: 11px;
cursor: pointer;
transition: all 0.15s;
font-family: 'JetBrains Mono', monospace;
}
.ev-quick-btn:hover {
border-color: var(--cyan);
color: var(--cyan) !important;
background: rgba(34,211,238,0.06);
}
/* ── SIGNAL STRIP ── */
.ev-signals {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.ev-signals { grid-template-columns: repeat(2, 1fr); }
}
.ev-signal {
background: var(--bg1);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
text-align: center;
transition: border-color 0.2s;
}
.ev-signal-val {
font-size: 28px;
font-weight: 800;
font-family: 'JetBrains Mono', monospace;
line-height: 1.1;
margin-bottom: 4px;
}
.ev-signal-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--text3) !important;
}
.ev-signal.trust-green { border-color: var(--green); }
.ev-signal.trust-green .ev-signal-val { color: var(--green) !important; }
.ev-signal.trust-yellow { border-color: var(--amber); }
.ev-signal.trust-yellow .ev-signal-val { color: var(--amber) !important; }
.ev-signal.trust-red { border-color: var(--red); }
.ev-signal.trust-red .ev-signal-val { color: var(--red) !important; }
.ev-signal.trust-unknown { border-color: var(--border); }
.ev-signal.trust-unknown .ev-signal-val { color: var(--text3) !important; }
/* ── MAIN GRID ── */
.ev-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 980px) { .ev-grid { grid-template-columns: 1fr; } }
.ev-panel {
background: var(--bg1);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.ev-panel-header {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.ev-panel-title {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--text2) !important;
}
.ev-panel-badge {
font-size: 10px;
padding: 2px 8px;
border-radius: 6px;
font-weight: 600;
letter-spacing: 0.05em;
}
.ev-panel-body {
padding: 16px;
}
/* ── NEC# GRID ── */
.nec-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
@media (max-width: 768px) { .nec-grid { grid-template-columns: repeat(4, 1fr); } }
.nec-cell {
border-radius: 10px;
padding: 10px 8px;
text-align: center;
border: 1px solid var(--border);
transition: all 0.2s;
cursor: default;
position: relative;
}
.nec-cell.present {
background: rgba(34,197,94,0.08);
border-color: rgba(34,197,94,0.3);
}
.nec-cell.absent {
background: rgba(239,68,68,0.08);
border-color: rgba(239,68,68,0.3);
}
.nec-cell.critical {
background: rgba(239,68,68,0.14);
border-color: rgba(239,68,68,0.5);
box-shadow: 0 0 12px rgba(239,68,68,0.15);
}
.nec-id {
font-size: 12px;
font-weight: 800;
font-family: 'JetBrains Mono', monospace;
}
.nec-cell.present .nec-id { color: var(--green) !important; }
.nec-cell.absent .nec-id { color: var(--red) !important; }
.nec-cell.critical .nec-id { color: var(--rose) !important; }
.nec-status {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-top: 2px;
color: var(--text3) !important;
}
.nec-severity {
font-size: 8px;
margin-top: 2px;
color: var(--text3) !important;
opacity: 0.7;
}
/* ── EVIDENCE STRENGTH METER ── */
.ev-meter {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
}
.ev-meter-bar-bg {
flex: 1;
height: 12px;
background: var(--bg2);
border-radius: 6px;
overflow: hidden;
border: 1px solid var(--border);
}
.ev-meter-bar {
height: 100%;
border-radius: 6px;
transition: width 0.8s ease;
}
.ev-meter-val {
font-size: 24px;
font-weight: 800;
font-family: 'JetBrains Mono', monospace;
min-width: 60px;
text-align: right;
}
/* ── CONSTELLATION SVG ── */
svg.ev-constellation {
width: 100%;
min-height: 380px;
border-radius: 12px;
background: radial-gradient(ellipse at 30% 20%, rgba(34,211,238,0.06), transparent 60%),
radial-gradient(ellipse at 70% 80%, rgba(139,92,246,0.06), transparent 60%),
var(--bg);
}
/* ── FORENSIC REPORT ── */
.ev-report {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 12px;
line-height: 1.7;
color: var(--text2) !important;
white-space: pre-wrap;
min-height: 200px;
padding: 16px;
background: var(--bg);
border-radius: 12px;
border: 1px solid var(--border);
}
/* ── CITATION BOX ── */
.ev-citation {
position: relative;
background: rgba(34,211,238,0.04);
border: 1px solid rgba(34,211,238,0.2);
border-radius: 12px;
padding: 16px 16px 16px 16px;
}
.ev-citation-text {
font-size: 13px;
line-height: 1.7;
color: var(--text2) !important;
font-style: italic;
}
.ev-citation-copy {
position: absolute;
top: 12px;
right: 12px;
background: var(--cyan);
border: none;
border-radius: 8px;
padding: 6px 14px;
color: var(--bg) !important;
font-weight: 700;
font-size: 11px;
cursor: pointer;
letter-spacing: 0.05em;
}
/* ── JURISDICTIONS ── */
.ev-juris {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.ev-juris-tag {
background: rgba(139,92,246,0.1);
border: 1px solid rgba(139,92,246,0.25);
border-radius: 8px;
padding: 4px 10px;
font-size: 11px;
color: var(--violet) !important;
}
/* ── OUTREACH STATUS ── */
.ev-outreach {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--bg2);
font-size: 12px;
}
.ev-outreach-dot {
width: 10px; height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
/* ── FULL WIDTH PANEL ── */
.ev-full { grid-column: 1 / -1; }
/* ── CAPSULE TAB ── */
.ev-tabs {
display: flex;
gap: 4px;
margin-bottom: 16px;
background: var(--bg1);
border-radius: 12px;
padding: 4px;
border: 1px solid var(--border);
width: fit-content;
}
.ev-tab {
padding: 8px 20px;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
cursor: pointer;
color: var(--text3) !important;
border: none;
background: transparent;
transition: all 0.15s;
}
.ev-tab.active {
background: var(--bg2);
color: var(--text) !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.ev-tab:hover:not(.active) {
color: var(--text2) !important;
}
/* ── ACTIVE MODEL BANNER ── */
.ev-active-model {
display: none;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding: 10px 16px;
background: rgba(34,211,238,0.06);
border: 1px solid rgba(34,211,238,0.2);
border-radius: 10px;
}
.ev-active-model-id {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
font-weight: 700;
color: var(--cyan) !important;
flex: 1;
}
.ev-new-search {
background: transparent;
border: 1px solid var(--border-h);
border-radius: 8px;
padding: 6px 16px;
color: var(--text2) !important;
font-size: 11px;
font-weight: 600;
cursor: pointer;
letter-spacing: 0.05em;
transition: all 0.15s;
}
.ev-new-search:hover {
border-color: var(--cyan);
color: var(--cyan) !important;
}
.ev-download-btn {
background: linear-gradient(135deg, var(--violet), var(--blue));
border: none;
border-radius: 8px;
padding: 6px 16px;
color: #fff !important;
font-size: 11px;
font-weight: 700;
cursor: pointer;
letter-spacing: 0.05em;
transition: transform 0.15s;
}
.ev-download-btn:hover { transform: scale(1.03); }
/* ── DOWNLOAD BAR (standalone) ── */
.ev-download-bar {
display: none;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
margin-bottom: 16px;
background: linear-gradient(135deg, rgba(139,92,246,0.08), rgba(59,130,246,0.08));
border: 1px solid rgba(139,92,246,0.25);
border-radius: 12px;
}
.ev-download-bar-text {
font-size: 12px;
color: var(--text2) !important;
}
.ev-download-bar-text strong {
color: var(--violet) !important;
}
.ev-download-bar-btn {
background: linear-gradient(135deg, var(--violet), var(--blue));
border: none;
border-radius: 10px;
padding: 10px 28px;
color: #fff !important;
font-size: 13px;
font-weight: 700;
cursor: pointer;
letter-spacing: 0.08em;
transition: transform 0.15s;
white-space: nowrap;
}
.ev-download-bar-btn:hover { transform: scale(1.03); }
/* ── MODEL BROWSER ── */
.ev-model-browser {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
margin-bottom: 6px;
}
.ev-model-browser label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--text3) !important;
white-space: nowrap;
}
.ev-model-select {
flex: 1;
height: 40px;
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 0 14px;
font-size: 12px;
color: var(--text2) !important;
font-family: 'JetBrains Mono', monospace;
cursor: pointer;
outline: none;
transition: border-color 0.2s;
}
.ev-model-select:hover,
.ev-model-select:focus {
border-color: var(--cyan);
}
.ev-model-select option {
background: #0f172a;
color: #f9fafb;
}
.ev-model-select optgroup {
background: #111827;
color: #22d3ee;
font-weight: 700;
}
/* ── HIDDEN (CSS, not Gradio visible=False) ── */
.ev-css-hidden { display: none !important; position: absolute !important; }
/* ── DISCLAIMER ── */
.ev-disclaimer {
margin-top: 24px;
padding: 16px;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--bg1);
font-size: 11px;
line-height: 1.7;
color: var(--text3) !important;
text-align: center;
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&family=JetBrains+Mono:wght@400;600;700;800&display=swap" rel="stylesheet">
"""
# =============================================================================
# HTML
# =============================================================================
UI_HTML = """
<!-- TOPBAR -->
<div class="ev-topbar">
<div class="ev-brand">
<div class="ev-logo">C</div>
<div>
<div class="ev-brand-text">Crovia · Evidence Machine</div>
<div class="ev-brand-sub">AI Forensic Evidence Console</div>
</div>
</div>
<div class="ev-live">
<div class="ev-live-dot"></div>
<span>LIVE · <span id="ev-stats-models">—</span> models · chain:<span id="ev-stats-chain">—</span></span>
</div>
</div>
<!-- TABS -->
<div class="ev-tabs">
<button class="ev-tab active" id="tab-evidence" onclick="switchTab('evidence')">Evidence Machine</button>
<button class="ev-tab" id="tab-capsules" onclick="switchTab('capsules')">CEP Capsules</button>
</div>
<!-- TAB: EVIDENCE MACHINE -->
<div id="panel-evidence">
<!-- COMMAND BAR -->
<div class="ev-command">
<span class="ev-command-icon">⌘</span>
<input class="ev-command-input" id="ev-search" type="text"
placeholder="Enter model ID — e.g. google/gemma-3-12b-it"
list="ev-model-list" autocomplete="off" />
<datalist id="ev-model-list"></datalist>
<button class="ev-command-btn" id="ev-go">INVESTIGATE</button>
</div>
<div class="ev-quick" id="ev-quick-targets"></div>
<!-- MODEL BROWSER -->
<div class="ev-model-browser">
<label>Browse all models:</label>
<select class="ev-model-select" id="ev-model-select">
<option value="" disabled selected>Select from 200+ monitored models...</option>
</select>
</div>
<!-- ACTIVE MODEL BANNER (shown after investigation) -->
<div class="ev-active-model" id="ev-active-model">
<span style="font-size:11px;color:var(--text3);letter-spacing:0.1em;">INVESTIGATING:</span>
<span class="ev-active-model-id" id="ev-active-id">—</span>
<button class="ev-download-btn" id="ev-download" onclick="downloadEvidence()">&#11015; DOWNLOAD JSON</button>
<button class="ev-new-search" id="ev-new-search" onclick="resetSearch()">&#8592; NEW SEARCH</button>
</div>
<!-- SIGNAL STRIP -->
<div class="ev-signals" id="ev-signals">
<div class="ev-signal trust-unknown" id="sig-trust">
<div class="ev-signal-val" id="sig-trust-val">—</div>
<div class="ev-signal-label">Trust Level</div>
</div>
<div class="ev-signal" id="sig-strength">
<div class="ev-signal-val" style="color:var(--cyan)!important" id="sig-strength-val">—</div>
<div class="ev-signal-label">Evidence Strength</div>
</div>
<div class="ev-signal">
<div class="ev-signal-val" style="color:var(--violet)!important" id="sig-chain">—</div>
<div class="ev-signal-label">Chain Height</div>
</div>
<div class="ev-signal">
<div class="ev-signal-val" style="color:var(--amber)!important" id="sig-gaps">—</div>
<div class="ev-signal-label">NEC# Gaps</div>
</div>
<div class="ev-signal">
<div class="ev-signal-val" style="color:var(--blue)!important" id="sig-juris">—</div>
<div class="ev-signal-label">Jurisdictions</div>
</div>
</div>
<!-- EVIDENCE STRENGTH METER -->
<div class="ev-meter" id="ev-meter" style="display:none;">
<div class="ev-meter-val" id="meter-val" style="color:var(--red)!important">0%</div>
<div class="ev-meter-bar-bg">
<div class="ev-meter-bar" id="meter-bar" style="width:0%;background:var(--red);"></div>
</div>
<div style="font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:0.1em;">Evidence Coverage</div>
</div>
<!-- MAIN GRID -->
<div class="ev-grid">
<!-- NEC# Constellation -->
<div class="ev-panel">
<div class="ev-panel-header">
<div class="ev-panel-title">NEC# Constellation</div>
<div class="ev-panel-badge" id="nec-badge" style="background:var(--bg2);color:var(--text3);">—</div>
</div>
<div class="ev-panel-body">
<svg class="ev-constellation" id="ev-constellation" viewBox="0 0 600 380" preserveAspectRatio="xMidYMid meet"></svg>
</div>
</div>
<!-- NEC# Grid -->
<div class="ev-panel">
<div class="ev-panel-header">
<div class="ev-panel-title">NEC# Element Grid</div>
<div class="ev-panel-badge" id="nec-grid-badge" style="background:var(--bg2);color:var(--text3);">20 elements</div>
</div>
<div class="ev-panel-body">
<div class="nec-grid" id="nec-grid"></div>
</div>
</div>
</div>
<!-- FORENSIC REPORT + JURISDICTIONS -->
<div class="ev-grid">
<div class="ev-panel">
<div class="ev-panel-header">
<div class="ev-panel-title">Forensic Report</div>
<div class="ev-panel-badge" style="background:rgba(34,211,238,0.1);color:var(--cyan);">LIVE</div>
</div>
<div class="ev-panel-body">
<pre class="ev-report" id="ev-report">Select a model to generate forensic evidence report.</pre>
</div>
</div>
<div class="ev-panel">
<div class="ev-panel-header">
<div class="ev-panel-title">Jurisdictions & Outreach</div>
</div>
<div class="ev-panel-body">
<div style="margin-bottom:12px;font-size:10px;text-transform:uppercase;letter-spacing:0.15em;color:var(--text3)!important;">Applicable Jurisdictions</div>
<div class="ev-juris" id="ev-juris">
<span style="color:var(--text3);font-size:12px;">—</span>
</div>
<div style="margin:16px 0 8px;font-size:10px;text-transform:uppercase;letter-spacing:0.15em;color:var(--text3)!important;">Outreach Status</div>
<div class="ev-outreach" id="ev-outreach">
<div class="ev-outreach-dot" style="background:var(--text3);"></div>
<span style="color:var(--text3)!important;">Select a model to see outreach status</span>
</div>
<div style="margin:16px 0 8px;font-size:10px;text-transform:uppercase;letter-spacing:0.15em;color:var(--text3)!important;">Peer Context</div>
<div id="ev-peer" style="font-size:12px;color:var(--text3)!important;">—</div>
</div>
</div>
</div>
<!-- CITATION ENGINE -->
<div class="ev-panel ev-full" style="margin-bottom:16px;">
<div class="ev-panel-header">
<div class="ev-panel-title">Citation Engine</div>
<div class="ev-panel-badge" style="background:rgba(34,211,238,0.1);color:var(--cyan);">LEGAL-GRADE</div>
</div>
<div class="ev-panel-body">
<div class="ev-citation" id="ev-citation-box">
<div class="ev-citation-text" id="ev-citation">Select a model to generate a citation-ready evidence statement.</div>
<button class="ev-citation-copy" id="ev-citation-copy" onclick="copyCitation()">COPY</button>
</div>
</div>
</div>
<!-- DOWNLOAD BAR -->
<div class="ev-download-bar" id="ev-download-bar">
<div class="ev-download-bar-text">
<strong>Evidence Package ready</strong> — Download the complete forensic evidence as JSON for legal, journalistic, or audit use.
</div>
<button class="ev-download-bar-btn" onclick="downloadEvidence()">DOWNLOAD EVIDENCE JSON</button>
</div>
</div><!-- end panel-evidence -->
<!-- TAB: CEP CAPSULES -->
<div id="panel-capsules" style="display:none;">
<!-- LIVE EVIDENCE CAPSULES -->
<div style="padding:20px 24px;margin-bottom:20px;background:#0a0f1a;border:2px solid rgba(34,211,238,0.3);border-radius:14px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
<div style="width:12px;height:12px;border-radius:50%;background:#22c55e;box-shadow:0 0 12px rgba(34,197,94,0.6);"></div>
<span style="color:#f9fafb;font-size:18px;font-weight:800;letter-spacing:0.08em;text-transform:uppercase;">LIVE EVIDENCE CAPSULES</span>
<span id="live-capsule-count" style="font-size:14px;color:#22d3ee;font-family:'JetBrains Mono',monospace;font-weight:700;">— models</span>
</div>
<div style="font-size:14px;line-height:1.7;color:#d1d5db;">
Each monitored model has a <strong style="color:#22d3ee;">live evidence capsule</strong> generated from the Temporal Proof Registry.
Contains NEC# observations, Pedersen commitments, chain anchors, and trust assessment.
<strong style="color:#f9fafb;">Select a model below to inspect its capsule.</strong>
</div>
</div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px;padding:0 4px;">
<div style="font-size:14px;font-weight:800;text-transform:uppercase;letter-spacing:0.15em;color:#22d3ee;white-space:nowrap;min-width:130px;">SELECT MODEL</div>
<select class="ev-model-select" id="live-capsule-select" style="flex:1;height:52px;font-size:15px;padding:0 16px;background:#111827;border:2px solid rgba(34,211,238,0.3);color:#f9fafb;border-radius:10px;">
<option value="" disabled selected>Browse all 200+ monitored models...</option>
</select>
</div>
<div class="ev-panel" id="live-capsule-panel" style="border:2px solid rgba(255,255,255,0.1);">
<div class="ev-panel-header" style="padding:16px 20px;">
<div class="ev-panel-title" style="font-size:14px;letter-spacing:0.15em;color:#f9fafb;">Live Capsule Inspector</div>
<div class="ev-panel-badge" id="live-capsule-badge" style="background:rgba(34,197,94,0.15);color:#22c55e;font-size:12px;padding:4px 12px;">LIVE</div>
</div>
<div class="ev-panel-body" style="padding:20px;">
<pre class="ev-report" id="live-capsule-report" style="min-height:350px;font-size:13px;line-height:1.8;color:#d1d5db;">Select a model above to generate and inspect its live evidence capsule.
Each capsule contains:
- schema identifier (crovia_evidence_capsule.v2)
- model metadata (ID, organization, source)
- NEC# observations (20 documentation requirements)
- Pedersen commitments (cryptographic anchors per observation)
- temporal chain data (height, anchor hash, merkle root)
- trust level assessment (GREEN/YELLOW/RED)
- content hash (SHA-256 of capsule content)
Capsules are generated in real-time from the Crovia Temporal Proof Registry.</pre>
</div>
</div>
<div id="live-capsule-download-bar" style="display:none;margin-top:12px;padding:14px 20px;background:linear-gradient(135deg,rgba(34,211,238,0.08),rgba(139,92,246,0.08));border:2px solid rgba(34,211,238,0.25);border-radius:12px;text-align:center;">
<button onclick="downloadLiveCapsule()" style="background:linear-gradient(135deg,#22d3ee,#8b5cf6);color:#030712;border:none;padding:12px 32px;border-radius:8px;font-size:14px;font-weight:800;letter-spacing:0.1em;cursor:pointer;text-transform:uppercase;font-family:'JetBrains Mono',monospace;">DOWNLOAD CAPSULE JSON</button>
</div>
<!-- REFERENCE CAPSULES -->
<div style="margin-top:24px;border-top:2px solid rgba(255,255,255,0.08);padding-top:20px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;cursor:pointer;padding:10px 16px;background:#111827;border-radius:10px;border:1px solid rgba(255,255,255,0.08);" onclick="document.getElementById('ref-capsule-section').style.display = document.getElementById('ref-capsule-section').style.display === 'none' ? 'block' : 'none'; this.querySelector('.ref-arrow').textContent = document.getElementById('ref-capsule-section').style.display === 'none' ? '\u25b8' : '\u25be';">
<div style="display:flex;align-items:center;gap:10px;">
<span class="ref-arrow" style="color:#9ca3af;font-size:16px;">\u25b8</span>
<span style="font-size:13px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;color:#d1d5db;">Reference Capsules (CRC-1 Format)</span>
</div>
<span style="font-size:12px;color:#9ca3af;font-family:'JetBrains Mono',monospace;" id="ref-capsule-count">— capsules</span>
</div>
<div id="ref-capsule-section" style="display:none;">
<div style="padding:14px 18px;margin-bottom:14px;background:#0a0f1a;border:1px solid rgba(255,255,255,0.1);border-radius:10px;font-size:13px;line-height:1.6;color:#9ca3af;">
Static reference capsules from <a href="https://huggingface.co/datasets/Crovia/cep-capsules" target="_blank" style="color:#22d3ee;font-weight:600;">Crovia/cep-capsules</a>.
These are offline-verifiable CRC-1 evidence envelopes with schema, fingerprint, receipts, hashchain, and signatures.
</div>
<div class="ev-command">
<span class="ev-command-icon">📦</span>
<select class="ev-command-input" id="capsule-select" style="padding-left:52px;cursor:pointer;font-size:14px;">
<option value="" disabled selected>Select a reference capsule...</option>
</select>
</div>
<div class="ev-panel" style="margin-top:14px;">
<div class="ev-panel-header">
<div class="ev-panel-title" style="font-size:13px;color:#d1d5db;">Reference Capsule Inspector</div>
<div class="ev-panel-badge" style="background:#1f2937;color:#9ca3af;font-size:11px;">CRC-1</div>
</div>
<div class="ev-panel-body">
<pre class="ev-report" id="capsule-report" style="min-height:200px;font-size:13px;color:#d1d5db;">Select a reference capsule to inspect.</pre>
</div>
</div>
</div>
</div>
</div>
<!-- DISCLAIMER -->
<div class="ev-disclaimer">
CROVIA EVIDENCE MACHINE · Observation, not judgment. All data derived from publicly observable artifacts.
No model audit. No legal claim. Presence/absence only. Cryptographic commitments anchor observations immutably.
<br>Source: <a href="https://registry.croviatrust.com" target="_blank" style="color:var(--cyan);">registry.croviatrust.com</a>
· <a href="https://huggingface.co/datasets/Crovia/cep-capsules" target="_blank" style="color:var(--cyan);">Crovia/cep-capsules</a>
</div>
"""
# =============================================================================
# JAVASCRIPT
# =============================================================================
JS = r"""
() => {
const $ = q => document.querySelector(q);
const $$ = q => document.querySelectorAll(q);
// --- Tab switching ---
window.switchTab = function(tab) {
document.querySelectorAll('.ev-tab').forEach(t => t.classList.remove('active'));
document.querySelector('#tab-' + tab).classList.add('active');
document.querySelector('#panel-evidence').style.display = tab === 'evidence' ? 'block' : 'none';
document.querySelector('#panel-capsules').style.display = tab === 'capsules' ? 'block' : 'none';
};
// --- Current payloads for download ---
let _currentPayload = null;
let _currentLiveCapsule = null;
let _pendingCapsuleModel = null;
// --- Copy citation ---
window.copyCitation = function() {
const text = $('#ev-citation').textContent;
navigator.clipboard.writeText(text).then(() => {
const btn = $('#ev-citation-copy');
btn.textContent = 'COPIED \u2713';
setTimeout(() => btn.textContent = 'COPY', 2000);
});
};
// --- Download evidence package ---
window.downloadEvidence = function() {
if (!_currentPayload) return;
const blob = new Blob([JSON.stringify(_currentPayload, null, 2)], {type:'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const safe = (_currentPayload.model_id || 'unknown').replace(/\//g, '_');
a.download = 'crovia_evidence_' + safe + '_' + new Date().toISOString().slice(0,10) + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// --- Download live capsule ---
window.downloadLiveCapsule = function() {
if (!_currentLiveCapsule) return;
const blob = new Blob([JSON.stringify(_currentLiveCapsule, null, 2)], {type:'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const cid = _currentLiveCapsule.capsule_id || 'unknown';
a.download = cid + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// --- Build live capsule report text ---
function buildLiveCapsuleReport(c) {
const lines = [];
lines.push('CROVIA \u00b7 LIVE EVIDENCE CAPSULE');
lines.push('\u2550'.repeat(50));
lines.push('');
lines.push('CAPSULE-ID: ' + c.capsule_id);
lines.push('SCHEMA: ' + c.schema);
lines.push('MODE: ' + c.mode);
lines.push('GENERATED: ' + c.generated);
lines.push('CONTENT HASH: ' + c.content_hash);
lines.push('');
lines.push('\u2500\u2500\u2500 MODEL \u2500\u2500\u2500');
lines.push('ID: ' + c.model.model_id);
lines.push('ORG: ' + c.model.organization);
lines.push('SOURCE: ' + c.model.source);
lines.push('TRUST LEVEL: ' + c.trust_level);
lines.push('');
lines.push('\u2500\u2500\u2500 EVIDENCE \u2500\u2500\u2500');
lines.push('TPA-ID: ' + c.evidence.tpa_id);
lines.push('CHAIN HEIGHT: ' + c.evidence.chain_height);
lines.push('EPOCH: ' + (c.evidence.epoch || 'N/A'));
lines.push('OBSERVATIONS: ' + c.evidence.observation_count);
lines.push('PRESENT: ' + c.evidence.present_count + '/' + c.evidence.observation_count);
lines.push('ABSENT: ' + c.evidence.absent_count + ' (' + c.evidence.critical_gaps + ' critical)');
lines.push('STRENGTH: ' + c.evidence.evidence_strength + '%');
lines.push('');
lines.push('\u2500\u2500\u2500 NEC# OBSERVATIONS \u2500\u2500\u2500');
(c.evidence.observations || []).forEach(function(o) {
var icon = o.is_present ? '\u2713' : '\u2717';
var sev = o.is_present ? '' : ' [' + o.severity_label + ']';
lines.push(icon + ' ' + (o.necessity_id || '').padEnd(7) + (o.necessity_name || '').substring(0,45) + sev);
});
lines.push('');
lines.push('\u2500\u2500\u2500 CRYPTOGRAPHIC ANCHORS \u2500\u2500\u2500');
lines.push('TYPE: ' + c.cryptographic_anchors.type);
lines.push('COMMITMENTS: ' + c.cryptographic_anchors.commitment_count);
lines.push('CHAIN TYPE: ' + c.cryptographic_anchors.chain_type);
lines.push('CHAIN HEIGHT: ' + c.cryptographic_anchors.chain_height);
if (c.cryptographic_anchors.anchor_hash) {
lines.push('ANCHOR: ' + c.cryptographic_anchors.anchor_hash.substring(0,32) + '...');
}
if (c.cryptographic_anchors.merkle_root) {
lines.push('MERKLE ROOT: ' + c.cryptographic_anchors.merkle_root.substring(0,32) + '...');
}
lines.push('');
var withCx = (c.evidence.observations || []).filter(function(o) { return o.commitment && o.commitment.x; });
if (withCx.length > 0) {
lines.push('\u2500\u2500\u2500 PEDERSEN COMMITMENTS (sample) \u2500\u2500\u2500');
withCx.slice(0, 4).forEach(function(o) {
lines.push(o.necessity_id + ':');
lines.push(' Cx: ' + o.commitment.x.substring(0,40) + '...');
lines.push(' Cy: ' + o.commitment.y.substring(0,40) + '...');
});
if (withCx.length > 4) lines.push(' ... + ' + (withCx.length - 4) + ' more commitments');
lines.push('');
}
if (c.lineage) {
lines.push('\u2500\u2500\u2500 LINEAGE \u2500\u2500\u2500');
lines.push('COMPLIANCE: ' + c.lineage.compliance_score);
lines.push('SEVERITY: ' + c.lineage.severity);
lines.push('');
}
if (c.outreach) {
lines.push('\u2500\u2500\u2500 OUTREACH \u2500\u2500\u2500');
lines.push('STATUS: ' + c.outreach.status);
lines.push('CONTACTED: ' + (c.outreach.contacted ? 'YES' : 'NO'));
lines.push('');
}
lines.push('\u2500\u2500\u2500 DISCLAIMER \u2500\u2500\u2500');
lines.push('Observation, not judgment. All data from public artifacts.');
lines.push('Capsule generated in OPEN EVIDENCE MODE (read-only).');
return lines.join('\n');
}
// --- Reset search ---
window.resetSearch = function() {
_currentPayload = null;
$('#ev-search').value = '';
$('#ev-active-model').style.display = 'none';
$('#sig-trust-val').textContent = '\u2014';
$('#sig-trust').className = 'ev-signal trust-unknown';
$('#sig-strength-val').textContent = '\u2014';
$('#sig-chain').textContent = '\u2014';
$('#sig-gaps').textContent = '\u2014';
$('#sig-juris').textContent = '\u2014';
$('#ev-meter').style.display = 'none';
drawConstellation([]);
buildNecGrid([]);
$('#ev-report').textContent = 'Select a model to generate forensic evidence report.';
$('#ev-juris').innerHTML = '<span style="color:#9ca3af;font-size:12px;">\u2014</span>';
$('#ev-outreach').innerHTML = '<div class="ev-outreach-dot" style="background:var(--text3);"></div><span style="color:var(--text3)!important;">Select a model to see outreach status</span>';
$('#ev-peer').innerHTML = '\u2014';
$('#ev-citation').textContent = 'Select a model to generate a citation-ready evidence statement.';
const dlBar = $('#ev-download-bar');
if (dlBar) dlBar.style.display = 'none';
$('#nec-badge').textContent = '\u2014';
$('#nec-badge').style.background = 'var(--bg2)';
$('#nec-badge').style.color = 'var(--text3)';
$('#ev-search').focus();
};
// --- Draw NEC# Constellation ---
function drawConstellation(obs) {
const svg = $('#ev-constellation');
if (!svg) return;
while (svg.firstChild) svg.removeChild(svg.firstChild);
const W = 600, H = 380;
const cx = W / 2, cy = H / 2;
const R = 140;
// Background glow
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
const rg = document.createElementNS("http://www.w3.org/2000/svg", "radialGradient");
rg.id = "cg"; rg.setAttribute("cx","50%"); rg.setAttribute("cy","50%"); rg.setAttribute("r","50%");
const s1 = document.createElementNS("http://www.w3.org/2000/svg","stop");
s1.setAttribute("offset","0%"); s1.setAttribute("stop-color","rgba(34,211,238,0.08)");
const s2 = document.createElementNS("http://www.w3.org/2000/svg","stop");
s2.setAttribute("offset","100%"); s2.setAttribute("stop-color","transparent");
rg.appendChild(s1); rg.appendChild(s2); defs.appendChild(rg);
svg.appendChild(defs);
const bgc = document.createElementNS("http://www.w3.org/2000/svg","circle");
bgc.setAttribute("cx",cx); bgc.setAttribute("cy",cy); bgc.setAttribute("r", R + 30);
bgc.setAttribute("fill","url(#cg)"); svg.appendChild(bgc);
// Center node
const cc = document.createElementNS("http://www.w3.org/2000/svg","circle");
cc.setAttribute("cx",cx); cc.setAttribute("cy",cy); cc.setAttribute("r",16);
cc.setAttribute("fill","#22d3ee"); cc.setAttribute("opacity","0.9");
svg.appendChild(cc);
const ct = document.createElementNS("http://www.w3.org/2000/svg","text");
ct.setAttribute("x",cx); ct.setAttribute("y",cy+4);
ct.setAttribute("text-anchor","middle"); ct.setAttribute("fill","#030712");
ct.setAttribute("font-size","9"); ct.setAttribute("font-weight","800");
ct.textContent = "MODEL"; svg.appendChild(ct);
if (!obs || !obs.length) {
const nt = document.createElementNS("http://www.w3.org/2000/svg","text");
nt.setAttribute("x",cx); nt.setAttribute("y",cy+45);
nt.setAttribute("text-anchor","middle"); nt.setAttribute("fill","#9ca3af");
nt.setAttribute("font-size","12");
nt.textContent = "Select a model to illuminate the constellation";
svg.appendChild(nt);
return;
}
const n = obs.length;
obs.forEach((o, i) => {
const angle = (Math.PI * 2 * i / n) - Math.PI / 2;
const x = cx + R * Math.cos(angle);
const y = cy + R * Math.sin(angle);
// Connection line
const line = document.createElementNS("http://www.w3.org/2000/svg","line");
line.setAttribute("x1",cx); line.setAttribute("y1",cy);
line.setAttribute("x2",x); line.setAttribute("y2",y);
line.setAttribute("stroke", o.present ? "rgba(34,197,94,0.25)" : "rgba(239,68,68,0.25)");
line.setAttribute("stroke-width", o.severity === "CRITICAL" && !o.present ? "2" : "1");
svg.appendChild(line);
// Node
const r = o.severity === "CRITICAL" && !o.present ? 14 : 10;
const nc = document.createElementNS("http://www.w3.org/2000/svg","circle");
nc.setAttribute("cx",x); nc.setAttribute("cy",y); nc.setAttribute("r",r);
if (o.present) {
nc.setAttribute("fill","rgba(34,197,94,0.2)");
nc.setAttribute("stroke","#22c55e"); nc.setAttribute("stroke-width","2");
} else if (o.severity === "CRITICAL") {
nc.setAttribute("fill","rgba(239,68,68,0.2)");
nc.setAttribute("stroke","#ef4444"); nc.setAttribute("stroke-width","2");
} else {
nc.setAttribute("fill","rgba(245,158,11,0.15)");
nc.setAttribute("stroke","#f59e0b"); nc.setAttribute("stroke-width","1.5");
}
svg.appendChild(nc);
// Label
const lt = document.createElementNS("http://www.w3.org/2000/svg","text");
lt.setAttribute("x",x); lt.setAttribute("y", y + (y < cy ? -r-6 : r+14));
lt.setAttribute("text-anchor","middle");
lt.setAttribute("fill", o.present ? "#22c55e" : o.severity==="CRITICAL" ? "#ef4444" : "#f59e0b");
lt.setAttribute("font-size","9"); lt.setAttribute("font-weight","700");
lt.setAttribute("font-family","JetBrains Mono, monospace");
lt.textContent = o.nec_id;
svg.appendChild(lt);
});
}
// --- Build NEC# Grid ---
function buildNecGrid(obs) {
const grid = $('#nec-grid');
if (!grid) return;
grid.innerHTML = '';
if (!obs || !obs.length) {
grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:#9ca3af;font-size:12px;padding:40px;">Select a model</div>';
return;
}
obs.forEach(o => {
const cls = o.present ? 'present' : (o.severity === 'CRITICAL' ? 'critical' : 'absent');
const cell = document.createElement('div');
cell.className = 'nec-cell ' + cls;
cell.title = o.name + ' | ' + o.severity + ' | ' + o.jurisdictions + ' jurisdictions';
cell.innerHTML = '<div class="nec-id">' + o.nec_id + '</div>' +
'<div class="nec-status">' + (o.present ? '✓' : '✗') + '</div>' +
'<div class="nec-severity">' + o.severity + '</div>';
grid.appendChild(cell);
});
}
// --- Build Forensic Report ---
function buildReport(p) {
const lines = [];
lines.push('CROVIA EVIDENCE MACHINE — FORENSIC REPORT');
lines.push('═══════════════════════════════════════════');
lines.push('');
lines.push('TARGET: ' + p.model_id);
lines.push('ORG: ' + p.org);
lines.push('TIMESTAMP: ' + p.timestamp);
lines.push('TPA-ID: ' + (p.tpa_id || 'N/A'));
lines.push('CHAIN: ' + p.chain_height);
lines.push('');
lines.push('TRUST LEVEL: ' + p.trust_level);
lines.push('EVIDENCE: ' + p.strength.present + '/' + p.strength.total + ' NEC# present');
lines.push('GAPS: ' + p.strength.absent + ' (' + p.strength.critical_gaps + ' critical)');
lines.push('STRENGTH: ' + p.strength.score + '%');
lines.push('');
lines.push('─── NEC# OBSERVATIONS ───');
(p.observations || []).forEach(o => {
const icon = o.present ? '✓' : '✗';
const sev = o.present ? '' : ' [' + o.severity + ']';
lines.push(icon + ' ' + o.nec_id.padEnd(7) + o.name.substring(0,50) + sev);
});
lines.push('');
lines.push('─── CRYPTOGRAPHIC ANCHORS ───');
(p.observations || []).filter(o => o.commitment_x).slice(0,5).forEach(o => {
lines.push(o.nec_id + ' Cx: ' + o.commitment_x.substring(0,24) + '...');
lines.push(' Cy: ' + o.commitment_y.substring(0,24) + '...');
});
if (p.observations && p.observations.length > 5) {
lines.push('... + ' + (p.observations.length - 5) + ' more Pedersen commitments');
}
lines.push('');
if (p.lineage) {
lines.push('─── LINEAGE DATA ───');
lines.push('Compliance Score: ' + p.lineage.compliance_score);
lines.push('Severity: ' + p.lineage.severity);
lines.push('NEC Absent: ' + p.lineage.nec_absent);
lines.push('');
}
lines.push('─── DISCLAIMER ───');
lines.push('Observation, not judgment. All data from public artifacts.');
return lines.join('\n');
}
// --- Update UI from payload ---
function updateUI(p) {
if (!p || p.error) {
$('#ev-report').textContent = 'Error: ' + (p ? p.error : 'unknown');
return;
}
// Store for download
_currentPayload = p;
// --- LIVE CAPSULE: if pending, build capsule from this payload ---
if (_pendingCapsuleModel && p.model_id === _pendingCapsuleModel) {
const capsule = {
type: 'live_capsule',
schema: 'crovia_evidence_capsule.v2',
capsule_id: 'CEP-LIVE-' + (p.content_hash || '').substring(0,8).toUpperCase(),
generated: p.timestamp || new Date().toISOString(),
mode: 'OPEN_EVIDENCE',
model: { model_id: p.model_id, organization: p.org || '', source: 'huggingface.co' },
evidence: {
tpa_id: p.tpa_id || '',
chain_height: p.chain_height || 0,
observation_count: (p.observations || []).length,
present_count: p.strength ? p.strength.present : 0,
absent_count: p.strength ? p.strength.absent : 0,
critical_gaps: p.strength ? p.strength.critical_gaps : 0,
evidence_strength: p.strength ? p.strength.score : 0,
observations: (p.observations || []).map(o => ({
necessity_id: o.necessity_id || '',
necessity_name: o.necessity_name || '',
is_present: o.is_present || false,
severity_label: o.severity_label || '',
severity_weight: o.severity_weight || 0,
commitment: { x: o.commitment_x || '', y: o.commitment_y || '' }
})),
},
cryptographic_anchors: {
type: 'pedersen_secp256k1',
chain_type: 'sha256_temporal',
chain_height: p.chain_height || 0,
anchor_hash: p.anchor_hash || '',
merkle_root: p.merkle_root || '',
},
trust_level: p.trust_level || 'UNKNOWN',
content_hash: p.content_hash || '',
jurisdictions: p.jurisdictions || [],
citation: p.citation || '',
};
_currentLiveCapsule = capsule;
const report = $('#live-capsule-report');
if (report) report.textContent = buildLiveCapsuleReport(capsule);
const dlBtn = $('#live-capsule-download');
if (dlBtn) dlBtn.style.display = 'inline-block';
const badge = $('#live-capsule-badge');
if (badge) {
const tl = capsule.trust_level;
badge.textContent = tl;
badge.style.background = tl === 'GREEN' ? 'rgba(34,197,94,0.15)' : tl === 'RED' ? 'rgba(239,68,68,0.15)' : 'rgba(245,158,11,0.15)';
badge.style.color = tl === 'GREEN' ? '#22c55e' : tl === 'RED' ? '#ef4444' : '#f59e0b';
}
_pendingCapsuleModel = null;
}
// Active model banner
$('#ev-active-id').textContent = p.model_id;
$('#ev-active-model').style.display = 'flex';
// Download bar
const dlBar = $('#ev-download-bar');
if (dlBar) dlBar.style.display = 'flex';
// Signals
const trustEl = $('#sig-trust');
trustEl.className = 'ev-signal trust-' + p.trust_level.toLowerCase();
$('#sig-trust-val').textContent = p.trust_level;
$('#sig-strength-val').textContent = p.strength.score + '%';
$('#sig-chain').textContent = p.chain_height.toLocaleString();
$('#sig-gaps').textContent = p.strength.absent + '/' + p.strength.total;
$('#sig-juris').textContent = p.jurisdictions.length;
// Meter
const meter = $('#ev-meter');
meter.style.display = 'flex';
$('#meter-val').textContent = p.strength.score + '%';
const barColor = p.strength.score >= 60 ? '#22c55e' : p.strength.score >= 30 ? '#f59e0b' : '#ef4444';
$('#meter-bar').style.width = p.strength.score + '%';
$('#meter-bar').style.background = barColor;
$('#meter-val').style.color = barColor + '!important';
// NEC badge
$('#nec-badge').textContent = p.strength.present + '/' + p.strength.total + ' present';
$('#nec-badge').style.background = p.trust_level === 'GREEN' ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)';
$('#nec-badge').style.color = p.trust_level === 'GREEN' ? '#22c55e' : '#ef4444';
// Constellation + Grid
drawConstellation(p.observations);
buildNecGrid(p.observations);
// Report
$('#ev-report').textContent = buildReport(p);
// Jurisdictions
const jurisEl = $('#ev-juris');
jurisEl.innerHTML = '';
(p.jurisdictions || []).forEach(j => {
const tag = document.createElement('span');
tag.className = 'ev-juris-tag';
tag.textContent = j;
jurisEl.appendChild(tag);
});
if (!p.jurisdictions || !p.jurisdictions.length) {
jurisEl.innerHTML = '<span style="color:#9ca3af;font-size:12px;">No jurisdictions identified</span>';
}
// Outreach
const outEl = $('#ev-outreach');
if (p.outreach) {
const contacted = p.outreach.contacted;
const responded = p.outreach.response;
const color = responded ? '#22c55e' : contacted ? '#f59e0b' : '#ef4444';
const text = responded ? 'Contacted · Response received' :
contacted ? 'Contacted · Awaiting response' : 'Not yet contacted';
outEl.innerHTML = '<div class="ev-outreach-dot" style="background:' + color + ';box-shadow:0 0 8px ' + color + '44;"></div>' +
'<span style="color:' + color + '!important;">' + text + '</span>';
} else {
outEl.innerHTML = '<div class="ev-outreach-dot" style="background:#6b7280;"></div>' +
'<span style="color:#9ca3af!important;">No outreach data for this organization</span>';
}
// Peer context
const peerEl = $('#ev-peer');
if (p.peer) {
peerEl.innerHTML =
'<div style="margin-bottom:6px;"><span style="color:var(--text2);">Industry avg:</span> <strong style="color:var(--cyan);">' + p.peer.industry_avg + '%</strong> <span style="color:var(--text3);">(' + p.peer.total_models + ' models)</span></div>' +
'<div><span style="color:var(--text2);">Org avg (' + p.peer.org_models + ' models):</span> <strong style="color:var(--violet);">' + p.peer.org_avg + '%</strong></div>';
}
// Citation
$('#ev-citation').textContent = p.citation;
}
// --- Payload watcher ---
function attachWatcher() {
const root = document.querySelector('#ev_payload');
if (!root) return false;
const input = root.querySelector('textarea, input');
if (!input) return false;
let last = '';
const tick = () => {
const val = input.value || '';
if (val && val !== last) {
last = val;
try { updateUI(JSON.parse(val)); } catch(e) {}
}
};
input.addEventListener('input', tick);
setInterval(tick, 200);
return true;
}
// --- Stats watcher ---
function attachStatsWatcher() {
const root = document.querySelector('#ev_stats');
if (!root) return false;
const input = root.querySelector('textarea, input');
if (!input || !input.value) return false;
try {
const s = JSON.parse(input.value);
$('#ev-stats-models').textContent = s.models;
$('#ev-stats-chain').textContent = s.chain_height.toLocaleString();
} catch(e) {}
return true;
}
// --- Targets watcher ---
let _targetsPopulated = false;
function attachTargetsWatcher() {
const root = document.querySelector('#ev_targets');
if (!root) return false;
const input = root.querySelector('textarea, input');
if (!input || !input.value) return false;
if (_targetsPopulated) return true;
try {
const targets = JSON.parse(input.value);
// Quick target buttons (top 8)
const quick = $('#ev-quick-targets');
if (quick) {
targets.slice(0, 8).forEach(t => {
const btn = document.createElement('button');
btn.className = 'ev-quick-btn';
btn.textContent = t.id.split('/').pop().substring(0,16) + ' (' + t.gaps + ' gaps)';
btn.title = t.id;
btn.addEventListener('click', () => {
$('#ev-search').value = t.id;
triggerSearch(t.id);
});
quick.appendChild(btn);
});
}
// Model browser dropdown
const sel = $('#ev-model-select');
if (sel) {
// Group by org
const byOrg = {};
targets.forEach(t => {
const parts = t.id.split('/');
const org = parts.length > 1 ? parts[0] : 'other';
if (!byOrg[org]) byOrg[org] = [];
byOrg[org].push(t);
});
// Sort orgs alphabetically
Object.keys(byOrg).sort().forEach(org => {
const grp = document.createElement('optgroup');
grp.label = org + ' (' + byOrg[org].length + ' models)';
byOrg[org].forEach(t => {
const opt = document.createElement('option');
opt.value = t.id;
const name = t.id.split('/').pop();
opt.textContent = name + ' — ' + t.gaps + ' gaps';
grp.appendChild(opt);
});
sel.appendChild(grp);
});
sel.addEventListener('change', () => {
if (sel.value) {
$('#ev-search').value = sel.value;
triggerSearch(sel.value);
}
});
}
// Datalist for autocomplete
const dl = $('#ev-model-list');
if (dl) {
targets.forEach(t => {
const opt = document.createElement('option');
opt.value = t.id;
dl.appendChild(opt);
});
}
_targetsPopulated = true;
} catch(e) {}
return true;
}
// --- Connect command bar to Gradio ---
function attachCommandBar() {
const searchRoot = document.querySelector('#ev_search_in');
if (!searchRoot) return false;
const searchInput = searchRoot.querySelector('textarea, input');
if (!searchInput) return false;
window.triggerSearch = function(val) {
// Force re-trigger by clearing first, then setting after a tick
searchInput.value = '';
searchInput.dispatchEvent(new Event('input', {bubbles:true}));
searchInput.dispatchEvent(new Event('change', {bubbles:true}));
setTimeout(() => {
searchInput.value = val;
searchInput.dispatchEvent(new Event('input', {bubbles:true}));
searchInput.dispatchEvent(new Event('change', {bubbles:true}));
}, 50);
};
const doSearch = () => {
const val = $('#ev-search').value.trim();
if (val) {
$('#ev-search').value = val;
triggerSearch(val);
}
};
$('#ev-go').addEventListener('click', doSearch);
$('#ev-search').addEventListener('keydown', e => {
if (e.key === 'Enter') doSearch();
});
return true;
}
// --- Capsule tab (live + reference) ---
let _capsuleTabReady = false;
let _liveCapsuleDropdownPopulated = false;
function attachCapsuleTab() {
if (_capsuleTabReady) return true;
// --- LIVE CAPSULES: populate dropdown (once) ---
if (!_liveCapsuleDropdownPopulated) {
const targetsRoot = document.querySelector('#ev_targets');
const targetsInput = targetsRoot ? targetsRoot.querySelector('textarea, input') : null;
if (!targetsInput || !targetsInput.value) return false;
const liveSel = $('#live-capsule-select');
if (!liveSel) return false;
try {
const targets = JSON.parse(targetsInput.value);
const countEl = $('#live-capsule-count');
if (countEl) countEl.textContent = targets.length + ' models';
const byOrg = {};
targets.forEach(t => {
const parts = t.id.split('/');
const org = parts.length > 1 ? parts[0] : 'other';
if (!byOrg[org]) byOrg[org] = [];
byOrg[org].push(t);
});
Object.keys(byOrg).sort().forEach(org => {
const grp = document.createElement('optgroup');
grp.label = org + ' (' + byOrg[org].length + ')';
byOrg[org].forEach(t => {
const opt = document.createElement('option');
opt.value = t.id;
opt.textContent = t.id.split('/').pop() + ' \u2014 ' + t.gaps + ' gaps';
grp.appendChild(opt);
});
liveSel.appendChild(grp);
});
} catch(e) {}
// Wire dropdown: call Gradio API directly via fetch()
liveSel.addEventListener('change', () => {
if (!liveSel.value) return;
const modelId = liveSel.value;
$('#live-capsule-report').textContent = 'Generating live capsule for ' + modelId + '...';
const dlBtn = $('#live-capsule-download');
if (dlBtn) dlBtn.style.display = 'none';
// Call Gradio named API endpoint directly
fetch('./api/live_capsule', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({data: [modelId]})
})
.then(r => r.json())
.then(result => {
// Handle both Gradio response formats
let raw = '';
if (result.data) raw = result.data[0];
else if (Array.isArray(result)) raw = result[0];
else if (typeof result === 'string') raw = result;
if (!raw) {
$('#live-capsule-report').textContent = 'API response: ' + JSON.stringify(result).substring(0, 200);
return;
}
const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
if (data.error) {
$('#live-capsule-report').textContent = 'Error: ' + data.error;
return;
}
_currentLiveCapsule = data;
$('#live-capsule-report').textContent = buildLiveCapsuleReport(data);
var dlBar = $('#live-capsule-download-bar');
if (dlBar) dlBar.style.display = 'block';
const badge = $('#live-capsule-badge');
if (badge) {
const tl = data.trust_level;
badge.textContent = tl;
badge.style.background = tl === 'GREEN' ? 'rgba(34,197,94,0.15)' : tl === 'RED' ? 'rgba(239,68,68,0.15)' : 'rgba(245,158,11,0.15)';
badge.style.color = tl === 'GREEN' ? '#22c55e' : tl === 'RED' ? '#ef4444' : '#f59e0b';
}
})
.catch(err => {
$('#live-capsule-report').textContent = 'Fetch error: ' + err.message;
});
});
_liveCapsuleDropdownPopulated = true;
}
// --- REFERENCE CAPSULES ---
const capsRoot = document.querySelector('#ev_capsules_json');
const capsInput = capsRoot ? capsRoot.querySelector('textarea, input') : null;
if (!capsInput || !capsInput.value) return false;
const sel = $('#capsule-select');
if (!sel) return false;
if (sel.children.length <= 1) {
try {
const caps = JSON.parse(capsInput.value);
const refCount = $('#ref-capsule-count');
if (refCount) refCount.textContent = caps.length + ' capsules';
caps.forEach(c => {
const opt = document.createElement('option');
opt.value = c; opt.textContent = c;
sel.appendChild(opt);
});
} catch(e) {}
}
const cepRoot = document.querySelector('#ev_capsule_in');
const cepInput = cepRoot ? cepRoot.querySelector('textarea, input') : null;
if (!cepInput) return false;
sel.addEventListener('change', () => {
cepInput.value = sel.value;
cepInput.dispatchEvent(new Event('input', {bubbles:true}));
});
const resRoot = document.querySelector('#ev_capsule_result');
const resInput = resRoot ? resRoot.querySelector('textarea, input') : null;
if (!resInput) return false;
let lastCap = '';
setInterval(() => {
const val = resInput.value || '';
if (val && val !== lastCap) {
lastCap = val;
try {
const data = JSON.parse(val);
if (data.error) {
$('#capsule-report').textContent = 'Error: ' + data.error;
} else {
const lines = [];
lines.push('CROVIA \u00b7 CEP CAPSULE INSPECTOR');
lines.push('\u2550'.repeat(32));
lines.push('');
lines.push('CEP-ID: ' + data.cep_id);
lines.push('Schema: ' + data.schema);
lines.push('Model: ' + data.model_id);
lines.push('Evidence: ' + data.evidence_nodes + ' nodes');
lines.push('Signature: ' + (data.signature ? 'PRESENT \u2713' : 'MISSING \u2717'));
lines.push('Hashchain: ' + (data.hashchain ? 'sha256:' + data.hashchain_short + '...' : 'MISSING \u2717'));
lines.push('Capsule SHA: ' + data.capsule_sha256);
lines.push('');
lines.push('Evidence keys: ' + (data.evidence_keys || []).join(', '));
$('#capsule-report').textContent = lines.join('\n');
}
} catch(e) {}
}
}, 200);
_capsuleTabReady = true;
return true;
}
// --- URL params ---
function checkUrlParam() {
const params = new URLSearchParams(window.location.search);
const model = params.get('model');
if (model) {
$('#ev-search').value = model;
setTimeout(() => {
if (window.triggerSearch) triggerSearch(model);
}, 500);
}
}
// --- Boot ---
drawConstellation([]);
buildNecGrid([]);
const boot = setInterval(() => {
const ok1 = attachWatcher();
const ok2 = attachStatsWatcher();
const ok3 = attachTargetsWatcher();
const ok4 = attachCommandBar();
const ok5 = attachCapsuleTab();
if (ok1 && ok2 && ok3 && ok4 && ok5) {
clearInterval(boot);
checkUrlParam();
}
}, 200);
return [];
}
"""
# =============================================================================
# GRADIO APP
# =============================================================================
capsules = _list_capsules()
default_cap = capsules[0] if capsules else "CEP-2511-K4I7X2"
stats_json = get_registry_stats()
targets_json = get_targets_list()
with gr.Blocks(title="CROVIA · Evidence Machine", css=CSS, js=JS) as demo:
gr.HTML(UI_HTML)
# Hidden Gradio components (JS reads/writes via DOM)
ev_search_in = gr.Textbox(value="", visible=False, elem_id="ev_search_in")
ev_payload = gr.Textbox(value="", visible=False, elem_id="ev_payload")
ev_stats = gr.Textbox(value=stats_json, visible=False, elem_id="ev_stats")
ev_targets = gr.Textbox(value=targets_json, visible=False, elem_id="ev_targets")
# Capsule tab — reference
ev_capsules_json = gr.Textbox(value=json.dumps(capsules), visible=False, elem_id="ev_capsules_json")
ev_capsule_in = gr.Textbox(value=default_cap, visible=False, elem_id="ev_capsule_in")
ev_capsule_result = gr.Textbox(value="", visible=False, elem_id="ev_capsule_result")
# Live capsule API — exposed as named endpoint, called via fetch() from JS
ev_live_in = gr.Textbox(visible=False, elem_id="ev_live_in")
ev_live_out = gr.Textbox(visible=False, elem_id="ev_live_out")
# Events
ev_search_in.change(generate_evidence, inputs=ev_search_in, outputs=ev_payload)
ev_capsule_in.change(inspect_capsule, inputs=ev_capsule_in, outputs=ev_capsule_result)
ev_live_in.change(generate_live_capsule, inputs=ev_live_in, outputs=ev_live_out, api_name="live_capsule", queue=False)
demo.queue()
demo.launch()