kalanosdev commited on
Commit
6ec6311
·
1 Parent(s): 72ab5f3

Space UI: Hub design system, header/hero, KPI strip, status pills and episode bars

Browse files
Files changed (2) hide show
  1. app.py +31 -21
  2. cards.py +166 -18
app.py CHANGED
@@ -7,7 +7,7 @@ from urllib.parse import quote
7
  import gradio as gr
8
  import requests
9
 
10
- from cards import CSS, card_payload, render
11
  from quick_grade import parse_ref, quick_grade
12
 
13
  # ZeroGPU compatibility shim. If the Space is ever scheduled on ZeroGPU hardware,
@@ -42,12 +42,14 @@ def examples():
42
  with ThreadPoolExecutor(max_workers=3) as pool:
43
  payloads = [p for p in pool.map(published, FEATURED) if p]
44
  if not payloads:
45
- return '<p>Featured reports are unavailable. You can still grade a public dataset above.</p>'
46
- totals = {k: sum(p['counts'].get(k, 0) for p in payloads) for k in ('total', 'pass_count', 'review')}
47
- return (f'<p class="k-kpis">{len(payloads)} featured reports · {totals["total"]} episodes assessed · '
48
- f'{totals["pass_count"]} passed within scope · {totals["review"]} requiring review</p>'
49
- '<div class="k-cards">' + ''.join(render(p) for p in payloads) + '</div>'
50
- '<p>Totals cover these examples only. Reports may use different revisions and assessment scopes.</p>')
 
 
51
 
52
 
53
  def run(ref):
@@ -56,7 +58,7 @@ def run(ref):
56
  # Always run fresh. Existing published reports never short-circuit grading.
57
  card = quick_grade(ref)
58
  payload = card_payload(card, KALANOS_API)
59
- return render(payload, published=False), payload['markdown'], 'Quick audit complete. Video: not assessed.', card
60
  except ValueError as exc:
61
  return '', '', str(exc), None
62
  except Exception:
@@ -68,7 +70,7 @@ def lookup(ref):
68
  try:
69
  payload = published(parse_ref(ref))
70
  if payload:
71
- return render(payload), payload['markdown'], 'Stored assessment shown with its recorded date and scope. Grade dataset runs a fresh audit.', None
72
  except ValueError as exc:
73
  return '', '', str(exc), None
74
  return '', '', 'No published schema-7 report found. Grade dataset to run a fresh quick audit.', None
@@ -81,25 +83,33 @@ def lookup(ref):
81
  # Until then: copy the HF card Markdown, or publish from the CLI.
82
 
83
  with gr.Blocks(title='Kalanos · Robotics Dataset Quality') as demo:
84
- gr.Markdown('# Kalanos · Robotics Dataset Quality\nCheck a LeRobot dataset. Explore and share its quality report.')
85
- gr.Markdown('### Grade your dataset\nPublic LeRobot v2/v3 · numeric checks · video off. Up to 500 episodes, 128 MiB of numeric/metadata files, 3 minutes. Larger or private datasets: use the CLI.')
86
- with gr.Row():
87
- ref_in = gr.Textbox(label='Public HF dataset', placeholder='lerobot/pusht', scale=4)
88
- go = gr.Button('Grade dataset', variant='primary', scale=1)
89
- # Secondary action stays clear; report cards themselves use compact icons.
90
- lookup_btn = gr.Button('View published assessment', size='sm')
91
- status = gr.Textbox(label='Audit status', interactive=False)
 
92
  out = gr.HTML()
93
  report_state = gr.State(None, time_to_live=600)
94
- with gr.Accordion('HF card Markdown', open=False):
95
  card_text = gr.Code(language='markdown', label='Copy into your existing dataset README', interactive=False)
96
- gr.Markdown('### Featured dataset reports')
97
  featured = gr.HTML()
98
  demo.load(examples, outputs=featured)
99
  go.click(run, inputs=ref_in, outputs=[out, card_text, status, report_state], concurrency_limit=1, concurrency_id='audit')
100
  ref_in.submit(run, inputs=ref_in, outputs=[out, card_text, status, report_state], concurrency_limit=1, concurrency_id='audit')
101
  lookup_btn.click(lookup, inputs=ref_in, outputs=[out, card_text, status, report_state])
102
- gr.Markdown(f'[Kalanos Hub]({KALANOS_API}/datasets) · [CLI](https://github.com/KalanosAI) · [Method](https://kalanos.ai/methodology)')
 
 
 
 
 
 
 
103
 
104
  if __name__ == '__main__':
105
- demo.queue(max_size=4).launch(css=CSS)
 
7
  import gradio as gr
8
  import requests
9
 
10
+ from cards import CSS, card_payload, grader_intro, header, kpis, render, section, share_strip
11
  from quick_grade import parse_ref, quick_grade
12
 
13
  # ZeroGPU compatibility shim. If the Space is ever scheduled on ZeroGPU hardware,
 
42
  with ThreadPoolExecutor(max_workers=3) as pool:
43
  payloads = [p for p in pool.map(published, FEATURED) if p]
44
  if not payloads:
45
+ return '<div class="k-html"><p class="k-small k-muted">Featured reports are unavailable right now. You can still grade a public dataset above.</p></div>'
46
+ return ('<div class="k-html"><p class="k-small k-muted" style="margin:0">KPIs cover these example reports only.</p>'
47
+ + kpis(payloads) + '<div class="k-cards">' + ''.join(render(p) for p in payloads) + '</div>'
48
+ '<p class="k-foot">Published CLI assessments · outcomes apply within the recorded scope. Reports may use different revisions and assessment scopes.</p></div>')
49
+
50
+
51
+ def _one(html):
52
+ return f'<div class="k-html"><div class="k-cards" style="grid-template-columns:minmax(0,1fr);max-width:420px">{html}</div></div>' if html else ''
53
 
54
 
55
  def run(ref):
 
58
  # Always run fresh. Existing published reports never short-circuit grading.
59
  card = quick_grade(ref)
60
  payload = card_payload(card, KALANOS_API)
61
+ return _one(render(payload, published=False)), payload['markdown'], 'Quick audit complete. Video: not assessed.', card
62
  except ValueError as exc:
63
  return '', '', str(exc), None
64
  except Exception:
 
70
  try:
71
  payload = published(parse_ref(ref))
72
  if payload:
73
+ return _one(render(payload)), payload['markdown'], 'Stored assessment shown with its recorded date and scope. Grade dataset runs a fresh audit.', None
74
  except ValueError as exc:
75
  return '', '', str(exc), None
76
  return '', '', 'No published schema-7 report found. Grade dataset to run a fresh quick audit.', None
 
83
  # Until then: copy the HF card Markdown, or publish from the CLI.
84
 
85
  with gr.Blocks(title='Kalanos · Robotics Dataset Quality') as demo:
86
+ gr.HTML(header(KALANOS_API))
87
+ with gr.Group(elem_classes='k-grader'):
88
+ gr.HTML(grader_intro())
89
+ with gr.Row():
90
+ ref_in = gr.Textbox(label='Public HF dataset', placeholder='lerobot/pusht', scale=4, show_label=False, container=False)
91
+ go = gr.Button('Grade dataset', variant='primary', scale=1, elem_classes='k-primary')
92
+ # Secondary action stays clear; report cards themselves use compact icons.
93
+ lookup_btn = gr.Button('View published assessment', scale=1, elem_classes='k-secondary')
94
+ status = gr.Textbox(label='Audit status', interactive=False, lines=1, max_lines=2, elem_classes='k-status', container=False, placeholder='Audit status appears here.')
95
  out = gr.HTML()
96
  report_state = gr.State(None, time_to_live=600)
97
+ with gr.Accordion('HF card Markdown', open=False, elem_classes='k-accordion'):
98
  card_text = gr.Code(language='markdown', label='Copy into your existing dataset README', interactive=False)
99
+ gr.HTML(section('Featured dataset reports', 'Live from Kalanos Hub'))
100
  featured = gr.HTML()
101
  demo.load(examples, outputs=featured)
102
  go.click(run, inputs=ref_in, outputs=[out, card_text, status, report_state], concurrency_limit=1, concurrency_id='audit')
103
  ref_in.submit(run, inputs=ref_in, outputs=[out, card_text, status, report_state], concurrency_limit=1, concurrency_id='audit')
104
  lookup_btn.click(lookup, inputs=ref_in, outputs=[out, card_text, status, report_state])
105
+ gr.HTML(share_strip(KALANOS_API))
106
+
107
+ THEME = gr.themes.Base(
108
+ primary_hue=gr.themes.colors.slate, neutral_hue=gr.themes.colors.stone,
109
+ font=[gr.themes.GoogleFont('IBM Plex Sans'), 'sans-serif'],
110
+ font_mono=[gr.themes.GoogleFont('IBM Plex Mono'), 'monospace'],
111
+ ).set(body_background_fill='#f2efe6', body_text_color='#202622', block_radius='0px', button_large_radius='3px',
112
+ input_radius='3px', input_background_fill='#ffffff', input_border_color='#d2d3cb')
113
 
114
  if __name__ == '__main__':
115
+ demo.queue(max_size=4).launch(css=CSS, theme=THEME)
cards.py CHANGED
@@ -1,4 +1,8 @@
1
- """Escaped report cards; no invented scores or client-side regrading."""
 
 
 
 
2
  from html import escape
3
  from urllib.parse import quote
4
 
@@ -7,17 +11,73 @@ ICONS = {
7
  'card': '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M7 13h4M7 16h8"/>',
8
  'hf': '<path d="M14 3h7v7m0-7L10 14M10 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5"/>',
9
  }
10
- CSS = '''
11
- .k-cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
12
- .k-card{background:var(--block-background-fill,#fff);border:1px solid #d2d3cb;padding:16px;min-width:0}
13
- .k-card h3{font-size:17px!important;overflow-wrap:anywhere}.k-score{font-size:30px;margin:12px 0}
14
- .k-stats{display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:12px}.k-stats b{display:block;font-size:21px}
15
- .k-actions{display:flex;align-items:flex-start;gap:4px;margin-top:12px;flex-wrap:wrap}
16
- .k-icon{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer;list-style:none}
17
- .k-icon:hover{background:#e9efe8}.k-icon:focus-visible{outline:2px solid #176b62}.k-icon::-webkit-details-marker{display:none}
18
- .k-card details[open]{flex-basis:100%}.k-card textarea{width:100%;box-sizing:border-box;min-height:230px;font:12px/1.5 monospace}.k-card table{width:100%;font-size:12px}.k-card td{overflow-wrap:anywhere;padding:5px}
19
- .k-meta{font-size:11px;overflow-wrap:anywhere;margin-top:12px}.k-kpis{padding:12px 0;font-size:14px}
20
- @media(max-width:760px){.k-cards{grid-template-columns:1fr}}@media(pointer:coarse){.k-icon{width:44px;height:44px}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  '''
22
 
23
 
@@ -29,6 +89,41 @@ def link(url, label, kind):
29
  return f'<a class="k-icon" href="{escape(url, quote=True)}" title="{label}" aria-label="{label}" target="_blank" rel="noopener">{svg(kind)}</a>'
30
 
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  def card_payload(card, base):
33
  d = card['decision_summary']
34
  ref = card['source']['ref']
@@ -51,15 +146,68 @@ def card_payload(card, base):
51
  dataset_url='https://huggingface.co/datasets/' + quote(ref, safe='/'), report_url=None)
52
 
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  def render(payload, published=True):
55
  c = payload['counts']
56
  score = payload['readiness']
57
- score_text = 'Not established' if score is None else f'{score:g}/100'
58
- status = ('Incomplete evidence' if not c.get('inventory_complete') or c.get('unknown') or not c.get('total') else
59
- 'Blocking findings' if c.get('blocked') else 'Review required' if c.get('review') else 'Pass within scope')
 
60
  actions = link(payload['report_url'], 'View full report', 'report') if published and payload.get('report_url') else ''
61
  preview = '<table>' + ''.join(f'<tr><td>{escape(str(k))}</td><td>{escape(str(v))}</td></tr>' for k, v in payload['details']) + '</table>'
62
- actions += f'<details><summary class="k-icon" title="Preview and copy HF dataset card" aria-label="Preview and copy HF dataset card">{svg("card")}</summary>{preview}<p>Select and copy this Markdown into your existing HF README. Copying does not publish.</p><textarea readonly aria-label="HF dataset card Markdown">{escape(payload["markdown"])}</textarea></details>'
 
 
63
  actions += link(payload['dataset_url'], 'Open dataset on Hugging Face', 'hf')
64
- stats = ''.join(f'<div><b>{c.get(key, "—")}</b>{label}</div>' for key, label in [('total', 'Episodes assessed'), ('pass_count', 'Passed within scope'), ('review', 'Review'), ('blocked', 'Blocked')])
65
- return f'<article class="k-card"><small>{status}</small><h3>{escape(payload["dataset"])}</h3><small>Technical readiness</small><div class="k-score">{score_text}</div><div class="k-stats">{stats}</div><p class="k-meta">{escape(payload["requirements"])}<br>Assessed {escape(payload["assessed_at"])}<br>{"Published report" if published else "Unpublished · video not assessed"}</p><div class="k-actions">{actions}</div></article>'
 
 
 
 
 
 
 
 
 
 
1
+ """Escaped report cards and page chrome; no invented scores or client-side regrading.
2
+
3
+ Visual language follows design/hf-card-mockup in the publisher repo: IBM Plex,
4
+ paper/navy/sage palette, status pills, two-tone episode bars, compact icon actions.
5
+ """
6
  from html import escape
7
  from urllib.parse import quote
8
 
 
11
  'card': '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M7 13h4M7 16h8"/>',
12
  'hf': '<path d="M14 3h7v7m0-7L10 14M10 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5"/>',
13
  }
14
+
15
+ FONTS = ("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;450;500;600"
16
+ "&family=IBM+Plex+Mono:wght@400;500&display=swap")
17
+
18
+ # Design tokens mirror the mockup's :root. Everything below is scoped to the
19
+ # Gradio container (.gradio-container) or to k-* classes so it never leaks into
20
+ # the HF Space chrome around the app.
21
+ CSS = f'''
22
+ @import url('{FONTS}');
23
+ :root{{--k-paper:#f2efe6;--k-ink:#202622;--k-muted:#656962;--k-line:#d2d3cb;--k-navy:#102638;--k-green:#527664;--k-amber:#bd9552;--k-sage:#e9efe8;--k-link:#176b62}}
24
+ .gradio-container{{background:var(--k-paper)!important;color:var(--k-ink)!important;font-family:'IBM Plex Sans',sans-serif!important;max-width:1150px!important;margin:auto!important}}
25
+ .gradio-container .prose,.gradio-container label,.gradio-container input,.gradio-container textarea,.gradio-container button{{font-family:'IBM Plex Sans',sans-serif!important;color:var(--k-ink)}}
26
+ .gradio-container .prose a,.k-html a{{color:var(--k-link)}}
27
+ .gradio-container .block,.gradio-container .form{{background:transparent!important;border:0!important;box-shadow:none!important}}
28
+ .gradio-container input[type=text],.gradio-container textarea{{background:#fff!important;border:1px solid var(--k-line)!important;border-radius:3px!important;box-shadow:none!important}}
29
+ .gradio-container input[type=text]:focus-visible{{outline:3px solid #dc8437!important;outline-offset:3px}}
30
+ .gradio-container .k-primary{{background:var(--k-navy)!important;color:#fff!important;border:1px solid var(--k-navy)!important;border-radius:3px!important;font-weight:500}}
31
+ .gradio-container .k-primary:hover{{background:#24475a!important}}
32
+ .gradio-container .k-secondary{{background:#fff!important;border:1px solid var(--k-line)!important;color:var(--k-ink)!important;border-radius:3px!important}}
33
+ .gradio-container .k-secondary:hover{{border-color:#506d68!important;background:#f5f8f5!important}}
34
+ .gradio-container .k-status textarea{{font-family:'IBM Plex Mono',monospace!important;font-size:12px!important;color:var(--k-muted)!important;background:transparent!important;border:0!important;padding:0!important}}
35
+ .gradio-container .k-status label span{{display:none}}
36
+ .gradio-container .accordion,.gradio-container .k-accordion{{border:1px solid var(--k-line)!important;background:#fff!important;border-radius:0!important}}
37
+
38
+ .k-mono,.k-eyebrow{{font-family:'IBM Plex Mono',monospace;font-size:12px}}
39
+ .k-eyebrow{{letter-spacing:1px;text-transform:uppercase;color:var(--k-muted)}}
40
+ .k-eyebrow.accent{{color:var(--k-link)}}
41
+ .k-muted{{color:var(--k-muted)}}.k-small{{font-size:13px}}
42
+
43
+ .k-nav{{display:flex;justify-content:space-between;align-items:center;gap:20px;padding:18px 0;border-bottom:1px solid #afb4a9;margin-bottom:8px}}
44
+ .k-nav a{{color:var(--k-ink);text-decoration:none}}.k-nav .k-links{{display:flex;align-items:center;gap:22px;flex-wrap:wrap}}
45
+ .k-btn{{border:1px solid var(--k-line);background:#fff;padding:9px 14px;border-radius:3px;text-decoration:none;display:inline-block;color:var(--k-ink)}}
46
+ .k-btn.primary{{background:var(--k-navy);color:#fff!important;border-color:var(--k-navy)}}.k-btn.primary:hover{{background:#24475a}}
47
+ .k-hero{{max-width:750px;margin:22px 0 12px}}.k-hero h1{{font-size:34px;letter-spacing:-1px;line-height:1.2;margin:4px 0 6px;font-weight:500}}.k-hero p{{margin:6px 0 10px;color:var(--k-muted)}}
48
+ .k-hero .k-linklist{{display:flex;gap:18px;flex-wrap:wrap;font-size:13px}}
49
+ .k-grader{{background:var(--k-sage);border:1px solid #bdcbbb;padding:16px 20px 6px;margin:14px 0 22px}}.k-grader h3{{margin:0 0 4px;font-size:19px;font-weight:500}}.k-grader p{{margin:4px 0 8px;font-size:13px;color:var(--k-muted)}}
50
+ .k-section{{display:flex;justify-content:space-between;align-items:baseline;gap:16px;margin:20px 0 4px}}.k-section h2{{font-size:24px;letter-spacing:-.5px;margin:0;font-weight:500}}
51
+
52
+ .k-kpis{{display:grid;grid-template-columns:repeat(4,1fr);background:#fff;border:1px solid var(--k-line);margin:12px 0 16px}}
53
+ .k-kpis>div{{padding:12px 18px;border-right:1px solid var(--k-line)}}.k-kpis>div:last-child{{border:0}}
54
+ .k-kpis strong{{display:block;font-size:26px;font-weight:500;line-height:1.2}}.k-kpis span{{font-size:12px;color:var(--k-muted)}}
55
+
56
+ .k-cards{{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin:12px 0}}
57
+ .k-card{{background:#fff;border:1px solid var(--k-line);padding:17px;min-width:0;display:flex;flex-direction:column}}
58
+ .k-card h3{{font-size:21px;margin:8px 0 4px;font-weight:500}}
59
+ .k-pill{{display:inline-block;align-self:flex-start;padding:4px 9px;font-size:12px;border:0}}
60
+ .k-pill.review{{background:#fbedd4;color:#70501d}}.k-pill.pass{{background:#e8f0e9;color:#375b44}}.k-pill.blocked{{background:#f3dcd8;color:#6b2a22}}.k-pill.incomplete{{background:#ebebe6;color:#4b4f49}}
61
+ .k-card-id{{font:11px 'IBM Plex Mono',monospace;overflow-wrap:anywhere;min-height:32px;color:var(--k-muted)}}
62
+ .k-score{{min-height:82px;margin:10px 0 6px;border-bottom:1px solid var(--k-line);padding-bottom:10px}}
63
+ .k-score strong{{font-size:40px;font-weight:500;line-height:1.2;display:block}}.k-score strong.unestablished{{font-size:23px;margin:10px 0}}.k-score .k-small{{font-size:16px}}
64
+ .k-stats{{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin:6px 0 12px}}.k-stats strong{{display:block;font-size:21px;font-weight:500}}.k-stats span{{font-size:12px;color:var(--k-muted)}}
65
+ .k-bar{{height:8px;display:flex;margin:8px 0;border-radius:3px;overflow:hidden;background:#ebebe6}}
66
+ .k-bar span{{display:block;height:100%}}.k-bar .pass{{background:var(--k-green)}}.k-bar .review{{background:var(--k-amber)}}.k-bar .blocked{{background:#a6473a}}
67
+ .k-counts{{display:flex;gap:8px;flex-wrap:wrap;font-size:11px;justify-content:space-between;margin-bottom:12px}}
68
+ .k-note{{font-size:13px;min-height:58px;margin:0 0 12px}}
69
+ .k-meta{{font:11px/1.8 'IBM Plex Mono',monospace;border-top:1px solid var(--k-line);padding-top:12px;color:var(--k-muted);margin-top:auto;overflow-wrap:anywhere}}
70
+ .k-actions{{display:flex;align-items:flex-start;gap:4px;margin-top:10px;flex-wrap:wrap}}
71
+ .k-icon{{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border:0;border-radius:5px;background:transparent;color:var(--k-navy);cursor:pointer;list-style:none}}
72
+ .k-icon:hover{{background:var(--k-sage)}}.k-icon:focus-visible{{outline:3px solid #dc8437;outline-offset:3px}}.k-icon::-webkit-details-marker{{display:none}}
73
+ .k-card details[open]{{flex-basis:100%}}.k-card details textarea{{width:100%;box-sizing:border-box;min-height:230px;font:12px/1.5 'IBM Plex Mono',monospace;border:1px solid var(--k-line)}}
74
+ .k-card details table{{width:100%;font-size:12px;border-collapse:collapse}}.k-card details td{{overflow-wrap:anywhere;padding:6px;border-bottom:1px solid var(--k-line);vertical-align:top}}
75
+ .k-foot{{font-size:12px;color:var(--k-muted);margin:4px 0 16px}}
76
+ .k-share{{border:1px solid #b5c7bb;background:var(--k-sage);padding:18px 22px;margin:22px 0 10px;display:flex;justify-content:space-between;align-items:center;gap:20px}}
77
+ .k-share h3{{margin:0 0 4px;font-size:19px;font-weight:500}}.k-share p{{margin:0;font-size:13px;color:var(--k-muted)}}
78
+ .k-footer{{font:12px 'IBM Plex Mono',monospace;color:var(--k-muted);padding:18px 0 8px;border-top:1px solid #afb4a9;margin-top:20px}}
79
+ @media(max-width:760px){{.k-nav{{flex-direction:column;align-items:flex-start}}.k-hero h1{{font-size:28px}}.k-kpis{{grid-template-columns:1fr 1fr}}.k-kpis>div:nth-child(2){{border-right:0}}.k-kpis>div:nth-child(-n+2){{border-bottom:1px solid var(--k-line)}}.k-cards{{grid-template-columns:1fr}}.k-score,.k-note,.k-card-id{{min-height:0}}.k-share{{flex-direction:column;align-items:flex-start}}}}
80
+ @media(pointer:coarse){{.k-icon{{width:44px;height:44px}}}}
81
  '''
82
 
83
 
 
89
  return f'<a class="k-icon" href="{escape(url, quote=True)}" title="{label}" aria-label="{label}" target="_blank" rel="noopener">{svg(kind)}</a>'
90
 
91
 
92
+ def header(base):
93
+ """Site chrome matching the Hub nav: Hub → Search → GitHub → Method → Audit."""
94
+ return (f'<div class="k-html"><nav class="k-nav"><a class="k-mono" href="https://kalanos.ai/">KALANOS <span class="k-muted">/ ROBOTICS DATASET QUALITY</span></a>'
95
+ f'<div class="k-links k-mono"><a href="{base}/datasets">HUB</a><a href="{base}/search">SEARCH</a>'
96
+ f'<a href="https://github.com/KalanosAI" target="_blank" rel="noopener">GITHUB ↗</a><a href="https://kalanos.ai/methodology">METHOD</a>'
97
+ f'<a class="k-btn primary" href="{base}/dashboard">Audit your dataset</a></div></nav>'
98
+ f'<div class="k-hero"><div class="k-eyebrow accent">Public dataset assessments</div><h1>Kalanos · Robotics Dataset Quality</h1>'
99
+ f'<p>Check a LeRobot dataset. Explore and share its quality report.</p>'
100
+ f'<div class="k-linklist"><a href="{base}/datasets">Browse Kalanos Hub ↗</a>'
101
+ f'<a href="https://huggingface.co/collections/kalanosdev/robotics-dataset-quality-grades" target="_blank" rel="noopener">HF Collection ↗</a></div></div></div>')
102
+
103
+
104
+ def grader_intro():
105
+ return ('<div class="k-html"><h3 style="margin:0 0 4px;font-size:19px;font-weight:500">Grade your dataset</h3>'
106
+ '<p class="k-small k-muted" style="margin:0 0 6px">Public LeRobot v2/v3 · numeric checks · video off. Up to 500 episodes, 128 MiB of numeric/metadata files, 3 minutes. Larger or private datasets: use the CLI.</p></div>')
107
+
108
+
109
+ def section(title, note=''):
110
+ return f'<div class="k-html k-section"><h2>{escape(title)}</h2><span class="k-small k-muted">{escape(note)}</span></div>'
111
+
112
+
113
+ def share_strip(base):
114
+ return (f'<div class="k-html"><div class="k-share"><div><h3>Make your assessment easy to share.</h3>'
115
+ f'<p>Open the card icon on any report, or the Markdown panel above, and paste the section into your HF dataset README.</p></div>'
116
+ f'<a class="k-btn" href="{base}/datasets">See all reports →</a></div>'
117
+ f'<footer class="k-footer">KALANOS / DATA QUALITY FOR PHYSICAL AI · <a href="{base}/datasets">Hub</a> · <a href="https://github.com/KalanosAI" target="_blank" rel="noopener">CLI</a> · <a href="https://kalanos.ai/methodology">Method</a></footer></div>')
118
+
119
+
120
+ def kpis(payloads):
121
+ totals = {k: sum(p['counts'].get(k, 0) for p in payloads) for k in ('total', 'pass_count', 'review')}
122
+ tiles = [(len(payloads), 'Dataset reports'), (totals['total'], 'Episodes assessed'),
123
+ (totals['pass_count'], 'Episodes passed within scope'), (totals['review'], 'Episodes requiring review')]
124
+ return '<div class="k-kpis">' + ''.join(f'<div><strong>{n}</strong><span>{label}</span></div>' for n, label in tiles) + '</div>'
125
+
126
+
127
  def card_payload(card, base):
128
  d = card['decision_summary']
129
  ref = card['source']['ref']
 
146
  dataset_url='https://huggingface.co/datasets/' + quote(ref, safe='/'), report_url=None)
147
 
148
 
149
+ def _status(c):
150
+ if not c.get('inventory_complete') or c.get('unknown') or not c.get('total'):
151
+ return 'incomplete', 'Incomplete evidence'
152
+ if c.get('blocked'):
153
+ return 'blocked', 'Blocking findings'
154
+ if c.get('review'):
155
+ return 'review', 'Review required'
156
+ return 'pass', 'Pass within scope'
157
+
158
+
159
+ def _bar(c):
160
+ total = c.get('total') or 0
161
+ if not total:
162
+ return ''
163
+ segs = ''.join(f'<span class="{k}" style="width:{100 * c.get(key, 0) / total:.1f}%"></span>'
164
+ for k, key in (('pass', 'pass_count'), ('review', 'review'), ('blocked', 'blocked')))
165
+ label = f'{c.get("pass_count", 0)} pass, {c.get("review", 0)} review, {c.get("blocked", 0)} blocked, {c.get("unknown", 0)} unknown'
166
+ counts = ''.join(f'<span>{c.get(key, 0)} {name}</span>' for key, name in (('pass_count', 'pass'), ('review', 'review'), ('blocked', 'blocked'), ('unknown', 'unknown')))
167
+ return f'<div class="k-bar" role="img" aria-label="{label}">{segs}</div><div class="k-counts">{counts}</div>'
168
+
169
+
170
+ def _note(payload, status):
171
+ if status == 'pass':
172
+ return 'No blocking or review findings recorded within scope.'
173
+ if status == 'review':
174
+ return f'{payload["counts"].get("review", 0)} episode(s) carry findings that require review.'
175
+ if status == 'blocked':
176
+ return f'{payload["counts"].get("blocked", 0)} episode(s) have blocking findings.'
177
+ return 'Inventory incomplete or undetermined episodes; outcomes are not established.'
178
+
179
+
180
+ def _assessed(ts):
181
+ # "2026-10-05T12:12:03Z" -> "05 Oct 2026 · 12:12 UTC"; leave anything else alone.
182
+ try:
183
+ from datetime import datetime
184
+ dt = datetime.strptime(str(ts)[:19], '%Y-%m-%dT%H:%M:%S')
185
+ return dt.strftime('%d %b %Y · %H:%M UTC')
186
+ except ValueError:
187
+ return str(ts)
188
+
189
+
190
  def render(payload, published=True):
191
  c = payload['counts']
192
  score = payload['readiness']
193
+ status, status_text = _status(c)
194
+ score_html = ('<strong class="unestablished">Not established</strong>' if score is None
195
+ else f'<strong>{score:g}<span class="k-small"> /100</span></strong>')
196
+ name = payload['dataset'].split('/', 1)[-1].replace('_', ' ')
197
  actions = link(payload['report_url'], 'View full report', 'report') if published and payload.get('report_url') else ''
198
  preview = '<table>' + ''.join(f'<tr><td>{escape(str(k))}</td><td>{escape(str(v))}</td></tr>' for k, v in payload['details']) + '</table>'
199
+ actions += (f'<details><summary class="k-icon" title="Preview and copy HF dataset card" aria-label="Preview and copy HF dataset card">{svg("card")}</summary>'
200
+ f'{preview}<p class="k-small">Select and copy this Markdown into your existing HF README. Copying does not publish.</p>'
201
+ f'<textarea readonly aria-label="HF dataset card Markdown">{escape(payload["markdown"])}</textarea></details>')
202
  actions += link(payload['dataset_url'], 'Open dataset on Hugging Face', 'hf')
203
+ stats = ''.join(f'<div><strong>{v}</strong><span>{label}</span></div>' for v, label in [
204
+ (c.get('total', '—'), 'Episodes assessed'), (f'{c.get("pass_count", 0)}/{c.get("total", 0)}', 'Eligible episodes'),
205
+ (c.get('review', 0), 'Requiring review'), (c.get('blocked', 0), 'Blocked')])
206
+ kind = 'Published report' if published else 'Unpublished quick audit · video not assessed'
207
+ return (f'<article class="k-card"><span class="k-pill {status}">{status_text}</span><h3>{escape(name)}</h3>'
208
+ f'<div class="k-card-id">{escape(payload["dataset"])}</div>'
209
+ f'<div class="k-score"><span class="k-eyebrow">Technical readiness</span>{score_html}</div>'
210
+ f'<div class="k-stats">{stats}</div>{_bar(c)}'
211
+ f'<p class="k-note">{escape(_note(payload, status))}</p>'
212
+ f'<div class="k-meta">{escape(payload["requirements"])}<br>Assessed {escape(_assessed(payload["assessed_at"]))}<br>{kind}</div>'
213
+ f'<div class="k-actions">{actions}</div></article>')