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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user