Uanderson Silva
create new frontend interface
fc99222
Raw
History Blame Contribute Delete
15.4 kB
import type { CoderResult, AuditorResult, TesterResult, Finding } from "../types";
import type { DiffLine } from "../utils/diff";
import type { ArtifactTab } from "../App";
const C = {
bg: "#0d1117",
surface: "#161b22",
border: "#21262d",
text: "#c9d1d9",
muted: "#8b949e",
dim: "#484f58",
accent: "#58a6ff",
green: "#3fb950",
red: "#f85149",
yellow: "#d29922",
orange: "#db6d28",
addedBg: "rgba(46, 160, 67, 0.1)",
removedBg: "rgba(248, 81, 73, 0.09)",
};
const SEVERITY_COLOR: Record<string, string> = {
high: C.red,
medium: C.yellow,
low: "#58a6ff",
};
interface Props {
coder: CoderResult | null;
auditor: AuditorResult | null;
tester: TesterResult | null;
contractDiff: DiffLine[] | null;
pocDiff: DiffLine[] | null;
selectedTab: ArtifactTab;
onSelectTab: (tab: ArtifactTab) => void;
diffMode: Record<string, boolean>;
onToggleDiff: (key: string) => void;
}
export function ArtifactPanel({
coder,
auditor,
tester,
contractDiff,
pocDiff,
selectedTab,
onSelectTab,
diffMode,
onToggleDiff,
}: Props) {
const findingCount = auditor?.findings?.length ?? 0;
const hasPoc = !!tester?.pocCode;
const treeItems: {
id: ArtifactTab;
icon: string;
name: string;
badge?: string;
badgeColor?: string;
available: boolean;
}[] = [
{
id: "contract",
icon: "◈",
name: "Contract.sol",
badge: coder ? "sol" : undefined,
badgeColor: C.accent,
available: !!coder,
},
{
id: "findings",
icon: "⚑",
name: "findings/",
badge: auditor ? (findingCount > 0 ? `${findingCount} issue${findingCount > 1 ? "s" : ""}` : "clean") : undefined,
badgeColor: auditor ? (findingCount > 0 ? C.red : C.green) : C.dim,
available: !!auditor,
},
{
id: "poc",
icon: "◈",
name: "ExploitTest.t.sol",
badge: tester ? tester.status : undefined,
badgeColor: tester?.status === "success" ? C.green : tester?.status === "failed" ? C.red : C.yellow,
available: hasPoc,
},
];
return (
<div
style={{
flex: 1,
minWidth: 0,
display: "flex",
flexDirection: "column",
overflow: "hidden",
borderLeft: `1px solid ${C.border}`,
background: C.bg,
}}
>
{/* Panel titlebar */}
<div
style={{
height: 38,
display: "flex",
alignItems: "center",
gap: 8,
padding: "0 16px",
borderBottom: `1px solid ${C.border}`,
flexShrink: 0,
}}
>
<span style={{ fontSize: 10, color: C.dim, letterSpacing: "0.12em", textTransform: "uppercase" }}>
explorer
</span>
</div>
{/* File tree */}
<div
style={{
flexShrink: 0,
borderBottom: `1px solid ${C.border}`,
padding: "6px 0",
}}
>
<div style={{ padding: "3px 16px 4px", display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ fontSize: 10, color: C.dim, letterSpacing: "0.08em" }}></span>
<span style={{ fontSize: 11, color: C.muted, letterSpacing: "0.05em" }}>artifacts</span>
</div>
{treeItems.map((item) => {
const selected = selectedTab === item.id;
return (
<button
key={item.id}
onClick={() => item.available && onSelectTab(item.id)}
disabled={!item.available}
style={{
display: "flex",
alignItems: "center",
gap: 8,
width: "100%",
padding: "4px 16px 4px 28px",
background: selected ? "rgba(88, 166, 255, 0.08)" : "transparent",
border: "none",
borderLeft: selected ? `2px solid ${C.accent}` : "2px solid transparent",
cursor: item.available ? "pointer" : "default",
textAlign: "left",
color: selected ? C.text : item.available ? C.muted : C.dim,
fontSize: 13,
transition: "background 0.1s",
}}
>
<span style={{ fontSize: 10, color: selected ? C.accent : item.available ? C.dim : "#2d333b" }}>
{item.icon}
</span>
<span style={{ flex: 1 }}>{item.name}</span>
{item.badge && (
<span
style={{
fontSize: 10,
color: item.badgeColor,
padding: "1px 5px",
borderRadius: 3,
border: `1px solid ${item.badgeColor}30`,
background: `${item.badgeColor}12`,
}}
>
{item.badge}
</span>
)}
</button>
);
})}
</div>
{/* Content area */}
<div style={{ flex: 1, overflow: "hidden", display: "flex", flexDirection: "column" }}>
{selectedTab === "contract" && coder && contractDiff ? (
<CodeViewer
title="Contract.sol"
diff={contractDiff}
showDiff={diffMode["contract"] ?? false}
onToggleDiff={() => onToggleDiff("contract")}
/>
) : selectedTab === "findings" && auditor ? (
<FindingsViewer findings={auditor.findings} reviewSummary={coder?.reviewSummary} />
) : selectedTab === "poc" && tester?.pocCode && pocDiff ? (
<CodeViewer
title="ExploitTest.t.sol"
diff={pocDiff}
showDiff={diffMode["poc"] ?? false}
onToggleDiff={() => onToggleDiff("poc")}
status={tester.status}
iterations={tester.iterations}
/>
) : (
<EmptyState />
)}
</div>
</div>
);
}
function CodeViewer({
title,
diff,
showDiff,
onToggleDiff,
status,
iterations,
}: {
title: string;
diff: DiffLine[];
showDiff: boolean;
onToggleDiff: () => void;
status?: string;
iterations?: number;
}) {
const addedCount = diff.filter((l) => l.type === "added").length;
const removedCount = diff.filter((l) => l.type === "removed").length;
const totalLines = diff.filter((l) => l.type !== "removed").length;
const hasDiff = addedCount > 0 || removedCount > 0;
const hasChanges =
diff.some((l) => l.type === "removed") ||
diff.some((l) => l.type === "added" && diff.some((x) => x.type === "unchanged"));
return (
<div style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }}>
{/* Toolbar */}
<div
style={{
height: 36,
display: "flex",
alignItems: "center",
padding: "0 16px",
borderBottom: `1px solid ${C.border}`,
gap: 10,
flexShrink: 0,
background: C.bg,
}}
>
<span style={{ fontSize: 12, color: C.muted }}>{title}</span>
{status && (
<span
style={{
fontSize: 10,
color: status === "success" ? C.green : status === "failed" ? C.red : C.yellow,
padding: "1px 6px",
border: `1px solid currentColor`,
borderRadius: 3,
opacity: 0.8,
}}
>
{status}
</span>
)}
{iterations !== undefined && iterations > 0 && (
<span style={{ fontSize: 10, color: C.dim }}>{iterations} iter</span>
)}
<span style={{ flex: 1 }} />
{hasDiff && (
<>
{hasChanges && (
<span style={{ fontSize: 10, color: C.dim }}>
<span style={{ color: C.green }}>+{addedCount}</span>{" "}
<span style={{ color: C.red }}>-{removedCount}</span>
</span>
)}
<button
onClick={onToggleDiff}
style={{
padding: "2px 8px",
fontSize: 11,
background: showDiff ? "rgba(88, 166, 255, 0.12)" : "transparent",
border: `1px solid ${showDiff ? C.accent : C.border}`,
color: showDiff ? C.accent : C.muted,
cursor: "pointer",
borderRadius: 3,
fontFamily: "inherit",
}}
>
diff
</button>
</>
)}
<span style={{ fontSize: 10, color: C.dim }}>{totalLines}L</span>
</div>
{/* Code */}
<div style={{ flex: 1, overflow: "auto", background: "#0a0e14" }}>
<table
style={{
width: "100%",
borderCollapse: "collapse",
fontSize: 12.5,
lineHeight: 1.65,
}}
>
<tbody>
{diff.map((dl, idx) => {
if (!showDiff && dl.type === "removed") return null;
const isAdded = showDiff && dl.type === "added";
const isRemoved = showDiff && dl.type === "removed";
return (
<tr
key={idx}
style={{
background: isAdded ? C.addedBg : isRemoved ? C.removedBg : "transparent",
}}
>
{showDiff && (
<td
style={{
width: 18,
textAlign: "center",
color: isAdded ? C.green : isRemoved ? C.red : "transparent",
fontSize: 11,
userSelect: "none",
paddingLeft: 8,
paddingRight: 4,
}}
>
{isAdded ? "+" : isRemoved ? "−" : " "}
</td>
)}
<td
style={{
width: 44,
textAlign: "right",
paddingRight: 14,
paddingLeft: 8,
color: isRemoved ? "#5e3535" : C.dim,
userSelect: "none",
fontSize: 11,
}}
>
{dl.lineNo > 0 ? dl.lineNo : ""}
</td>
<td
style={{
padding: "0 16px 0 0",
color: isAdded ? "#b5e3b5" : isRemoved ? "#e5a0a0" : C.text,
whiteSpace: "pre",
fontFamily: "inherit",
}}
>
{dl.line}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}
function FindingsViewer({ findings, reviewSummary }: { findings: Finding[]; reviewSummary?: string }) {
return (
<div style={{ flex: 1, overflow: "auto", padding: 20 }}>
{/* Review summary */}
{reviewSummary && (
<div style={{ marginBottom: 20 }}>
<div
style={{
fontSize: 10,
color: C.dim,
letterSpacing: "0.1em",
textTransform: "uppercase",
marginBottom: 8,
}}
>
coder / security-review
</div>
<div
style={{
background: C.surface,
border: `1px solid ${C.border}`,
borderRadius: 6,
padding: 14,
fontSize: 12.5,
color: C.muted,
lineHeight: 1.7,
whiteSpace: "pre-wrap",
}}
>
{reviewSummary}
</div>
</div>
)}
{/* Findings */}
<div
style={{
fontSize: 10,
color: C.dim,
letterSpacing: "0.1em",
textTransform: "uppercase",
marginBottom: 8,
}}
>
auditor / findings — {findings.length} issue{findings.length !== 1 ? "s" : ""}
</div>
{findings.length === 0 ? (
<div
style={{
background: C.surface,
border: `1px solid ${C.border}`,
borderRadius: 6,
padding: 16,
color: C.green,
fontSize: 13,
}}
>
✓ Nenhuma vulnerabilidade encontrada.
</div>
) : (
findings.map((f, i) => <FindingCard key={i} finding={f} index={i} />)
)}
</div>
);
}
function FindingCard({ finding, index }: { finding: Finding; index: number }) {
const color = SEVERITY_COLOR[finding.severity] ?? C.muted;
return (
<div
style={{
background: C.surface,
border: `1px solid ${C.border}`,
borderLeft: `3px solid ${color}`,
borderRadius: 6,
marginBottom: 12,
overflow: "hidden",
animation: "fadein 0.2s ease",
}}
>
{/* Header */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: "10px 14px",
borderBottom: `1px solid ${C.border}`,
}}
>
<span
style={{
fontSize: 9,
fontWeight: 700,
color: color,
padding: "2px 6px",
border: `1px solid ${color}`,
borderRadius: 3,
letterSpacing: "0.1em",
background: `${color}15`,
}}
>
{finding.severity.toUpperCase()}
</span>
<span style={{ fontSize: 13, color: C.text, fontWeight: 600 }}>
[{index + 1}] {finding.title}
</span>
{finding.location && <span style={{ marginLeft: "auto", fontSize: 10, color: C.dim }}>{finding.location}</span>}
</div>
{/* Body */}
<div style={{ padding: 14 }}>
<p style={{ fontSize: 12.5, color: C.muted, lineHeight: 1.7, marginBottom: 10 }}>{finding.description}</p>
{finding.codeSnippet && (
<div
style={{
background: "#0a0e14",
border: `1px solid ${C.border}`,
borderRadius: 4,
padding: "8px 12px",
marginBottom: 10,
fontSize: 12,
color: "#b5c0cc",
whiteSpace: "pre",
overflow: "auto",
maxHeight: 140,
fontFamily: "inherit",
}}
>
{finding.codeSnippet}
</div>
)}
<div style={{ fontSize: 12, color: C.muted, marginBottom: 6 }}>
<span style={{ color: C.dim }}>recomendação: </span>
{finding.recommendation}
</div>
<div style={{ fontSize: 11, color: C.dim, marginTop: 8 }}>
confiança: {Math.round(finding.judgeReview.confidence)}% — {finding.judgeReview.review}
</div>
</div>
</div>
);
}
function EmptyState() {
return (
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
color: C.dim,
gap: 12,
}}
>
<div style={{ fontSize: 28, opacity: 0.3 }}></div>
<div style={{ fontSize: 12, letterSpacing: "0.05em" }}>aguardando execução do pipeline</div>
</div>
);
}