fix names

This commit is contained in:
Brandon Thomas committed 2026-07-01 23:16:58 -04:00
1 parent 5ca570e8af
commit 77383df886
4 files changed
+109 -45

No files matched your search

@@ -44,6 +44,11 @@ interface ClassyModelSelectorProps {
showProviderSelection?: boolean;
}
// Model instances are rebuilt when the backend listing hydrates, so compare by
// tauriId rather than object identity.
const isSameModel = (a: Model | undefined, b: Model | undefined): boolean =>
a !== undefined && b !== undefined && a.tauriId === b.tauriId;
const DEFAULT_PROVIDER_OPTIONS: GenerationProvider[] = [GenerationProvider.Artcraft];
function ProviderTooltipContent({
@@ -143,6 +148,20 @@ export function ClassyModelSelector({
}
}, []);
// The backend listing hydrates asynchronously and rebuilds the model
// instances with API capabilities. Swap a stale selected instance for the
// fresh one so capability-driven UI (keyframes, references, pickers)
// reflects the API data without needing a manual re-select.
useEffect(() => {
const selected = selectedModels[page];
if (!selected) return;
const fresh = itemModels.find((m) => m.tauriId === selected.tauriId);
if (fresh !== undefined && fresh !== selected) {
setSelectedModel(page, fresh);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items, selectedModels, page]);
// Initialize a default provider for each model so we can render icons even when not selected
useEffect(() => {
for (const item of items) {
@@ -175,7 +194,7 @@ export function ClassyModelSelector({
return {
...item,
selected: item.model === selectedModel,
selected: isSameModel(item.model as Model | undefined, selectedModel),
hoverTooltip: hasMultipleProviders
? (close: () => void) => (
<ProviderTooltipContent
@@ -190,7 +209,7 @@ export function ClassyModelSelector({
: undefined,
tooltipDelayMs: providerTooltipDelayMs,
trailing:
item.model !== selectedModel && hasMultipleProviders
!isSameModel(item.model as Model | undefined, selectedModel) && hasMultipleProviders
? (() => {
const prov = modelId
? selectedProvidersByModel[modelId]
@@ -206,7 +225,7 @@ export function ClassyModelSelector({
})()
: undefined,
selectedRight:
item.model === selectedModel &&
isSameModel(item.model as Model | undefined, selectedModel) &&
selectedProvider &&
hasMultipleProviders ? (
<div className="mr-1 rounded-md p-1.5 bg-primary/60 group-hover:bg-primary/80 transition-colors">
@@ -265,7 +284,7 @@ export function ClassyModelSelector({
{...popoverProps}
buttonClassName="rounded-xl bg-ui-controls/90 hover:bg-ui-controls text-left shadow-sm px-3 py-1 gap-3 border border-ui-controls-border"
renderTrigger={(selectedItem) => {
const modelTitle = selectedItem?.label ?? "";
const modelTitle = selectedItem?.label ?? selectedModel?.selectorName ?? "";
const providerIcon = selectedProvider
? getProviderIcon(selectedProvider)
: null;
@@ -6,35 +6,37 @@ import {
} from "@storyteller/model-list";
import { ModelPage } from "./model-pages";
const DEFAULT_MODEL_ID_FOR_PAGE: Partial<Record<ModelPage, string>> = {
[ModelPage.TextToImage]: "nano_banana_pro",
[ModelPage.ImageToVideo]: "seedance_2p0",
[ModelPage.Canvas2D]: "gpt_image_1p5",
[ModelPage.Stage3D]: "gpt_image_1p5",
[ModelPage.ImageEditor]: "nano_banana_pro",
[ModelPage.ImageTo3DWorld]: "marble_0p1_mini",
[ModelPage.Angles]: "flux_2_lora_angles",
};
export const defaultModelForPage = (
models: Model[],
page: ModelPage,
): Model => {
let imageModel: Model | undefined;
const defaultId = DEFAULT_MODEL_ID_FOR_PAGE[page];
switch (page) {
case ModelPage.TextToImage:
imageModel = IMAGE_MODELS_BY_ID.get("nano_banana_pro");
break;
case ModelPage.ImageToVideo:
imageModel = VIDEO_MODELS_BY_ID.get("seedance_2p0");
break;
case ModelPage.Canvas2D:
imageModel = IMAGE_MODELS_BY_ID.get("gpt_image_1p5");
break;
case ModelPage.Stage3D:
imageModel = IMAGE_MODELS_BY_ID.get("gpt_image_1p5");
break;
case ModelPage.ImageEditor:
imageModel = IMAGE_MODELS_BY_ID.get("nano_banana_pro");
break;
case ModelPage.ImageTo3DWorld:
imageModel = SPLAT_MODELS_BY_ID.get("marble_0p1_mini");
break;
case ModelPage.Angles:
imageModel = IMAGE_MODELS_BY_ID.get("flux_2_lora_angles");
break;
if (defaultId) {
// Prefer the instance from the caller's (backend-hydrated) list — the
// static map instances are presentation-only fallbacks without the API's
// capability data.
const fromList = models.find(
(m) => m.id === defaultId || m.tauriId === defaultId,
);
if (fromList) return fromList;
const fromStaticMaps =
IMAGE_MODELS_BY_ID.get(defaultId) ??
VIDEO_MODELS_BY_ID.get(defaultId) ??
SPLAT_MODELS_BY_ID.get(defaultId);
if (fromStaticMaps) return fromStaticMaps;
}
return imageModel || models[0];
return models[0];
};
@@ -76,43 +76,84 @@ export interface ListingVideoModel extends ListingModelBase {
export const buildImageModelsFromListing = (
overlay: ImageModel[],
listing: ListingImageModel[],
offeredModelIds: string[] = [],
): ImageModel[] =>
build(overlay, listing, BACKEND_TO_TAURI_IMAGE_ID, mergedImageModel);
build(overlay, listing, offeredModelIds, BACKEND_TO_TAURI_IMAGE_ID, mergedImageModel);
export const buildVideoModelsFromListing = (
overlay: VideoModel[],
listing: ListingVideoModel[],
offeredModelIds: string[] = [],
): VideoModel[] =>
build(overlay, listing, BACKEND_TO_TAURI_VIDEO_ID, mergedVideoModel);
build(overlay, listing, offeredModelIds, BACKEND_TO_TAURI_VIDEO_ID, mergedVideoModel);
// ── Core assembly ──────────────────────────────────────────────────────────
// Membership: a model appears in the picker when it is
// 1. OFFERED by a provider in the response's `providers[]` (the backend's
// publish switch — `models[]` details also contain disabled entries and
// internal variants that must NOT surface), or
// 2. an ENABLED `models[]` detail that the frontend knows (overlay entry) —
// covers desktop-native models like grok video that no backend provider
// offers yet, or
// 3. a frontend-only overlay model the backend has never heard of
// (switch_x, inpaint models, …).
// `models[]` details are the capability source either way (the enabled entry
// wins when a model is detailed twice).
const build = <T extends { tauriId: string }, L extends ListingModelBase>(
overlay: T[],
listing: L[],
offeredModelIds: string[],
alias: Record<string, string>,
merge: (m: L, tauriId: string, overlayEntry: T | undefined) => T,
): T[] => {
const toTauriId = (backendId: string) => alias[backendId] ?? backendId;
const overlayByTauriId = new Map(overlay.map((m) => [m.tauriId, m]));
const knownTauriIds = new Set(listing.map((m) => alias[m.model] ?? m.model));
const knownTauriIds = new Set(listing.map((m) => toTauriId(m.model)));
const offeredTauriIds = new Set(offeredModelIds.map(toTauriId));
// Capability lookup — prefer the enabled detail entry when duplicated.
const detailsByTauriId = new Map<string, L>();
for (const m of listing) {
const tauriId = toTauriId(m.model);
const existing = detailsByTauriId.get(tauriId);
if (existing === undefined || existing.is_disabled === true) {
detailsByTauriId.set(tauriId, m);
}
}
const result: T[] = [];
const seenTauriIds = new Set<string>();
// Backend drives membership + order; every entry is rebuilt with API-first
// capabilities.
// Backend details order drives ordering.
for (const m of listing) {
if (m.is_disabled === true) continue;
const tauriId = alias[m.model] ?? m.model;
// Guard: several backend ids aliasing to one frontend model would render
// duplicates — only surface the first.
const tauriId = toTauriId(m.model);
if (seenTauriIds.has(tauriId)) continue;
const overlayEntry = overlayByTauriId.get(tauriId);
const detail = detailsByTauriId.get(tauriId) ?? m;
const enabled = detail.is_disabled !== true;
const show =
offeredTauriIds.has(tauriId) || (enabled && overlayEntry !== undefined);
if (!show) continue;
seenTauriIds.add(tauriId);
result.push(merge(detail, tauriId, overlayEntry));
}
// Offered models with no detail entry at all (unusual, but the publish
// switch wins): surface minimally.
for (const backendId of offeredModelIds) {
const tauriId = toTauriId(backendId);
if (seenTauriIds.has(tauriId)) continue;
seenTauriIds.add(tauriId);
result.push(merge(m, tauriId, overlayByTauriId.get(tauriId)));
result.push(
merge({ model: backendId } as L, tauriId, overlayByTauriId.get(tauriId)),
);
}
// Append frontend-only models the backend has never heard of (switch_x,
// inpaint models, …).
// Append frontend-only models the backend has never heard of.
for (const m of overlay) {
if (!knownTauriIds.has(m.tauriId)) result.push(m);
if (!knownTauriIds.has(m.tauriId) && !seenTauriIds.has(m.tauriId)) {
result.push(m);
}
}
return result;
};
@@ -47,8 +47,9 @@ export const useModelsStore = create<ModelsStoreState>((set, get) => ({
const next: Partial<ModelsStoreState> = { isLoading: false, loaded: true };
if (imageResult.status === "fulfilled") {
const { models } = imageResult.value.payload;
const built = buildImageModelsFromListing(IMAGE_MODELS, models);
const { models, providers } = imageResult.value.payload;
const offered = providers.flatMap((p) => p.models.map((pm) => pm.model));
const built = buildImageModelsFromListing(IMAGE_MODELS, models, offered);
next.imageModels = built;
console.log(
`[models] image: backend returned ${models.length} → ${built.length} models shown`,
@@ -59,8 +60,9 @@ export const useModelsStore = create<ModelsStoreState>((set, get) => ({
}
if (videoResult.status === "fulfilled") {
const { models } = videoResult.value.payload;
const built = buildVideoModelsFromListing(VIDEO_MODELS, models);
const { models, providers } = videoResult.value.payload;
const offered = providers.flatMap((p) => p.models.map((pm) => pm.model));
const built = buildVideoModelsFromListing(VIDEO_MODELS, models, offered);
next.videoModels = built;
console.log(
`[models] video: backend returned ${models.length} → ${built.length} models shown`,