feat: Model unavailable UI state for model selector
This commit is contained in:
parent
076eec6d60
commit
db8ed5df9c
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue