Share library/extracted for schematic and PCB review.
PCB AI skips a second PDF exam (pintable cache, pdf_path=None) and the report gains a PCB exam section plus power-trace, thermal, via, Kelvin, and GND-stitch checks grounded in board geometry and extracted specs.
This commit is contained in:
@@ -2,12 +2,13 @@
|
||||
|
||||
import { use, useState, useCallback, useEffect, useLayoutEffect, useMemo, useRef, Suspense } from "react";
|
||||
import { Download, RotateCcw } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useOptionalUser } from "@/hooks/use-optional-auth";
|
||||
import { useReport } from "@/hooks/use-report";
|
||||
import { useReviewedFindings } from "@/hooks/use-reviewed-findings";
|
||||
import { ReportSummary } from "@/components/report/report-summary";
|
||||
import { FindingsList } from "@/components/report/findings-list";
|
||||
import { PcbExamSection } from "@/components/report/pcb-exam-section";
|
||||
import { FindingFocusView } from "@/components/report/finding-focus-view";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -28,6 +29,8 @@ interface FocusState {
|
||||
|
||||
function ReportContent({ projectId }: { projectId: string }) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const domainParam = searchParams.get("domain");
|
||||
const { report, graph, loading, error } = useReport(projectId);
|
||||
const { user } = useOptionalUser();
|
||||
const [focus, setFocus] = useState<FocusState | null>(null);
|
||||
@@ -349,7 +352,28 @@ function ReportContent({ projectId }: { projectId: string }) {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<FindingsList
|
||||
<>
|
||||
{domainParam !== "schema" && (
|
||||
<PcbExamSection
|
||||
findings={report.findings}
|
||||
onViewReference={handleViewReference}
|
||||
projectId={projectId}
|
||||
isReviewed={isReviewed}
|
||||
toggleReviewed={handleToggleReviewed}
|
||||
findingKey={(f) => keyByFinding.get(f) ?? getFindingKey(f, 0)}
|
||||
comments={comments}
|
||||
collaborators={collaborators}
|
||||
currentUserId={user?.id}
|
||||
currentUserName={user?.name ?? user?.email ?? "User"}
|
||||
onCommentAdded={handleCommentAdded}
|
||||
onCommentDeleted={handleCommentDeleted}
|
||||
onReportFinding={handleReportFinding}
|
||||
reportedFindingIds={reportedFindingIds}
|
||||
reviews={reviews}
|
||||
onReviewSaved={handleReviewSaved}
|
||||
/>
|
||||
)}
|
||||
<FindingsList
|
||||
findings={report.findings}
|
||||
graph={graph}
|
||||
onViewReference={handleViewReference}
|
||||
@@ -367,6 +391,7 @@ function ReportContent({ projectId }: { projectId: string }) {
|
||||
reviews={reviews}
|
||||
onReviewSaved={handleReviewSaved}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{focus && (
|
||||
|
||||
@@ -10,6 +10,7 @@ import { FindingComments } from "./finding-comments";
|
||||
import { FindingReviewControls } from "./finding-review-controls";
|
||||
import type { Finding, FindingComment, FindingReview, Collaborator } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { isLayoutFinding } from "@/lib/layout-finding";
|
||||
|
||||
const BORDER_COLOR: Record<string, string> = {
|
||||
ERROR: "border-l-rose-500",
|
||||
@@ -176,12 +177,7 @@ export function FindingCard({
|
||||
Automated check
|
||||
</span>
|
||||
)}
|
||||
{(finding.finding_id?.startsWith("PCB-") ||
|
||||
finding.source === "pcb_review" ||
|
||||
(finding.rule_id || "").startsWith("PE-PLC") ||
|
||||
(finding.rule_id || "").startsWith("PE-LAY") ||
|
||||
(finding.rule_id || "").startsWith("PE-SI") ||
|
||||
(finding.rule_id || "").startsWith("PE-DRT")) && (
|
||||
{isLayoutFinding(finding) && (
|
||||
<span className="inline-flex items-center rounded border border-emerald-500/30 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-medium text-emerald-800 dark:text-emerald-300">
|
||||
Layout
|
||||
</span>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ReportFilters } from "./report-filters";
|
||||
import { ReviewedSection } from "./reviewed-section";
|
||||
import type { Finding, FindingComment, FindingReview, FindingStatus, DesignGraph, Collaborator } from "@/lib/types";
|
||||
import { groupBy, getFindingKey } from "@/lib/utils";
|
||||
import { isLayoutFinding, isPcbExamFinding } from "@/lib/layout-finding";
|
||||
|
||||
interface FindingsListProps {
|
||||
findings: Finding[];
|
||||
@@ -85,19 +86,11 @@ export function FindingsList({ findings, graph, onViewReference, projectId, isRe
|
||||
const st = f.finding_id ? reviews?.[f.finding_id]?.state : undefined;
|
||||
if (st && st !== "open") return false;
|
||||
}
|
||||
if (isPcbExamFinding(f)) return false;
|
||||
if (domainParam === "layout") {
|
||||
const layout =
|
||||
(f.finding_id || "").startsWith("PCB-") ||
|
||||
f.source === "pcb_review" ||
|
||||
(f.rule_id || "").startsWith("PE-PLC") ||
|
||||
(f.rule_id || "").startsWith("PE-LAY") ||
|
||||
(f.rule_id || "").startsWith("PE-SI") ||
|
||||
(f.rule_id || "").startsWith("PE-DRT");
|
||||
if (!layout) return false;
|
||||
if (!isLayoutFinding(f)) return false;
|
||||
} else if (domainParam === "schema") {
|
||||
const layout =
|
||||
(f.finding_id || "").startsWith("PCB-");
|
||||
if (layout) return false;
|
||||
if (isLayoutFinding(f)) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@@ -176,7 +169,7 @@ export function FindingsList({ findings, graph, onViewReference, projectId, isRe
|
||||
onReviewSaved={onReviewSaved}
|
||||
/>
|
||||
))}
|
||||
{filtered.length === 0 && reviewedFindings.length === 0 && findings.length > 0 && (
|
||||
{filtered.length === 0 && reviewedFindings.length === 0 && findings.some((f) => !isPcbExamFinding(f)) && (
|
||||
<p className="text-sm text-muted-foreground text-center py-12">
|
||||
No findings match your filters.
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { CircuitBoard } from "lucide-react";
|
||||
import { FindingCard } from "@/components/report/finding-card";
|
||||
import { isPcbExamFinding } from "@/lib/layout-finding";
|
||||
import type { Finding, FindingComment, FindingReview, Collaborator } from "@/lib/types";
|
||||
|
||||
interface PcbExamSectionProps {
|
||||
findings: Finding[];
|
||||
onViewReference: (finding: Finding) => void;
|
||||
projectId: string;
|
||||
isReviewed: (key: string) => boolean;
|
||||
toggleReviewed: (key: string) => void;
|
||||
findingKey: (f: Finding) => string;
|
||||
comments?: Record<string, FindingComment[]>;
|
||||
collaborators?: Collaborator[];
|
||||
currentUserId?: string;
|
||||
currentUserName?: string;
|
||||
onCommentAdded?: (comment: FindingComment) => void;
|
||||
onCommentDeleted?: (commentId: string, findingId: string) => void;
|
||||
onReportFinding?: (finding: Finding) => void;
|
||||
reportedFindingIds?: Set<string>;
|
||||
reviews?: Record<string, FindingReview>;
|
||||
onReviewSaved?: (findingId: string, review: FindingReview) => void;
|
||||
}
|
||||
|
||||
export function PcbExamSection(props: PcbExamSectionProps) {
|
||||
const exam = props.findings.filter(isPcbExamFinding);
|
||||
if (exam.length === 0) return null;
|
||||
return (
|
||||
<section className="rounded-lg border border-emerald-500/30 bg-emerald-500/5 p-4 space-y-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold flex items-center gap-2">
|
||||
<CircuitBoard className="h-4 w-4" />
|
||||
PCB exam
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Layout-only AI using the shared datasheet library (no second PDF pass),
|
||||
plus power-trace / copper / thermal / via / Kelvin checks grounded in
|
||||
board geometry and extracted specs.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{exam.map((f) => {
|
||||
const key = props.findingKey(f);
|
||||
return (
|
||||
<FindingCard
|
||||
key={key}
|
||||
finding={f}
|
||||
onViewReference={props.onViewReference}
|
||||
checked={props.isReviewed(key)}
|
||||
onCheckedChange={() => props.toggleReviewed(key)}
|
||||
comments={f.finding_id ? props.comments?.[f.finding_id] : undefined}
|
||||
projectId={props.projectId}
|
||||
collaborators={props.collaborators}
|
||||
currentUserId={props.currentUserId}
|
||||
currentUserName={props.currentUserName}
|
||||
onCommentAdded={props.onCommentAdded}
|
||||
onCommentDeleted={props.onCommentDeleted}
|
||||
onReportFinding={props.onReportFinding}
|
||||
isReported={
|
||||
!!f.finding_id && props.reportedFindingIds?.has(f.finding_id)
|
||||
}
|
||||
review={f.finding_id ? props.reviews?.[f.finding_id] : undefined}
|
||||
onReviewSaved={props.onReviewSaved}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -10,8 +10,8 @@ const PCB_STAGES = [
|
||||
{ id: "parse_pcb", title: "Parse PCB", description: "Build layout_graph.json from .kicad_pcb" },
|
||||
{ id: "classify", title: "Classify domains", description: "Domains and functional groups" },
|
||||
{ id: "inventory", title: "Inventory traces", description: "Lengths, pairs, buses, Z0" },
|
||||
{ id: "checks", title: "Deterministic checks", description: "Placement, SI, pad nets, derating" },
|
||||
{ id: "ai_review", title: "AI datasheet exam", description: "Per-IC layout vs datasheet" },
|
||||
{ id: "checks", title: "Deterministic checks", description: "Placement, SI, power traces, thermal, Kelvin" },
|
||||
{ id: "ai_review", title: "PCB AI exam", description: "Layout vs shared library extraction (no second PDF pass)" },
|
||||
{ id: "write_report", title: "Write report", description: "pcb_report.json findings" },
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/** Layout / PCB-exam findings (merged report). */
|
||||
|
||||
export function isLayoutFinding(f: {
|
||||
finding_id?: string | null;
|
||||
source?: string | null;
|
||||
rule_id?: string | null;
|
||||
}): boolean {
|
||||
const id = f.finding_id || "";
|
||||
const rid = f.rule_id || "";
|
||||
return (
|
||||
id.startsWith("PCB-") ||
|
||||
f.source === "pcb_review" ||
|
||||
f.source === "pcb_power_thermal" ||
|
||||
rid.startsWith("PE-PLC") ||
|
||||
rid.startsWith("PE-LAY") ||
|
||||
rid.startsWith("PE-SI") ||
|
||||
rid.startsWith("PE-DRT") ||
|
||||
rid.startsWith("PE-PWR") ||
|
||||
rid.startsWith("PE-THM") ||
|
||||
rid.startsWith("PE-VIA") ||
|
||||
rid.startsWith("PE-KEL") ||
|
||||
rid.startsWith("PE-STCH")
|
||||
);
|
||||
}
|
||||
|
||||
export function isPcbExamFinding(f: {
|
||||
source?: string | null;
|
||||
rule_id?: string | null;
|
||||
}): boolean {
|
||||
const rid = f.rule_id || "";
|
||||
return (
|
||||
f.source === "pcb_review" ||
|
||||
f.source === "pcb_power_thermal" ||
|
||||
rid.startsWith("PE-PWR") ||
|
||||
rid.startsWith("PE-THM") ||
|
||||
rid.startsWith("PE-VIA") ||
|
||||
rid.startsWith("PE-KEL") ||
|
||||
rid.startsWith("PE-STCH")
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user