Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ To build and preview the `probe-viewer` web-app locally:
```bash
cd apps/probe-viewer
uv run build.py
# build
npm run build
# run
npx vite preview
# build & run
npm run build && npx vite preview
```
12 changes: 10 additions & 2 deletions apps/probe-viewer/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class ManifestEntry:
json_url: str
contact_count: int
shank_count: int
num_sides: int
has_3d_geometry: bool
annotations: dict

Expand All @@ -74,12 +75,18 @@ def load_probe_metadata(json_path: Path) -> ManifestEntry:
if not probes:
raise ValueError(f"No probes found in {json_path}")

if len(probes) > 1:
raise ValueError(f"Multiple probes found in {json_path}; expected one")
probe = probes[0]

manufacturer = json_path.parents[1].name
model = json_path.parent.name
probe_id = f"{manufacturer}:{model}"

total_contacts = sum(len(probe.get("contact_positions", [])) for probe in probes)
shank_count = max(len(set(probe.get("shank_ids") or [None])) for probe in probes)
total_contacts = len(probe.get("contact_positions", []))
shank_count = len(set(probe.get("shank_ids") or [None]))
num_sides = 1 if probe.get("contact_sides") is None else len(set(probe.get("contact_sides")))

has_3d = any(probe.get("ndim") == 3 for probe in probes)
annotations = probes[0].get("annotations") or {}
display_name = annotations.get("model_name") or model
Expand All @@ -92,6 +99,7 @@ def load_probe_metadata(json_path: Path) -> ManifestEntry:
json_url=json_path.name,
contact_count=total_contacts,
shank_count=shank_count,
num_sides=num_sides,
has_3d_geometry=has_3d,
annotations=annotations,
)
Expand Down
60 changes: 59 additions & 1 deletion apps/probe-viewer/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ export function Sidebar() {
const navigate = useNavigate();
const searchQuery = useAppStore((state) => state.searchQuery);
const setSearchQuery = useAppStore((state) => state.setSearchQuery);
const sideFilter = useAppStore((state) => state.sideFilter);
const setSideFilter = useAppStore((state) => state.setSideFilter);

// Only manufacturers with at least one multi-sided probe show the "sides"
// count; when every probe of a manufacturer is single-sided the metric is
// noise, so it's omitted for all of that manufacturer's items.
const multiSideManufacturers = useMemo(() => {
const set = new Set<string>();
manifest.forEach((entry) => {
if (entry.numSides > 1) set.add(entry.manufacturer);
});
return set;
}, [manifest]);

const manufacturers = useMemo(() => {
const unique = new Set<string>();
Expand All @@ -41,12 +54,30 @@ export function Sidebar() {
}
}, [manufacturers, selectedManufacturer, selectManufacturer]);

// Distinct side counts among the selected manufacturer's probes. The side
// dropdown only appears when this has more than one value (e.g. a catalog
// mixing single- and double-sided probes).
const availableSideCounts = useMemo(() => {
const counts = new Set<number>();
manifest.forEach((entry) => {
if (!selectedManufacturer || entry.manufacturer === selectedManufacturer) {
counts.add(entry.numSides);
}
});
return Array.from(counts.values()).sort((a, b) => a - b);
}, [manifest, selectedManufacturer]);

const showSideFilter = availableSideCounts.length > 1;

const filteredEntries = useMemo(() => {
const query = searchQuery.trim().toLowerCase();
return manifest.filter((entry) => {
if (selectedManufacturer && entry.manufacturer !== selectedManufacturer) {
return false;
}
if (sideFilter !== null && entry.numSides !== sideFilter) {
return false;
}
if (!query) {
return true;
}
Expand All @@ -55,7 +86,7 @@ export function Sidebar() {
entry.displayName.toLowerCase().includes(query)
);
});
}, [manifest, selectedManufacturer, searchQuery]);
}, [manifest, selectedManufacturer, searchQuery, sideFilter]);

useEffect(() => {
// Re-pick a probe only when a stale one is selected (e.g. after switching
Expand Down Expand Up @@ -111,6 +142,8 @@ export function Sidebar() {
<span className="sidebar-item-name">{entry.displayName}</span>
<span className="sidebar-item-meta">
{entry.contactCount} contacts · {entry.shankCount} shanks
{multiSideManufacturers.has(entry.manufacturer) &&
` · ${entry.numSides} ${entry.numSides === 1 ? "side" : "sides"}`}
</span>
</button>
);
Expand Down Expand Up @@ -226,6 +259,31 @@ export function Sidebar() {
/>
</div>

{showSideFilter && (
<div className="sidebar-control">
<label className="sidebar-label" htmlFor="side-select">
Number of sides
</label>
<select
id="side-select"
value={sideFilter === null ? "" : String(sideFilter)}
onChange={(event) =>
setSideFilter(
event.target.value === "" ? null : Number(event.target.value),
)
}
disabled={manifestStatus !== "success"}
>
<option value="">All sides</option>
{availableSideCounts.map((count) => (
<option key={count} value={String(count)}>
{count} {count === 1 ? "side" : "sides"}
</option>
))}
</select>
</div>
)}

<div className="sidebar-list" role="list">
{manifestStatus === "loading" && (
<p className="sidebar-hint">Loading manifest…</p>
Expand Down
1 change: 1 addition & 0 deletions apps/probe-viewer/src/services/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function normalizeEntry(raw: RawManifestEntry): ManifestEntry {
jsonUrl: `${import.meta.env.BASE_URL}${raw.json_url}`,
contactCount: raw.contact_count,
shankCount: raw.shank_count,
numSides: raw.num_sides,
has3dGeometry: raw.has_3d_geometry,
annotations: raw.annotations ?? {},
};
Expand Down
12 changes: 11 additions & 1 deletion apps/probe-viewer/src/state/useAppStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ interface AppState {
selectedManufacturer?: string;
selectedProbeId?: string;
searchQuery: string;
// Filter the list to probes with this many sides; null = show all. Only
// meaningful for manufacturers whose catalog mixes side counts.
sideFilter: number | null;
probeCache: Record<string, ProbeInterfaceFile>;
probeStatus: Record<string, ProbeLoadState>;
view: ViewState;
Expand All @@ -42,6 +45,7 @@ interface AppState {
loadManifest: () => Promise<void>;
selectManufacturer: (manufacturer?: string) => void;
setSearchQuery: (query: string) => void;
setSideFilter: (sides: number | null) => void;
selectProbe: (probeId?: string) => void;
ensureProbeLoaded: (probeId: string) => Promise<ProbeInterfaceFile | undefined>;
setZoom: (zoom: number) => void;
Expand Down Expand Up @@ -88,6 +92,7 @@ export const useAppStore = create<AppState>((set, get) => ({
selectedManufacturer: undefined,
selectedProbeId: undefined,
searchQuery: "",
sideFilter: null,
probeCache: {},
probeStatus: {},
view: INITIAL_VIEW_STATE,
Expand Down Expand Up @@ -121,10 +126,15 @@ export const useAppStore = create<AppState>((set, get) => ({
}
},

selectManufacturer: (manufacturer) => set({ selectedManufacturer: manufacturer }),
// Switching manufacturer clears the side filter — it's specific to whichever
// catalog was showing and rarely applies to the next one.
selectManufacturer: (manufacturer) =>
set({ selectedManufacturer: manufacturer, sideFilter: null }),

setSearchQuery: (query) => set({ searchQuery: query }),

setSideFilter: (sides) => set({ sideFilter: sides }),

selectProbe: (probeId) =>
set((state) => {
if (!probeId) {
Expand Down
2 changes: 2 additions & 0 deletions apps/probe-viewer/src/types/probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface RawManifestEntry {
json_url: string;
contact_count: number;
shank_count: number;
num_sides: number;
has_3d_geometry: boolean;
annotations: Record<string, unknown>;
}
Expand All @@ -18,6 +19,7 @@ export interface ManifestEntry {
jsonUrl: string;
contactCount: number;
shankCount: number;
numSides: number;
has3dGeometry: boolean;
annotations: Record<string, unknown>;
}
Expand Down
Loading