mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-13 13:22:57 +00:00
fix names
This commit is contained in:
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`,
|
||||
|
||||
Reference in new issue
Block a user