Finish the mobile performance pass
- Cache the model catalog once per user and each conversation's setting separately; clear both on logout and after model saves - Compress hashed assets asynchronously with a byte budget and set Vary - Memoize message parsing and throttle selection reads - Debounce draft writes to localStorage - Keep the composer editor on the page and focused, as before - Add a bundle budget check and run it in CI - Show a loading block while sessions load and a workspace avatar in the mobile conversation header
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import type { HarnessOverview } from "@/src/types";
|
||||
|
||||
/**
|
||||
* Bumped when `HarnessOverview` changes shape, so an older cache is
|
||||
* fetched again instead of read.
|
||||
*/
|
||||
const CacheVersion = 1;
|
||||
|
||||
type Key = {
|
||||
bot_id: number;
|
||||
workspace_id?: number | null;
|
||||
};
|
||||
|
||||
export type CachedHarnessOverview = {
|
||||
v: number;
|
||||
/**
|
||||
* Unix ms it was fetched.
|
||||
*/
|
||||
at: number;
|
||||
harnesses: HarnessOverview[];
|
||||
};
|
||||
|
||||
function cacheKey({ bot_id, workspace_id }: Key) {
|
||||
return `${AppData.HarnessOverviewStorageKey}:${bot_id}:${workspace_id || 0}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The models modal's data last fetched for this conversation, from
|
||||
* `localStorage`; null when there is none or it can't be read.
|
||||
*/
|
||||
export function readHarnessOverviewCache(key: Key): CachedHarnessOverview | null {
|
||||
try {
|
||||
const cached = JSON.parse(localStorage.getItem(cacheKey(key)) || "null");
|
||||
|
||||
return cached?.v == CacheVersion &&
|
||||
Array.isArray(cached.harnesses) &&
|
||||
typeof cached.at == "number"
|
||||
? cached
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeHarnessOverviewCache({ harnesses, ...key }: Key & { harnesses: HarnessOverview[] }) {
|
||||
try {
|
||||
localStorage.setItem(cacheKey(key), JSON.stringify({ v: CacheVersion, at: Date.now(), harnesses }));
|
||||
} catch {}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { HarnessOverview } from "@/src/types";
|
||||
import {
|
||||
harnessCatalogFromOverview,
|
||||
harnessSettingsFromOverview,
|
||||
mergeHarnessOverview,
|
||||
} from "./harness-overview-parts";
|
||||
|
||||
const overview: HarnessOverview = {
|
||||
harness: "text",
|
||||
title: "Text",
|
||||
description: "Chats.",
|
||||
required: true,
|
||||
automatic: true,
|
||||
model_id: 4,
|
||||
provider_id: 1,
|
||||
providers: [{ id: 1, name: "OpenRouter" }],
|
||||
fallback: false,
|
||||
enabled: true,
|
||||
options: { voice: "alloy" },
|
||||
models: [
|
||||
{
|
||||
id: 4,
|
||||
provider_id: 1,
|
||||
provider_name: "OpenRouter",
|
||||
slug: "deepseek",
|
||||
name: "DeepSeek",
|
||||
free: true,
|
||||
},
|
||||
],
|
||||
current: {
|
||||
id: 4,
|
||||
provider_id: 1,
|
||||
provider_name: "OpenRouter",
|
||||
slug: "deepseek",
|
||||
name: "DeepSeek",
|
||||
free: true,
|
||||
source: "pinned",
|
||||
},
|
||||
candidate_count: 2,
|
||||
inherit: false,
|
||||
};
|
||||
|
||||
describe("harness overview parts", () => {
|
||||
test("splits the install-wide catalog from the conversation's setting", () => {
|
||||
const [catalog] = harnessCatalogFromOverview({ harnesses: [overview] });
|
||||
const [setting] = harnessSettingsFromOverview({ harnesses: [overview] });
|
||||
|
||||
expect(catalog).toMatchObject({
|
||||
harness: "text",
|
||||
title: "Text",
|
||||
providers: overview.providers,
|
||||
models: overview.models,
|
||||
});
|
||||
expect(setting).not.toHaveProperty("models");
|
||||
expect(setting).not.toHaveProperty("providers");
|
||||
expect(setting).toMatchObject({
|
||||
harness: "text",
|
||||
model_id: 4,
|
||||
provider_id: 1,
|
||||
fallback: false,
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("merges a cached catalog and setting back into the form's shape", () => {
|
||||
const [catalog] = harnessCatalogFromOverview({ harnesses: [overview] });
|
||||
const [setting] = harnessSettingsFromOverview({ harnesses: [overview] });
|
||||
|
||||
expect(
|
||||
mergeHarnessOverview({ catalog: [catalog!], settings: [setting!] }),
|
||||
).toEqual([overview]);
|
||||
});
|
||||
|
||||
test("waits for both parts", () => {
|
||||
const [catalog] = harnessCatalogFromOverview({ harnesses: [overview] });
|
||||
|
||||
expect(mergeHarnessOverview({ catalog: [catalog!], settings: [] })).toBeUndefined();
|
||||
expect(mergeHarnessOverview({ catalog: [], settings: [] })).toBeUndefined();
|
||||
});
|
||||
|
||||
test("keeps a picked model the catalog no longer lists", () => {
|
||||
const [catalog] = harnessCatalogFromOverview({
|
||||
harnesses: [{ ...overview, models: [] }],
|
||||
});
|
||||
const [setting] = harnessSettingsFromOverview({ harnesses: [overview] });
|
||||
const [merged] = mergeHarnessOverview({ catalog: [catalog!], settings: [setting!] })!;
|
||||
|
||||
expect(setting?.pinned_model).toEqual(overview.models[0]);
|
||||
expect(merged?.models).toEqual(overview.models);
|
||||
});
|
||||
|
||||
test("keeps a provider the catalog no longer lists", () => {
|
||||
const without_providers = { ...overview, providers: [], models: [] };
|
||||
const [catalog] = harnessCatalogFromOverview({ harnesses: [without_providers] });
|
||||
const [setting] = harnessSettingsFromOverview({ harnesses: [without_providers] });
|
||||
const [merged] = mergeHarnessOverview({ catalog: [catalog!], settings: [setting!] })!;
|
||||
|
||||
expect(merged?.providers).toEqual([
|
||||
{ id: 1, name: "Unavailable provider" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import type {
|
||||
HarnessCatalog,
|
||||
HarnessOverview,
|
||||
HarnessSettingOverview,
|
||||
} from "@/src/types";
|
||||
|
||||
type Harnesses = {
|
||||
harnesses: HarnessOverview[];
|
||||
};
|
||||
|
||||
/**
|
||||
* The install-wide part of a model overview: everything except the bot's
|
||||
* own settings.
|
||||
*/
|
||||
export function harnessCatalogFromOverview({ harnesses }: Harnesses): HarnessCatalog[] {
|
||||
return harnesses.map(({ harness, title, description, required, automatic, providers, models }) => ({
|
||||
harness,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
automatic,
|
||||
providers,
|
||||
models,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* A bot's (or workspace's) part of a model overview: the settings, what
|
||||
* runs now, and what the default runs on. A picked model or provider the
|
||||
* catalog no longer lists travels with it, so it isn't lost.
|
||||
*/
|
||||
export function harnessSettingsFromOverview({ harnesses }: Harnesses): HarnessSettingOverview[] {
|
||||
return harnesses.map(({ title, description, required, automatic, providers, models, ...setting }) => {
|
||||
const pinned_model = setting.model_id
|
||||
? models.find((model) => model.id == setting.model_id)
|
||||
: undefined;
|
||||
const unavailable_provider =
|
||||
setting.provider_id && !providers.some((provider) => provider.id == setting.provider_id)
|
||||
? { id: setting.provider_id, name: "Unavailable provider" }
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...setting,
|
||||
...(pinned_model ? { pinned_model } : {}),
|
||||
...(unavailable_provider ? { unavailable_provider } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A catalog and a conversation's settings as the models form wants them:
|
||||
* the catalog's list of models and providers under the setting. Undefined
|
||||
* until both parts are there.
|
||||
*/
|
||||
export function mergeHarnessOverview({
|
||||
catalog,
|
||||
settings,
|
||||
}: {
|
||||
catalog?: HarnessCatalog[] | null;
|
||||
settings?: HarnessSettingOverview[] | null;
|
||||
}): HarnessOverview[] | undefined {
|
||||
if (!catalog?.length || !settings?.length) return undefined;
|
||||
|
||||
const by_harness = new Map(catalog.map((entry) => [entry.harness, entry]));
|
||||
|
||||
return settings.flatMap((setting) => {
|
||||
const entry = by_harness.get(setting.harness);
|
||||
|
||||
if (!entry) return [];
|
||||
|
||||
const { pinned_model, unavailable_provider, ...rest } = setting;
|
||||
const models =
|
||||
pinned_model && !entry.models.some((model) => model.id == pinned_model.id)
|
||||
? [...entry.models, pinned_model]
|
||||
: entry.models;
|
||||
const providers =
|
||||
unavailable_provider &&
|
||||
!entry.providers.some((provider) => provider.id == unavailable_provider.id)
|
||||
? [...entry.providers, unavailable_provider]
|
||||
: entry.providers;
|
||||
|
||||
return [{ ...entry, ...rest, models, providers }];
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user