Skip to content

Commit c13eda4

Browse files
committed
feat: Improve Keyboard Shortcuts UI & logic
1 parent 0fb9310 commit c13eda4

File tree

7 files changed

+125
-21
lines changed

7 files changed

+125
-21
lines changed

tools/server/webui/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ A modern, feature-rich web interface for llama.cpp built with SvelteKit. This UI
99
- **Conversation Management** - Create, edit, branch, and search conversations
1010
- **Advanced Markdown** - Code highlighting, math formulas (KaTeX), and content blocks
1111
- **Reasoning Content** - Support for models with thinking blocks
12-
- **Keyboard Shortcuts** - Full keyboard navigation (Ctrl+K, Ctrl+V, Ctrl+B, etc.)
12+
- **Keyboard Shortcuts** - Keyboard navigation (Shift+Ctrl/Cmd+O for new chat, Shift+Ctrl/Cmdt+E for edit conversation, Shift+Ctrl/Cmdt+D for delete conversation, Ctrl/Cmd+K for search, Ctrl/Cmd+V for paste, Ctrl/Cmd+B for opening/collapsing sidebar)
1313
- **Request Tracking** - Monitor processing with slots endpoint integration
1414
- **UI Testing** - Storybook component library with automated tests
1515

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,6 @@
130130
function handleKeydown(event: KeyboardEvent) {
131131
const isCtrlOrCmd = event.ctrlKey || event.metaKey;
132132
133-
if (isCtrlOrCmd && event.key === 'k') {
134-
event.preventDefault();
135-
goto('/?new_chat=true');
136-
}
137-
138133
if (isCtrlOrCmd && event.shiftKey && (event.key === 'd' || event.key === 'D')) {
139134
event.preventDefault();
140135
if (activeConversation()) {

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,23 @@
4242
}
4343
}
4444
45+
export function activateSearchMode() {
46+
isSearchModeActive = true;
47+
}
48+
49+
export function editActiveConversation() {
50+
if (currentChatId) {
51+
const activeConversation = filteredConversations.find(conv => conv.id === currentChatId);
52+
53+
if (activeConversation) {
54+
const event = new CustomEvent('edit-active-conversation', {
55+
detail: { conversationId: currentChatId }
56+
});
57+
document.dispatchEvent(event);
58+
}
59+
}
60+
}
61+
4562
async function selectConversation(id: string) {
4663
if (isSearchModeActive) {
4764
isSearchModeActive = false;

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { Button } from '$lib/components/ui/button';
33
import { Input } from '$lib/components/ui/input';
44
import { Search, SquarePen, X } from '@lucide/svelte';
5+
import KeyboardShortcutInfo from '$lib/components/ui/KeyboardShortcutInfo.svelte';
56
67
interface Props {
78
handleMobileSidebarItemClick: () => void;
@@ -49,26 +50,32 @@
4950
</div>
5051
{:else}
5152
<Button
52-
class="w-full justify-start gap-2"
53+
class="w-full justify-between hover:[&>kbd]:opacity-100"
5354
href="/?new_chat=true"
5455
onclick={handleMobileSidebarItemClick}
5556
variant="ghost"
5657
>
57-
<SquarePen class="h-4 w-4" />
58+
<div class="flex items-center gap-2">
59+
<SquarePen class="h-4 w-4" />
60+
New chat
61+
</div>
5862

59-
New chat
63+
<KeyboardShortcutInfo keys={['shift', 'cmd', 'o']} />
6064
</Button>
6165

6266
<Button
63-
class="w-full justify-start gap-2"
67+
class="w-full justify-between hover:[&>kbd]:opacity-100"
6468
onclick={() => {
6569
isSearchModeActive = true;
6670
}}
6771
variant="ghost"
6872
>
69-
<Search class="h-4 w-4" />
73+
<div class="flex items-center gap-2">
74+
<Search class="h-4 w-4" />
75+
Search conversations
76+
</div>
7077

71-
Search conversations
78+
<KeyboardShortcutInfo keys={['cmd', 'k']} />
7279
</Button>
7380
{/if}
7481
</div>

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

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
44
import Input from '$lib/components/ui/input/input.svelte';
55
import { Trash2, Pencil, MoreHorizontal } from '@lucide/svelte';
6+
import KeyboardShortcutInfo from '$lib/components/ui/KeyboardShortcutInfo.svelte';
67
78
interface Props {
89
isActive?: boolean;
@@ -58,6 +59,23 @@
5859
function handleSelect() {
5960
onSelect?.(conversation.id);
6061
}
62+
63+
// Listen for global edit event
64+
function handleGlobalEditEvent(event: Event) {
65+
const customEvent = event as CustomEvent<{ conversationId: string }>;
66+
if (customEvent.detail.conversationId === conversation.id && isActive) {
67+
handleEdit(event);
68+
}
69+
}
70+
71+
// Add event listener when component mounts
72+
$effect(() => {
73+
document.addEventListener('edit-active-conversation', handleGlobalEditEvent as EventListener);
74+
75+
return () => {
76+
document.removeEventListener('edit-active-conversation', handleGlobalEditEvent as EventListener);
77+
};
78+
});
6179
</script>
6280

6381
<button
@@ -93,25 +111,29 @@
93111
</DropdownMenu.Trigger>
94112

95113
<DropdownMenu.Content align="end" class="z-999 w-48">
96-
<DropdownMenu.Item onclick={handleEdit} class="flex items-center gap-2">
97-
<Pencil class="h-4 w-4" />
98-
99-
Edit
114+
<DropdownMenu.Item onclick={handleEdit} class="flex items-center justify-between hover:[&>kbd]:opacity-100">
115+
<div class="flex items-center gap-2">
116+
<Pencil class="h-4 w-4" />
117+
Edit
118+
</div>
119+
<KeyboardShortcutInfo keys={['shift', 'cmd', 'e']} />
100120
</DropdownMenu.Item>
101121

102122
<DropdownMenu.Separator />
103123

104124
<DropdownMenu.Item
105125
variant="destructive"
106-
class="flex items-center gap-2"
126+
class="flex items-center justify-between hover:[&>kbd]:opacity-100"
107127
onclick={(e) => {
108128
e.stopPropagation();
109129
showDeleteDialog = true;
110130
}}
111131
>
112-
<Trash2 class="h-4 w-4" />
113-
114-
Delete
132+
<div class="flex items-center gap-2">
133+
<Trash2 class="h-4 w-4 text-destructive" />
134+
Delete
135+
</div>
136+
<KeyboardShortcutInfo keys={['shift', 'cmd', 'd']} variant="destructive" />
115137
</DropdownMenu.Item>
116138
</DropdownMenu.Content>
117139
</DropdownMenu.Root>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<script lang="ts">
2+
import { ArrowBigUp } from '@lucide/svelte';
3+
4+
interface Props {
5+
keys: string[];
6+
variant?: 'default' | 'destructive';
7+
class?: string;
8+
}
9+
10+
let {
11+
keys,
12+
variant = 'default',
13+
class: className = ''
14+
}: Props = $props();
15+
16+
let baseClasses = 'px-1 pointer-events-none inline-flex select-none items-center gap-0.5 font-mono text-md font-medium opacity-0 transition-opacity';
17+
let variantClasses = variant === 'destructive' ? 'text-destructive' : 'text-muted-foreground';
18+
</script>
19+
20+
<kbd class="{baseClasses} {variantClasses} {className}">
21+
{#each keys as key, index}
22+
{#if key === 'shift'}
23+
<ArrowBigUp class="h-2 w-2 {variant === 'destructive' ? 'text-destructive' : ''} -mr-1" />
24+
{:else if key === 'cmd'}
25+
<span class="text-lg {variant === 'destructive' ? 'text-destructive' : ''}">⌘</span>
26+
{:else}
27+
{key.toUpperCase()}
28+
{/if}
29+
{#if index < keys.length - 1}
30+
<span> </span>
31+
{/if}
32+
{/each}
33+
</kbd>

tools/server/webui/src/routes/+layout.svelte

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { serverStore } from '$lib/stores/server.svelte';
88
import { ModeWatcher } from 'mode-watcher';
99
import { Toaster } from 'svelte-sonner';
10+
import { goto } from '$app/navigation';
1011
1112
let { children } = $props();
1213
@@ -15,6 +16,7 @@
1516
let isNewChatMode = $derived(page.url.searchParams.get('new_chat') === 'true');
1617
let showSidebarByDefault = $derived(activeMessages().length > 0 || isLoading());
1718
let sidebarOpen = $state(false);
19+
let chatSidebar: any = $state();
1820
1921
$effect(() => {
2022
if (isHomeRoute && !isNewChatMode) {
@@ -36,6 +38,32 @@
3638
$effect(() => {
3739
serverStore.fetchServerProps();
3840
});
41+
42+
// Global keyboard shortcuts
43+
function handleKeydown(event: KeyboardEvent) {
44+
const isCtrlOrCmd = event.ctrlKey || event.metaKey;
45+
46+
if (isCtrlOrCmd && event.key === 'k') {
47+
event.preventDefault();
48+
if (chatSidebar?.activateSearchMode) {
49+
chatSidebar.activateSearchMode();
50+
sidebarOpen = true;
51+
}
52+
}
53+
54+
if (isCtrlOrCmd && event.shiftKey && event.key === 'o') {
55+
event.preventDefault();
56+
goto('/?new_chat=true');
57+
}
58+
59+
if (event.shiftKey && isCtrlOrCmd && event.key === 'e') {
60+
event.preventDefault();
61+
62+
if (chatSidebar?.editActiveConversation) {
63+
chatSidebar.editActiveConversation();
64+
}
65+
}
66+
}
3967
</script>
4068

4169
<ModeWatcher />
@@ -47,7 +75,7 @@
4775
<Sidebar.Provider bind:open={sidebarOpen}>
4876
<div class="flex h-screen w-full">
4977
<Sidebar.Root class="h-full">
50-
<ChatSidebar />
78+
<ChatSidebar bind:this={chatSidebar} />
5179
</Sidebar.Root>
5280

5381
<Sidebar.Trigger
@@ -62,3 +90,5 @@
6290
</Sidebar.Inset>
6391
</div>
6492
</Sidebar.Provider>
93+
94+
<svelte:window onkeydown={handleKeydown} />

0 commit comments

Comments
 (0)