devaanand commited on
Commit
db8b05c
·
1 Parent(s): 8b9d470

fix(ui): responsive layout, focus states, table overflow (reviewed external-agent change)

Browse files

Reviewed and validated a UI pass made via a second AI tool (Codex):
- Responsive breakpoints (820px/460px): stacked layout, full-width sidebar,
grid stat cards, stacked run controls, min-width tables with scroll wrap
- :focus-visible outlines on all controls (accessibility, WCAG keyboard nav)
- Table overflow wrappers + overflow-wrap for long corpus texts
- Inline styles moved to classes (results-toolbar, language-control)

Governance fixes from review:
- AGENTS.md was a full duplicate of CLAUDE.md -> now a symlink (single
source of truth; prevents instruction drift between AI tools)
- .codex/ and .agents/ (machine-local tool state) gitignored
- Frontend rebuilt from source; output hashes match committed bundle exactly

Validation: 32/32 backend tests, both registry validators green, CSS balanced.

.gitignore CHANGED
@@ -12,3 +12,7 @@ node_modules/
12
 
13
  # os
14
  .DS_Store
 
 
 
 
 
12
 
13
  # os
14
  .DS_Store
15
+
16
+ # local AI-tool state (machine-specific)
17
+ .codex/
18
+ .agents/
AGENTS.md ADDED
@@ -0,0 +1 @@
 
 
1
+ CLAUDE.md
CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@ Format: [Keep a Changelog](https://keepachangelog.com/). User-visible changes on
4
 
5
  ## [Unreleased]
6
 
 
 
 
 
7
  ### Added
8
  - Repository initialized from the public demo (v0.1 feature set: projects, tolerant CSV/XLSX
9
  ingestion, seeded constructs, MiniLM/mpnet/multilingual models, async runs with progress,
 
4
 
5
  ## [Unreleased]
6
 
7
+ ### Fixed
8
+ - Improved dashboard responsive layout, focus states, and table overflow handling on
9
+ narrow screens.
10
+
11
  ### Added
12
  - Repository initialized from the public demo (v0.1 feature set: projects, tolerant CSV/XLSX
13
  ingestion, seeded constructs, MiniLM/mpnet/multilingual models, async runs with progress,
backend/static/assets/index-CPF9wqlU.css ADDED
@@ -0,0 +1 @@
 
 
1
+ :root{--maroon: #7a1f3d;--maroon-dark: #5e1730;--ink: #1d2129;--muted: #667085;--line: #e5e7eb;--bg: #f7f7f8;--card: #ffffff;--ok: #157f3d;--err: #b42318;--accent-soft: #f6ebef}*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;color:var(--ink);background:var(--bg);font-size:14.5px;line-height:1.5}.app{display:flex;flex-direction:column;min-height:100vh}.header{background:var(--maroon);color:#fff;padding:14px 28px;display:flex;align-items:baseline;flex-wrap:wrap;gap:14px}.header h1{flex:0 0 auto;font-size:17px;margin:0;font-weight:650;letter-spacing:.2px;white-space:nowrap}.header .sub{flex:1 1 280px;min-width:0;font-size:12.5px;opacity:.85}.layout{display:flex;flex:1;min-width:0;min-height:0}.sidebar{width:250px;background:var(--card);border-right:1px solid var(--line);padding:18px 14px;flex-shrink:0}.sidebar h2{font-size:11.5px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);margin:0 0 10px 4px;display:flex;align-items:center;gap:8px}.sidebar h2 .count{background:var(--bg);border:1px solid var(--line);border-radius:999px;padding:0 8px;font-size:10.5px;letter-spacing:0;color:var(--muted)}.sidebar-filter{width:100%;padding:7px 10px;margin-bottom:8px;border:1px solid var(--line);border-radius:8px;font:inherit;font-size:13px;background:#fff;color:var(--ink)}.sidebar-filter:focus{outline:none;border-color:var(--maroon)}.project-list{max-height:calc(100vh - 210px);overflow-y:auto;margin:0 -4px;padding:0 4px}.project-item{display:block;width:100%;text-align:left;padding:9px 12px;margin-bottom:4px;border:1px solid transparent;border-radius:8px;background:none;cursor:pointer;font:inherit;color:var(--ink)}.project-item:hover{background:var(--bg)}.project-item.active{background:var(--accent-soft);border-color:var(--maroon);font-weight:600}.project-item .date{display:block;font-size:11.5px;color:var(--muted);font-weight:400}.main{flex:1;padding:22px 28px;overflow-y:auto;min-width:0}.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:18px 20px;margin-bottom:16px}.card h3{margin:0 0 4px;font-size:15px}.card .hint{color:var(--muted);font-size:12.5px;margin:0 0 12px}.step-badge{display:inline-flex;align-items:center;justify-content:center;width:21px;height:21px;border-radius:50%;background:var(--maroon);color:#fff;font-size:12px;font-weight:700;margin-right:8px;vertical-align:-3px}button{white-space:nowrap}button.primary{background:var(--maroon);color:#fff;border:none;padding:9px 18px;border-radius:8px;font:inherit;font-weight:600;cursor:pointer}button.primary:hover{background:var(--maroon-dark)}button.primary:disabled{background:#c9ccd1;cursor:not-allowed}button.ghost{background:none;border:1px solid var(--line);color:var(--ink);padding:8px 14px;border-radius:8px;font:inherit;cursor:pointer}button.ghost:hover{border-color:var(--maroon);color:var(--maroon)}button.linkish{background:none;border:none;color:var(--maroon);font:inherit;cursor:pointer;padding:0;text-decoration:underline}input[type=text],textarea,select{width:100%;padding:8px 10px;border:1px solid var(--line);border-radius:8px;font:inherit;background:#fff;color:var(--ink)}button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid rgba(122,31,61,.38);outline-offset:2px}input[type=file]{display:block;max-width:100%;margin-top:6px;font-size:13px;color:var(--muted)}input[type=file]::file-selector-button{background:#fff;border:1px solid var(--line);color:var(--ink);padding:7px 14px;border-radius:8px;font:inherit;font-size:13px;cursor:pointer;margin-right:10px}input[type=file]::file-selector-button:hover{border-color:var(--maroon);color:var(--maroon)}.row>button{align-self:flex-end;margin-bottom:1px}textarea{resize:vertical}label.field{display:block;margin-bottom:10px;font-size:13px;font-weight:600}label.field>*{margin-top:4px;font-weight:400}.row{display:flex;gap:14px;flex-wrap:wrap}.row>*{min-width:0}.row>.grow{flex:1;min-width:min(220px,100%)}.language-control{min-width:170px}.results-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:14px;flex-wrap:wrap}.result-actions{justify-content:flex-end}.result-actions a{display:inline-flex;text-decoration:none}.pill{display:inline-block;padding:2px 10px;border-radius:999px;font-size:11.5px;font-weight:600}.pill.completed{background:#e6f4ea;color:var(--ok)}.pill.running{background:#fff3e0;color:#b45309}.pill.queued{background:#eef2f7;color:var(--muted)}.pill.failed{background:#fdecea;color:var(--err)}.progress-track{background:var(--line);border-radius:999px;height:7px;overflow:hidden}.progress-fill{background:var(--maroon);height:100%;transition:width .4s ease}.warnings{background:#fff8e6;border:1px solid #f2dfa8;color:#7a5b00;border-radius:8px;padding:10px 14px}.error-banner{background:#fdecea;color:var(--err);border:1px solid #f5c6c0;padding:10px 14px;border-radius:8px;margin-bottom:14px;font-size:13px}.table-wrap{width:100%;overflow-x:auto}table.docs{width:100%;border-collapse:collapse;font-size:13px}table.docs th{text-align:left;color:var(--muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;padding:6px 8px;border-bottom:1px solid var(--line)}table.docs td{padding:7px 8px;border-bottom:1px solid var(--bg);vertical-align:top;overflow-wrap:anywhere}table.docs td.score{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}.stat-grid{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:4px}.stat{flex:1;min-width:110px;background:var(--bg);border-radius:8px;padding:10px 14px}.stat .v{font-size:20px;font-weight:700;font-variant-numeric:tabular-nums}.stat .k{font-size:11.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}.item-bar-row{display:flex;align-items:center;gap:10px;margin-bottom:7px}.item-bar-label{flex:1;font-size:12.5px;min-width:0;overflow-wrap:anywhere}.item-bar-track{flex:1.2;background:var(--bg);border-radius:999px;height:10px}.item-bar-fill{background:var(--maroon);opacity:.85;height:100%;border-radius:999px}.item-bar-val{width:52px;text-align:right;font-variant-numeric:tabular-nums;font-size:12.5px;font-weight:600}.construct-items{margin:8px 0 0;padding-left:20px;color:var(--muted);font-size:12.5px}.construct-items li{margin-bottom:2px}.meta-footer{font-size:12px;color:var(--muted);background:var(--bg);border-radius:8px;padding:10px 14px;margin-top:14px;font-variant-numeric:tabular-nums}.meta-footer code{font-size:11.5px}.muted{color:var(--muted)}.small{font-size:12.5px}.mt{margin-top:12px}@media (max-width: 820px){body{font-size:14px}.header{padding:12px 16px;align-items:flex-start;gap:2px 12px}.header h1{font-size:16px}.header .sub{flex:1 1 210px;font-size:12px;line-height:1.35}.layout{display:block}.sidebar{width:100%;border-right:0;border-bottom:1px solid var(--line);padding:14px}.project-list{max-height:220px;margin-right:0}.main{width:100%;padding:16px 14px 28px;overflow:visible}.card{padding:16px;margin-bottom:14px}.row,.results-toolbar,.result-actions{flex-direction:column;align-items:stretch;gap:10px}.row>.grow,.language-control{width:100%;min-width:0}.main .row>button,.main .row>a,.main .row>a>button,.results-toolbar>button{align-self:stretch;margin-bottom:0;width:100%}.stat-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.stat{min-width:0}.item-bar-row{display:grid;grid-template-columns:minmax(0,1fr) 56px;gap:6px 10px}.item-bar-track{grid-column:1 / -1;width:100%}.item-bar-val{width:auto}table.docs{min-width:520px}}@media (max-width: 460px){.header .sub{flex-basis:100%}.stat-grid{grid-template-columns:1fr}}
backend/static/assets/index-DPgajiM_.css DELETED
@@ -1 +0,0 @@
1
- :root{--maroon: #7a1f3d;--maroon-dark: #5e1730;--ink: #1d2129;--muted: #667085;--line: #e5e7eb;--bg: #f7f7f8;--card: #ffffff;--ok: #157f3d;--err: #b42318;--accent-soft: #f6ebef}*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;color:var(--ink);background:var(--bg);font-size:14.5px;line-height:1.5}.app{display:flex;flex-direction:column;min-height:100vh}.header{background:var(--maroon);color:#fff;padding:14px 28px;display:flex;align-items:baseline;gap:14px}.header h1{font-size:17px;margin:0;font-weight:650;letter-spacing:.2px}.header .sub{font-size:12.5px;opacity:.85}.layout{display:flex;flex:1;min-height:0}.sidebar{width:250px;background:var(--card);border-right:1px solid var(--line);padding:18px 14px;flex-shrink:0}.sidebar h2{font-size:11.5px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);margin:0 0 10px 4px;display:flex;align-items:center;gap:8px}.sidebar h2 .count{background:var(--bg);border:1px solid var(--line);border-radius:999px;padding:0 8px;font-size:10.5px;letter-spacing:0;color:var(--muted)}.sidebar-filter{width:100%;padding:7px 10px;margin-bottom:8px;border:1px solid var(--line);border-radius:8px;font:inherit;font-size:13px;background:#fff;color:var(--ink)}.sidebar-filter:focus{outline:none;border-color:var(--maroon)}.project-list{max-height:calc(100vh - 210px);overflow-y:auto;margin:0 -4px;padding:0 4px}.project-item{display:block;width:100%;text-align:left;padding:9px 12px;margin-bottom:4px;border:1px solid transparent;border-radius:8px;background:none;cursor:pointer;font:inherit;color:var(--ink)}.project-item:hover{background:var(--bg)}.project-item.active{background:var(--accent-soft);border-color:var(--maroon);font-weight:600}.project-item .date{display:block;font-size:11.5px;color:var(--muted);font-weight:400}.main{flex:1;padding:22px 28px;overflow-y:auto;min-width:0}.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:18px 20px;margin-bottom:16px}.card h3{margin:0 0 4px;font-size:15px}.card .hint{color:var(--muted);font-size:12.5px;margin:0 0 12px}.step-badge{display:inline-flex;align-items:center;justify-content:center;width:21px;height:21px;border-radius:50%;background:var(--maroon);color:#fff;font-size:12px;font-weight:700;margin-right:8px;vertical-align:-3px}button.primary{background:var(--maroon);color:#fff;border:none;padding:9px 18px;border-radius:8px;font:inherit;font-weight:600;cursor:pointer}button.primary:hover{background:var(--maroon-dark)}button.primary:disabled{background:#c9ccd1;cursor:not-allowed}button.ghost{background:none;border:1px solid var(--line);color:var(--ink);padding:8px 14px;border-radius:8px;font:inherit;cursor:pointer}button.ghost:hover{border-color:var(--maroon);color:var(--maroon)}button.linkish{background:none;border:none;color:var(--maroon);font:inherit;cursor:pointer;padding:0;text-decoration:underline}input[type=text],textarea,select{width:100%;padding:8px 10px;border:1px solid var(--line);border-radius:8px;font:inherit;background:#fff;color:var(--ink)}input[type=file]{display:block;margin-top:6px;font-size:13px;color:var(--muted)}input[type=file]::file-selector-button{background:#fff;border:1px solid var(--line);color:var(--ink);padding:7px 14px;border-radius:8px;font:inherit;font-size:13px;cursor:pointer;margin-right:10px}input[type=file]::file-selector-button:hover{border-color:var(--maroon);color:var(--maroon)}.row>button{align-self:flex-end;margin-bottom:1px}textarea{resize:vertical}label.field{display:block;margin-bottom:10px;font-size:13px;font-weight:600}label.field>*{margin-top:4px;font-weight:400}.row{display:flex;gap:14px;flex-wrap:wrap}.row>.grow{flex:1;min-width:220px}.pill{display:inline-block;padding:2px 10px;border-radius:999px;font-size:11.5px;font-weight:600}.pill.completed{background:#e6f4ea;color:var(--ok)}.pill.running{background:#fff3e0;color:#b45309}.pill.queued{background:#eef2f7;color:var(--muted)}.pill.failed{background:#fdecea;color:var(--err)}.progress-track{background:var(--line);border-radius:999px;height:7px;overflow:hidden}.progress-fill{background:var(--maroon);height:100%;transition:width .4s ease}.warnings{background:#fff8e6;border:1px solid #f2dfa8;color:#7a5b00;border-radius:8px;padding:10px 14px}.error-banner{background:#fdecea;color:var(--err);border:1px solid #f5c6c0;padding:10px 14px;border-radius:8px;margin-bottom:14px;font-size:13px}table.docs{width:100%;border-collapse:collapse;font-size:13px}table.docs th{text-align:left;color:var(--muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;padding:6px 8px;border-bottom:1px solid var(--line)}table.docs td{padding:7px 8px;border-bottom:1px solid var(--bg);vertical-align:top}table.docs td.score{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}.stat-grid{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:4px}.stat{flex:1;min-width:110px;background:var(--bg);border-radius:8px;padding:10px 14px}.stat .v{font-size:20px;font-weight:700;font-variant-numeric:tabular-nums}.stat .k{font-size:11.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}.item-bar-row{display:flex;align-items:center;gap:10px;margin-bottom:7px}.item-bar-label{flex:1;font-size:12.5px;min-width:0}.item-bar-track{flex:1.2;background:var(--bg);border-radius:999px;height:10px}.item-bar-fill{background:var(--maroon);opacity:.85;height:100%;border-radius:999px}.item-bar-val{width:52px;text-align:right;font-variant-numeric:tabular-nums;font-size:12.5px;font-weight:600}.construct-items{margin:8px 0 0;padding-left:20px;color:var(--muted);font-size:12.5px}.construct-items li{margin-bottom:2px}.meta-footer{font-size:12px;color:var(--muted);background:var(--bg);border-radius:8px;padding:10px 14px;margin-top:14px;font-variant-numeric:tabular-nums}.meta-footer code{font-size:11.5px}.muted{color:var(--muted)}.small{font-size:12.5px}.mt{margin-top:12px}
 
 
backend/static/assets/{index-DOnuuYyH.js → index-gF2iJ3kV.js} RENAMED
The diff for this file is too large to render. See raw diff
 
backend/static/index.html CHANGED
@@ -4,8 +4,8 @@
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>CCR Platform — Contextualized Construct Representations</title>
7
- <script type="module" crossorigin src="/assets/index-DOnuuYyH.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-DPgajiM_.css">
9
  </head>
10
  <body>
11
  <div id="root"></div>
 
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>CCR Platform — Contextualized Construct Representations</title>
7
+ <script type="module" crossorigin src="/assets/index-gF2iJ3kV.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-CPF9wqlU.css">
9
  </head>
10
  <body>
11
  <div id="root"></div>
frontend/src/ResultsView.jsx CHANGED
@@ -25,11 +25,11 @@ export default function ResultsView({ jobId, onBack }) {
25
 
26
  return (
27
  <>
28
- <div className="row" style={{ justifyContent: "space-between", marginBottom: 14 }}>
29
  <button className="ghost" onClick={onBack}>
30
  ← Back to workspace
31
  </button>
32
- <div className="row">
33
  <a href={api.exportUrl(jobId)}>
34
  <button className="primary">Export results CSV</button>
35
  </a>
@@ -148,22 +148,24 @@ function Stat({ k, v }) {
148
 
149
  function DocTable({ docs }) {
150
  return (
151
- <table className="docs">
152
- <thead>
153
- <tr>
154
- <th style={{ width: 60 }}>Score</th>
155
- <th>Text</th>
156
- </tr>
157
- </thead>
158
- <tbody>
159
- {docs.map((d) => (
160
- <tr key={d.row}>
161
- <td className="score">{d.score.toFixed(3)}</td>
162
- <td>{d.text}</td>
163
  </tr>
164
- ))}
165
- </tbody>
166
- </table>
 
 
 
 
 
 
 
 
167
  );
168
  }
169
 
 
25
 
26
  return (
27
  <>
28
+ <div className="results-toolbar">
29
  <button className="ghost" onClick={onBack}>
30
  ← Back to workspace
31
  </button>
32
+ <div className="row result-actions">
33
  <a href={api.exportUrl(jobId)}>
34
  <button className="primary">Export results CSV</button>
35
  </a>
 
148
 
149
  function DocTable({ docs }) {
150
  return (
151
+ <div className="table-wrap">
152
+ <table className="docs">
153
+ <thead>
154
+ <tr>
155
+ <th style={{ width: 60 }}>Score</th>
156
+ <th>Text</th>
 
 
 
 
 
 
157
  </tr>
158
+ </thead>
159
+ <tbody>
160
+ {docs.map((d) => (
161
+ <tr key={d.row}>
162
+ <td className="score">{d.score.toFixed(3)}</td>
163
+ <td>{d.text}</td>
164
+ </tr>
165
+ ))}
166
+ </tbody>
167
+ </table>
168
+ </div>
169
  );
170
  }
171
 
frontend/src/Workspace.jsx CHANGED
@@ -243,7 +243,7 @@ export default function Workspace({ project }) {
243
  model doesn&apos;t support it, you&apos;ll get a warning — never a silent result.
244
  </p>
245
  <div className="row">
246
- <div style={{ minWidth: 170 }}>
247
  <label className="field">
248
  Text language
249
  <select value={language} onChange={(e) => setLanguage(e.target.value)}>
@@ -282,50 +282,54 @@ export default function Workspace({ project }) {
282
  {jobs.length > 0 && (
283
  <div className="card">
284
  <h3>Runs</h3>
285
- <table className="docs">
286
- <thead>
287
- <tr>
288
- <th>Started</th>
289
- <th>Corpus</th>
290
- <th>Construct</th>
291
- <th style={{ width: "24%" }}>Status</th>
292
- <th />
293
- </tr>
294
- </thead>
295
- <tbody>
296
- {jobs.map((j) => (
297
- <tr key={j.id}>
298
- <td className="muted">{(j.started_at || j.created_at).replace("T", " ").slice(0, 16)}</td>
299
- <td>{j.corpus_filename}</td>
300
- <td>{j.construct_name}</td>
301
- <td>
302
- {j.status === "running" ? (
303
- <div className="progress-track" title={`${Math.round(j.progress * 100)}%`}>
304
- <div
305
- className="progress-fill"
306
- style={{ width: `${Math.max(3, j.progress * 100)}%` }}
307
- />
308
- </div>
309
- ) : (
310
- <span className={`pill ${j.status}`}>{j.status}</span>
311
- )}
312
- {j.status === "failed" && (
313
- <div className="small muted" title={j.error}>
314
- {j.error.split("\n").pop()}
315
- </div>
316
- )}
317
- </td>
318
- <td>
319
- {j.status === "completed" && (
320
- <button className="linkish" onClick={() => setViewJobId(j.id)}>
321
- View results
322
- </button>
323
- )}
324
- </td>
325
  </tr>
326
- ))}
327
- </tbody>
328
- </table>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  </div>
330
  )}
331
  </>
 
243
  model doesn&apos;t support it, you&apos;ll get a warning — never a silent result.
244
  </p>
245
  <div className="row">
246
+ <div className="language-control">
247
  <label className="field">
248
  Text language
249
  <select value={language} onChange={(e) => setLanguage(e.target.value)}>
 
282
  {jobs.length > 0 && (
283
  <div className="card">
284
  <h3>Runs</h3>
285
+ <div className="table-wrap">
286
+ <table className="docs">
287
+ <thead>
288
+ <tr>
289
+ <th>Started</th>
290
+ <th>Corpus</th>
291
+ <th>Construct</th>
292
+ <th style={{ width: "24%" }}>Status</th>
293
+ <th />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  </tr>
295
+ </thead>
296
+ <tbody>
297
+ {jobs.map((j) => (
298
+ <tr key={j.id}>
299
+ <td className="muted">
300
+ {(j.started_at || j.created_at).replace("T", " ").slice(0, 16)}
301
+ </td>
302
+ <td>{j.corpus_filename}</td>
303
+ <td>{j.construct_name}</td>
304
+ <td>
305
+ {j.status === "running" ? (
306
+ <div className="progress-track" title={`${Math.round(j.progress * 100)}%`}>
307
+ <div
308
+ className="progress-fill"
309
+ style={{ width: `${Math.max(3, j.progress * 100)}%` }}
310
+ />
311
+ </div>
312
+ ) : (
313
+ <span className={`pill ${j.status}`}>{j.status}</span>
314
+ )}
315
+ {j.status === "failed" && (
316
+ <div className="small muted" title={j.error}>
317
+ {j.error.split("\n").pop()}
318
+ </div>
319
+ )}
320
+ </td>
321
+ <td>
322
+ {j.status === "completed" && (
323
+ <button className="linkish" onClick={() => setViewJobId(j.id)}>
324
+ View results
325
+ </button>
326
+ )}
327
+ </td>
328
+ </tr>
329
+ ))}
330
+ </tbody>
331
+ </table>
332
+ </div>
333
  </div>
334
  )}
335
  </>
frontend/src/styles.css CHANGED
@@ -31,12 +31,20 @@ body {
31
  padding: 14px 28px;
32
  display: flex;
33
  align-items: baseline;
 
34
  gap: 14px;
35
  }
36
- .header h1 { font-size: 17px; margin: 0; font-weight: 650; letter-spacing: 0.2px; }
37
- .header .sub { font-size: 12.5px; opacity: 0.85; }
 
 
 
 
 
 
 
38
 
39
- .layout { display: flex; flex: 1; min-height: 0; }
40
 
41
  /* ---------- sidebar ---------- */
42
  .sidebar {
@@ -79,7 +87,7 @@ body {
79
 
80
  .card {
81
  background: var(--card); border: 1px solid var(--line);
82
- border-radius: 10px; padding: 18px 20px; margin-bottom: 16px;
83
  }
84
  .card h3 { margin: 0 0 4px; font-size: 15px; }
85
  .card .hint { color: var(--muted); font-size: 12.5px; margin: 0 0 12px; }
@@ -92,6 +100,8 @@ body {
92
  }
93
 
94
  /* ---------- controls ---------- */
 
 
95
  button.primary {
96
  background: var(--maroon); color: #fff; border: none;
97
  padding: 9px 18px; border-radius: 8px; font: inherit; font-weight: 600; cursor: pointer;
@@ -113,10 +123,18 @@ input[type="text"], textarea, select {
113
  border-radius: 8px; font: inherit; background: #fff; color: var(--ink);
114
  }
115
 
 
 
 
 
 
 
 
 
116
  /* File input: block-level with breathing room under its label, and a styled
117
  picker button so it matches the rest of the controls. */
118
  input[type="file"] {
119
- display: block; margin-top: 6px; font-size: 13px; color: var(--muted);
120
  }
121
  input[type="file"]::file-selector-button {
122
  background: #fff; border: 1px solid var(--line); color: var(--ink);
@@ -135,7 +153,19 @@ label.field { display: block; margin-bottom: 10px; font-size: 13px; font-weight:
135
  label.field > * { margin-top: 4px; font-weight: 400; }
136
 
137
  .row { display: flex; gap: 14px; flex-wrap: wrap; }
138
- .row > .grow { flex: 1; min-width: 220px; }
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
  /* ---------- misc ---------- */
141
  .pill {
@@ -160,12 +190,16 @@ label.field > * { margin-top: 4px; font-weight: 400; }
160
  padding: 10px 14px; border-radius: 8px; margin-bottom: 14px; font-size: 13px;
161
  }
162
 
 
163
  table.docs { width: 100%; border-collapse: collapse; font-size: 13px; }
164
  table.docs th {
165
  text-align: left; color: var(--muted); font-size: 11.5px; text-transform: uppercase;
166
  letter-spacing: 0.5px; padding: 6px 8px; border-bottom: 1px solid var(--line);
167
  }
168
- table.docs td { padding: 7px 8px; border-bottom: 1px solid var(--bg); vertical-align: top; }
 
 
 
169
  table.docs td.score { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }
170
 
171
  .stat-grid { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 4px; }
@@ -177,7 +211,7 @@ table.docs td.score { font-variant-numeric: tabular-nums; font-weight: 600; whit
177
  .stat .k { font-size: 11.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
178
 
179
  .item-bar-row { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; }
180
- .item-bar-label { flex: 1; font-size: 12.5px; min-width: 0; }
181
  .item-bar-track { flex: 1.2; background: var(--bg); border-radius: 999px; height: 10px; }
182
  .item-bar-fill { background: var(--maroon); opacity: 0.85; height: 100%; border-radius: 999px; }
183
  .item-bar-val { width: 52px; text-align: right; font-variant-numeric: tabular-nums; font-size: 12.5px; font-weight: 600; }
@@ -195,3 +229,77 @@ table.docs td.score { font-variant-numeric: tabular-nums; font-weight: 600; whit
195
  .muted { color: var(--muted); }
196
  .small { font-size: 12.5px; }
197
  .mt { margin-top: 12px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  padding: 14px 28px;
32
  display: flex;
33
  align-items: baseline;
34
+ flex-wrap: wrap;
35
  gap: 14px;
36
  }
37
+ .header h1 {
38
+ flex: 0 0 auto;
39
+ font-size: 17px;
40
+ margin: 0;
41
+ font-weight: 650;
42
+ letter-spacing: 0.2px;
43
+ white-space: nowrap;
44
+ }
45
+ .header .sub { flex: 1 1 280px; min-width: 0; font-size: 12.5px; opacity: 0.85; }
46
 
47
+ .layout { display: flex; flex: 1; min-width: 0; min-height: 0; }
48
 
49
  /* ---------- sidebar ---------- */
50
  .sidebar {
 
87
 
88
  .card {
89
  background: var(--card); border: 1px solid var(--line);
90
+ border-radius: 8px; padding: 18px 20px; margin-bottom: 16px;
91
  }
92
  .card h3 { margin: 0 0 4px; font-size: 15px; }
93
  .card .hint { color: var(--muted); font-size: 12.5px; margin: 0 0 12px; }
 
100
  }
101
 
102
  /* ---------- controls ---------- */
103
+ button { white-space: nowrap; }
104
+
105
  button.primary {
106
  background: var(--maroon); color: #fff; border: none;
107
  padding: 9px 18px; border-radius: 8px; font: inherit; font-weight: 600; cursor: pointer;
 
123
  border-radius: 8px; font: inherit; background: #fff; color: var(--ink);
124
  }
125
 
126
+ button:focus-visible,
127
+ input:focus-visible,
128
+ textarea:focus-visible,
129
+ select:focus-visible {
130
+ outline: 2px solid rgba(122, 31, 61, 0.38);
131
+ outline-offset: 2px;
132
+ }
133
+
134
  /* File input: block-level with breathing room under its label, and a styled
135
  picker button so it matches the rest of the controls. */
136
  input[type="file"] {
137
+ display: block; max-width: 100%; margin-top: 6px; font-size: 13px; color: var(--muted);
138
  }
139
  input[type="file"]::file-selector-button {
140
  background: #fff; border: 1px solid var(--line); color: var(--ink);
 
153
  label.field > * { margin-top: 4px; font-weight: 400; }
154
 
155
  .row { display: flex; gap: 14px; flex-wrap: wrap; }
156
+ .row > * { min-width: 0; }
157
+ .row > .grow { flex: 1; min-width: min(220px, 100%); }
158
+ .language-control { min-width: 170px; }
159
+ .results-toolbar {
160
+ display: flex;
161
+ align-items: center;
162
+ justify-content: space-between;
163
+ gap: 14px;
164
+ margin-bottom: 14px;
165
+ flex-wrap: wrap;
166
+ }
167
+ .result-actions { justify-content: flex-end; }
168
+ .result-actions a { display: inline-flex; text-decoration: none; }
169
 
170
  /* ---------- misc ---------- */
171
  .pill {
 
190
  padding: 10px 14px; border-radius: 8px; margin-bottom: 14px; font-size: 13px;
191
  }
192
 
193
+ .table-wrap { width: 100%; overflow-x: auto; }
194
  table.docs { width: 100%; border-collapse: collapse; font-size: 13px; }
195
  table.docs th {
196
  text-align: left; color: var(--muted); font-size: 11.5px; text-transform: uppercase;
197
  letter-spacing: 0.5px; padding: 6px 8px; border-bottom: 1px solid var(--line);
198
  }
199
+ table.docs td {
200
+ padding: 7px 8px; border-bottom: 1px solid var(--bg); vertical-align: top;
201
+ overflow-wrap: anywhere;
202
+ }
203
  table.docs td.score { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }
204
 
205
  .stat-grid { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 4px; }
 
211
  .stat .k { font-size: 11.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
212
 
213
  .item-bar-row { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; }
214
+ .item-bar-label { flex: 1; font-size: 12.5px; min-width: 0; overflow-wrap: anywhere; }
215
  .item-bar-track { flex: 1.2; background: var(--bg); border-radius: 999px; height: 10px; }
216
  .item-bar-fill { background: var(--maroon); opacity: 0.85; height: 100%; border-radius: 999px; }
217
  .item-bar-val { width: 52px; text-align: right; font-variant-numeric: tabular-nums; font-size: 12.5px; font-weight: 600; }
 
229
  .muted { color: var(--muted); }
230
  .small { font-size: 12.5px; }
231
  .mt { margin-top: 12px; }
232
+
233
+ @media (max-width: 820px) {
234
+ body { font-size: 14px; }
235
+
236
+ .header {
237
+ padding: 12px 16px;
238
+ align-items: flex-start;
239
+ gap: 2px 12px;
240
+ }
241
+ .header h1 { font-size: 16px; }
242
+ .header .sub {
243
+ flex: 1 1 210px;
244
+ font-size: 12px;
245
+ line-height: 1.35;
246
+ }
247
+
248
+ .layout { display: block; }
249
+ .sidebar {
250
+ width: 100%;
251
+ border-right: 0;
252
+ border-bottom: 1px solid var(--line);
253
+ padding: 14px;
254
+ }
255
+ .project-list {
256
+ max-height: 220px;
257
+ margin-right: 0;
258
+ }
259
+ .main {
260
+ width: 100%;
261
+ padding: 16px 14px 28px;
262
+ overflow: visible;
263
+ }
264
+ .card {
265
+ padding: 16px;
266
+ margin-bottom: 14px;
267
+ }
268
+
269
+ .row,
270
+ .results-toolbar,
271
+ .result-actions {
272
+ flex-direction: column;
273
+ align-items: stretch;
274
+ gap: 10px;
275
+ }
276
+ .row > .grow,
277
+ .language-control {
278
+ width: 100%;
279
+ min-width: 0;
280
+ }
281
+ .main .row > button,
282
+ .main .row > a,
283
+ .main .row > a > button,
284
+ .results-toolbar > button {
285
+ align-self: stretch;
286
+ margin-bottom: 0;
287
+ width: 100%;
288
+ }
289
+
290
+ .stat-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
291
+ .stat { min-width: 0; }
292
+ .item-bar-row {
293
+ display: grid;
294
+ grid-template-columns: minmax(0, 1fr) 56px;
295
+ gap: 6px 10px;
296
+ }
297
+ .item-bar-track { grid-column: 1 / -1; width: 100%; }
298
+ .item-bar-val { width: auto; }
299
+ table.docs { min-width: 520px; }
300
+ }
301
+
302
+ @media (max-width: 460px) {
303
+ .header .sub { flex-basis: 100%; }
304
+ .stat-grid { grid-template-columns: 1fr; }
305
+ }