Webui: Disable attachment button and model selector button when prompt textbox is disabled. (#17925)
* Pass disabled state to the file attachments button and the model selector button. * Update index.html.gz * Fix model info card in non-router mode. * Update index.html.gz
This commit is contained in:
parent
d6a1e18c65
commit
40d9c394f4
Binary file not shown.
|
|
@ -35,7 +35,7 @@
|
||||||
|
|
||||||
<div class="flex items-center gap-1 {className}">
|
<div class="flex items-center gap-1 {className}">
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger name="Attach files">
|
<DropdownMenu.Trigger name="Attach files" {disabled}>
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger>
|
<Tooltip.Trigger>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ModelsSelector
|
<ModelsSelector
|
||||||
|
{disabled}
|
||||||
bind:this={selectorModelRef}
|
bind:this={selectorModelRef}
|
||||||
currentModel={conversationModel}
|
currentModel={conversationModel}
|
||||||
forceForegroundText={true}
|
forceForegroundText={true}
|
||||||
|
|
|
||||||
|
|
@ -179,51 +179,37 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle changes to the model selector pop-down or the model dialog, depending on if the server is in
|
||||||
|
// router mode or not.
|
||||||
function handleOpenChange(open: boolean) {
|
function handleOpenChange(open: boolean) {
|
||||||
if (loading || updating) return;
|
if (loading || updating) return;
|
||||||
|
|
||||||
if (open) {
|
if (isRouter) {
|
||||||
isOpen = true;
|
if (open) {
|
||||||
searchTerm = '';
|
isOpen = true;
|
||||||
highlightedIndex = -1;
|
searchTerm = '';
|
||||||
|
highlightedIndex = -1;
|
||||||
|
|
||||||
// Focus search input after popover opens
|
// Focus search input after popover opens
|
||||||
tick().then(() => {
|
tick().then(() => {
|
||||||
requestAnimationFrame(() => searchInputRef?.focus());
|
requestAnimationFrame(() => searchInputRef?.focus());
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isRouter) {
|
|
||||||
modelsStore.fetchRouterModels().then(() => {
|
modelsStore.fetchRouterModels().then(() => {
|
||||||
modelsStore.fetchModalitiesForLoadedModels();
|
modelsStore.fetchModalitiesForLoadedModels();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
isOpen = false;
|
||||||
|
searchTerm = '';
|
||||||
|
highlightedIndex = -1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
isOpen = false;
|
showModelDialog = open;
|
||||||
searchTerm = '';
|
|
||||||
highlightedIndex = -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTriggerClick() {
|
|
||||||
if (loading || updating) return;
|
|
||||||
|
|
||||||
if (!isRouter) {
|
|
||||||
// Single model mode: show dialog instead of popover
|
|
||||||
showModelDialog = true;
|
|
||||||
}
|
|
||||||
// For router mode, the Popover handles open/close
|
|
||||||
}
|
|
||||||
|
|
||||||
export function open() {
|
export function open() {
|
||||||
if (isRouter) {
|
handleOpenChange(true);
|
||||||
handleOpenChange(true);
|
|
||||||
} else {
|
|
||||||
showModelDialog = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeMenu() {
|
|
||||||
handleOpenChange(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearchKeyDown(event: KeyboardEvent) {
|
function handleSearchKeyDown(event: KeyboardEvent) {
|
||||||
|
|
@ -292,7 +278,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldCloseMenu) {
|
if (shouldCloseMenu) {
|
||||||
closeMenu();
|
handleOpenChange(false);
|
||||||
|
|
||||||
// Focus the chat textarea after model selection
|
// Focus the chat textarea after model selection
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
|
@ -360,8 +346,181 @@
|
||||||
{:else}
|
{:else}
|
||||||
{@const selectedOption = getDisplayOption()}
|
{@const selectedOption = getDisplayOption()}
|
||||||
|
|
||||||
<Popover.Root bind:open={isOpen} onOpenChange={handleOpenChange}>
|
{#if isRouter}
|
||||||
<Popover.Trigger
|
<Popover.Root bind:open={isOpen} onOpenChange={handleOpenChange}>
|
||||||
|
<Popover.Trigger
|
||||||
|
class={cn(
|
||||||
|
`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' : ''
|
||||||
|
)}
|
||||||
|
style="max-width: min(calc(100cqw - 6.5rem), 32rem)"
|
||||||
|
disabled={disabled || updating}
|
||||||
|
>
|
||||||
|
<Package class="h-3.5 w-3.5" />
|
||||||
|
|
||||||
|
<span class="truncate font-medium">
|
||||||
|
{selectedOption?.model || 'Select model'}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{#if updating}
|
||||||
|
<Loader2 class="h-3 w-3.5 animate-spin" />
|
||||||
|
{:else}
|
||||||
|
<ChevronDown class="h-3 w-3.5" />
|
||||||
|
{/if}
|
||||||
|
</Popover.Trigger>
|
||||||
|
|
||||||
|
<Popover.Content
|
||||||
|
class="group/popover-content w-96 max-w-[calc(100vw-2rem)] p-0"
|
||||||
|
align="end"
|
||||||
|
sideOffset={8}
|
||||||
|
collisionPadding={16}
|
||||||
|
>
|
||||||
|
<div class="flex max-h-[50dvh] flex-col overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="order-1 shrink-0 border-b p-4 group-data-[side=top]/popover-content:order-2 group-data-[side=top]/popover-content:border-t group-data-[side=top]/popover-content:border-b-0"
|
||||||
|
>
|
||||||
|
<SearchInput
|
||||||
|
id="model-search"
|
||||||
|
placeholder="Search models..."
|
||||||
|
bind:value={searchTerm}
|
||||||
|
bind:ref={searchInputRef}
|
||||||
|
onClose={() => handleOpenChange(false)}
|
||||||
|
onKeyDown={handleSearchKeyDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="models-list order-2 min-h-0 flex-1 overflow-y-auto group-data-[side=top]/popover-content:order-1"
|
||||||
|
>
|
||||||
|
{#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-4 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}
|
||||||
|
{#if filteredOptions.length === 0}
|
||||||
|
<p class="px-4 py-3 text-sm text-muted-foreground">No models found.</p>
|
||||||
|
{/if}
|
||||||
|
{#each filteredOptions as option, index (option.id)}
|
||||||
|
{@const status = getModelStatus(option.model)}
|
||||||
|
{@const isLoaded = status === ServerModelStatus.LOADED}
|
||||||
|
{@const isLoading = status === ServerModelStatus.LOADING}
|
||||||
|
{@const isSelected = currentModel === option.model || activeId === option.id}
|
||||||
|
{@const isCompatible = isModelCompatible(option)}
|
||||||
|
{@const isHighlighted = index === highlightedIndex}
|
||||||
|
{@const missingModalities = getMissingModalities(option)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
'group flex w-full items-center gap-2 px-4 py-2 text-left text-sm transition focus:outline-none',
|
||||||
|
isCompatible
|
||||||
|
? 'cursor-pointer hover:bg-muted focus:bg-muted'
|
||||||
|
: 'cursor-not-allowed opacity-50',
|
||||||
|
isSelected || isHighlighted
|
||||||
|
? 'bg-accent text-accent-foreground'
|
||||||
|
: isCompatible
|
||||||
|
? 'hover:bg-accent hover:text-accent-foreground'
|
||||||
|
: '',
|
||||||
|
isLoaded ? 'text-popover-foreground' : 'text-muted-foreground'
|
||||||
|
)}
|
||||||
|
role="option"
|
||||||
|
aria-selected={isSelected || isHighlighted}
|
||||||
|
aria-disabled={!isCompatible}
|
||||||
|
tabindex={isCompatible ? 0 : -1}
|
||||||
|
onclick={() => isCompatible && handleSelect(option.id)}
|
||||||
|
onmouseenter={() => (highlightedIndex = index)}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (isCompatible && (e.key === 'Enter' || e.key === ' ')) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelect(option.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="min-w-0 flex-1 truncate">{option.model}</span>
|
||||||
|
|
||||||
|
{#if missingModalities}
|
||||||
|
<span class="flex shrink-0 items-center gap-1 text-muted-foreground/70">
|
||||||
|
{#if missingModalities.vision}
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
<EyeOff class="h-3.5 w-3.5" />
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content class="z-[9999]">
|
||||||
|
<p>No vision support</p>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
{/if}
|
||||||
|
{#if missingModalities.audio}
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
<MicOff class="h-3.5 w-3.5" />
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content class="z-[9999]">
|
||||||
|
<p>No audio support</p>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if isLoading}
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
<Loader2 class="h-4 w-4 shrink-0 animate-spin text-muted-foreground" />
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content class="z-[9999]">
|
||||||
|
<p>Loading model...</p>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
{:else if isLoaded}
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="relative ml-2 flex h-4 w-4 shrink-0 items-center justify-center"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
modelsStore.unloadModel(option.model);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mr-2 h-2 w-2 rounded-full bg-green-500 transition-opacity group-hover:opacity-0"
|
||||||
|
></span>
|
||||||
|
<Power
|
||||||
|
class="absolute mr-2 h-4 w-4 text-red-500 opacity-0 transition-opacity group-hover:opacity-100 hover:text-red-600"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content class="z-[9999]">
|
||||||
|
<p>Unload model</p>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
{:else}
|
||||||
|
<span class="mx-2 h-2 w-2 rounded-full bg-muted-foreground/50"></span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
class={cn(
|
class={cn(
|
||||||
`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`,
|
`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()
|
!isCurrentModelInCache()
|
||||||
|
|
@ -374,165 +533,20 @@
|
||||||
isOpen ? 'text-foreground' : ''
|
isOpen ? 'text-foreground' : ''
|
||||||
)}
|
)}
|
||||||
style="max-width: min(calc(100cqw - 6.5rem), 32rem)"
|
style="max-width: min(calc(100cqw - 6.5rem), 32rem)"
|
||||||
onclick={handleTriggerClick}
|
onclick={() => handleOpenChange(true)}
|
||||||
disabled={disabled || updating || !isRouter}
|
disabled={disabled || updating}
|
||||||
>
|
>
|
||||||
<Package class="h-3.5 w-3.5" />
|
<Package class="h-3.5 w-3.5" />
|
||||||
|
|
||||||
<span class="truncate font-medium">
|
<span class="truncate font-medium">
|
||||||
{selectedOption?.model || 'Select model'}
|
{selectedOption?.model}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{#if updating}
|
{#if updating}
|
||||||
<Loader2 class="h-3 w-3.5 animate-spin" />
|
<Loader2 class="h-3 w-3.5 animate-spin" />
|
||||||
{:else if isRouter}
|
|
||||||
<ChevronDown class="h-3 w-3.5" />
|
|
||||||
{/if}
|
{/if}
|
||||||
</Popover.Trigger>
|
</button>
|
||||||
|
{/if}
|
||||||
<Popover.Content
|
|
||||||
class="group/popover-content w-96 max-w-[calc(100vw-2rem)] p-0"
|
|
||||||
align="end"
|
|
||||||
sideOffset={8}
|
|
||||||
collisionPadding={16}
|
|
||||||
>
|
|
||||||
<div class="flex max-h-[50dvh] flex-col overflow-hidden">
|
|
||||||
<div
|
|
||||||
class="order-1 shrink-0 border-b p-4 group-data-[side=top]/popover-content:order-2 group-data-[side=top]/popover-content:border-t group-data-[side=top]/popover-content:border-b-0"
|
|
||||||
>
|
|
||||||
<SearchInput
|
|
||||||
id="model-search"
|
|
||||||
placeholder="Search models..."
|
|
||||||
bind:value={searchTerm}
|
|
||||||
bind:ref={searchInputRef}
|
|
||||||
onClose={closeMenu}
|
|
||||||
onKeyDown={handleSearchKeyDown}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="models-list order-2 min-h-0 flex-1 overflow-y-auto group-data-[side=top]/popover-content:order-1"
|
|
||||||
>
|
|
||||||
{#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-4 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}
|
|
||||||
{#if filteredOptions.length === 0}
|
|
||||||
<p class="px-4 py-3 text-sm text-muted-foreground">No models found.</p>
|
|
||||||
{/if}
|
|
||||||
{#each filteredOptions as option, index (option.id)}
|
|
||||||
{@const status = getModelStatus(option.model)}
|
|
||||||
{@const isLoaded = status === ServerModelStatus.LOADED}
|
|
||||||
{@const isLoading = status === ServerModelStatus.LOADING}
|
|
||||||
{@const isSelected = currentModel === option.model || activeId === option.id}
|
|
||||||
{@const isCompatible = isModelCompatible(option)}
|
|
||||||
{@const isHighlighted = index === highlightedIndex}
|
|
||||||
{@const missingModalities = getMissingModalities(option)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
class={cn(
|
|
||||||
'group flex w-full items-center gap-2 px-4 py-2 text-left text-sm transition focus:outline-none',
|
|
||||||
isCompatible
|
|
||||||
? 'cursor-pointer hover:bg-muted focus:bg-muted'
|
|
||||||
: 'cursor-not-allowed opacity-50',
|
|
||||||
isSelected || isHighlighted
|
|
||||||
? 'bg-accent text-accent-foreground'
|
|
||||||
: isCompatible
|
|
||||||
? 'hover:bg-accent hover:text-accent-foreground'
|
|
||||||
: '',
|
|
||||||
isLoaded ? 'text-popover-foreground' : 'text-muted-foreground'
|
|
||||||
)}
|
|
||||||
role="option"
|
|
||||||
aria-selected={isSelected || isHighlighted}
|
|
||||||
aria-disabled={!isCompatible}
|
|
||||||
tabindex={isCompatible ? 0 : -1}
|
|
||||||
onclick={() => isCompatible && handleSelect(option.id)}
|
|
||||||
onmouseenter={() => (highlightedIndex = index)}
|
|
||||||
onkeydown={(e) => {
|
|
||||||
if (isCompatible && (e.key === 'Enter' || e.key === ' ')) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSelect(option.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span class="min-w-0 flex-1 truncate">{option.model}</span>
|
|
||||||
|
|
||||||
{#if missingModalities}
|
|
||||||
<span class="flex shrink-0 items-center gap-1 text-muted-foreground/70">
|
|
||||||
{#if missingModalities.vision}
|
|
||||||
<Tooltip.Root>
|
|
||||||
<Tooltip.Trigger>
|
|
||||||
<EyeOff class="h-3.5 w-3.5" />
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Content class="z-[9999]">
|
|
||||||
<p>No vision support</p>
|
|
||||||
</Tooltip.Content>
|
|
||||||
</Tooltip.Root>
|
|
||||||
{/if}
|
|
||||||
{#if missingModalities.audio}
|
|
||||||
<Tooltip.Root>
|
|
||||||
<Tooltip.Trigger>
|
|
||||||
<MicOff class="h-3.5 w-3.5" />
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Content class="z-[9999]">
|
|
||||||
<p>No audio support</p>
|
|
||||||
</Tooltip.Content>
|
|
||||||
</Tooltip.Root>
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if isLoading}
|
|
||||||
<Tooltip.Root>
|
|
||||||
<Tooltip.Trigger>
|
|
||||||
<Loader2 class="h-4 w-4 shrink-0 animate-spin text-muted-foreground" />
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Content class="z-[9999]">
|
|
||||||
<p>Loading model...</p>
|
|
||||||
</Tooltip.Content>
|
|
||||||
</Tooltip.Root>
|
|
||||||
{:else if isLoaded}
|
|
||||||
<Tooltip.Root>
|
|
||||||
<Tooltip.Trigger>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="relative ml-2 flex h-4 w-4 shrink-0 items-center justify-center"
|
|
||||||
onclick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
modelsStore.unloadModel(option.model);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="mr-2 h-2 w-2 rounded-full bg-green-500 transition-opacity group-hover:opacity-0"
|
|
||||||
></span>
|
|
||||||
<Power
|
|
||||||
class="absolute mr-2 h-4 w-4 text-red-500 opacity-0 transition-opacity group-hover:opacity-100 hover:text-red-600"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Content class="z-[9999]">
|
|
||||||
<p>Unload model</p>
|
|
||||||
</Tooltip.Content>
|
|
||||||
</Tooltip.Root>
|
|
||||||
{:else}
|
|
||||||
<span class="mx-2 h-2 w-2 rounded-full bg-muted-foreground/50"></span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover.Root>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue