Default to DeepSeek V4.1 Flash, show API cost in USD, and re-analyze after replacing BOM and netlist.

V4.1 is natively multimodal so every stage uses deepseek-flash; pricing and the UI now surface dollars instead of empty credits. KiCad netlists and neighborhood fingerprints land in the same cut so a second run can keep the project and skip unchanged ICs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-10 20:55:35 +02:00
co-authored by Cursor
parent d454cf75af
commit 61f85f519b
29 changed files with 1174 additions and 118 deletions
@@ -463,6 +463,8 @@ interface CreateProjectDialogProps {
onRerunDone?: () => void;
cloneAsNewProject?: Project | null;
onCloneAsNewDone?: () => void;
/** Hide the "New Project" trigger — used when the parent opens rerun mode. */
hideTrigger?: boolean;
}
function countNetsInNetlist(text: string): number {
@@ -475,6 +477,16 @@ function isEdifNetlist(text: string): boolean {
return text.slice(0, 1024).trimStart().slice(0, 5).toLowerCase() === "(edif";
}
function isKicadNetlist(text: string): boolean {
const head = text.slice(0, 2048).trimStart().slice(0, 40).toLowerCase();
return (
head.startsWith("(kicad_sch") ||
head.startsWith("(export") ||
head.startsWith("<?xml") ||
head.startsWith("<export")
);
}
function SuggestedDatasheetLinks({ urls }: { urls: string[] }) {
if (!urls.length) return null;
return (
@@ -512,6 +524,7 @@ export function CreateProjectDialog({
onRerunDone,
cloneAsNewProject,
onCloneAsNewDone,
hideTrigger = false,
}: CreateProjectDialogProps) {
const [open, setOpen] = useState(false);
const [step, setStep] = useState<WizardStep>("details");
@@ -889,6 +902,12 @@ export function CreateProjectDialog({
setNetlistIsEdif(true);
return;
}
if (isKicadNetlist(text)) {
setNetlistNetCount(null);
setNetlistPreview([]);
setNetlistIsEdif(false);
return;
}
setNetlistIsEdif(false);
setNetlistNetCount(countNetsInNetlist(text));
try {
@@ -1152,20 +1171,20 @@ export function CreateProjectDialog({
// the user just uploaded a new BOM via the modal haven't been pushed
// to the backend yet, so skip until there's something to estimate.
useEffect(() => {
if (!authEnabled) return; // OSS mode: no credits — no estimate UI
if (!existingProjectId) return;
if (step !== activeSteps[activeSteps.length - 1]?.key) return;
if (!initialBomFile) return;
let cancelled = false;
setEstimateLoading(true);
Promise.all([
fetchPipelineEstimate(existingProjectId),
fetchCredits(),
])
.then(([est, cr]) => {
const jobs: Promise<unknown>[] = [fetchPipelineEstimate(existingProjectId)];
if (authEnabled) jobs.push(fetchCredits());
Promise.all(jobs)
.then((results) => {
if (cancelled) return;
setEstimate(est);
setBalance(cr.balance);
setEstimate(results[0] as CostEstimate);
if (authEnabled && results[1]) {
setBalance((results[1] as { balance: number }).balance);
}
})
.catch(() => {
/* Estimate is best-effort; swallow so the Run button still works. */
@@ -1940,7 +1959,7 @@ export function CreateProjectDialog({
else resetAndClose();
}}
>
{disabled ? (
{hideTrigger ? null : disabled ? (
<Tooltip>
<TooltipTrigger
render={
@@ -2060,8 +2079,8 @@ export function CreateProjectDialog({
</div>
<div className="space-y-1.5">
<FileUploadZone
label="Netlist (.asc / .net / .txt / .edn)"
accept=".asc,.net,.NET,.txt,.edn,.edif,.edf"
label="Netlist (.asc / .net / .edn / .kicad_sch)"
accept=".asc,.net,.NET,.txt,.edn,.edif,.edf,.xml,.kicad_sch,.kicad_net"
files={netlistFile ? [netlistFile] : []}
onFilesChange={handleNetlistChange}
/>
@@ -2076,11 +2095,11 @@ export function CreateProjectDialog({
</p>
) : netlistFile ? (
<p className="text-[11px] text-muted-foreground leading-tight px-1">
EDIF detected net count will appear after upload.
Net count will appear after upload.
</p>
) : (
<p className="text-[11px] text-muted-foreground leading-tight px-1">
PADS-PCB ASCII or EDIF 2.0.0. .asc / .net / .txt / .edn all work.{" "}
PADS-PCB, EDIF, or KiCad netlist / .kicad_sch.{" "}
<a
href="/file-guide#the-netlist"
target="_blank"
@@ -3056,20 +3075,27 @@ export function CreateProjectDialog({
<Loader2 className="h-3 w-3 animate-spin" />
Estimating
</span>
) : estimate && balance !== null ? (
) : estimate ? (
<>
<span>
Balance:{" "}
<span className="font-mono text-foreground">
{balance.toFixed(2)}
{balance !== null && (
<span>
Balance:{" "}
<span className="font-mono text-foreground">
{balance.toFixed(2)}
</span>
</span>
</span>
)}
<span>
Est:{" "}
<span className="font-mono text-foreground">
{estimate.credits_low.toFixed(2)}{estimate.credits_high.toFixed(2)}
</span>{" "}
credits
${estimate.api_cost_low.toFixed(2)}${estimate.api_cost_high.toFixed(2)}
</span>
{authEnabled && (
<>
{" "}
({estimate.credits_low.toFixed(2)}{estimate.credits_high.toFixed(2)} credits)
</>
)}
</span>
{!canRunRerun && (
<span className="flex items-center gap-1 text-amber-600 dark:text-amber-400">
@@ -36,6 +36,13 @@ export function ProjectCard({
const checkedCount = useReviewedCount(project.id);
const isCancelled = project.status === "cancelled";
const isDraft = project.status === "draft";
const canReplaceFiles =
!isShared &&
onRerun != null &&
(project.status === "complete" ||
project.status === "error" ||
project.status === "cancelled" ||
project.status === "draft");
const opensModalOnClick = isDraft && !isShared && onRerun != null;
async function handleDelete(e: React.MouseEvent) {
@@ -79,10 +86,10 @@ export function ProjectCard({
<Badge variant="outline" className={cn("text-xs capitalize", STATUS_STYLES[project.status])}>
{project.status}
</Badge>
{isCancelled && !isShared && onRerun && (
{canReplaceFiles && (
<button
onClick={handleRerun}
title="Rerun project"
title="Replace files and re-analyze"
className="p-1 rounded-md text-muted-foreground hover:text-emerald-600 dark:hover:text-emerald-400 hover:bg-emerald-500/10 transition-colors"
>
<RotateCcw className="h-3.5 w-3.5" />
@@ -71,6 +71,13 @@ function ProjectRow({
const checkedCount = useReviewedCount(project.id);
const isCancelled = project.status === "cancelled";
const isDraft = project.status === "draft";
const canReplaceFiles =
!isShared &&
onRerun != null &&
(project.status === "complete" ||
project.status === "error" ||
project.status === "cancelled" ||
project.status === "draft");
const opensModalOnClick = isDraft && !isShared && onRerun != null;
async function handleDelete(e: React.MouseEvent) {
@@ -156,10 +163,10 @@ function ProjectRow({
</td>
<td className="px-3 py-2">
<div className="flex items-center justify-end gap-0.5">
{isCancelled && !isShared && onRerun && (
{canReplaceFiles && (
<button
onClick={handleRerun}
title="Rerun project"
title="Replace files and re-analyze"
className="p-1 rounded-md text-muted-foreground hover:text-emerald-600 dark:hover:text-emerald-400 hover:bg-emerald-500/10 transition-colors"
>
<RotateCcw className="h-3.5 w-3.5" />
@@ -7,6 +7,7 @@ interface ReportSummaryProps {
summary: Record<string, number>;
reviewedCount: number;
creditsSpent?: number;
totalCostUsd?: number | null;
}
const STAT_CONFIG = [
@@ -20,16 +21,19 @@ export function ReportSummary({
summary,
reviewedCount,
creditsSpent,
totalCostUsd,
}: ReportSummaryProps) {
const total = summary.total || 0;
const showCredits = typeof creditsSpent === "number" && creditsSpent > 0;
const showCost = typeof totalCostUsd === "number" && totalCostUsd > 0;
const extraCols = (showCredits ? 1 : 0) + (showCost ? 1 : 0);
return (
<div className="space-y-4">
<div
className={cn(
"grid gap-4",
showCredits ? "grid-cols-6" : "grid-cols-5",
extraCols === 2 ? "grid-cols-7" : extraCols === 1 ? "grid-cols-6" : "grid-cols-5",
)}
>
{STAT_CONFIG.map(({ key, label, color }) => (
@@ -50,6 +54,16 @@ export function ReportSummary({
</p>
</CardContent>
</Card>
{showCost && (
<Card>
<CardContent className="pt-4 pb-4">
<p className="text-sm text-muted-foreground">API cost</p>
<p className="text-3xl font-semibold font-mono tabular-nums text-foreground">
${totalCostUsd!.toFixed(2)}
</p>
</CardContent>
</Card>
)}
{showCredits && (
<Card>
<CardContent className="pt-4 pb-4">