AgentIC / web /src /components /ApprovalCard.tsx
vxkyyy's picture
feat: add Supabase auth + 5 new pipeline stages
1d4d3e9
import React, { useState } from 'react';
import { API_BASE } from '../api';
interface StageCompleteData {
stage_name: string;
summary: string;
artifacts: Array<{ name: string; path: string; description: string }>;
decisions: string[];
warnings: string[];
next_stage_name: string;
next_stage_preview: string;
}
interface Props {
data: StageCompleteData;
designName: string;
jobId: string;
onApprove: () => void;
onReject: (feedback: string) => void;
isSubmitting: boolean;
}
const STAGE_ICONS: Record<string, string> = {
INIT: '⚙', SPEC: '◈', SPEC_VALIDATE: '⊘', HIERARCHY_EXPAND: '⊞', FEASIBILITY_CHECK: '⚖', CDC_ANALYZE: '↔', VERIFICATION_PLAN: '☑', RTL_GEN: '⌨', RTL_FIX: '◪',
VERIFICATION: '◉', FORMAL_VERIFY: '◈', COVERAGE_CHECK: '◎',
REGRESSION: '↺', SDC_GEN: '⧗', FLOORPLAN: '▣',
HARDENING: '⬡', CONVERGENCE_REVIEW: '◎', ECO_PATCH: '⟴',
SIGNOFF: '✓', SUCCESS: '✦', FAIL: '✗',
};
const ARTIFACT_TYPE_LABELS: Record<string, string> = {
rtl: 'RTL', waveform: 'Waveform', layout: 'Layout',
constraints: 'SDC', config: 'Config', script: 'Script',
formal: 'Formal', log: 'Log', report: 'Report', other: 'File',
};
function fmtStage(name: string): string {
return name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}
function guessType(name: string): string {
const ext = name.split('.').pop()?.toLowerCase() || '';
const map: Record<string, string> = {
v: 'rtl', sv: 'rtl', vcd: 'waveform', gds: 'layout', def: 'layout',
sdc: 'constraints', json: 'config', tcl: 'script', sby: 'formal',
log: 'log', csv: 'report',
};
return map[ext] || 'other';
}
export const ApprovalCard: React.FC<Props> = ({ data, designName, jobId, onApprove, onReject, isSubmitting }) => {
const [showFeedback, setShowFeedback] = useState(false);
const [feedback, setFeedback] = useState('');
const [artifactsExpanded, setArtifactsExpanded] = useState(false);
const hasWarnings = data.warnings && data.warnings.length > 0;
const hasErrors = data.decisions?.some((d: string) => /error|fail/i.test(d));
const artifactCount = data.artifacts?.length || 0;
const hasNext = data.next_stage_name && data.next_stage_name !== 'DONE';
const icon = STAGE_ICONS[data.stage_name] || '◆';
const handleReject = () => {
onReject(feedback);
setShowFeedback(false);
setFeedback('');
};
return (
<div className={`ac-card ${hasErrors ? 'ac-card--error' : hasWarnings ? 'ac-card--warn' : 'ac-card--ok'}`}>
{/* Header — stage identity */}
<div className="ac-header">
<div className="ac-stage-id">
<span className="ac-stage-symbol">{icon}</span>
<span className="ac-stage-label">{fmtStage(data.stage_name)}</span>
{hasErrors && <span className="ac-badge ac-badge--error">Issue detected</span>}
{!hasErrors && hasWarnings && <span className="ac-badge ac-badge--warn">Warning</span>}
</div>
{artifactCount > 0 && (
<button
className="ac-artifact-pill ac-artifact-toggle"
onClick={() => setArtifactsExpanded(v => !v)}
>
{artifactCount} artifact{artifactCount !== 1 ? 's' : ''}
<span className={`ac-artifact-chevron ${artifactsExpanded ? 'ac-artifact-chevron--open' : ''}`}></span>
</button>
)}
</div>
{/* Summary — primary content */}
<p className="ac-summary">
{data.summary || `${fmtStage(data.stage_name)} completed successfully.`}
</p>
{/* Artifact file list */}
{artifactsExpanded && data.artifacts && data.artifacts.length > 0 && (
<div className="ac-artifacts">
{data.artifacts.map((a, i) => {
const aType = guessType(a.name);
return (
<div key={i} className="ac-artifact-row">
<span className={`ac-artifact-type ac-artifact-type--${aType}`}>
{ARTIFACT_TYPE_LABELS[aType] || aType}
</span>
<span className="ac-artifact-name">{a.name}</span>
{a.description && (
<span className="ac-artifact-desc">{a.description}</span>
)}
<a
className="ac-artifact-dl"
href={`${API_BASE}/build/artifacts/${designName}/${encodeURIComponent(a.name)}`}
download
title={`Download ${a.name}`}
>
</a>
</div>
);
})}
</div>
)}
{/* Next stage preview */}
{hasNext && data.next_stage_preview && (
<div className="ac-next-hint">
<span className="ac-next-arrow"></span>
<span className="ac-next-text">{data.next_stage_preview}</span>
</div>
)}
{/* Action footer */}
<div className="ac-footer">
{/* Stage report downloads */}
{jobId && (
<div className="ac-report-downloads">
<a
className="ac-report-btn"
href={`${API_BASE}/report/${jobId}/stage/${data.stage_name}.pdf`}
download
title="Download stage report as PDF"
>
↓ PDF
</a>
<a
className="ac-report-btn"
href={`${API_BASE}/report/${jobId}/stage/${data.stage_name}.docx`}
download
title="Download stage report as DOCX"
>
↓ DOCX
</a>
</div>
)}
{!showFeedback ? (
<>
<button className="ac-give-feedback" onClick={() => setShowFeedback(true)}>
Give feedback
</button>
<button className="ac-continue-btn" onClick={onApprove} disabled={isSubmitting}>
{isSubmitting ? 'Continuing…' : 'Continue'}
{!isSubmitting && <span className="ac-chevron"></span>}
</button>
</>
) : (
<div className="ac-feedback-row">
<input
className="ac-feedback-input"
type="text"
placeholder="What should the agent do differently?"
value={feedback}
onChange={e => setFeedback(e.target.value)}
autoFocus
onKeyDown={e => {
if (e.key === 'Enter') handleReject();
if (e.key === 'Escape') { setShowFeedback(false); setFeedback(''); }
}}
/>
<button className="ac-reject-btn" onClick={handleReject} disabled={isSubmitting}>
{isSubmitting ? 'Sending…' : 'Reject & redirect'}
</button>
<button className="ac-cancel-btn" onClick={() => { setShowFeedback(false); setFeedback(''); }}>
Cancel
</button>
</div>
)}
</div>
</div>
);
};