From c6c41da6ccf2f704612fbe18a14c41495b8a8eb2 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Wed, 15 Jul 2026 12:35:40 +0200 Subject: [PATCH 1/4] Fix: update old versions and skip probe_ids/version from json check --- .../A4x8-5mm-100-200-177.json | 2 +- .../A4x8-5mm-100-400-177.json | 2 +- .../A4x8-5mm-100-400-703.json | 2 +- .../A4x8-5mm-200-400-703.json | 2 +- plexon/4S1024/4S1024.json | 2 +- plexon/8S1024/8S1024.json | 2 +- scripts/check_for_json_changes_in_probes.py | 17 +++++++++++------ 7 files changed, 17 insertions(+), 12 deletions(-) diff --git a/neuronexus/A4x8-5mm-100-200-177/A4x8-5mm-100-200-177.json b/neuronexus/A4x8-5mm-100-200-177/A4x8-5mm-100-200-177.json index cac96f8..5dbe52a 100644 --- a/neuronexus/A4x8-5mm-100-200-177/A4x8-5mm-100-200-177.json +++ b/neuronexus/A4x8-5mm-100-200-177/A4x8-5mm-100-200-177.json @@ -1,6 +1,6 @@ { "specification": "probeinterface", - "version": "0.2.19", + "version": "0.3.2", "probes": [ { "ndim": 2, diff --git a/neuronexus/A4x8-5mm-100-400-177/A4x8-5mm-100-400-177.json b/neuronexus/A4x8-5mm-100-400-177/A4x8-5mm-100-400-177.json index f1204a6..c3567b6 100644 --- a/neuronexus/A4x8-5mm-100-400-177/A4x8-5mm-100-400-177.json +++ b/neuronexus/A4x8-5mm-100-400-177/A4x8-5mm-100-400-177.json @@ -1,6 +1,6 @@ { "specification": "probeinterface", - "version": "0.2.17", + "version": "0.3.2", "probes": [ { "ndim": 2, diff --git a/neuronexus/A4x8-5mm-100-400-703/A4x8-5mm-100-400-703.json b/neuronexus/A4x8-5mm-100-400-703/A4x8-5mm-100-400-703.json index 6bdc31d..a70ab4b 100644 --- a/neuronexus/A4x8-5mm-100-400-703/A4x8-5mm-100-400-703.json +++ b/neuronexus/A4x8-5mm-100-400-703/A4x8-5mm-100-400-703.json @@ -1,6 +1,6 @@ { "specification": "probeinterface", - "version": "0.2.17", + "version": "0.3.2", "probes": [ { "ndim": 2, diff --git a/neuronexus/A4x8-5mm-200-400-703/A4x8-5mm-200-400-703.json b/neuronexus/A4x8-5mm-200-400-703/A4x8-5mm-200-400-703.json index c41a964..adca227 100644 --- a/neuronexus/A4x8-5mm-200-400-703/A4x8-5mm-200-400-703.json +++ b/neuronexus/A4x8-5mm-200-400-703/A4x8-5mm-200-400-703.json @@ -1,6 +1,6 @@ { "specification": "probeinterface", - "version": "0.2.19", + "version": "0.3.2", "probes": [ { "ndim": 2, diff --git a/plexon/4S1024/4S1024.json b/plexon/4S1024/4S1024.json index a82348f..5f196ea 100644 --- a/plexon/4S1024/4S1024.json +++ b/plexon/4S1024/4S1024.json @@ -1,6 +1,6 @@ { "specification": "probeinterface", - "version": "0.2.23", + "version": "0.3.2", "probes": [ { "ndim": 2, diff --git a/plexon/8S1024/8S1024.json b/plexon/8S1024/8S1024.json index 5f2ef34..bb4fee8 100644 --- a/plexon/8S1024/8S1024.json +++ b/plexon/8S1024/8S1024.json @@ -1,6 +1,6 @@ { "specification": "probeinterface", - "version": "0.2.23", + "version": "0.3.2", "probes": [ { "ndim": 2, diff --git a/scripts/check_for_json_changes_in_probes.py b/scripts/check_for_json_changes_in_probes.py index dee4c1c..47a2cb6 100644 --- a/scripts/check_for_json_changes_in_probes.py +++ b/scripts/check_for_json_changes_in_probes.py @@ -1,5 +1,6 @@ from argparse import ArgumentParser from pathlib import Path +import json import shutil @@ -37,13 +38,17 @@ if old_probe_json_path.is_file(): with open(temp_probe_json_path, 'r') as f1, open(old_probe_json_path, 'r') as f2: # Read in json files - lines1 = f1.readlines() - lines2 = f2.readlines() + new_probe_dict = json.load(f1) + old_probe_dict = json.load(f2) - # We don't want to update the probes just because of a probeinterface version update. - # The probeinterface version is stored on the 3rd line of the json file, so we only - # compare the json files from line 3 and down. - if lines1[3:] == lines2[3:]: + # We don't want to update the probes just because of a probeinterface version update + # or because a `probe_ids` entry was added (both are introduced automatically by newer + # probeinterface versions), so we ignore those keys when comparing. + for probe_dict in (new_probe_dict, old_probe_dict): + probe_dict.pop("version", None) + probe_dict.pop("probe_ids", None) + + if new_probe_dict == old_probe_dict: continue else: shutil.copy(f"{temp_probe_json_path}", old_dir / probe_name) From b88bf0995ade359a362fb8dcf6c394d96eef0644 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Wed, 15 Jul 2026 12:39:45 +0200 Subject: [PATCH 2/4] Fix: update old versions and skip probe_ids/version from json check 2 --- neuronexus/A4x8-5mm-200-400-177/A4x8-5mm-200-400-177.json | 2 +- neuronexus/A4x8-5mm-50-200-177/A4x8-5mm-50-200-177.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/neuronexus/A4x8-5mm-200-400-177/A4x8-5mm-200-400-177.json b/neuronexus/A4x8-5mm-200-400-177/A4x8-5mm-200-400-177.json index 8442dec..6585de6 100644 --- a/neuronexus/A4x8-5mm-200-400-177/A4x8-5mm-200-400-177.json +++ b/neuronexus/A4x8-5mm-200-400-177/A4x8-5mm-200-400-177.json @@ -1,6 +1,6 @@ { "specification": "probeinterface", - "version": "0.2.19", + "version": "0.3.2", "probes": [ { "ndim": 2, diff --git a/neuronexus/A4x8-5mm-50-200-177/A4x8-5mm-50-200-177.json b/neuronexus/A4x8-5mm-50-200-177/A4x8-5mm-50-200-177.json index 61d6f61..a9364c9 100644 --- a/neuronexus/A4x8-5mm-50-200-177/A4x8-5mm-50-200-177.json +++ b/neuronexus/A4x8-5mm-50-200-177/A4x8-5mm-50-200-177.json @@ -1,6 +1,6 @@ { "specification": "probeinterface", - "version": "0.2.19", + "version": "0.3.2", "probes": [ { "ndim": 2, From 2cf3de37ef5c6492577a698b8cf7da1fd79cd1d5 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Wed, 15 Jul 2026 13:11:17 +0200 Subject: [PATCH 3/4] feat: add num_sides in app --- apps/probe-viewer/build.py | 12 ++++++++++-- apps/probe-viewer/src/components/Sidebar.tsx | 13 +++++++++++++ apps/probe-viewer/src/services/manifest.ts | 1 + apps/probe-viewer/src/types/probe.ts | 2 ++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/apps/probe-viewer/build.py b/apps/probe-viewer/build.py index 77f2e90..5c6c6ac 100755 --- a/apps/probe-viewer/build.py +++ b/apps/probe-viewer/build.py @@ -52,6 +52,7 @@ class ManifestEntry: json_url: str contact_count: int shank_count: int + num_sides: int has_3d_geometry: bool annotations: dict @@ -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 @@ -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, ) diff --git a/apps/probe-viewer/src/components/Sidebar.tsx b/apps/probe-viewer/src/components/Sidebar.tsx index e90bb69..96bbebb 100644 --- a/apps/probe-viewer/src/components/Sidebar.tsx +++ b/apps/probe-viewer/src/components/Sidebar.tsx @@ -27,6 +27,17 @@ export function Sidebar() { const searchQuery = useAppStore((state) => state.searchQuery); const setSearchQuery = useAppStore((state) => state.setSearchQuery); + // 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(); + manifest.forEach((entry) => { + if (entry.numSides > 1) set.add(entry.manufacturer); + }); + return set; + }, [manifest]); + const manufacturers = useMemo(() => { const unique = new Set(); manifest.forEach((entry) => unique.add(entry.manufacturer)); @@ -111,6 +122,8 @@ export function Sidebar() { {entry.displayName} {entry.contactCount} contacts · {entry.shankCount} shanks + {multiSideManufacturers.has(entry.manufacturer) && + ` · ${entry.numSides} ${entry.numSides === 1 ? "side" : "sides"}`} ); diff --git a/apps/probe-viewer/src/services/manifest.ts b/apps/probe-viewer/src/services/manifest.ts index b80864d..d2bafce 100644 --- a/apps/probe-viewer/src/services/manifest.ts +++ b/apps/probe-viewer/src/services/manifest.ts @@ -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 ?? {}, }; diff --git a/apps/probe-viewer/src/types/probe.ts b/apps/probe-viewer/src/types/probe.ts index 68f6158..91ce905 100644 --- a/apps/probe-viewer/src/types/probe.ts +++ b/apps/probe-viewer/src/types/probe.ts @@ -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; } @@ -18,6 +19,7 @@ export interface ManifestEntry { jsonUrl: string; contactCount: number; shankCount: number; + numSides: number; has3dGeometry: boolean; annotations: Record; } From 8ee3499db059d1e213f45853425e92b4d81deeee Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Wed, 15 Jul 2026 13:15:39 +0200 Subject: [PATCH 4/4] feat: add side selection in case of manufacturers with multiple sides --- README.md | 6 +-- apps/probe-viewer/src/components/Sidebar.tsx | 47 +++++++++++++++++++- apps/probe-viewer/src/state/useAppStore.ts | 12 ++++- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5d3300f..d988268 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/apps/probe-viewer/src/components/Sidebar.tsx b/apps/probe-viewer/src/components/Sidebar.tsx index 96bbebb..91a99cb 100644 --- a/apps/probe-viewer/src/components/Sidebar.tsx +++ b/apps/probe-viewer/src/components/Sidebar.tsx @@ -26,6 +26,8 @@ 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 @@ -52,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(); + 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; } @@ -66,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 @@ -239,6 +259,31 @@ export function Sidebar() { /> + {showSideFilter && ( +
+ + +
+ )} +
{manifestStatus === "loading" && (

Loading manifest…

diff --git a/apps/probe-viewer/src/state/useAppStore.ts b/apps/probe-viewer/src/state/useAppStore.ts index 2904f13..64c5124 100644 --- a/apps/probe-viewer/src/state/useAppStore.ts +++ b/apps/probe-viewer/src/state/useAppStore.ts @@ -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; probeStatus: Record; view: ViewState; @@ -42,6 +45,7 @@ interface AppState { loadManifest: () => Promise; selectManufacturer: (manufacturer?: string) => void; setSearchQuery: (query: string) => void; + setSideFilter: (sides: number | null) => void; selectProbe: (probeId?: string) => void; ensureProbeLoaded: (probeId: string) => Promise; setZoom: (zoom: number) => void; @@ -88,6 +92,7 @@ export const useAppStore = create((set, get) => ({ selectedManufacturer: undefined, selectedProbeId: undefined, searchQuery: "", + sideFilter: null, probeCache: {}, probeStatus: {}, view: INITIAL_VIEW_STATE, @@ -121,10 +126,15 @@ export const useAppStore = create((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) {