Skip to content

Commit 6642cfb

Browse files
webui: move model selector into ChatForm (idea by @allozaur)
1 parent ec11455 commit 6642cfb

File tree

5 files changed

+135
-127
lines changed

5 files changed

+135
-127
lines changed

tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions.svelte

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { Button } from '$lib/components/ui/button';
44
import ChatFormActionFileAttachments from './ChatFormActionFileAttachments.svelte';
55
import ChatFormActionRecord from './ChatFormActionRecord.svelte';
6+
import ChatFormModelSelector from './ChatFormModelSelector.svelte';
67
import type { FileTypeCategory } from '$lib/enums/files';
78
89
interface Props {
@@ -28,30 +29,30 @@
2829
}: Props = $props();
2930
</script>
3031

31-
<div class="flex items-center justify-between gap-1 {className}">
32+
<div class="flex w-full items-center gap-2 {className}">
3233
<ChatFormActionFileAttachments {disabled} {onFileUpload} />
3334

34-
<div class="flex gap-2">
35-
{#if isLoading}
36-
<Button
37-
type="button"
38-
onclick={onStop}
39-
class="h-8 w-8 bg-transparent p-0 hover:bg-destructive/20"
40-
>
41-
<span class="sr-only">Stop</span>
42-
<Square class="h-8 w-8 fill-destructive stroke-destructive" />
43-
</Button>
44-
{:else}
45-
<ChatFormActionRecord {disabled} {isLoading} {isRecording} {onMicClick} />
35+
<ChatFormModelSelector class="min-w-[140px] flex-1" />
4636

47-
<Button
48-
type="submit"
49-
disabled={!canSend || disabled || isLoading}
50-
class="h-8 w-8 rounded-full p-0"
51-
>
52-
<span class="sr-only">Send</span>
53-
<ArrowUp class="h-12 w-12" />
54-
</Button>
55-
{/if}
56-
</div>
37+
{#if isLoading}
38+
<Button
39+
type="button"
40+
onclick={onStop}
41+
class="h-8 w-8 bg-transparent p-0 hover:bg-destructive/20"
42+
>
43+
<span class="sr-only">Stop</span>
44+
<Square class="h-8 w-8 fill-destructive stroke-destructive" />
45+
</Button>
46+
{:else}
47+
<ChatFormActionRecord {disabled} {isLoading} {isRecording} {onMicClick} />
48+
49+
<Button
50+
type="submit"
51+
disabled={!canSend || disabled || isLoading}
52+
class="h-8 w-8 rounded-full p-0"
53+
>
54+
<span class="sr-only">Send</span>
55+
<ArrowUp class="h-12 w-12" />
56+
</Button>
57+
{/if}
5758
</div>
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<script lang="ts">
2+
import { onMount } from 'svelte';
3+
import { Loader2 } from '@lucide/svelte';
4+
import * as Select from '$lib/components/ui/select';
5+
import { cn } from '$lib/components/ui/utils';
6+
import {
7+
fetchModels,
8+
modelOptions,
9+
modelsError,
10+
modelsLoading,
11+
modelsUpdating,
12+
selectModel,
13+
selectedModelId
14+
} from '$lib/stores/models.svelte';
15+
import type { ModelOption } from '$lib/stores/models.svelte';
16+
17+
interface Props {
18+
class?: string;
19+
}
20+
21+
let { class: className = '' }: Props = $props();
22+
23+
let options = $derived(modelOptions());
24+
let loading = $derived(modelsLoading());
25+
let updating = $derived(modelsUpdating());
26+
let error = $derived(modelsError());
27+
let activeId = $derived(selectedModelId());
28+
29+
let isMounted = $state(false);
30+
31+
onMount(async () => {
32+
try {
33+
await fetchModels();
34+
} catch (error) {
35+
console.error('Unable to load models:', error);
36+
} finally {
37+
isMounted = true;
38+
}
39+
});
40+
41+
async function handleSelect(value: string | undefined) {
42+
if (!value) return;
43+
44+
const option = options.find((item) => item.id === value);
45+
if (!option) {
46+
console.error('Model is no longer available');
47+
return;
48+
}
49+
50+
try {
51+
await selectModel(option.id);
52+
} catch (error) {
53+
console.error('Failed to switch model:', error);
54+
}
55+
}
56+
57+
function getDisplayOption(): ModelOption | undefined {
58+
if (activeId) {
59+
return options.find((option) => option.id === activeId);
60+
}
61+
62+
return options[0];
63+
}
64+
</script>
65+
66+
<div class={cn('flex min-w-0 flex-col gap-1', className)}>
67+
{#if loading && options.length === 0 && !isMounted}
68+
<div class="flex items-center gap-2 text-xs text-muted-foreground">
69+
<Loader2 class="h-4 w-4 animate-spin" />
70+
Loading models…
71+
</div>
72+
{:else if options.length === 0}
73+
<p class="text-xs text-muted-foreground">No models available.</p>
74+
{:else}
75+
{@const selectedOption = getDisplayOption()}
76+
77+
<Select.Root
78+
type="single"
79+
value={selectedOption?.id ?? ''}
80+
onValueChange={handleSelect}
81+
disabled={loading || updating}
82+
>
83+
<Select.Trigger
84+
class="h-9 w-full min-w-[140px] justify-between rounded-full border border-input bg-background/80 px-3 text-left text-sm sm:min-w-[200px]"
85+
>
86+
<span class="truncate font-medium">{selectedOption?.name || 'Select model'}</span>
87+
88+
{#if updating}
89+
<Loader2 class="h-4 w-4 animate-spin text-muted-foreground" />
90+
{/if}
91+
</Select.Trigger>
92+
93+
<Select.Content class="z-[100000]">
94+
{#each options as option (option.id)}
95+
<Select.Item value={option.id} label={option.name}>
96+
<span class="text-sm font-medium">{option.name}</span>
97+
98+
{#if option.description}
99+
<span class="text-xs text-muted-foreground">{option.description}</span>
100+
{/if}
101+
</Select.Item>
102+
{/each}
103+
</Select.Content>
104+
</Select.Root>
105+
{/if}
106+
107+
{#if error}
108+
<p class="text-xs text-destructive">{error}</p>
109+
{/if}
110+
</div>

tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebar.svelte

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
updateConversationName
1414
} from '$lib/stores/chat.svelte';
1515
import ChatSidebarActions from './ChatSidebarActions.svelte';
16-
import ModelSelector from './ModelSelector.svelte';
1716
1817
const sidebar = Sidebar.useSidebar();
1918
@@ -111,8 +110,6 @@
111110
<h1 class="inline-flex items-center gap-1 px-2 text-xl font-semibold">llama.cpp</h1>
112111
</a>
113112

114-
<ModelSelector />
115-
116113
<ChatSidebarActions {handleMobileSidebarItemClick} bind:isSearchModeActive bind:searchQuery />
117114
</Sidebar.Header>
118115

tools/server/webui/src/lib/components/app/chat/ChatSidebar/ModelSelector.svelte

Lines changed: 0 additions & 99 deletions
This file was deleted.

tools/server/webui/src/lib/components/app/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export { default as ChatFormTextarea } from './chat/ChatForm/ChatFormTextarea.sv
88
export { default as ChatFormActions } from './chat/ChatForm/ChatFormActions.svelte';
99
export { default as ChatFormActionFileAttachments } from './chat/ChatForm/ChatFormActionFileAttachments.svelte';
1010
export { default as ChatFormActionRecord } from './chat/ChatForm/ChatFormActionRecord.svelte';
11+
export { default as ChatFormModelSelector } from './chat/ChatForm/ChatFormModelSelector.svelte';
1112
export { default as ChatFormHelperText } from './chat/ChatForm/ChatFormHelperText.svelte';
1213
export { default as ChatFormFileInputInvisible } from './chat/ChatForm/ChatFormFileInputInvisible.svelte';
1314

@@ -31,8 +32,6 @@ export { default as ParameterSourceIndicator } from './chat/ChatSettings/Paramet
3132
export { default as ChatSidebar } from './chat/ChatSidebar/ChatSidebar.svelte';
3233
export { default as ChatSidebarConversationItem } from './chat/ChatSidebar/ChatSidebarConversationItem.svelte';
3334
export { default as ChatSidebarSearch } from './chat/ChatSidebar/ChatSidebarSearch.svelte';
34-
export { default as ChatSidebarModelSelector } from './chat/ChatSidebar/ModelSelector.svelte';
35-
3635
export { default as ChatErrorDialog } from './dialogs/ChatErrorDialog.svelte';
3736
export { default as EmptyFileAlertDialog } from './dialogs/EmptyFileAlertDialog.svelte';
3837

0 commit comments

Comments
 (0)