Spaces:
Paused
Paused
| 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()">⬇ DOWNLOAD JSON</button> | |
| <button class="ev-new-search" id="ev-new-search" onclick="resetSearch()">← 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() | |