diff --git a/examples/example15-spice-error-test.fixture.tsx b/examples/example15-spice-error-test.fixture.tsx new file mode 100644 index 0000000..e600167 --- /dev/null +++ b/examples/example15-spice-error-test.fixture.tsx @@ -0,0 +1,17 @@ +import { SchematicViewer } from "lib/components/SchematicViewer" +import { renderToCircuitJson } from "lib/dev/render-to-circuit-json" + +export default () => ( + + {/* This circuit should trigger a SPICE error due to missing output specification */} + + + {/* No trace or source - this should cause validation errors */} + , + )} + containerStyle={{ height: "100%" }} + spiceSimulationEnabled={true} + /> +) diff --git a/lib/components/LoadingState.tsx b/lib/components/LoadingState.tsx new file mode 100644 index 0000000..f5f7ca4 --- /dev/null +++ b/lib/components/LoadingState.tsx @@ -0,0 +1,92 @@ +interface LoadingStateProps { + message?: string + subMessage?: string + progress?: number +} + +export const LoadingState: React.FC = ({ + message = "Running simulation...", + subMessage = "Analyzing circuit and computing results", + progress, +}) => { + return ( +
+
+ +
+
+ {message} +
+
{subMessage}
+
+ + {progress !== undefined && ( +
+
+
+
+
+ {Math.round(progress)}% complete +
+
+ )} + + +
+ ) +} diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index 51e782a..3344b0d 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -85,13 +85,25 @@ export const SchematicViewer = ({ [circuitJson], ) - const spiceString = useMemo(() => { - if (!spiceSimulationEnabled) return null + const [spiceString, setSpiceString] = useState(null) + const [spiceGenerationError, setSpiceGenerationError] = useState< + string | null + >(null) + + useMemo(() => { + if (!spiceSimulationEnabled) { + setSpiceString(null) + setSpiceGenerationError(null) + return + } try { - return getSpiceFromCircuitJson(circuitJson, spiceSimOptions) - } catch (e) { + const generated = getSpiceFromCircuitJson(circuitJson, spiceSimOptions) + setSpiceString(generated) + setSpiceGenerationError(null) + } catch (e: any) { console.error("Failed to generate SPICE string", e) - return null + setSpiceString(null) + setSpiceGenerationError(e?.message || "Failed to generate SPICE netlist") } }, [ circuitJsonKey, @@ -101,6 +113,7 @@ export const SchematicViewer = ({ ]) const [hasSpiceSimRun, setHasSpiceSimRun] = useState(false) + const [spiceRetryCounter, setSpiceRetryCounter] = useState(0) useEffect(() => { setHasSpiceSimRun(false) @@ -111,7 +124,7 @@ export const SchematicViewer = ({ nodes, isLoading: isSpiceSimLoading, error: spiceSimError, - } = useSpiceSimulation(hasSpiceSimRun ? spiceString : null) + } = useSpiceSimulation(hasSpiceSimRun ? spiceString : null, spiceRetryCounter) const [editModeEnabled, setEditModeEnabled] = useState(defaultEditMode) const [snapToGrid, setSnapToGrid] = useState(true) @@ -339,7 +352,9 @@ export const SchematicViewer = ({ {onSchematicComponentClicked && ( )}
{ setHasSpiceSimRun(true) setSpiceSimOptions(options) }} hasRun={hasSpiceSimRun} + onRetry={() => { + setHasSpiceSimRun(true) + setSpiceRetryCounter((prev) => prev + 1) + }} /> )} {onSchematicComponentClicked && diff --git a/lib/components/SpiceErrorDisplay.tsx b/lib/components/SpiceErrorDisplay.tsx new file mode 100644 index 0000000..d7c4a5e --- /dev/null +++ b/lib/components/SpiceErrorDisplay.tsx @@ -0,0 +1,277 @@ +import { useState } from "react" +import { + categorizeSpiceError, + getErrorIcon, + getErrorColor, +} from "../utils/spice-error-utils" + +interface SpiceErrorDisplayProps { + error: string + onRetry?: () => void + onCopyDetails?: () => void + showTechnicalDetails?: boolean +} + +export const SpiceErrorDisplay: React.FC = ({ + error, + onRetry, + onCopyDetails, + showTechnicalDetails = false, +}) => { + const [showFullDetails, setShowFullDetails] = useState(false) + const errorDetails = categorizeSpiceError(error) + + const handleCopyDetails = () => { + const fullErrorText = `Error Type: ${errorDetails.type}\nTitle: ${errorDetails.title}\nMessage: ${errorDetails.userMessage}\nTechnical Details: ${errorDetails.technicalMessage}\nSuggestions:\n${errorDetails.suggestions.map((s) => `- ${s}`).join("\n")}` + + if (navigator.clipboard) { + navigator.clipboard.writeText(fullErrorText) + } else { + // Fallback for older browsers + const textArea = document.createElement("textarea") + textArea.value = fullErrorText + document.body.appendChild(textArea) + textArea.select() + document.execCommand("copy") + document.body.removeChild(textArea) + } + + onCopyDetails?.() + } + + return ( +
+
+ {/* Error Header */} +
+
+

+ {errorDetails.title} +

+
+ {errorDetails.type} error +
+
+
+ + {/* User-friendly message */} +

+ {errorDetails.userMessage} +

+ + {/* Suggestions */} + {errorDetails.suggestions.length > 0 && ( +
+
+ Suggestions: +
+
    + {errorDetails.suggestions.map((suggestion, index) => ( +
  • + {suggestion} +
  • + ))} +
+
+ )} + + {/* Action buttons */} +
+ {errorDetails.canRetry && onRetry && ( + + )} + + + + {(showTechnicalDetails || + errorDetails.technicalMessage !== error) && ( + + )} +
+ + {/* Technical details (expandable) */} + {showFullDetails && ( +
+ {errorDetails.technicalMessage} +
+ )} +
+
+ ) +} diff --git a/lib/components/SpicePlot.tsx b/lib/components/SpicePlot.tsx index 1873507..2d56982 100644 --- a/lib/components/SpicePlot.tsx +++ b/lib/components/SpicePlot.tsx @@ -12,6 +12,8 @@ import { } from "chart.js" import { Line } from "react-chartjs-2" import type { PlotPoint } from "../hooks/useSpiceSimulation" +import { SpiceErrorDisplay } from "./SpiceErrorDisplay" +import { LoadingState } from "./LoadingState" ChartJS.register( CategoryScale, @@ -57,12 +59,16 @@ export const SpicePlot = ({ isLoading, error, hasRun, + onRetry, + spiceGenerationError, }: { plotData: PlotPoint[] nodes: string[] isLoading: boolean error: string | null hasRun: boolean + onRetry?: () => void + spiceGenerationError?: string | null }) => { const yAxisLabel = useMemo(() => { const hasVoltage = nodes.some((n) => n.toLowerCase().startsWith("v(")) @@ -74,18 +80,19 @@ export const SpicePlot = ({ }, [nodes]) if (isLoading) { + return + } + + if (spiceGenerationError) { return ( -
{ + console.log("Error details copied to clipboard") }} - > - Running simulation... -
+ showTechnicalDetails={true} + /> ) } @@ -107,18 +114,15 @@ export const SpicePlot = ({ if (error) { return ( -
{ + // Optional: show toast notification when copied + console.log("Error details copied to clipboard") }} - > - Error: {error} -
+ showTechnicalDetails={true} + /> ) } diff --git a/lib/components/SpiceSimulationOverlay.tsx b/lib/components/SpiceSimulationOverlay.tsx index d241287..8224222 100644 --- a/lib/components/SpiceSimulationOverlay.tsx +++ b/lib/components/SpiceSimulationOverlay.tsx @@ -9,6 +9,7 @@ interface SpiceSimulationOverlayProps { nodes: string[] isLoading: boolean error: string | null + spiceGenerationError?: string | null simOptions: { showVoltage: boolean showCurrent: boolean @@ -19,6 +20,7 @@ interface SpiceSimulationOverlayProps { options: SpiceSimulationOverlayProps["simOptions"], ) => void hasRun: boolean + onRetry?: () => void } export const SpiceSimulationOverlay = ({ @@ -28,9 +30,11 @@ export const SpiceSimulationOverlay = ({ nodes, isLoading, error, + spiceGenerationError, simOptions, onSimOptionsChange, hasRun, + onRetry, }: SpiceSimulationOverlayProps) => { const [startTimeDraft, setStartTimeDraft] = useState( String(simOptions.startTime), @@ -107,6 +111,7 @@ export const SpiceSimulationOverlay = ({ SPICE Simulation