Skip to content

Commit 5b668bf

Browse files
committed
feat: UI/UX improvements
1 parent 2d0b873 commit 5b668bf

25 files changed

+1178
-90
lines changed

tools/server/public/index.html

Lines changed: 712 additions & 0 deletions
Large diffs are not rendered by default.

tools/server/webui/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/server/webui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"@eslint/compat": "^1.2.5",
2424
"@eslint/js": "^9.18.0",
2525
"@internationalized/date": "^3.8.2",
26-
"@lucide/svelte": "^0.525.0",
26+
"@lucide/svelte": "^0.515.0",
2727
"@playwright/test": "^1.49.1",
2828
"@storybook/addon-a11y": "^9.0.17",
2929
"@storybook/addon-docs": "^9.0.17",

tools/server/webui/src/lib/components/chat/ChatAttachmentsList.svelte

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,6 @@
5757
previewDialogOpen = true;
5858
}
5959
60-
function closePreview() {
61-
previewDialogOpen = false;
62-
previewItem = null;
63-
}
64-
6560
function getDisplayItems() {
6661
const items: Array<{
6762
id: string;
@@ -135,7 +130,7 @@
135130
</script>
136131

137132
{#if displayItems.length > 0}
138-
<div class="flex flex-wrap items-start gap-3 {className}">
133+
<div class="flex flex-wrap items-start justify-end gap-3 {className}">
139134
{#each displayItems as item (item.id)}
140135
{#if item.isImage && item.preview}
141136
<ChatAttachmentImagePreview
@@ -171,7 +166,6 @@
171166
{#if previewItem}
172167
<ChatAttachmentPreviewDialog
173168
bind:open={previewDialogOpen}
174-
onClose={closePreview}
175169
uploadedFile={previewItem.uploadedFile}
176170
attachment={previewItem.attachment}
177171
preview={previewItem.preview}

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

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { Square, Paperclip, Mic, ArrowUp, Upload, X } from '@lucide/svelte';
55
import type { ChatUploadedFile } from '$lib/types/chat.d.ts';
66
import { ChatAttachmentsList } from '$lib/components';
7+
import { inputClasses } from '$lib/constants/input-classes';
78
89
interface Props {
910
class?: string;
@@ -39,15 +40,25 @@
3940
event.preventDefault();
4041
if (!message.trim() || disabled || isLoading) return;
4142
42-
const success = await onSend?.(message.trim(), uploadedFiles);
43+
// Store the message and files before clearing
44+
const messageToSend = message.trim();
45+
const filesToSend = [...uploadedFiles];
4346
44-
if (success) {
45-
message = '';
46-
uploadedFiles = [];
47+
// Clear the form immediately to hide message and attachments
48+
message = '';
49+
uploadedFiles = [];
4750
48-
if (textareaElement) {
49-
textareaElement.style.height = 'auto';
50-
}
51+
if (textareaElement) {
52+
textareaElement.style.height = 'auto';
53+
}
54+
55+
// Send the message with the stored data
56+
const success = await onSend?.(messageToSend, filesToSend);
57+
58+
// If sending failed, restore the form state
59+
if (!success) {
60+
message = messageToSend;
61+
uploadedFiles = filesToSend;
5162
}
5263
}
5364
@@ -57,15 +68,25 @@
5768
5869
if (!message.trim() || disabled || isLoading) return;
5970
60-
const success = await onSend?.(message.trim(), uploadedFiles);
71+
// Store the message and files before clearing
72+
const messageToSend = message.trim();
73+
const filesToSend = [...uploadedFiles];
74+
75+
// Clear the form immediately to hide message and attachments
76+
message = '';
77+
uploadedFiles = [];
78+
79+
if (textareaElement) {
80+
textareaElement.style.height = 'auto';
81+
}
6182
62-
if (success) {
63-
message = '';
64-
uploadedFiles = [];
83+
// Send the message with the stored data
84+
const success = await onSend?.(messageToSend, filesToSend);
6585
66-
if (textareaElement) {
67-
textareaElement.style.height = 'auto';
68-
}
86+
// If sending failed, restore the form state
87+
if (!success) {
88+
message = messageToSend;
89+
uploadedFiles = filesToSend;
6990
}
7091
}
7192
}
@@ -127,7 +148,7 @@
127148

128149
<form
129150
onsubmit={handleSubmit}
130-
class="bg-muted/30 border-border/40 focus-within:border-primary/40 bg-background dark:bg-muted border-radius-bottom-none mx-auto max-w-4xl overflow-hidden rounded-3xl border {className}"
151+
class="{inputClasses} border-radius-bottom-none mx-auto max-w-4xl overflow-hidden rounded-3xl {className}"
131152
>
132153
<ChatAttachmentsList bind:uploadedFiles {onFileRemove} class="mb-3 px-5 pt-5" />
133154

tools/server/webui/src/lib/components/chat/ChatScreen.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@
289289
class="z-999 sticky bottom-0 mx-auto mt-auto max-w-[56rem]"
290290
in:slide={{ duration: 400, axis: 'y' }}
291291
>
292-
<div class="bg-background m-auto min-w-[56rem] rounded-t-3xl border-t pb-4">
292+
<div class="bg-background m-auto min-w-[56rem] rounded-t-3xl pb-4">
293293
<ChatForm
294294
isLoading={isLoading()}
295295
showHelperText={false}

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
let currentChatId = $derived(page.params.id);
1212
let searchQuery = $state('');
1313
let filteredConversations = $derived(
14-
conversations().filter((conversation: { name: string }) => conversation.name.toLowerCase().includes(searchQuery.toLowerCase()))
14+
conversations().filter((conversation: { name: string }) =>
15+
conversation.name.toLowerCase().includes(searchQuery.toLowerCase())
16+
)
1517
);
1618
1719
async function selectConversation(id: string) {
@@ -27,22 +29,20 @@
2729
}
2830
</script>
2931

30-
<Sidebar.Header>
31-
<div class="px-2 py-2">
32+
<Sidebar.Header class="px-0 pb-4">
33+
<div class="py-2">
3234
<a href="/">
3335
<h1 class="text-xl font-semibold">llama.cpp</h1>
3436
</a>
3537
</div>
3638
</Sidebar.Header>
3739

38-
<div class="px-2 pb-4">
39-
<div class="relative">
40-
<Search class="text-muted-foreground absolute left-2 top-2.5 h-4 w-4" />
41-
<Input bind:value={searchQuery} placeholder="Search conversations..." class="pl-8" />
42-
</div>
40+
<div class="relative pb-4">
41+
<Search class="text-muted-foreground absolute left-2 top-2.5 h-4 w-4" />
42+
<Input bind:value={searchQuery} placeholder="Search conversations..." class="pl-8" />
4343
</div>
4444

45-
<div class="px-2 pb-4">
45+
<div class="pb-4">
4646
<Button
4747
href="/?new_chat=true"
4848
class="border-muted-foreground/25 hover:bg-accent hover:border-accent-foreground/25 w-full justify-start gap-2 rounded-lg border-2 border-dashed bg-transparent transition-colors"
@@ -54,11 +54,11 @@
5454
</Button>
5555
</div>
5656

57-
<Sidebar.Group class="space-y-2">
57+
<Sidebar.Group class="space-y-2 p-0">
5858
<Sidebar.GroupLabel>Conversations</Sidebar.GroupLabel>
5959

6060
<Sidebar.GroupContent>
61-
<Sidebar.Menu class="space-y-2">
61+
<Sidebar.Menu class="space-y-0.5">
6262
{#each filteredConversations as conversation (conversation.id)}
6363
<Sidebar.MenuItem>
6464
<ChatSidebarConversationItem

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

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<script lang="ts">
22
import { Button } from '$lib/components/ui/button';
33
import * as AlertDialog from '$lib/components/ui/alert-dialog';
4-
import { Trash2, Pencil } from '@lucide/svelte';
4+
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
5+
import { Trash2, Pencil, MoreHorizontal } from '@lucide/svelte';
56
import type { DatabaseConversation } from '$lib/types/database';
67
78
interface Props {
@@ -22,6 +23,8 @@
2223
showLastModified = false
2324
}: Props = $props();
2425
26+
let showDeleteDialog = $state(false);
27+
2528
function formatLastModified(timestamp: number) {
2629
const now = Date.now();
2730
const diff = now - timestamp;
@@ -55,9 +58,9 @@
5558
</script>
5659

5760
<button
58-
class="hover:bg-accent group flex w-full cursor-pointer items-center justify-between space-x-3 rounded-lg p-3 text-left transition-colors {isActive
59-
? 'bg-accent text-accent-foreground border-border border'
60-
: 'border border-transparent'}"
61+
class="hover:bg-foreground/10 group flex w-full cursor-pointer items-center justify-between space-x-3 rounded-lg px-3 py-1.5 text-left transition-colors {isActive
62+
? 'bg-foreground/5 text-accent-foreground'
63+
: ''}"
6164
onclick={handleSelect}
6265
>
6366
<div class="text flex min-w-0 flex-1 items-center space-x-3">
@@ -74,20 +77,36 @@
7477
</div>
7578
</div>
7679

77-
<div class="actions flex items-center space-x-1">
78-
<Button size="sm" variant="ghost" class="h-6 w-6 p-0" onclick={handleEdit}>
79-
<Pencil class="h-3 w-3" />
80-
</Button>
81-
<AlertDialog.Root>
82-
<AlertDialog.Trigger onclick={handleDeleteClick}>
83-
<Button
84-
size="sm"
85-
variant="ghost"
86-
class="text-destructive hover:text-destructive h-6 w-6 p-0"
80+
<div class="actions flex items-center">
81+
<DropdownMenu.Root>
82+
<DropdownMenu.Trigger
83+
class="flex h-6 w-6 items-center justify-center rounded-md p-0 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground"
84+
onclick={(e) => e.stopPropagation()}
85+
>
86+
<MoreHorizontal class="h-3 w-3" />
87+
<span class="sr-only">More actions</span>
88+
</DropdownMenu.Trigger>
89+
<DropdownMenu.Content align="end" class="w-48">
90+
<DropdownMenu.Item onclick={handleEdit} class="flex items-center gap-2">
91+
<Pencil class="h-4 w-4" />
92+
Edit
93+
</DropdownMenu.Item>
94+
<DropdownMenu.Separator />
95+
<DropdownMenu.Item
96+
variant="destructive"
97+
class="flex items-center gap-2"
98+
onclick={(e) => {
99+
e.stopPropagation();
100+
showDeleteDialog = true;
101+
}}
87102
>
88-
<Trash2 class="h-3 w-3" />
89-
</Button>
90-
</AlertDialog.Trigger>
103+
<Trash2 class="h-4 w-4" />
104+
Delete
105+
</DropdownMenu.Item>
106+
</DropdownMenu.Content>
107+
</DropdownMenu.Root>
108+
109+
<AlertDialog.Root bind:open={showDeleteDialog}>
91110
<AlertDialog.Content>
92111
<AlertDialog.Header>
93112
<AlertDialog.Title>Delete Conversation</AlertDialog.Title>
@@ -110,12 +129,13 @@
110129

111130
<style lang="postcss">
112131
.actions {
113-
button & {
132+
& > * {
114133
width: 0;
115134
opacity: 0;
135+
transition: all 0.2s ease;
116136
}
117137
118-
button:hover & {
138+
button:hover & > * {
119139
width: auto;
120140
opacity: 1;
121141
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<script lang="ts">
2+
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
3+
import CheckIcon from "@lucide/svelte/icons/check";
4+
import MinusIcon from "@lucide/svelte/icons/minus";
5+
import { cn, type WithoutChildrenOrChild } from "$lib/components/ui/utils.js";
6+
import type { Snippet } from "svelte";
7+
8+
let {
9+
ref = $bindable(null),
10+
checked = $bindable(false),
11+
indeterminate = $bindable(false),
12+
class: className,
13+
children: childrenProp,
14+
...restProps
15+
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
16+
children?: Snippet;
17+
} = $props();
18+
</script>
19+
20+
<DropdownMenuPrimitive.CheckboxItem
21+
bind:ref
22+
bind:checked
23+
bind:indeterminate
24+
data-slot="dropdown-menu-checkbox-item"
25+
class={cn(
26+
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
27+
className
28+
)}
29+
{...restProps}
30+
>
31+
{#snippet children({ checked, indeterminate })}
32+
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
33+
{#if indeterminate}
34+
<MinusIcon class="size-4" />
35+
{:else}
36+
<CheckIcon class={cn("size-4", !checked && "text-transparent")} />
37+
{/if}
38+
</span>
39+
{@render childrenProp?.()}
40+
{/snippet}
41+
</DropdownMenuPrimitive.CheckboxItem>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script lang="ts">
2+
import { cn } from '$lib/components/ui/utils.js';
3+
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
4+
5+
let {
6+
ref = $bindable(null),
7+
sideOffset = 4,
8+
portalProps,
9+
class: className,
10+
...restProps
11+
}: DropdownMenuPrimitive.ContentProps & {
12+
portalProps?: DropdownMenuPrimitive.PortalProps;
13+
} = $props();
14+
</script>
15+
16+
<DropdownMenuPrimitive.Portal {...portalProps}>
17+
<DropdownMenuPrimitive.Content
18+
bind:ref
19+
data-slot="dropdown-menu-content"
20+
{sideOffset}
21+
class={cn(
22+
'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-dropdown-menu-content-available-height) origin-(--bits-dropdown-menu-content-transform-origin) border-border dark:border-border/20 z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md outline-none',
23+
className
24+
)}
25+
{...restProps}
26+
/>
27+
</DropdownMenuPrimitive.Portal>

0 commit comments

Comments
 (0)