feat: Model unavailable UI state for model selector

This commit is contained in:
Aleksander Grygier 2025-11-22 19:02:50 +01:00
parent 076eec6d60
commit db8ed5df9c
1 changed files with 54 additions and 4 deletions

View File

@ -20,13 +20,15 @@
currentModel?: string | null; currentModel?: string | null;
onModelChange?: (modelId: string, modelName: string) => void; onModelChange?: (modelId: string, modelName: string) => void;
disabled?: boolean; disabled?: boolean;
forceForegroundText?: boolean;
} }
let { let {
class: className = '', class: className = '',
currentModel = null, currentModel = null,
onModelChange, onModelChange,
disabled = false disabled = false,
forceForegroundText = false
}: Props = $props(); }: Props = $props();
let options = $derived(modelOptions()); let options = $derived(modelOptions());
@ -36,6 +38,22 @@
let isRouter = $derived(isRouterMode()); let isRouter = $derived(isRouterMode());
let serverModel = $derived(serverStore.modelName); let serverModel = $derived(serverStore.modelName);
let isHighlightedCurrentModelActive = $derived(
!isRouter || !currentModel
? false
: (() => {
const currentOption = options.find((option) => option.model === currentModel);
return currentOption ? currentOption.id === activeId : false;
})()
);
let isCurrentModelInCache = $derived(() => {
if (!isRouter || !currentModel) return true;
return options.some((option) => option.model === currentModel);
});
let isOpen = $state(false); let isOpen = $state(false);
let showModelDialog = $state(false); let showModelDialog = $state(false);
let container: HTMLDivElement | null = null; let container: HTMLDivElement | null = null;
@ -221,7 +239,6 @@
function getDisplayOption(): ModelOption | undefined { function getDisplayOption(): ModelOption | undefined {
if (!isRouter) { if (!isRouter) {
// Single model mode: create fake option from server model
if (serverModel) { if (serverModel) {
return { return {
id: 'current', id: 'current',
@ -230,16 +247,27 @@
capabilities: [] // Empty array for single model mode capabilities: [] // Empty array for single model mode
}; };
} }
return undefined; return undefined;
} }
// Router mode: use existing logic
if (currentModel) { if (currentModel) {
if (!isCurrentModelInCache()) {
return {
id: 'not-in-cache',
model: currentModel,
name: currentModel.split('/').pop() || currentModel,
capabilities: []
};
}
return options.find((option) => option.model === currentModel); return options.find((option) => option.model === currentModel);
} }
if (activeId) { if (activeId) {
return options.find((option) => option.id === activeId); return options.find((option) => option.id === activeId);
} }
return options[0]; return options[0];
} }
</script> </script>
@ -262,7 +290,14 @@
<button <button
type="button" type="button"
class={cn( class={cn(
'inline-flex cursor-pointer items-center gap-1.5 rounded-sm bg-muted-foreground/15 px-1.5 py-0.75 text-xs text-muted-foreground transition hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60', `inline-flex cursor-pointer items-center gap-1.5 rounded-sm bg-muted-foreground/10 px-1.5 py-1 text-xs transition hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60`,
!isCurrentModelInCache()
? 'bg-red-400/10 !text-red-400 hover:bg-red-400/20 hover:text-red-400'
: forceForegroundText
? 'text-foreground'
: isHighlightedCurrentModelActive
? 'text-foreground'
: 'text-muted-foreground',
isOpen ? 'text-foreground' : '' isOpen ? 'text-foreground' : ''
)} )}
style="max-width: min(calc(100vw - 2rem), 32rem)" style="max-width: min(calc(100vw - 2rem), 32rem)"
@ -305,6 +340,21 @@
? `${menuPosition.maxHeight}px` ? `${menuPosition.maxHeight}px`
: undefined} : undefined}
> >
{#if !isCurrentModelInCache() && currentModel}
<!-- Show unavailable model as first option (disabled) -->
<button
type="button"
class="flex w-full cursor-not-allowed items-center bg-red-400/10 px-3 py-2 text-left text-sm text-red-400"
role="option"
aria-selected="true"
aria-disabled="true"
disabled
>
<span class="truncate">{selectedOption?.name || currentModel}</span>
<span class="ml-2 text-xs whitespace-nowrap opacity-70">(not available)</span>
</button>
<div class="my-1 h-px bg-border"></div>
{/if}
{#each options as option (option.id)} {#each options as option (option.id)}
<button <button
type="button" type="button"