fix(qa-lab): skip render when poll data unchanged and use dropdown model selectors

This commit is contained in:
Peter Steinberger
2026-04-06 19:25:43 +01:00
parent 9663343183
commit 9f4c2caf06
2 changed files with 82 additions and 34 deletions

View File

@@ -85,6 +85,40 @@ export async function createQaLabApp(root: HTMLDivElement) {
let chatScrollLocked = true;
let previousMessageCount = 0;
/* ---------- Render guards (avoid DOM churn during polling) ---------- */
let lastFingerprint = "";
let renderDeferred = false;
function stateFingerprint(): string {
const msgs = state.snapshot?.messages;
const ev = state.snapshot?.events;
return JSON.stringify({
mc: msgs?.length ?? 0,
lm: msgs && msgs.length > 0 ? msgs[msgs.length - 1].id : null,
cc: state.snapshot?.conversations.length ?? 0,
tc: state.snapshot?.threads.length ?? 0,
ec: ev?.length ?? 0,
lc: ev && ev.length > 0 ? ev[ev.length - 1].cursor : -1,
rs: state.bootstrap?.runner.status,
ra: state.bootstrap?.runner.startedAt,
rf: state.bootstrap?.runner.finishedAt,
re: state.bootstrap?.runner.error,
ss: state.scenarioRun?.status,
sc: state.scenarioRun?.counts,
so: state.scenarioRun?.scenarios.map((o) => o.status).join(","),
rp: state.latestReport?.generatedAt,
cs: state.bootstrap?.runnerCatalog.status,
cl: state.bootstrap?.runnerCatalog.real.length ?? 0,
er: state.error,
});
}
function isSelectOpen(): boolean {
const active = document.activeElement;
return !!active && root.contains(active) && active.tagName === "SELECT";
}
/* ---------- Data fetching ---------- */
async function refresh() {
@@ -125,7 +159,17 @@ export async function createQaLabApp(root: HTMLDivElement) {
} catch (error) {
state.error = error instanceof Error ? error.message : String(error);
}
render();
/* Only re-render when data actually changed; defer if a <select> is open */
const fp = stateFingerprint();
if (fp !== lastFingerprint) {
lastFingerprint = fp;
renderDeferred = true;
}
if (renderDeferred && !isSelectOpen()) {
renderDeferred = false;
render();
}
}
/* ---------- Draft mutations ---------- */
@@ -453,16 +497,16 @@ export async function createQaLabApp(root: HTMLDivElement) {
root.querySelector<HTMLInputElement>("#fast-mode")?.addEventListener("change", (e) => {
updateRunnerDraft((d) => ({ ...d, fastMode: (e.currentTarget as HTMLInputElement).checked }));
});
root.querySelector<HTMLElement>("#primary-model")?.addEventListener("input", (e) => {
root.querySelector<HTMLSelectElement>("#primary-model")?.addEventListener("change", (e) => {
updateRunnerDraft((d) => ({
...d,
primaryModel: (e.currentTarget as HTMLInputElement).value,
primaryModel: (e.currentTarget as HTMLSelectElement).value,
}));
});
root.querySelector<HTMLElement>("#alternate-model")?.addEventListener("input", (e) => {
root.querySelector<HTMLSelectElement>("#alternate-model")?.addEventListener("change", (e) => {
updateRunnerDraft((d) => ({
...d,
alternateModel: (e.currentTarget as HTMLInputElement).value,
alternateModel: (e.currentTarget as HTMLSelectElement).value,
}));
});

View File

@@ -209,6 +209,23 @@ const AVATAR_COLORS = [
"#e879f9",
];
const MOCK_MODELS: RunnerModelOption[] = [
{
key: "mock-openai/gpt-5.4",
name: "GPT-5.4 (mock)",
provider: "mock-openai",
input: "text",
preferred: true,
},
{
key: "mock-openai/gpt-5.4-alt",
name: "GPT-5.4 Alt (mock)",
provider: "mock-openai",
input: "text",
preferred: false,
},
];
function avatarColor(name: string): string {
let h = 0;
for (const ch of name) {
@@ -330,7 +347,8 @@ function renderSidebar(state: UiState): string {
const run = state.scenarioRun;
const isRunning = runner?.status === "running";
const realModels = state.bootstrap?.runnerCatalog.real ?? [];
const usesRealCatalog = selection?.providerMode === "live-openai" && realModels.length > 0;
const modelOptions =
selection?.providerMode === "live-openai" && realModels.length > 0 ? realModels : MOCK_MODELS;
const selectedIds = new Set(selection?.scenarioIds ?? []);
return `
@@ -345,34 +363,20 @@ function renderSidebar(state: UiState): string {
<option value="live-openai"${selection?.providerMode === "live-openai" ? " selected" : ""}>Real providers</option>
</select>
</div>
${
usesRealCatalog
? renderModelSelect({
id: "primary-model",
label: "Primary model",
value: selection?.primaryModel ?? "",
options: realModels,
disabled: isRunning,
})
: `<div class="config-field">
<span class="config-label">Primary model</span>
<input id="primary-model" value="${esc(selection?.primaryModel ?? "")}"${isRunning ? " disabled" : ""} />
</div>`
}
${
usesRealCatalog
? renderModelSelect({
id: "alternate-model",
label: "Alternate model",
value: selection?.alternateModel ?? "",
options: realModels,
disabled: isRunning,
})
: `<div class="config-field">
<span class="config-label">Alternate model</span>
<input id="alternate-model" value="${esc(selection?.alternateModel ?? "")}"${isRunning ? " disabled" : ""} />
</div>`
}
${renderModelSelect({
id: "primary-model",
label: "Primary model",
value: selection?.primaryModel ?? "",
options: modelOptions,
disabled: isRunning,
})}
${renderModelSelect({
id: "alternate-model",
label: "Alternate model",
value: selection?.alternateModel ?? "",
options: modelOptions,
disabled: isRunning,
})}
<div class="config-row">
<label><input id="fast-mode" type="checkbox"${selection?.fastMode ? " checked" : ""}${isRunning ? " disabled" : ""} /> Fast mode</label>
</div>