vxkyyy commited on
Commit
f41a080
Β·
1 Parent(s): 477f274

feat: add expandable artifact file list to HITL approval cards + download endpoint

Browse files

- ApprovalCard.tsx: artifact pill is now clickable toggle that expands
to show each file with type badge, name, description, and download link
- api.py: added GET /build/artifacts/{design_name}/{filename} endpoint
for individual artifact file downloads (with path traversal protection)
- hitl.css: added ac-artifact-* styles for the expandable file list

server/api.py CHANGED
@@ -14,7 +14,7 @@ from typing import Any, Dict, List, Optional
14
 
15
  from fastapi import Depends, FastAPI, HTTPException, Request
16
  from fastapi.middleware.cors import CORSMiddleware
17
- from fastapi.responses import StreamingResponse
18
  from pydantic import BaseModel
19
 
20
  from server.approval import approval_manager
@@ -1230,6 +1230,30 @@ def get_partial_artifacts(design_name: str):
1230
  return {"design_name": design_name, "artifacts": artifacts[:50]} # Cap at 50
1231
 
1232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1233
  def _classify_artifact(filename: str) -> str:
1234
  """Classify a file by its extension."""
1235
  ext = os.path.splitext(filename)[1].lower()
 
14
 
15
  from fastapi import Depends, FastAPI, HTTPException, Request
16
  from fastapi.middleware.cors import CORSMiddleware
17
+ from fastapi.responses import FileResponse, StreamingResponse
18
  from pydantic import BaseModel
19
 
20
  from server.approval import approval_manager
 
1230
  return {"design_name": design_name, "artifacts": artifacts[:50]} # Cap at 50
1231
 
1232
 
1233
+ @app.get("/build/artifacts/{design_name}/{filename}")
1234
+ def download_artifact(design_name: str, filename: str):
1235
+ """Download an individual artifact file from a design's output directory."""
1236
+ # Sanitize filename to prevent path traversal
1237
+ safe_name = os.path.basename(filename)
1238
+ if safe_name != filename or ".." in filename:
1239
+ raise HTTPException(status_code=400, detail="Invalid filename")
1240
+
1241
+ # Search workspace designs/ first, then OpenLane designs/
1242
+ search_dirs = [os.path.join(_repo_root(), "designs", design_name)]
1243
+ openlane_root = os.environ.get("OPENLANE_ROOT", os.path.expanduser("~/OpenLane"))
1244
+ search_dirs.append(os.path.join(openlane_root, "designs", design_name))
1245
+
1246
+ for base_dir in search_dirs:
1247
+ if not os.path.isdir(base_dir):
1248
+ continue
1249
+ for root_dir, _dirs, files in os.walk(base_dir):
1250
+ if safe_name in files:
1251
+ fpath = os.path.join(root_dir, safe_name)
1252
+ return FileResponse(fpath, filename=safe_name)
1253
+
1254
+ raise HTTPException(status_code=404, detail="Artifact not found")
1255
+
1256
+
1257
  def _classify_artifact(filename: str) -> str:
1258
  """Classify a file by its extension."""
1259
  ext = os.path.splitext(filename)[1].lower()
web/src/components/ApprovalCard.tsx CHANGED
@@ -1,4 +1,5 @@
1
  import React, { useState } from 'react';
 
2
 
3
  interface StageCompleteData {
4
  stage_name: string;
@@ -26,13 +27,30 @@ const STAGE_ICONS: Record<string, string> = {
26
  SIGNOFF: 'βœ“', SUCCESS: '✦', FAIL: 'βœ—',
27
  };
28
 
 
 
 
 
 
 
29
  function fmtStage(name: string): string {
30
  return name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
31
  }
32
 
33
- export const ApprovalCard: React.FC<Props> = ({ data, onApprove, onReject, isSubmitting }) => {
 
 
 
 
 
 
 
 
 
 
34
  const [showFeedback, setShowFeedback] = useState(false);
35
  const [feedback, setFeedback] = useState('');
 
36
 
37
  const hasWarnings = data.warnings && data.warnings.length > 0;
38
  const hasErrors = data.decisions?.some((d: string) => /error|fail/i.test(d));
@@ -58,9 +76,13 @@ export const ApprovalCard: React.FC<Props> = ({ data, onApprove, onReject, isSub
58
  {!hasErrors && hasWarnings && <span className="ac-badge ac-badge--warn">Warning</span>}
59
  </div>
60
  {artifactCount > 0 && (
61
- <span className="ac-artifact-pill">
 
 
 
62
  {artifactCount} artifact{artifactCount !== 1 ? 's' : ''}
63
- </span>
 
64
  )}
65
  </div>
66
 
@@ -69,6 +91,34 @@ export const ApprovalCard: React.FC<Props> = ({ data, onApprove, onReject, isSub
69
  {data.summary || `${fmtStage(data.stage_name)} completed successfully.`}
70
  </p>
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  {/* Next stage preview */}
73
  {hasNext && data.next_stage_preview && (
74
  <div className="ac-next-hint">
 
1
  import React, { useState } from 'react';
2
+ import { API_BASE } from '../api';
3
 
4
  interface StageCompleteData {
5
  stage_name: string;
 
27
  SIGNOFF: 'βœ“', SUCCESS: '✦', FAIL: 'βœ—',
28
  };
29
 
30
+ const ARTIFACT_TYPE_LABELS: Record<string, string> = {
31
+ rtl: 'RTL', waveform: 'Waveform', layout: 'Layout',
32
+ constraints: 'SDC', config: 'Config', script: 'Script',
33
+ formal: 'Formal', log: 'Log', report: 'Report', other: 'File',
34
+ };
35
+
36
  function fmtStage(name: string): string {
37
  return name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
38
  }
39
 
40
+ function guessType(name: string): string {
41
+ const ext = name.split('.').pop()?.toLowerCase() || '';
42
+ const map: Record<string, string> = {
43
+ v: 'rtl', sv: 'rtl', vcd: 'waveform', gds: 'layout', def: 'layout',
44
+ sdc: 'constraints', json: 'config', tcl: 'script', sby: 'formal',
45
+ log: 'log', csv: 'report',
46
+ };
47
+ return map[ext] || 'other';
48
+ }
49
+
50
+ export const ApprovalCard: React.FC<Props> = ({ data, designName, onApprove, onReject, isSubmitting }) => {
51
  const [showFeedback, setShowFeedback] = useState(false);
52
  const [feedback, setFeedback] = useState('');
53
+ const [artifactsExpanded, setArtifactsExpanded] = useState(false);
54
 
55
  const hasWarnings = data.warnings && data.warnings.length > 0;
56
  const hasErrors = data.decisions?.some((d: string) => /error|fail/i.test(d));
 
76
  {!hasErrors && hasWarnings && <span className="ac-badge ac-badge--warn">Warning</span>}
77
  </div>
78
  {artifactCount > 0 && (
79
+ <button
80
+ className="ac-artifact-pill ac-artifact-toggle"
81
+ onClick={() => setArtifactsExpanded(v => !v)}
82
+ >
83
  {artifactCount} artifact{artifactCount !== 1 ? 's' : ''}
84
+ <span className={`ac-artifact-chevron ${artifactsExpanded ? 'ac-artifact-chevron--open' : ''}`}>β€Ί</span>
85
+ </button>
86
  )}
87
  </div>
88
 
 
91
  {data.summary || `${fmtStage(data.stage_name)} completed successfully.`}
92
  </p>
93
 
94
+ {/* Artifact file list */}
95
+ {artifactsExpanded && data.artifacts && data.artifacts.length > 0 && (
96
+ <div className="ac-artifacts">
97
+ {data.artifacts.map((a, i) => {
98
+ const aType = guessType(a.name);
99
+ return (
100
+ <div key={i} className="ac-artifact-row">
101
+ <span className={`ac-artifact-type ac-artifact-type--${aType}`}>
102
+ {ARTIFACT_TYPE_LABELS[aType] || aType}
103
+ </span>
104
+ <span className="ac-artifact-name">{a.name}</span>
105
+ {a.description && (
106
+ <span className="ac-artifact-desc">{a.description}</span>
107
+ )}
108
+ <a
109
+ className="ac-artifact-dl"
110
+ href={`${API_BASE}/build/artifacts/${designName}/${encodeURIComponent(a.name)}`}
111
+ download
112
+ title={`Download ${a.name}`}
113
+ >
114
+ ↓
115
+ </a>
116
+ </div>
117
+ );
118
+ })}
119
+ </div>
120
+ )}
121
+
122
  {/* Next stage preview */}
123
  {hasNext && data.next_stage_preview && (
124
  <div className="ac-next-hint">
web/src/hitl.css CHANGED
@@ -1822,3 +1822,107 @@
1822
  }
1823
 
1824
  .ac-cancel-btn:hover { color: var(--text-secondary); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1822
  }
1823
 
1824
  .ac-cancel-btn:hover { color: var(--text-secondary); }
1825
+
1826
+ /* ── Artifact list (expandable) ─────────────────────────────────── */
1827
+ .ac-artifact-toggle {
1828
+ cursor: pointer;
1829
+ background: none;
1830
+ border: 1px solid var(--border-subtle);
1831
+ border-radius: 6px;
1832
+ padding: 0.175rem 0.5rem;
1833
+ display: inline-flex;
1834
+ align-items: center;
1835
+ gap: 0.25rem;
1836
+ transition: background 120ms, border-color 120ms;
1837
+ }
1838
+ .ac-artifact-toggle:hover {
1839
+ background: rgba(255,255,255,0.03);
1840
+ border-color: var(--text-tertiary);
1841
+ }
1842
+
1843
+ .ac-artifact-chevron {
1844
+ display: inline-block;
1845
+ font-size: 0.75rem;
1846
+ transition: transform 160ms ease;
1847
+ transform: rotate(0deg);
1848
+ }
1849
+ .ac-artifact-chevron--open {
1850
+ transform: rotate(90deg);
1851
+ }
1852
+
1853
+ .ac-artifacts {
1854
+ display: flex;
1855
+ flex-direction: column;
1856
+ gap: 0.25rem;
1857
+ padding: 0.5rem 0;
1858
+ border-top: 1px solid var(--border-subtle);
1859
+ border-bottom: 1px solid var(--border-subtle);
1860
+ margin: 0.25rem 0;
1861
+ }
1862
+
1863
+ .ac-artifact-row {
1864
+ display: flex;
1865
+ align-items: center;
1866
+ gap: 0.5rem;
1867
+ padding: 0.3125rem 0.375rem;
1868
+ border-radius: 4px;
1869
+ transition: background 100ms;
1870
+ }
1871
+ .ac-artifact-row:hover {
1872
+ background: rgba(255,255,255,0.025);
1873
+ }
1874
+
1875
+ .ac-artifact-type {
1876
+ font-size: 0.625rem;
1877
+ font-weight: 600;
1878
+ letter-spacing: 0.03em;
1879
+ text-transform: uppercase;
1880
+ padding: 0.1rem 0.375rem;
1881
+ border-radius: 3px;
1882
+ flex-shrink: 0;
1883
+ font-family: 'JetBrains Mono', monospace;
1884
+ }
1885
+ .ac-artifact-type--rtl { background: rgba(99,179,237,0.12); color: #63b3ed; }
1886
+ .ac-artifact-type--waveform { background: rgba(129,230,217,0.12); color: #81e6d9; }
1887
+ .ac-artifact-type--layout { background: rgba(183,148,244,0.12); color: #b794f4; }
1888
+ .ac-artifact-type--constraints{ background: rgba(246,173,85,0.12); color: #f6ad55; }
1889
+ .ac-artifact-type--config { background: rgba(160,174,192,0.12); color: #a0aec0; }
1890
+ .ac-artifact-type--script { background: rgba(160,174,192,0.12); color: #a0aec0; }
1891
+ .ac-artifact-type--formal { background: rgba(246,173,85,0.12); color: #f6ad55; }
1892
+ .ac-artifact-type--log { background: rgba(160,174,192,0.08); color: #718096; }
1893
+ .ac-artifact-type--report { background: rgba(72,187,120,0.12); color: #48bb78; }
1894
+ .ac-artifact-type--other { background: rgba(160,174,192,0.08); color: #718096; }
1895
+
1896
+ .ac-artifact-name {
1897
+ font-family: 'JetBrains Mono', monospace;
1898
+ font-size: 0.78rem;
1899
+ color: var(--text-primary);
1900
+ white-space: nowrap;
1901
+ overflow: hidden;
1902
+ text-overflow: ellipsis;
1903
+ min-width: 0;
1904
+ flex: 1;
1905
+ }
1906
+
1907
+ .ac-artifact-desc {
1908
+ font-size: 0.72rem;
1909
+ color: var(--text-tertiary);
1910
+ white-space: nowrap;
1911
+ overflow: hidden;
1912
+ text-overflow: ellipsis;
1913
+ max-width: 200px;
1914
+ }
1915
+
1916
+ .ac-artifact-dl {
1917
+ flex-shrink: 0;
1918
+ font-size: 0.8rem;
1919
+ color: var(--text-tertiary);
1920
+ text-decoration: none;
1921
+ padding: 0.125rem 0.25rem;
1922
+ border-radius: 3px;
1923
+ transition: color 120ms, background 120ms;
1924
+ }
1925
+ .ac-artifact-dl:hover {
1926
+ color: var(--text-primary);
1927
+ background: rgba(255,255,255,0.05);
1928
+ }