Skip to content

Commit f314976

Browse files
gguf-viewer: refactor
1 parent 089b724 commit f314976

File tree

10 files changed

+614
-295
lines changed

10 files changed

+614
-295
lines changed

tools/gguf-viewer/public/architecture.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,13 +585,15 @@
585585

586586
const heatmapButton = document.createElement("button");
587587
heatmapButton.type = "button";
588+
heatmapButton.className = "viewer-button";
588589
heatmapButton.dataset.action = "heatmap";
589590
heatmapButton.dataset.name = encodedName;
590591
heatmapButton.textContent = "Heatmap";
591592
actions.appendChild(heatmapButton);
592593

593594
const statisticsButton = document.createElement("button");
594595
statisticsButton.type = "button";
596+
statisticsButton.className = "viewer-button";
595597
statisticsButton.dataset.action = "statistics";
596598
statisticsButton.dataset.name = encodedName;
597599
statisticsButton.textContent = "Statistics";
@@ -839,11 +841,13 @@
839841

840842
const prevButton = document.createElement("button");
841843
prevButton.type = "button";
844+
prevButton.className = "viewer-button";
842845
prevButton.textContent = "Prev";
843846
navControls.appendChild(prevButton);
844847

845848
const nextButton = document.createElement("button");
846849
nextButton.type = "button";
850+
nextButton.className = "viewer-button";
847851
nextButton.textContent = "Next";
848852
navControls.appendChild(nextButton);
849853

tools/gguf-viewer/public/browser.js

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,7 @@
1-
async function requestModelSelection(path) {
2-
if (!path) {
3-
throw new Error("Model path is required");
4-
}
5-
const response = await fetchJSON(`api/models/select?model=${encodeURIComponent(path)}`, { method: "POST" });
6-
const selected = typeof response?.selected === "string" && response.selected.length > 0
7-
? response.selected
8-
: path;
9-
backendSelectedModel = selected;
10-
return selected;
11-
}
12-
131
function setCurrentModel(path, options = {}) {
142
const { updateBrowser = true, updateUrl = true } = options;
153
const normalized = typeof path === "string" && path.length > 0 ? path : null;
164
currentModelPath = normalized;
17-
if (!normalized) {
18-
backendSelectedModel = null;
19-
}
205
const items = Array.isArray(modelBrowserState.items) ? modelBrowserState.items : [];
216
const updatedItems = items.map((item) => ({ ...item, selected: !!normalized && item.path === normalized }));
227
modelBrowserState = { ...modelBrowserState, selected: normalized, items: updatedItems };
@@ -61,7 +46,7 @@ function renderModelBrowser() {
6146

6247
const button = document.createElement("button");
6348
button.type = "button";
64-
button.className = "model-button";
49+
button.className = "viewer-button model-button";
6550
const isSelected = !!item.selected;
6651
button.dataset.selected = isSelected ? "true" : "false";
6752
button.disabled = selectingModel;
@@ -140,19 +125,26 @@ async function selectModel(path) {
140125

141126
selectingModel = true;
142127
let errorMessage = null;
128+
const previousPath = currentModelPath;
143129
try {
144-
const selectedPath = await requestModelSelection(path);
145-
setCurrentModel(selectedPath, { updateBrowser: false });
130+
setCurrentModel(path, { updateBrowser: false, updateUrl: false });
146131
pendingHeatmapState.applied = true;
147132
pendingHeatmapState.tensor = null;
148133
pendingHeatmapState.slice = null;
149134
pendingHeatmapState.min = null;
150135
pendingHeatmapState.max = null;
151136
pendingHeatmapState.x = null;
152137
pendingHeatmapState.y = null;
153-
await refreshAll({ ensureSelected: false });
138+
const result = await refreshAll();
139+
if (!result || result.ok !== true) {
140+
errorMessage = result?.message || NO_MODEL_MESSAGE;
141+
setCurrentModel(previousPath, { updateBrowser: false, updateUrl: false });
142+
updateModelUrlState();
143+
}
154144
} catch (err) {
155-
errorMessage = err.status === 409 ? NO_MODEL_MESSAGE : err.message;
145+
errorMessage = isModelUnavailableError(err) ? NO_MODEL_MESSAGE : err.message;
146+
setCurrentModel(previousPath, { updateBrowser: false, updateUrl: false });
147+
updateModelUrlState();
156148
} finally {
157149
selectingModel = false;
158150
renderModelBrowser();

tools/gguf-viewer/public/common.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ applyPendingHeatmapPreferences();
141141

142142
let urlSyncReady = false;
143143
let currentModelPath = currentUrlState.model;
144-
let backendSelectedModel = null;
145144

146145
async function syncStateFromSearchParams() {
147146
const nextState = readUrlState();
@@ -151,7 +150,6 @@ async function syncStateFromSearchParams() {
151150
applyPendingHeatmapPreferences();
152151

153152
if (modelChanged) {
154-
backendSelectedModel = null;
155153
setCurrentModel(nextState.model, { updateBrowser: true, updateUrl: false });
156154
if (nextState.model) {
157155
pendingHeatmapState.applied = false;
@@ -554,6 +552,13 @@ const LOADING_MESSAGE = "Loading…";
554552
const BROWSER_EMPTY_MESSAGE = "No GGUF files found in the root directory.";
555553
const HEATMAP_DEFAULT_HEADER_MESSAGE = "Select a tensor to open the heatmap viewer.";
556554
const HEATMAP_DEFAULT_STREAM_MESSAGE = "Select a tensor to stream its weights.";
555+
556+
function isModelUnavailableError(err) {
557+
if (!err || typeof err.status !== "number") {
558+
return false;
559+
}
560+
return err.status === 400 || err.status === 404 || err.status === 409;
561+
}
557562
let modelBrowserState = { root: "", items: [], selected: currentModelPath || null };
558563
let selectingModel = false;
559564

@@ -578,6 +583,8 @@ const heatmapState = {
578583
viewWidth: heatmapCanvas ? heatmapCanvas.width : 0,
579584
viewHeight: heatmapCanvas ? heatmapCanvas.height : 0,
580585
controller: null,
586+
sliceController: null,
587+
sliceRequestId: 0,
581588
fetching: false,
582589
imageReady: false,
583590
values: [],
@@ -927,8 +934,7 @@ function syncHeatmapControls() {
927934
}
928935

929936
if (heatmapSliceButton) {
930-
const hasSliceScale = Number.isFinite(heatmapState.sliceMin) && Number.isFinite(heatmapState.sliceMax);
931-
heatmapSliceButton.disabled = !ready || !hasSliceScale;
937+
heatmapSliceButton.disabled = !ready;
932938
}
933939

934940
const hasPercentiles = ready && heatmapState.valid > 0;

tools/gguf-viewer/public/heatmap.js

Lines changed: 126 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,11 @@ function requestHeatmapValue(globalX, globalY, slice) {
9292
heatmapHoverState.controller.abort();
9393
heatmapHoverState.controller = null;
9494
}
95+
const baseSlice = Number.isFinite(slice) ? slice : heatmapState.slice;
96+
const requestSlice = clampSlice(baseSlice);
9597
heatmapHoverState.requestX = globalX;
9698
heatmapHoverState.requestY = globalY;
97-
heatmapHoverState.requestSlice = slice;
99+
heatmapHoverState.requestSlice = requestSlice;
98100
heatmapHoverState.data = null;
99101

100102
const controller = new AbortController();
@@ -103,9 +105,7 @@ function requestHeatmapValue(globalX, globalY, slice) {
103105
const params = new URLSearchParams();
104106
params.set("x", String(globalX));
105107
params.set("y", String(globalY));
106-
if (heatmapState.layout && typeof heatmapState.layout.depth === "number" && heatmapState.layout.depth > 1) {
107-
params.set("slice", String(slice));
108-
}
108+
params.set("slice", String(requestSlice));
109109
appendModelParam(params);
110110

111111
const tensorName = encodeURIComponent(heatmapState.tensor.name);
@@ -123,7 +123,7 @@ function requestHeatmapValue(globalX, globalY, slice) {
123123
return;
124124
}
125125
heatmapHoverState.controller = null;
126-
if (heatmapHoverState.requestX !== globalX || heatmapHoverState.requestY !== globalY || heatmapHoverState.requestSlice !== slice) {
126+
if (heatmapHoverState.requestX !== globalX || heatmapHoverState.requestY !== globalY || heatmapHoverState.requestSlice !== requestSlice) {
127127
return;
128128
}
129129
heatmapHoverState.data = data;
@@ -134,7 +134,7 @@ function requestHeatmapValue(globalX, globalY, slice) {
134134
return;
135135
}
136136
heatmapHoverState.controller = null;
137-
if (heatmapHoverState.requestX !== globalX || heatmapHoverState.requestY !== globalY || heatmapHoverState.requestSlice !== slice) {
137+
if (heatmapHoverState.requestX !== globalX || heatmapHoverState.requestY !== globalY || heatmapHoverState.requestSlice !== requestSlice) {
138138
return;
139139
}
140140
heatmapHoverState.data = null;
@@ -153,6 +153,10 @@ function resetHeatmap(message = HEATMAP_DEFAULT_STREAM_MESSAGE) {
153153
heatmapState.controller.abort();
154154
heatmapState.controller = null;
155155
}
156+
if (heatmapState.sliceController) {
157+
heatmapState.sliceController.abort();
158+
heatmapState.sliceController = null;
159+
}
156160
hideHeatmapTooltip();
157161
const preserveGrid = !!heatmapState.gridVisible;
158162
heatmapState.tensor = null;
@@ -178,6 +182,7 @@ function resetHeatmap(message = HEATMAP_DEFAULT_STREAM_MESSAGE) {
178182
heatmapState.viewWidth = heatmapCanvas.width;
179183
heatmapState.viewHeight = heatmapCanvas.height;
180184
heatmapState.pendingScale = null;
185+
heatmapState.sliceRequestId = 0;
181186
if (heatmapGainOffsetToggle) {
182187
heatmapGainOffsetToggle.checked = false;
183188
}
@@ -532,15 +537,14 @@ async function fetchHeatmapWindow() {
532537
const tensor = heatmapState.tensor;
533538
const layout = heatmapState.layout || { width: heatmapState.viewWidth, height: heatmapState.viewHeight, depth: 1 };
534539
heatmapState.slice = clampSlice(heatmapState.slice);
540+
const requestedSlice = heatmapState.slice;
535541

536542
const params = new URLSearchParams();
537543
params.set("x", String(heatmapState.windowX));
538544
params.set("y", String(heatmapState.windowY));
539545
params.set("width", String(heatmapState.viewWidth));
540546
params.set("height", String(heatmapState.viewHeight));
541-
if (layout.depth > 1) {
542-
params.set("slice", String(heatmapState.slice));
543-
}
547+
params.set("slice", String(requestedSlice));
544548
appendModelParam(params);
545549

546550
try {
@@ -577,8 +581,9 @@ async function fetchHeatmapWindow() {
577581
} else {
578582
heatmapState.windowY = clampWindowY(heatmapState.windowY);
579583
}
584+
let nextSlice = heatmapState.slice;
580585
if (typeof origin.slice === "number") {
581-
heatmapState.slice = clampSlice(origin.slice);
586+
nextSlice = clampSlice(origin.slice);
582587
}
583588

584589
const viewport = data.viewport || {};
@@ -597,17 +602,29 @@ async function fetchHeatmapWindow() {
597602

598603
heatmapState.windowX = clampWindowX(heatmapState.windowX);
599604
heatmapState.windowY = clampWindowY(heatmapState.windowY);
600-
heatmapState.slice = clampSlice(heatmapState.slice);
605+
nextSlice = clampSlice(nextSlice);
606+
const previousSlice = heatmapState.slice;
607+
heatmapState.slice = nextSlice;
608+
const sliceChanged = heatmapState.slice !== previousSlice;
609+
if (sliceChanged) {
610+
if (heatmapState.sliceController) {
611+
heatmapState.sliceController.abort();
612+
heatmapState.sliceController = null;
613+
}
614+
heatmapState.sliceRequestId = (heatmapState.sliceRequestId || 0) + 1;
615+
heatmapState.sliceMin = undefined;
616+
heatmapState.sliceMax = undefined;
617+
}
601618
if (histogramState.slice !== heatmapState.slice && heatmapState.tensor) {
602619
histogramState.slice = heatmapState.slice;
603620
void fetchHistogram();
604621
}
622+
if (sliceChanged) {
623+
void fetchSliceProperties();
624+
}
605625

606626
heatmapState.viewMin = typeof data.min === "number" ? data.min : undefined;
607627
heatmapState.viewMax = typeof data.max === "number" ? data.max : undefined;
608-
heatmapState.sliceMin = typeof data.sliceMin === "number" ? data.sliceMin : undefined;
609-
heatmapState.sliceMax = typeof data.sliceMax === "number" ? data.sliceMax : undefined;
610-
611628
if (!heatmapState.scaleInitialized) {
612629
setHeatmapScale(heatmapState.viewMin, heatmapState.viewMax, { reapply: false, sync: false });
613630
} else {
@@ -656,6 +673,91 @@ async function fetchHeatmapWindow() {
656673
}
657674
}
658675

676+
async function fetchSliceProperties(options = {}) {
677+
if (!heatmapState.tensor) {
678+
return null;
679+
}
680+
if (!currentModelPath) {
681+
return null;
682+
}
683+
684+
if (heatmapState.sliceController) {
685+
heatmapState.sliceController.abort();
686+
}
687+
688+
const controller = new AbortController();
689+
const requestId = (heatmapState.sliceRequestId || 0) + 1;
690+
heatmapState.sliceController = controller;
691+
heatmapState.sliceRequestId = requestId;
692+
693+
const params = new URLSearchParams();
694+
const requestedSlice = Number.isInteger(options.slice)
695+
? clampSlice(options.slice)
696+
: clampSlice(heatmapState.slice);
697+
params.set("slice", String(requestedSlice));
698+
appendModelParam(params);
699+
700+
try {
701+
const res = await fetch(`api/tensors/${encodeURIComponent(heatmapState.tensor.name)}/slice/properties?${params.toString()}`, {
702+
signal: controller.signal,
703+
});
704+
if (!res.ok) {
705+
const text = await res.text();
706+
throw new Error(text || `Request failed: ${res.status}`);
707+
}
708+
const data = await res.json();
709+
if (controller.signal.aborted || heatmapState.sliceRequestId !== requestId) {
710+
return null;
711+
}
712+
713+
if (heatmapState.sliceController === controller) {
714+
heatmapState.sliceController = null;
715+
}
716+
717+
const hasMin = typeof data.min === "number" && Number.isFinite(data.min);
718+
const hasMax = typeof data.max === "number" && Number.isFinite(data.max);
719+
heatmapState.sliceMin = hasMin ? data.min : undefined;
720+
heatmapState.sliceMax = hasMax ? data.max : undefined;
721+
722+
const valid = typeof data.valid === "number" && data.valid > 0 ? data.valid : 0;
723+
724+
if (options.applyScale) {
725+
if (hasMin && hasMax && valid > 0) {
726+
setHeatmapScale(data.min, data.max);
727+
} else {
728+
const reason = valid > 0
729+
? "Slice scale unavailable."
730+
: "Slice has no finite values to scale.";
731+
heatmapHeaderMessage = reason;
732+
updateHeatmapHeader();
733+
}
734+
}
735+
736+
syncHeatmapControls();
737+
return {
738+
slice: typeof data.slice === "number" ? data.slice : requestedSlice,
739+
min: heatmapState.sliceMin,
740+
max: heatmapState.sliceMax,
741+
valid,
742+
};
743+
} catch (err) {
744+
if (controller.signal.aborted || heatmapState.sliceRequestId !== requestId) {
745+
return null;
746+
}
747+
if (heatmapState.sliceController === controller) {
748+
heatmapState.sliceController = null;
749+
}
750+
if (options.applyScale) {
751+
const message = err instanceof Error ? err.message : String(err);
752+
heatmapHeaderMessage = `Slice scale request failed: ${message}`;
753+
updateHeatmapHeader();
754+
}
755+
console.error("Failed to fetch slice properties", err);
756+
syncHeatmapControls();
757+
return null;
758+
}
759+
}
760+
659761
function updateHeatmapImage(values, minValue, maxValue) {
660762
ensureHeatmapBuffer(heatmapState.viewWidth, heatmapState.viewHeight);
661763

@@ -811,6 +913,13 @@ function commitSliceInput() {
811913
heatmapSliceInput.value = String(value);
812914
const nextSlice = clampSlice(value - 1);
813915
if (nextSlice !== heatmapState.slice) {
916+
if (heatmapState.sliceController) {
917+
heatmapState.sliceController.abort();
918+
heatmapState.sliceController = null;
919+
}
920+
heatmapState.sliceRequestId = (heatmapState.sliceRequestId || 0) + 1;
921+
heatmapState.sliceMin = undefined;
922+
heatmapState.sliceMax = undefined;
814923
heatmapState.slice = nextSlice;
815924
histogramState.slice = nextSlice;
816925
if (heatmapState.tensor) {
@@ -888,10 +997,11 @@ if (heatmapSliceButton) {
888997
if (!heatmapState.tensor) {
889998
return;
890999
}
891-
if (!Number.isFinite(heatmapState.sliceMin) || !Number.isFinite(heatmapState.sliceMax)) {
1000+
if (Number.isFinite(heatmapState.sliceMin) && Number.isFinite(heatmapState.sliceMax)) {
1001+
setHeatmapScale(heatmapState.sliceMin, heatmapState.sliceMax);
8921002
return;
8931003
}
894-
setHeatmapScale(heatmapState.sliceMin, heatmapState.sliceMax);
1004+
void fetchSliceProperties({ applyScale: true });
8951005
});
8961006
}
8971007

0 commit comments

Comments
 (0)