webui: Stop generation from chat sidebar (#17806)

* feat: Add stop generation button for Conversation Item

* chore: update webui build output
This commit is contained in:
Aleksander Grygier 2025-12-06 13:29:15 +01:00 committed by GitHub
parent e31b5c55c3
commit a28e3c7567
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 66 additions and 7 deletions

Binary file not shown.

View File

@ -8,6 +8,7 @@
import * as AlertDialog from '$lib/components/ui/alert-dialog'; import * as AlertDialog from '$lib/components/ui/alert-dialog';
import Input from '$lib/components/ui/input/input.svelte'; import Input from '$lib/components/ui/input/input.svelte';
import { conversationsStore, conversations } from '$lib/stores/conversations.svelte'; import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
import { chatStore } from '$lib/stores/chat.svelte';
import ChatSidebarActions from './ChatSidebarActions.svelte'; import ChatSidebarActions from './ChatSidebarActions.svelte';
const sidebar = Sidebar.useSidebar(); const sidebar = Sidebar.useSidebar();
@ -98,6 +99,10 @@
await goto(`#/chat/${id}`); await goto(`#/chat/${id}`);
} }
function handleStopGeneration(id: string) {
chatStore.stopGenerationForChat(id);
}
</script> </script>
<ScrollArea class="h-[100vh]"> <ScrollArea class="h-[100vh]">
@ -132,6 +137,7 @@
onSelect={selectConversation} onSelect={selectConversation}
onEdit={handleEditConversation} onEdit={handleEditConversation}
onDelete={handleDeleteConversation} onDelete={handleDeleteConversation}
onStop={handleStopGeneration}
/> />
</Sidebar.MenuItem> </Sidebar.MenuItem>
{/each} {/each}

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Trash2, Pencil, MoreHorizontal, Download, Loader2 } from '@lucide/svelte'; import { Trash2, Pencil, MoreHorizontal, Download, Loader2, Square } from '@lucide/svelte';
import { ActionDropdown } from '$lib/components/app'; import { ActionDropdown } from '$lib/components/app';
import * as Tooltip from '$lib/components/ui/tooltip';
import { getAllLoadingChats } from '$lib/stores/chat.svelte'; import { getAllLoadingChats } from '$lib/stores/chat.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte'; import { conversationsStore } from '$lib/stores/conversations.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@ -12,6 +13,7 @@
onDelete?: (id: string) => void; onDelete?: (id: string) => void;
onEdit?: (id: string) => void; onEdit?: (id: string) => void;
onSelect?: (id: string) => void; onSelect?: (id: string) => void;
onStop?: (id: string) => void;
} }
let { let {
@ -20,6 +22,7 @@
onDelete, onDelete,
onEdit, onEdit,
onSelect, onSelect,
onStop,
isActive = false isActive = false
}: Props = $props(); }: Props = $props();
@ -38,8 +41,14 @@
onDelete?.(conversation.id); onDelete?.(conversation.id);
} }
function handleStop(event: Event) {
event.stopPropagation();
onStop?.(conversation.id);
}
function handleGlobalEditEvent(event: Event) { function handleGlobalEditEvent(event: Event) {
const customEvent = event as CustomEvent<{ conversationId: string }>; const customEvent = event as CustomEvent<{ conversationId: string }>;
if (customEvent.detail.conversationId === conversation.id && isActive) { if (customEvent.detail.conversationId === conversation.id && isActive) {
handleEdit(event); handleEdit(event);
} }
@ -88,8 +97,28 @@
> >
<div class="flex min-w-0 flex-1 items-center gap-2"> <div class="flex min-w-0 flex-1 items-center gap-2">
{#if isLoading} {#if isLoading}
<Loader2 class="h-3.5 w-3.5 shrink-0 animate-spin text-muted-foreground" /> <Tooltip.Root>
<Tooltip.Trigger>
<div
class="stop-button flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground"
onclick={handleStop}
onkeydown={(e) => e.key === 'Enter' && handleStop(e)}
role="button"
tabindex="0"
aria-label="Stop generation"
>
<Loader2 class="loading-icon h-3.5 w-3.5 animate-spin" />
<Square class="stop-icon hidden h-3 w-3 fill-current text-destructive" />
</div>
</Tooltip.Trigger>
<Tooltip.Content>
<p>Stop generation</p>
</Tooltip.Content>
</Tooltip.Root>
{/if} {/if}
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<span class="truncate text-sm font-medium" onclick={handleMobileSidebarItemClick}> <span class="truncate text-sm font-medium" onclick={handleMobileSidebarItemClick}>
@ -147,5 +176,25 @@
opacity: 1 !important; opacity: 1 !important;
} }
} }
.stop-button {
:global(.stop-icon) {
display: none;
}
:global(.loading-icon) {
display: block;
}
}
&:is(:hover) .stop-button {
:global(.stop-icon) {
display: block;
}
:global(.loading-icon) {
display: none;
}
}
} }
</style> </style>

View File

@ -701,13 +701,17 @@ class ChatStore {
if (!activeConv) return; if (!activeConv) return;
await this.savePartialResponseIfNeeded(activeConv.id); await this.stopGenerationForChat(activeConv.id);
}
async stopGenerationForChat(convId: string): Promise<void> {
await this.savePartialResponseIfNeeded(convId);
this.stopStreaming(); this.stopStreaming();
this.abortRequest(activeConv.id); this.abortRequest(convId);
this.setChatLoading(activeConv.id, false); this.setChatLoading(convId, false);
this.clearChatStreaming(activeConv.id); this.clearChatStreaming(convId);
this.clearProcessingState(activeConv.id); this.clearProcessingState(convId);
} }
/** /**