Compare commits
2 Commits
fe6d20afbd
...
5f5e0da3c4
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f5e0da3c4 | |||
| 2ddc08cd3a |
@ -47,6 +47,8 @@ class NotebookSummary(BaseModel):
|
||||
tags: list[str]
|
||||
cell_count: int
|
||||
modified: str
|
||||
description: str | None = None
|
||||
schematic_svg: str | None = None
|
||||
|
||||
|
||||
class CreateNotebookRequest(BaseModel):
|
||||
|
||||
@ -51,6 +51,44 @@ def _notebook_id_from_path(path: Path) -> str:
|
||||
return path.stem
|
||||
|
||||
|
||||
def _extract_card_data(nb: Notebook) -> tuple[str | None, str | None]:
|
||||
"""Extract a description snippet and schematic SVG from notebook cells.
|
||||
|
||||
- description: text from the first markdown cell, with the title header
|
||||
stripped and truncated to ~200 characters.
|
||||
- schematic_svg: SVG string from the first successful schematic output.
|
||||
"""
|
||||
description: str | None = None
|
||||
schematic_svg: str | None = None
|
||||
|
||||
for cell in nb.cells:
|
||||
if description is None and cell.type == CellType.MARKDOWN and cell.source.strip():
|
||||
lines = cell.source.strip().splitlines()
|
||||
# Strip leading "# Title" header line
|
||||
if lines and lines[0].lstrip().startswith("#"):
|
||||
lines = lines[1:]
|
||||
text = " ".join(line.strip() for line in lines if line.strip())
|
||||
# Strip basic markdown syntax
|
||||
text = re.sub(r"[*_`~\[\]#>]", "", text).strip()
|
||||
if text:
|
||||
description = text[:200].rstrip()
|
||||
|
||||
if schematic_svg is None:
|
||||
for output in cell.outputs:
|
||||
if (
|
||||
output.output_type == "schematic"
|
||||
and output.data.get("success")
|
||||
and output.data.get("svg")
|
||||
):
|
||||
schematic_svg = output.data["svg"]
|
||||
break
|
||||
|
||||
if description is not None and schematic_svg is not None:
|
||||
break
|
||||
|
||||
return description, schematic_svg
|
||||
|
||||
|
||||
def list_notebooks(directory: Path) -> list[NotebookSummary]:
|
||||
"""List all notebooks in the directory and its examples/ subdirectory."""
|
||||
summaries: list[NotebookSummary] = []
|
||||
@ -66,6 +104,7 @@ def list_notebooks(directory: Path) -> list[NotebookSummary]:
|
||||
|
||||
try:
|
||||
nb = _read_notebook_file(path)
|
||||
description, schematic_svg = _extract_card_data(nb)
|
||||
summaries.append(NotebookSummary(
|
||||
id=nb_id,
|
||||
title=nb.metadata.title,
|
||||
@ -73,6 +112,8 @@ def list_notebooks(directory: Path) -> list[NotebookSummary]:
|
||||
tags=nb.metadata.tags,
|
||||
cell_count=len(nb.cells),
|
||||
modified=nb.metadata.modified,
|
||||
description=description,
|
||||
schematic_svg=schematic_svg,
|
||||
))
|
||||
except Exception:
|
||||
logger.warning("Skipping corrupt notebook: %s", path)
|
||||
|
||||
@ -4,23 +4,50 @@ services:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
target: dev
|
||||
ports:
|
||||
- "${BACKEND_PORT:-8099}:8000"
|
||||
expose:
|
||||
- "8000"
|
||||
volumes:
|
||||
- ./backend/src:/app/src
|
||||
- ./notebooks:/app/notebooks
|
||||
# Mount mcltspice for live development
|
||||
- ../mcp-ltspice/src/mcltspice:/app/mcltspice-lib:ro
|
||||
command: ["uv", "run", "uvicorn", "spicebook.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||
environment:
|
||||
- PYTHONPATH=/app/src
|
||||
command: ["python", "-m", "uvicorn", "spicebook.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||
networks:
|
||||
- default
|
||||
- caddy
|
||||
labels:
|
||||
caddy: "${SPICEBOOK_DOMAIN:-spicebook.localhost}"
|
||||
caddy.@api.path: "/api/* /health /docs /openapi.json /redoc"
|
||||
caddy.reverse_proxy_0: "@api {{upstreams 8000}}"
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
target: dev
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-4321}:4321"
|
||||
expose:
|
||||
- "4321"
|
||||
volumes:
|
||||
- ./frontend/src:/app/src
|
||||
- ./frontend/public:/app/public
|
||||
command: ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||
networks:
|
||||
- default
|
||||
- caddy
|
||||
labels:
|
||||
caddy: "${SPICEBOOK_DOMAIN:-spicebook.localhost}"
|
||||
caddy.reverse_proxy_1: "{{upstreams 4321}}"
|
||||
caddy.reverse_proxy_1.flush_interval: "-1"
|
||||
caddy.reverse_proxy_1.transport: "http"
|
||||
caddy.reverse_proxy_1.transport.read_timeout: "0"
|
||||
caddy.reverse_proxy_1.transport.write_timeout: "0"
|
||||
caddy.reverse_proxy_1.transport.keepalive: "5m"
|
||||
caddy.reverse_proxy_1.transport.keepalive_idle_conns: "10"
|
||||
caddy.reverse_proxy_1.stream_timeout: "24h"
|
||||
caddy.reverse_proxy_1.stream_close_delay: "5s"
|
||||
|
||||
networks:
|
||||
caddy:
|
||||
external: true
|
||||
|
||||
@ -25,14 +25,21 @@ interface NotebookCardProps {
|
||||
}
|
||||
|
||||
export function NotebookCard({ notebook }: NotebookCardProps) {
|
||||
const { id, title, engine, tags, cell_count, modified } = notebook;
|
||||
const { id, title, engine, tags, cell_count, modified, description, schematic_svg } = notebook;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`/notebook/${encodeURIComponent(id)}`}
|
||||
className="group block rounded-lg border border-slate-800 bg-slate-900/50 overflow-hidden hover:border-blue-500/40 hover:bg-slate-800/70 transition-all"
|
||||
>
|
||||
<div className="notebook-card-strip" data-engine={engine} />
|
||||
{schematic_svg ? (
|
||||
<div
|
||||
className="w-full h-[120px] bg-[#f8faf6] border-b border-slate-800/60 overflow-hidden flex items-center justify-center [&_svg]:max-w-full [&_svg]:max-h-full [&_svg]:w-auto [&_svg]:h-auto"
|
||||
dangerouslySetInnerHTML={{ __html: schematic_svg }}
|
||||
/>
|
||||
) : (
|
||||
<div className="notebook-card-strip" data-engine={engine} />
|
||||
)}
|
||||
<div className="p-5">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h3 className="text-base font-semibold text-slate-100 group-hover:text-blue-400 transition-colors line-clamp-1">
|
||||
@ -43,6 +50,12 @@ export function NotebookCard({ notebook }: NotebookCardProps) {
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{description && (
|
||||
<p className="text-sm text-slate-400 leading-relaxed mb-2 line-clamp-2">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-slate-500 mb-3">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<LayoutGrid className="w-3 h-3" />
|
||||
|
||||
199
frontend/src/components/ResistorColorGuide.astro
Normal file
199
frontend/src/components/ResistorColorGuide.astro
Normal file
@ -0,0 +1,199 @@
|
||||
---
|
||||
/**
|
||||
* Resistor Color Code Guide — educational reference section.
|
||||
*
|
||||
* Left column: annotated SVG showing a 10k resistor zigzag with labeled bands.
|
||||
* Right column: compact reference table mapping colors to digits and multipliers.
|
||||
*
|
||||
* Colors match BAND_HEX from resistor_colors.py so dots match schematic rendering.
|
||||
*/
|
||||
|
||||
// BAND_HEX values from backend/src/spicebook/engine/resistor_colors.py
|
||||
const bandHex: Record<string, string> = {
|
||||
black: '#1a1a1a',
|
||||
brown: '#8B4513',
|
||||
red: '#CC0000',
|
||||
orange: '#E67300',
|
||||
yellow: '#B8960B',
|
||||
green: '#006400',
|
||||
blue: '#0000CC',
|
||||
violet: '#7B00B3',
|
||||
gray: '#666666',
|
||||
white: '#D0D0D0',
|
||||
gold: '#B8860B',
|
||||
silver: '#808080',
|
||||
};
|
||||
|
||||
const digitBands = [
|
||||
{ color: 'Black', hex: bandHex.black, digit: 0, multiplier: '×1 \u03A9' },
|
||||
{ color: 'Brown', hex: bandHex.brown, digit: 1, multiplier: '×10 \u03A9' },
|
||||
{ color: 'Red', hex: bandHex.red, digit: 2, multiplier: '×100 \u03A9' },
|
||||
{ color: 'Orange', hex: bandHex.orange, digit: 3, multiplier: '×1 k\u03A9' },
|
||||
{ color: 'Yellow', hex: bandHex.yellow, digit: 4, multiplier: '×10 k\u03A9' },
|
||||
{ color: 'Green', hex: bandHex.green, digit: 5, multiplier: '×100 k\u03A9' },
|
||||
{ color: 'Blue', hex: bandHex.blue, digit: 6, multiplier: '×1 M\u03A9' },
|
||||
{ color: 'Violet', hex: bandHex.violet, digit: 7, multiplier: '×10 M\u03A9' },
|
||||
{ color: 'Gray', hex: bandHex.gray, digit: 8, multiplier: '\u2014' },
|
||||
{ color: 'White', hex: bandHex.white, digit: 9, multiplier: '\u2014' },
|
||||
];
|
||||
|
||||
const toleranceBands = [
|
||||
{ color: 'Gold', hex: bandHex.gold, value: '\u00B15%' },
|
||||
{ color: 'Silver', hex: bandHex.silver, value: '\u00B110%' },
|
||||
];
|
||||
---
|
||||
|
||||
<section class="max-w-6xl mx-auto px-6 py-16">
|
||||
<p class="text-sm font-semibold tracking-widest text-blue-400 uppercase mb-2">Reference</p>
|
||||
<h2 class="text-2xl font-bold text-slate-100 mb-3">Reading Resistor Color Codes</h2>
|
||||
<p class="text-sm text-slate-400 mb-8 max-w-2xl leading-relaxed">
|
||||
Resistors in SpiceBook schematics use color-coded bands — the same EIA standard
|
||||
printed on physical components. Each colored segment of the zigzag symbol encodes
|
||||
a digit, multiplier, or tolerance value.
|
||||
</p>
|
||||
|
||||
<div class="grid gap-8 lg:grid-cols-[1fr_1fr] items-start">
|
||||
|
||||
<!-- Left: Annotated resistor diagram -->
|
||||
<div class="rcg-diagram-card">
|
||||
<svg viewBox="0 0 460 200" xmlns="http://www.w3.org/2000/svg" class="w-full h-auto" aria-label="Annotated 10k ohm resistor showing 4 color bands">
|
||||
|
||||
<!-- Mims graph paper background -->
|
||||
<defs>
|
||||
<pattern id="rcg-minor" width="10" height="10" patternUnits="userSpaceOnUse">
|
||||
<path d="M 10 0 V 10 H 0" fill="none" stroke="#d4e4d4" stroke-width="0.3"/>
|
||||
</pattern>
|
||||
<pattern id="rcg-major" width="50" height="50" patternUnits="userSpaceOnUse">
|
||||
<path d="M 50 0 V 50 H 0" fill="none" stroke="#b0ccb0" stroke-width="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="460" height="200" fill="#f8faf6" rx="4"/>
|
||||
<rect width="460" height="200" fill="url(#rcg-minor)" rx="4"/>
|
||||
<rect width="460" height="200" fill="url(#rcg-major)" rx="4"/>
|
||||
|
||||
<!-- Lead wires -->
|
||||
<line x1="40" y1="100" x2="100" y2="100" stroke="#555" stroke-width="2"/>
|
||||
<line x1="360" y1="100" x2="420" y2="100" stroke="#555" stroke-width="2"/>
|
||||
|
||||
<!-- Zigzag body — 7 segments like backend mapping -->
|
||||
<!-- seg 0: entry half-rise (wire) -->
|
||||
<polyline points="100,100 120,72" fill="none" stroke="#555" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- seg 1: Band 1 - Brown (1st digit = 1) -->
|
||||
<polyline points="120,72 160,128" fill="none" stroke={bandHex.brown} stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- seg 2: Band 2 - Black (2nd digit = 0) -->
|
||||
<polyline points="160,128 200,72" fill="none" stroke={bandHex.black} stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- seg 3: Band 3 - Orange (multiplier = ×1kΩ) -->
|
||||
<polyline points="200,72 240,128" fill="none" stroke={bandHex.orange} stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- seg 4: gap (wire) -->
|
||||
<polyline points="240,128 280,72" fill="none" stroke="#555" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- seg 5: Band 4 - Gold (tolerance = ±5%) -->
|
||||
<polyline points="280,72 320,128" fill="none" stroke={bandHex.gold} stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- seg 6: exit half-rise (wire) -->
|
||||
<polyline points="320,128 340,100" fill="none" stroke="#555" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
<!-- Lead wire continuation -->
|
||||
<line x1="340" y1="100" x2="360" y2="100" stroke="#555" stroke-width="2"/>
|
||||
|
||||
<!-- Callout labels with lines -->
|
||||
<!-- Band 1: Brown -->
|
||||
<line x1="140" y1="68" x2="140" y2="36" stroke="#888" stroke-width="0.8" stroke-dasharray="2,2"/>
|
||||
<text x="140" y="30" text-anchor="middle" font-family="ui-monospace, monospace" font-size="10" fill="#555" font-weight="600">1st digit</text>
|
||||
<text x="140" y="18" text-anchor="middle" font-family="ui-monospace, monospace" font-size="8.5" fill={bandHex.brown}>Brown = 1</text>
|
||||
|
||||
<!-- Band 2: Black -->
|
||||
<line x1="180" y1="132" x2="180" y2="164" stroke="#888" stroke-width="0.8" stroke-dasharray="2,2"/>
|
||||
<text x="180" y="175" text-anchor="middle" font-family="ui-monospace, monospace" font-size="10" fill="#555" font-weight="600">2nd digit</text>
|
||||
<text x="180" y="188" text-anchor="middle" font-family="ui-monospace, monospace" font-size="8.5" fill="#444">Black = 0</text>
|
||||
|
||||
<!-- Band 3: Orange -->
|
||||
<line x1="220" y1="68" x2="220" y2="36" stroke="#888" stroke-width="0.8" stroke-dasharray="2,2"/>
|
||||
<text x="220" y="30" text-anchor="middle" font-family="ui-monospace, monospace" font-size="10" fill="#555" font-weight="600">Multiplier</text>
|
||||
<text x="220" y="18" text-anchor="middle" font-family="ui-monospace, monospace" font-size="8.5" fill={bandHex.orange}>Orange = ×1kΩ</text>
|
||||
|
||||
<!-- Band 4: Gold -->
|
||||
<line x1="300" y1="132" x2="300" y2="164" stroke="#888" stroke-width="0.8" stroke-dasharray="2,2"/>
|
||||
<text x="300" y="175" text-anchor="middle" font-family="ui-monospace, monospace" font-size="10" fill="#555" font-weight="600">Tolerance</text>
|
||||
<text x="300" y="188" text-anchor="middle" font-family="ui-monospace, monospace" font-size="8.5" fill={bandHex.gold}>Gold = ±5%</text>
|
||||
|
||||
<!-- Result value label -->
|
||||
<rect x="360" y="58" width="90" height="24" rx="3" fill="#f0f2ee" stroke="#b0ccb0" stroke-width="0.8"/>
|
||||
<text x="405" y="74" text-anchor="middle" font-family="ui-monospace, monospace" font-size="11" fill="#333" font-weight="700">10 kΩ ±5%</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Right: Color code reference table -->
|
||||
<div class="rcg-table-card">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-slate-700/60">
|
||||
<th class="rcg-th text-left py-2.5 pr-2">Color</th>
|
||||
<th class="rcg-th text-center py-2.5 px-2">Digit</th>
|
||||
<th class="rcg-th text-left py-2.5 pl-2">Multiplier</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{digitBands.map((band) => (
|
||||
<tr class="border-b border-slate-800/40">
|
||||
<td class="py-1.5 pr-2">
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span class="rcg-dot" style={`background: ${band.hex};`}></span>
|
||||
<span class="text-slate-300 text-xs">{band.color}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center py-1.5 px-2 text-slate-300 font-mono text-xs">{band.digit}</td>
|
||||
<td class="py-1.5 pl-2 text-slate-400 font-mono text-xs">{band.multiplier}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Tolerance row -->
|
||||
<div class="mt-4 pt-3 border-t border-slate-700/60">
|
||||
<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Tolerance</p>
|
||||
<div class="flex gap-6">
|
||||
{toleranceBands.map((band) => (
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span class="rcg-dot" style={`background: ${band.hex};`}></span>
|
||||
<span class="text-slate-300 text-xs">{band.color}</span>
|
||||
<span class="text-slate-400 font-mono text-xs">{band.value}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.rcg-diagram-card {
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #1e293b;
|
||||
overflow: hidden;
|
||||
background: #f8faf6;
|
||||
}
|
||||
|
||||
.rcg-table-card {
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #1e293b;
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.rcg-th {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.rcg-dot {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
</style>
|
||||
@ -34,6 +34,8 @@ export interface NotebookSummary {
|
||||
tags: string[];
|
||||
cell_count: number;
|
||||
modified: string;
|
||||
description?: string;
|
||||
schematic_svg?: string;
|
||||
}
|
||||
|
||||
export interface WaveformVariable {
|
||||
|
||||
@ -3,6 +3,7 @@ import { Icon } from 'astro-icon/components';
|
||||
import NotebookLayout from '../layouts/NotebookLayout.astro';
|
||||
import NotebookGallery from '../components/NotebookGallery';
|
||||
import PipelineStrip from '../components/PipelineStrip.astro';
|
||||
import ResistorColorGuide from '../components/ResistorColorGuide.astro';
|
||||
import FeaturedNotebooks from '../components/FeaturedNotebooks.astro';
|
||||
import OscilloscopeDisplay from '../components/OscilloscopeDisplay.astro';
|
||||
import { fetchNotebookList } from '../lib/server-api';
|
||||
@ -72,6 +73,11 @@ try {
|
||||
<PipelineStrip />
|
||||
</section>
|
||||
|
||||
<!-- Resistor Color Code Guide -->
|
||||
<section class="border-b border-slate-800/60">
|
||||
<ResistorColorGuide />
|
||||
</section>
|
||||
|
||||
<!-- Featured Notebooks -->
|
||||
<section class="border-b border-slate-800/60">
|
||||
<FeaturedNotebooks />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user