Skip to content

Commit c81046e

Browse files
committed
feat: Adds theme selection to chat settings
1 parent 923a088 commit c81046e

File tree

13 files changed

+326
-4
lines changed

13 files changed

+326
-4
lines changed

tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsDialog.svelte

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
<script lang="ts">
2-
import { Settings, Filter, AlertTriangle, Brain, Cog } from '@lucide/svelte';
2+
import { Settings, Filter, AlertTriangle, Brain, Cog, Monitor, Sun, Moon } from '@lucide/svelte';
33
import { ChatSettingsFooter, ChatSettingsSection } from '$lib/components/app';
44
import { Checkbox } from '$lib/components/ui/checkbox';
55
import * as Dialog from '$lib/components/ui/dialog';
66
import { Input } from '$lib/components/ui/input';
77
import { ScrollArea } from '$lib/components/ui/scroll-area';
8+
import * as Select from '$lib/components/ui/select';
89
import { Textarea } from '$lib/components/ui/textarea';
10+
import { setMode } from 'mode-watcher';
911
import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config';
1012
import {
1113
config,
@@ -22,13 +24,21 @@
2224
let { onOpenChange, open = false }: Props = $props();
2325
2426
let localConfig: SettingsConfigType = $state({ ...config() });
27+
let originalTheme: string = $state('');
2528
2629
$effect(() => {
2730
if (open) {
2831
localConfig = { ...config() };
32+
originalTheme = config().theme as string;
2933
}
3034
});
3135
36+
function handleThemeChange(newTheme: string) {
37+
localConfig.theme = newTheme;
38+
39+
setMode(newTheme as 'light' | 'dark' | 'system');
40+
}
41+
3242
const defaultConfig = SETTING_CONFIG_DEFAULT;
3343
3444
function handleSave() {
@@ -71,9 +81,15 @@
7181
resetConfig();
7282
7383
localConfig = { ...SETTING_CONFIG_DEFAULT };
84+
85+
setMode(SETTING_CONFIG_DEFAULT.theme as 'light' | 'dark' | 'system');
86+
originalTheme = SETTING_CONFIG_DEFAULT.theme as string;
7487
}
7588
7689
function handleClose() {
90+
if (localConfig.theme !== originalTheme) {
91+
setMode(originalTheme as 'light' | 'dark' | 'system');
92+
}
7793
onOpenChange?.(false);
7894
}
7995
@@ -94,6 +110,16 @@
94110
label: 'System Message (will be disabled if left empty)',
95111
type: 'textarea'
96112
},
113+
{
114+
key: 'theme',
115+
label: 'Theme',
116+
type: 'select',
117+
options: [
118+
{ value: 'system', label: 'System', icon: Monitor },
119+
{ value: 'light', label: 'Light', icon: Sun },
120+
{ value: 'dark', label: 'Dark', icon: Moon }
121+
]
122+
},
97123
{
98124
key: 'showTokensPerSecond',
99125
label: 'Show tokens per second',
@@ -265,7 +291,7 @@
265291
);
266292
</script>
267293

268-
<Dialog.Root {open} {onOpenChange}>
294+
<Dialog.Root {open} onOpenChange={handleClose}>
269295
<Dialog.Content class="flex h-[64vh] flex-col gap-0 p-0" style="max-width: 48rem;">
270296
<div class="flex flex-1 overflow-hidden">
271297
<div class="border-border/30 w-64 border-r p-6">
@@ -329,6 +355,49 @@
329355
{field.help || SETTING_CONFIG_INFO[field.key]}
330356
</p>
331357
{/if}
358+
{:else if field.type === 'select'}
359+
{@const selectedOption = field.options?.find((opt: { value: string; label: string; icon?: any }) => opt.value === localConfig[field.key])}
360+
<label for={field.key} class="block text-sm font-medium">
361+
{field.label}
362+
</label>
363+
364+
<Select.Root type="single" value={localConfig[field.key]} onValueChange={(value) => {
365+
if (field.key === 'theme' && value) {
366+
handleThemeChange(value);
367+
} else {
368+
localConfig[field.key] = value;
369+
}
370+
}}>
371+
<Select.Trigger class="max-w-md">
372+
<div class="flex items-center gap-2">
373+
{#if selectedOption?.icon}
374+
{@const IconComponent = selectedOption.icon}
375+
<IconComponent class="h-4 w-4" />
376+
{/if}
377+
{selectedOption?.label || `Select ${field.label.toLowerCase()}`}
378+
</div>
379+
</Select.Trigger>
380+
<Select.Content>
381+
{#if field.options}
382+
{#each field.options as option}
383+
<Select.Item value={option.value} label={option.label}>
384+
<div class="flex items-center gap-2">
385+
{#if option.icon}
386+
{@const IconComponent = option.icon}
387+
<IconComponent class="h-4 w-4" />
388+
{/if}
389+
{option.label}
390+
</div>
391+
</Select.Item>
392+
{/each}
393+
{/if}
394+
</Select.Content>
395+
</Select.Root>
396+
{#if field.help || SETTING_CONFIG_INFO[field.key]}
397+
<p class="text-muted-foreground mt-1 text-xs">
398+
{field.help || SETTING_CONFIG_INFO[field.key]}
399+
</p>
400+
{/if}
332401
{:else if field.type === 'checkbox'}
333402
{@const isDisabled = field.key === 'pdfAsImage' && !supportsVision()}
334403
<div class="flex items-start space-x-3">
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Select as SelectPrimitive } from "bits-ui";
2+
3+
import Group from "./select-group.svelte";
4+
import Label from "./select-label.svelte";
5+
import Item from "./select-item.svelte";
6+
import Content from "./select-content.svelte";
7+
import Trigger from "./select-trigger.svelte";
8+
import Separator from "./select-separator.svelte";
9+
import ScrollDownButton from "./select-scroll-down-button.svelte";
10+
import ScrollUpButton from "./select-scroll-up-button.svelte";
11+
import GroupHeading from "./select-group-heading.svelte";
12+
13+
const Root = SelectPrimitive.Root;
14+
15+
export {
16+
Root,
17+
Group,
18+
Label,
19+
Item,
20+
Content,
21+
Trigger,
22+
Separator,
23+
ScrollDownButton,
24+
ScrollUpButton,
25+
GroupHeading,
26+
//
27+
Root as Select,
28+
Group as SelectGroup,
29+
Label as SelectLabel,
30+
Item as SelectItem,
31+
Content as SelectContent,
32+
Trigger as SelectTrigger,
33+
Separator as SelectSeparator,
34+
ScrollDownButton as SelectScrollDownButton,
35+
ScrollUpButton as SelectScrollUpButton,
36+
GroupHeading as SelectGroupHeading,
37+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<script lang="ts">
2+
import { Select as SelectPrimitive } from "bits-ui";
3+
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
4+
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
5+
import { cn, type WithoutChild } from "$lib/components/ui/utils.js";
6+
7+
let {
8+
ref = $bindable(null),
9+
class: className,
10+
sideOffset = 4,
11+
portalProps,
12+
children,
13+
...restProps
14+
}: WithoutChild<SelectPrimitive.ContentProps> & {
15+
portalProps?: SelectPrimitive.PortalProps;
16+
} = $props();
17+
</script>
18+
19+
<SelectPrimitive.Portal {...portalProps}>
20+
<SelectPrimitive.Content
21+
bind:ref
22+
{sideOffset}
23+
data-slot="select-content"
24+
class={cn(
25+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--bits-select-content-available-height) origin-(--bits-select-content-transform-origin) relative z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
26+
className
27+
)}
28+
{...restProps}
29+
>
30+
<SelectScrollUpButton />
31+
<SelectPrimitive.Viewport
32+
class={cn(
33+
"h-(--bits-select-anchor-height) min-w-(--bits-select-anchor-width) w-full scroll-my-1 p-1"
34+
)}
35+
>
36+
{@render children?.()}
37+
</SelectPrimitive.Viewport>
38+
<SelectScrollDownButton />
39+
</SelectPrimitive.Content>
40+
</SelectPrimitive.Portal>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script lang="ts">
2+
import { Select as SelectPrimitive } from "bits-ui";
3+
import { cn } from "$lib/components/ui/utils.js";
4+
import type { ComponentProps } from "svelte";
5+
6+
let {
7+
ref = $bindable(null),
8+
class: className,
9+
children,
10+
...restProps
11+
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
12+
</script>
13+
14+
<SelectPrimitive.GroupHeading
15+
bind:ref
16+
data-slot="select-group-heading"
17+
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
18+
{...restProps}
19+
>
20+
{@render children?.()}
21+
</SelectPrimitive.GroupHeading>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script lang="ts">
2+
import { Select as SelectPrimitive } from "bits-ui";
3+
4+
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
5+
</script>
6+
7+
<SelectPrimitive.Group data-slot="select-group" {...restProps} />
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script lang="ts">
2+
import CheckIcon from "@lucide/svelte/icons/check";
3+
import { Select as SelectPrimitive } from "bits-ui";
4+
import { cn, type WithoutChild } from "$lib/components/ui/utils.js";
5+
6+
let {
7+
ref = $bindable(null),
8+
class: className,
9+
value,
10+
label,
11+
children: childrenProp,
12+
...restProps
13+
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
14+
</script>
15+
16+
<SelectPrimitive.Item
17+
bind:ref
18+
{value}
19+
data-slot="select-item"
20+
class={cn(
21+
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
22+
className
23+
)}
24+
{...restProps}
25+
>
26+
{#snippet children({ selected, highlighted })}
27+
<span class="absolute right-2 flex size-3.5 items-center justify-center">
28+
{#if selected}
29+
<CheckIcon class="size-4" />
30+
{/if}
31+
</span>
32+
{#if childrenProp}
33+
{@render childrenProp({ selected, highlighted })}
34+
{:else}
35+
{label || value}
36+
{/if}
37+
{/snippet}
38+
</SelectPrimitive.Item>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script lang="ts">
2+
import { cn, type WithElementRef } from "$lib/components/ui/utils.js";
3+
import type { HTMLAttributes } from "svelte/elements";
4+
5+
let {
6+
ref = $bindable(null),
7+
class: className,
8+
children,
9+
...restProps
10+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
11+
</script>
12+
13+
<div
14+
bind:this={ref}
15+
data-slot="select-label"
16+
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
17+
{...restProps}
18+
>
19+
{@render children?.()}
20+
</div>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script lang="ts">
2+
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
3+
import { Select as SelectPrimitive } from "bits-ui";
4+
import { cn, type WithoutChildrenOrChild } from "$lib/components/ui/utils.js";
5+
6+
let {
7+
ref = $bindable(null),
8+
class: className,
9+
...restProps
10+
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
11+
</script>
12+
13+
<SelectPrimitive.ScrollDownButton
14+
bind:ref
15+
data-slot="select-scroll-down-button"
16+
class={cn("flex cursor-default items-center justify-center py-1", className)}
17+
{...restProps}
18+
>
19+
<ChevronDownIcon class="size-4" />
20+
</SelectPrimitive.ScrollDownButton>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script lang="ts">
2+
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
3+
import { Select as SelectPrimitive } from "bits-ui";
4+
import { cn, type WithoutChildrenOrChild } from "$lib/components/ui/utils.js";
5+
6+
let {
7+
ref = $bindable(null),
8+
class: className,
9+
...restProps
10+
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
11+
</script>
12+
13+
<SelectPrimitive.ScrollUpButton
14+
bind:ref
15+
data-slot="select-scroll-up-button"
16+
class={cn("flex cursor-default items-center justify-center py-1", className)}
17+
{...restProps}
18+
>
19+
<ChevronUpIcon class="size-4" />
20+
</SelectPrimitive.ScrollUpButton>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script lang="ts">
2+
import type { Separator as SeparatorPrimitive } from "bits-ui";
3+
import { Separator } from "$lib/components/ui/separator/index.js";
4+
import { cn } from "$lib/components/ui/utils.js";
5+
6+
let {
7+
ref = $bindable(null),
8+
class: className,
9+
...restProps
10+
}: SeparatorPrimitive.RootProps = $props();
11+
</script>
12+
13+
<Separator
14+
bind:ref
15+
data-slot="select-separator"
16+
class={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
17+
{...restProps}
18+
/>

0 commit comments

Comments
 (0)