akankshanc's picture
Update app.js
107c961 verified
Raw
History Blame Contribute Delete
28.6 kB
const DATASET =
"akankshanc/inception-v1-microscope-data";
const LAYERS = [
{ name: "conv2d0", channels: 64 },
{ name: "conv2d1", channels: 64 },
{ name: "conv2d2", channels: 192 },
{ name: "mixed3a", channels: 256 },
{ name: "mixed3b", channels: 480 },
{ name: "mixed4a", channels: 508 },
{ name: "mixed4b", channels: 512 },
{ name: "mixed4c", channels: 512 },
{ name: "mixed4d", channels: 528 },
{ name: "mixed4e", channels: 832 },
{ name: "mixed5a", channels: 832 },
{ name: "mixed5b", channels: 1024 },
];
let previewLayer = null;
let exampleDisplayMode = "paired";
const channelPanel =
document.querySelector(".channel-panel");
const previousNeuronButton =
document.querySelector("#previous-neuron");
const nextNeuronButton =
document.querySelector("#next-neuron");
const copyNeuronLinkButton =
document.querySelector("#copy-neuron-link");
const randomNeuronButton =
document.querySelector("#random-neuron");
const layerList = document.querySelector(".layer-list");
const channelHeading =
document.querySelector(".channel-heading");
const channelSearch =
document.querySelector(".channel-search");
const channelGrid =
document.querySelector(".channel-grid");
const neuronTitle =
document.querySelector(".neuron-header h2");
const neuronId =
document.querySelector(".neuron-header p");
const imageFrame =
document.querySelector(".image-frame");
const examplesPanel =
document.querySelector(".examples-panel");
const imageModal =
document.querySelector("#image-modal");
const modalImage =
document.querySelector("#modal-image");
const modalCaption =
document.querySelector("#modal-caption");
const modalOriginal =
document.querySelector("#modal-original");
const modalClose =
document.querySelector("#modal-close");
const rowCache = new Map();
const aboutButton =
document.querySelector("#about-button");
const aboutDialog =
document.querySelector("#about-dialog");
const aboutClose =
document.querySelector("#about-close");
let previouslyFocusedElement = null;
let selectedNeuronLoadTimer = null;
let selectedLayer = LAYERS[0];
let selectedChannel = 0;
let searchTerm = "";
let requestNumber = 0;
function formatChannel(channel) {
return String(channel).padStart(4, "0");
}
function buildDatasetUrl(config, split, where, length) {
const parameters = [
["dataset", DATASET],
["config", config],
["split", split],
["where", where],
["offset", "0"],
["length", String(length)],
];
const query = parameters
.map(([key, value]) => {
return (
`${encodeURIComponent(key)}=` +
`${encodeURIComponent(value)}`
);
})
.join("&");
return (
"https://datasets-server.huggingface.co/filter?" +
query
);
}
async function fetchRows(config, split, where, length) {
const url = buildDatasetUrl(
config,
split,
where,
length
);
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`${config}/${split} returned ${response.status}`
);
}
const data = await response.json();
return (data.rows || []).map((item) => item.row);
}
const pendingRequests = new Map();
function wait(milliseconds) {
return new Promise((resolve) => {
window.setTimeout(resolve, milliseconds);
});
}
async function fetchRowsByOffset(
config,
split,
offset,
length
) {
const cacheKey =
`${config}/${split}/${offset}/${length}`;
// Reuse successfully loaded records.
if (rowCache.has(cacheKey)) {
return rowCache.get(cacheKey);
}
// If this exact request is already running, reuse it.
if (pendingRequests.has(cacheKey)) {
return pendingRequests.get(cacheKey);
}
const parameters = [
["dataset", DATASET],
["config", config],
["split", split],
["offset", String(offset)],
["length", String(length)],
];
const query = parameters
.map(([key, value]) => {
return (
`${encodeURIComponent(key)}=` +
`${encodeURIComponent(value)}`
);
})
.join("&");
const url =
"https://datasets-server.huggingface.co/rows?" +
query;
const request = (async () => {
const maximumAttempts = 4;
for (
let attempt = 1;
attempt <= maximumAttempts;
attempt += 1
) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`${config}/${split} returned ${response.status}`
);
}
const data = await response.json();
const rows = (data.rows || []).map(
(item) => item.row
);
rowCache.set(cacheKey, rows);
return rows;
} catch (error) {
if (attempt === maximumAttempts) {
throw error;
}
// 1.5s, 3s, then 6s, plus a little randomness.
const delay =
1500 * (2 ** (attempt - 1)) +
Math.random() * 500;
console.warn(
`Request failed; retrying in ${Math.round(delay)} ms`,
error
);
await wait(delay);
}
}
return [];
})();
pendingRequests.set(cacheKey, request);
try {
return await request;
} finally {
pendingRequests.delete(cacheKey);
}
}
function renderLayers() {
layerList.innerHTML = "";
for (const layer of LAYERS) {
const button = document.createElement("button");
button.type = "button";
button.className = "layer-button";
const isSelected =
layer.name === selectedLayer.name;
button.classList.toggle(
"active",
isSelected
);
button.setAttribute(
"aria-pressed",
String(isSelected)
);
button.setAttribute(
"aria-label",
`${layer.name}, ${layer.channels} channels`
);
const name = document.createElement("span");
name.textContent = layer.name;
const count = document.createElement("span");
count.className = "layer-count";
count.textContent = layer.channels;
button.append(name, count);
button.addEventListener("mouseenter", () => {
if (previewLayer?.name === layer.name) {
return;
}
previewLayer = layer;
searchTerm = "";
channelSearch.value = "";
renderChannels();
});
button.addEventListener("click", () => {
selectLayer(layer);
});
layerList.appendChild(button);
}
}
function renderChannels() {
const visibleLayer =
previewLayer || selectedLayer;
channelHeading.textContent =
`${visibleLayer.name} channels`;
channelGrid.innerHTML = "";
let matches = 0;
for (
let channel = 0;
channel < visibleLayer.channels;
channel += 1
) {
const paddedChannel = formatChannel(channel);
if (
searchTerm &&
!String(channel).includes(searchTerm) &&
!paddedChannel.includes(searchTerm)
) {
continue;
}
matches += 1;
const button = document.createElement("button");
button.type = "button";
button.className = "channel-button";
button.textContent = paddedChannel;
const isSelected =
visibleLayer.name === selectedLayer.name &&
channel === selectedChannel;
button.classList.toggle(
"active",
isSelected
);
button.setAttribute(
"aria-pressed",
String(isSelected)
);
button.setAttribute(
"aria-label",
`${visibleLayer.name}, channel ${channel}`
);
button.addEventListener("click", () => {
selectChannel(channel, visibleLayer);
});
channelGrid.appendChild(button);
}
if (matches === 0) {
channelGrid.innerHTML = `
<p class="placeholder">
No matching channels.
</p>
`;
}
}
function updateNeuronHeading() {
neuronTitle.textContent =
`${selectedLayer.name} · Channel ${selectedChannel}`;
neuronId.textContent =
`Neuron ID: ${selectedLayer.name}_` +
formatChannel(selectedChannel);
document.title =
`${selectedLayer.name} · Channel ${selectedChannel} ` +
"— Inception V1 Microscope";
}
function showLoadingState() {
imageFrame.innerHTML = `
<div
class="skeleton activation-skeleton"
aria-hidden="true"
></div>
<p class="sr-only" role="status">
Loading activation image
</p>
`;
examplesPanel.innerHTML = `
<h3 class="examples-heading">
Top dataset examples
</h3>
<div class="example-skeleton-list">
${createExampleSkeleton()}
${createExampleSkeleton()}
${createExampleSkeleton()}
</div>
<p class="sr-only" role="status">
Loading ranked dataset examples
</p>
`;
}
function createExampleSkeleton() {
return `
<div class="example-card skeleton-card">
<div class="skeleton skeleton-line"></div>
<div class="example-images">
<div class="skeleton skeleton-image"></div>
<div class="skeleton skeleton-image"></div>
</div>
</div>
`;
}
function openImageModal(src, caption) {
previouslyFocusedElement =
document.activeElement;
modalImage.src = src;
modalImage.alt = caption;
modalCaption.textContent = caption;
modalOriginal.href = src;
imageModal.hidden = false;
document.body.classList.add("modal-open");
modalClose.focus();
}
function closeImageModal() {
imageModal.hidden = true;
modalImage.src = "";
document.body.classList.remove("modal-open");
previouslyFocusedElement?.focus();
}
function enableImageEnlargement(image, caption) {
image.classList.add("enlargeable-image");
image.tabIndex = 0;
image.setAttribute("role", "button");
image.setAttribute(
"aria-label",
`Enlarge ${caption}`
);
image.addEventListener("click", () => {
openImageModal(image.src, caption);
});
image.addEventListener("keydown", (event) => {
if (
event.key === "Enter" ||
event.key === " "
) {
event.preventDefault();
openImageModal(image.src, caption);
}
});
}
function renderActivation(record) {
if (!record?.image?.src) {
imageFrame.innerHTML = `
<p class="placeholder">
Activation image is not available.
</p>
`;
return;
}
const image = document.createElement("img");
image.src = record.image.src;
image.alt =
`${record.layer}, channel ${record.channel} ` +
"activation maximization";
enableImageEnlargement(
image,
`${record.layer} · Channel ${record.channel} ` +
"activation maximization"
);
imageFrame.innerHTML = "";
imageFrame.appendChild(image);
}
function createExampleCard(example) {
const card = document.createElement("article");
card.className = "example-card";
const header = document.createElement("header");
header.className = "example-header";
const rank = document.createElement("strong");
rank.textContent =
`Rank ${String(example.rank).padStart(2, "0")}`;
const score = document.createElement("span");
score.textContent =
`Activation: ${Number(
example.activation_score
).toFixed(3)}`;
header.append(rank, score);
const imagePair = document.createElement("div");
imagePair.className = "example-images";
const fullFigure = createExampleFigure(
"Full image",
example.full_image?.src,
`Rank ${example.rank} full image`
);
fullFigure.classList.add("full-example");
const cropFigure = createExampleFigure(
"Triggering crop",
example.crop_image?.src,
`Rank ${example.rank} triggering crop`
);
cropFigure.classList.add("crop-example");
imagePair.append(fullFigure, cropFigure);
const source = document.createElement("p");
source.className = "example-source";
source.textContent =
`Source: ${example.source_image_id}`;
card.append(header, imagePair, source);
return card;
}
function createExampleFigure(label, src, alt) {
const figure = document.createElement("figure");
const caption = document.createElement("figcaption");
caption.textContent = label;
figure.appendChild(caption);
if (src) {
const image = document.createElement("img");
image.src = src;
image.alt = alt;
image.loading = "lazy";
enableImageEnlargement(image, alt);
figure.appendChild(image);
} else {
const missing = document.createElement("p");
missing.className = "missing-image";
missing.textContent = "Image unavailable";
figure.appendChild(missing);
}
return figure;
}
function configureDisplayControls() {
const buttons = examplesPanel.querySelectorAll(
"[data-display-mode]"
);
for (const button of buttons) {
button.addEventListener("click", () => {
exampleDisplayMode =
button.dataset.displayMode;
applyExampleDisplayMode();
});
}
}
function applyExampleDisplayMode() {
examplesPanel.dataset.displayMode =
exampleDisplayMode;
const buttons = examplesPanel.querySelectorAll(
"[data-display-mode]"
);
for (const button of buttons) {
const isActive =
button.dataset.displayMode ===
exampleDisplayMode;
button.classList.toggle(
"active",
isActive
);
button.setAttribute(
"aria-pressed",
String(isActive)
);
}
}
function renderExamples(examples) {
examplesPanel.innerHTML = `
<div class="examples-title-row">
<h3 class="examples-heading">
Top dataset examples
</h3>
<div
class="display-controls"
role="group"
aria-label="Dataset example display mode"
>
<button
type="button"
data-display-mode="paired"
>
Paired
</button>
<button
type="button"
data-display-mode="full"
>
Full
</button>
<button
type="button"
data-display-mode="crop"
>
Crops
</button>
</div>
</div>
`;
if (examples.length === 0) {
examplesPanel.insertAdjacentHTML(
"beforeend",
`
<div class="example-placeholder">
Dataset examples are not currently available
for this neuron.
</div>
`
);
return;
}
examples.sort((a, b) => a.rank - b.rank);
for (const example of examples) {
examplesPanel.appendChild(
createExampleCard(example)
);
}
configureDisplayControls();
applyExampleDisplayMode();
}
function showDataError(error) {
console.error(error);
imageFrame.innerHTML = `
<p class="placeholder">
Unable to load this activation image.
</p>
`;
examplesPanel.innerHTML = `
<h3 class="examples-heading">
Top dataset examples
</h3>
<div class="example-placeholder">
Unable to load this neuron’s dataset records.
Please try refreshing the page.
</div>
`;
}
function getActivationOffset(layer, channel) {
const layerIndex = LAYERS.findIndex(
(item) => item.name === layer.name
);
const layerStartOffset = LAYERS
.slice(0, layerIndex)
.reduce(
(total, item) => total + item.channels,
0
);
/*
* Activation images use lexicographic filename
* ordering: 0, 1, 10, 100, ... 2, 20, ...
*/
const channelOrder = Array.from(
{ length: layer.channels },
(_, channelNumber) => channelNumber
).sort((first, second) => {
const firstText = String(first);
const secondText = String(second);
if (firstText < secondText) {
return -1;
}
if (firstText > secondText) {
return 1;
}
return 0;
});
const channelPosition =
channelOrder.indexOf(channel);
return layerStartOffset + channelPosition;
}
function addRetryButton(container) {
const button = document.createElement("button");
button.type = "button";
button.className = "retry-button";
button.textContent = "Try again";
button.addEventListener("click", () => {
// Remove cached signed image URLs before requesting fresh ones.
rowCache.clear();
loadSelectedNeuron();
});
container.appendChild(button);
}
function loadSelectedNeuron() {
window.clearTimeout(selectedNeuronLoadTimer);
selectedNeuronLoadTimer = window.setTimeout(() => {
selectedNeuronLoadTimer = null;
performSelectedNeuronLoad();
}, 500);
}
async function performSelectedNeuronLoad() {
const currentRequest = ++requestNumber;
showLoadingState();
/*const selectedLayerIndex = LAYERS.findIndex(
(layer) => layer.name === selectedLayer.name
);
const layerStartOffset = LAYERS
.slice(0, selectedLayerIndex)
.reduce(
(total, layer) => total + layer.channels,
0
);
*/
/*
* Activation files were uploaded in lexicographic
* filename order: 0, 1, 10, 100, ... rather than
* numerical channel order.
*/
/*const activationChannelOrder = Array.from(
{ length: selectedLayer.channels },
(_, channel) => channel
).sort((first, second) => {
const firstText = String(first);
const secondText = String(second);
if (firstText < secondText) {
return -1;
}
if (firstText > secondText) {
return 1;
}
return 0;
});
const activationPosition =
activationChannelOrder.indexOf(selectedChannel);
const activationOffset =
layerStartOffset + activationPosition; */
const activationOffset =
getActivationOffset(
selectedLayer,
selectedChannel
);
const activationRequest = fetchRowsByOffset(
"activation_maximization",
"train",
activationOffset,
1
)
.then((rows) => {
if (currentRequest !== requestNumber) {
return;
}
const record = rows[0];
if (
!record ||
record.layer !== selectedLayer.name ||
record.channel !== selectedChannel
) {
throw new Error(
"Activation offset returned the wrong neuron."
);
}
renderActivation(record);
})
.catch((error) => {
if (currentRequest !== requestNumber) {
return;
}
console.error(
"Activation request failed:",
error
);
imageFrame.innerHTML = `
<p class="placeholder">
Unable to load the activation image.
</p>
`;
addRetryButton(imageFrame);
});
const exampleOffset = selectedChannel * 10;
const examplesRequest = fetchRowsByOffset(
"dataset_examples",
selectedLayer.name,
exampleOffset,
10
)
.then((rows) => {
if (currentRequest !== requestNumber) {
return;
}
const wrongRecord = rows.some((record) => {
return (
record.layer !== selectedLayer.name ||
record.channel !== selectedChannel
);
});
const returnedRanks = rows
.map((record) => record.rank)
.sort((a, b) => a - b);
const expectedRanks = [
1, 2, 3, 4, 5,
6, 7, 8, 9, 10,
];
const ranksAreCorrect =
JSON.stringify(returnedRanks) ===
JSON.stringify(expectedRanks);
if (
rows.length !== 10 ||
wrongRecord ||
!ranksAreCorrect
) {
throw new Error(
"Example offset returned unexpected records."
);
}
renderExamples(rows);
})
.catch((error) => {
if (currentRequest !== requestNumber) {
return;
}
console.error(
"Dataset-examples request failed:",
error
);
examplesPanel.innerHTML = `
<h3 class="examples-heading">
Top dataset examples
</h3>
<div class="example-placeholder">
Unable to load the dataset examples.
Please try again.
</div>
`;
const examplePlaceholder =
examplesPanel.querySelector(
".example-placeholder"
);
addRetryButton(examplePlaceholder);
});
await Promise.allSettled([
activationRequest,
examplesRequest,
]);
}
function getAdjacentNeuron(direction) {
let layerIndex = LAYERS.findIndex(
(layer) => layer.name === selectedLayer.name
);
let channel = selectedChannel + direction;
if (channel < 0) {
layerIndex -= 1;
if (layerIndex < 0) {
return null;
}
const layer = LAYERS[layerIndex];
return {
layer,
channel: layer.channels - 1,
};
}
if (channel >= selectedLayer.channels) {
layerIndex += 1;
if (layerIndex >= LAYERS.length) {
return null;
}
return {
layer: LAYERS[layerIndex],
channel: 0,
};
}
return {
layer: selectedLayer,
channel,
};
}
async function prefetchNeuronRecords(
layer,
channel
) {
const activationOffset =
getActivationOffset(layer, channel);
const exampleOffset = channel * 10;
await Promise.allSettled([
fetchRowsByOffset(
"activation_maximization",
"train",
activationOffset,
1
),
fetchRowsByOffset(
"dataset_examples",
layer.name,
exampleOffset,
10
),
]);
}
function prefetchAdjacentNeurons() {
const previous = getAdjacentNeuron(-1);
const next = getAdjacentNeuron(1);
if (previous) {
prefetchNeuronRecords(
previous.layer,
previous.channel
);
}
if (next) {
prefetchNeuronRecords(
next.layer,
next.channel
);
}
}
function updateNavigationControls() {
const selectedLayerIndex = LAYERS.findIndex(
(layer) => layer.name === selectedLayer.name
);
const isFirstNeuron =
selectedLayerIndex === 0 &&
selectedChannel === 0;
const isLastNeuron =
selectedLayerIndex === LAYERS.length - 1 &&
selectedChannel === selectedLayer.channels - 1;
previousNeuronButton.disabled = isFirstNeuron;
nextNeuronButton.disabled = isLastNeuron;
}
function navigateNeuron(direction) {
previewLayer = null;
let layerIndex = LAYERS.findIndex(
(layer) => layer.name === selectedLayer.name
);
let newChannel = selectedChannel + direction;
if (newChannel < 0) {
layerIndex -= 1;
if (layerIndex < 0) {
return;
}
selectedLayer = LAYERS[layerIndex];
newChannel = selectedLayer.channels - 1;
} else if (
newChannel >= selectedLayer.channels
) {
layerIndex += 1;
if (layerIndex >= LAYERS.length) {
return;
}
selectedLayer = LAYERS[layerIndex];
newChannel = 0;
}
selectedChannel = newChannel;
searchTerm = "";
channelSearch.value = "";
renderLayers();
renderChannels();
updateNeuronHeading();
updateNavigationControls();
updateUrl();
loadSelectedNeuron();
const activeChannel =
channelGrid.querySelector(
".channel-button.active"
);
activeChannel?.scrollIntoView({
block: "nearest",
});
}
function updateUrl(historyMode = "push") {
const url = new URL(window.location.href);
url.searchParams.set(
"layer",
selectedLayer.name
);
url.searchParams.set(
"channel",
String(selectedChannel)
);
if (historyMode === "replace") {
window.history.replaceState({}, "", url);
} else {
window.history.pushState({}, "", url);
}
}
function selectLayer(layer) {
previewLayer = null;
selectedLayer = layer;
selectedChannel = 0;
searchTerm = "";
channelSearch.value = "";
renderLayers();
renderChannels();
updateNeuronHeading();
updateNavigationControls();
updateUrl();
loadSelectedNeuron();
}
function selectChannel(
channel,
layer = selectedLayer
) {
selectedLayer = layer;
selectedChannel = channel;
previewLayer = null;
renderLayers();
renderChannels();
updateNeuronHeading();
updateNavigationControls();
updateUrl();
loadSelectedNeuron();
}
function restoreSelectionFromUrl() {
const parameters =
new URLSearchParams(window.location.search);
const requestedLayer =
parameters.get("layer");
const requestedChannel =
Number(parameters.get("channel"));
const matchingLayer = LAYERS.find(
(layer) => layer.name === requestedLayer
);
if (matchingLayer) {
selectedLayer = matchingLayer;
}
if (
Number.isInteger(requestedChannel) &&
requestedChannel >= 0 &&
requestedChannel < selectedLayer.channels
) {
selectedChannel = requestedChannel;
}
}
async function copyNeuronLink() {
const url = new URL(window.location.href);
url.searchParams.set(
"layer",
selectedLayer.name
);
url.searchParams.set(
"channel",
String(selectedChannel)
);
const link = url.toString();
try {
await navigator.clipboard.writeText(link);
} catch (error) {
const temporaryInput =
document.createElement("textarea");
temporaryInput.value = link;
temporaryInput.setAttribute("readonly", "");
temporaryInput.style.position = "fixed";
temporaryInput.style.opacity = "0";
document.body.appendChild(temporaryInput);
temporaryInput.select();
document.execCommand("copy");
temporaryInput.remove();
}
copyNeuronLinkButton.textContent = "Copied!";
window.setTimeout(() => {
copyNeuronLinkButton.textContent =
"Copy link";
}, 1600);
}
function selectRandomNeuron() {
const totalChannels = LAYERS.reduce(
(total, layer) => total + layer.channels,
0
);
let randomIndex = Math.floor(
Math.random() * totalChannels
);
for (const layer of LAYERS) {
if (randomIndex < layer.channels) {
selectChannel(randomIndex, layer);
return;
}
randomIndex -= layer.channels;
}
}
channelSearch.addEventListener("input", (event) => {
searchTerm = event.target.value.trim();
renderChannels();
});
modalClose.addEventListener(
"click",
closeImageModal
);
channelSearch.addEventListener(
"keydown",
(event) => {
const visibleLayer =
previewLayer || selectedLayer;
if (event.key === "Escape") {
searchTerm = "";
channelSearch.value = "";
renderChannels();
return;
}
if (event.key !== "Enter") {
return;
}
const enteredValue =
channelSearch.value.trim();
if (!/^\d+$/.test(enteredValue)) {
return;
}
const channel = Number(enteredValue);
if (
channel < 0 ||
channel >= visibleLayer.channels
) {
return;
}
selectChannel(channel, visibleLayer);
}
);
imageModal.addEventListener("click", (event) => {
if (event.target === imageModal) {
closeImageModal();
}
});
document.addEventListener("keydown", (event) => {
/*
* When the image modal is open, only handle Escape.
*/
if (!imageModal.hidden) {
if (event.key === "Escape") {
closeImageModal();
}
return;
}
/*
* Do not change neurons while someone is typing
* in the channel-search field.
*/
const activeElement = document.activeElement;
const isTyping =
activeElement?.tagName === "INPUT" ||
activeElement?.tagName === "TEXTAREA" ||
activeElement?.tagName === "SELECT" ||
activeElement?.isContentEditable;
if (isTyping) {
return;
}
if (event.key === "ArrowLeft") {
event.preventDefault();
navigateNeuron(-1);
}
if (event.key === "ArrowRight") {
event.preventDefault();
navigateNeuron(1);
}
});
channelPanel.addEventListener(
"mouseleave",
() => {
if (!previewLayer) {
return;
}
previewLayer = null;
searchTerm = "";
channelSearch.value = "";
renderChannels();
}
);
previousNeuronButton.addEventListener(
"click",
() => navigateNeuron(-1)
);
nextNeuronButton.addEventListener(
"click",
() => navigateNeuron(1)
);
copyNeuronLinkButton.addEventListener(
"click",
copyNeuronLink
);
randomNeuronButton.addEventListener(
"click",
selectRandomNeuron
);
aboutButton.addEventListener("click", () => {
aboutDialog.showModal();
});
aboutClose.addEventListener("click", () => {
aboutDialog.close();
});
aboutDialog.addEventListener("click", (event) => {
const bounds =
aboutDialog.getBoundingClientRect();
const clickedOutside =
event.clientX < bounds.left ||
event.clientX > bounds.right ||
event.clientY < bounds.top ||
event.clientY > bounds.bottom;
if (clickedOutside) {
aboutDialog.close();
}
});
window.addEventListener("popstate", () => {
previewLayer = null;
searchTerm = "";
channelSearch.value = "";
restoreSelectionFromUrl();
renderLayers();
renderChannels();
updateNeuronHeading();
updateNavigationControls();
loadSelectedNeuron();
});
restoreSelectionFromUrl();
renderLayers();
renderChannels();
updateNeuronHeading();
updateNavigationControls();
updateUrl("replace");
loadSelectedNeuron();