Skip to content

Commit ba08bde

Browse files
allozaurfm240223
authored andcommitted
Enable per-conversation loading states to allow having parallel conversations (ggml-org#16327)
* feat: Per-conversation loading states and tracking streaming stats * chore: update webui build output * refactor: Chat state management Consolidates loading state management by using a global `isLoading` store synchronized with individual conversation states. This change ensures proper reactivity and avoids potential race conditions when updating the UI based on the loading status of different conversations. It also improves the accuracy of statistics displayed. Additionally, slots service methods are updated to use conversation IDs for per-conversation state management, avoiding global state pollution. * feat: Adds loading indicator to conversation items * chore: update webui build output * fix: Fix aborting chat streaming Improves the chat stream abortion process by ensuring that partial responses are saved before the abort signal is sent. This avoids a race condition where the onError callback could clear the streaming state before the partial response is saved. Additionally, the stream reading loop and callbacks are now checked for abort signals to prevent further processing after abortion. * refactor: Remove redundant comments * chore: build webui static output * refactor: Cleanup * chore: update webui build output * chore: update webui build output * fix: Conversation loading indicator for regenerating messages * chore: update webui static build * feat: Improve configuration * feat: Install `http-server` as dev dependency to not need to rely on `npx` in CI
1 parent d3c2eed commit ba08bde

File tree

13 files changed

+991
-242
lines changed

13 files changed

+991
-242
lines changed

tools/server/public/index.html.gz

871 Bytes
Binary file not shown.

tools/server/webui/package-lock.json

Lines changed: 527 additions & 0 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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"eslint-plugin-svelte": "^3.0.0",
5353
"fflate": "^0.8.2",
5454
"globals": "^16.0.0",
55+
"http-server": "^14.1.1",
5556
"mdast": "^3.0.0",
5657
"mdsvex": "^0.12.3",
5758
"playwright": "^1.53.0",

tools/server/webui/playwright.config.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { defineConfig } from '@playwright/test';
22

33
export default defineConfig({
44
webServer: {
5-
command: 'npm run build && npx http-server ../public -p 8181',
6-
port: 8181
5+
command: 'npm run build && http-server ../public -p 8181',
6+
port: 8181,
7+
timeout: 120000,
8+
reuseExistingServer: false
79
},
810
testDir: 'e2e'
911
});

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

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@
77
88
const processingState = useProcessingState();
99
10+
let isCurrentConversationLoading = $derived(isLoading());
1011
let processingDetails = $derived(processingState.getProcessingDetails());
12+
let showSlotsInfo = $derived(isCurrentConversationLoading || config().keepStatsVisible);
1113
12-
let showSlotsInfo = $derived(isLoading() || config().keepStatsVisible);
13-
14+
// Track loading state reactively by checking if conversation ID is in loading conversations array
1415
$effect(() => {
1516
const keepStatsVisible = config().keepStatsVisible;
1617
17-
if (keepStatsVisible || isLoading()) {
18+
if (keepStatsVisible || isCurrentConversationLoading) {
1819
processingState.startMonitoring();
1920
}
2021
21-
if (!isLoading() && !keepStatsVisible) {
22+
if (!isCurrentConversationLoading && !keepStatsVisible) {
2223
setTimeout(() => {
2324
if (!config().keepStatsVisible) {
2425
processingState.stopMonitoring();
@@ -27,18 +28,20 @@
2728
}
2829
});
2930
31+
// Update processing state from stored timings
3032
$effect(() => {
31-
activeConversation();
32-
33+
const conversation = activeConversation();
3334
const messages = activeMessages() as DatabaseMessage[];
3435
const keepStatsVisible = config().keepStatsVisible;
3536
36-
if (keepStatsVisible) {
37+
if (keepStatsVisible && conversation) {
3738
if (messages.length === 0) {
38-
slotsService.clearState();
39+
slotsService.clearConversationState(conversation.id);
3940
return;
4041
}
4142
43+
// Search backwards through messages to find most recent assistant message with timing data
44+
// Using reverse iteration for performance - avoids array copy and stops at first match
4245
let foundTimingData = false;
4346
4447
for (let i = messages.length - 1; i >= 0; i--) {
@@ -47,15 +50,18 @@
4750
foundTimingData = true;
4851
4952
slotsService
50-
.updateFromTimingData({
51-
prompt_n: message.timings.prompt_n || 0,
52-
predicted_n: message.timings.predicted_n || 0,
53-
predicted_per_second:
54-
message.timings.predicted_n && message.timings.predicted_ms
55-
? (message.timings.predicted_n / message.timings.predicted_ms) * 1000
56-
: 0,
57-
cache_n: message.timings.cache_n || 0
58-
})
53+
.updateFromTimingData(
54+
{
55+
prompt_n: message.timings.prompt_n || 0,
56+
predicted_n: message.timings.predicted_n || 0,
57+
predicted_per_second:
58+
message.timings.predicted_n && message.timings.predicted_ms
59+
? (message.timings.predicted_n / message.timings.predicted_ms) * 1000
60+
: 0,
61+
cache_n: message.timings.cache_n || 0
62+
},
63+
conversation.id
64+
)
5965
.catch((error) => {
6066
console.warn('Failed to update processing state from stored timings:', error);
6167
});
@@ -64,7 +70,7 @@
6470
}
6571
6672
if (!foundTimingData) {
67-
slotsService.clearState();
73+
slotsService.clearConversationState(conversation.id);
6874
}
6975
}
7076
});

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383
let activeErrorDialog = $derived(errorDialog());
8484
let isServerLoading = $derived(serverLoading());
8585
86+
let isCurrentConversationLoading = $derived(isLoading());
87+
8688
async function handleDeleteConfirm() {
8789
const conversation = activeConversation();
8890
if (conversation) {
@@ -254,7 +256,7 @@
254256
});
255257
256258
$effect(() => {
257-
if (isLoading() && autoScrollEnabled) {
259+
if (isCurrentConversationLoading && autoScrollEnabled) {
258260
scrollInterval = setInterval(scrollChatToBottom, AUTO_SCROLL_INTERVAL);
259261
} else if (scrollInterval) {
260262
clearInterval(scrollInterval);
@@ -305,7 +307,7 @@
305307

306308
<div class="conversation-chat-form pointer-events-auto rounded-t-3xl pb-4">
307309
<ChatForm
308-
isLoading={isLoading()}
310+
isLoading={isCurrentConversationLoading}
309311
onFileRemove={handleFileRemove}
310312
onFileUpload={handleFileUpload}
311313
onSend={handleSendMessage}
@@ -348,7 +350,7 @@
348350

349351
<div in:fly={{ y: 10, duration: 250, delay: 300 }}>
350352
<ChatForm
351-
isLoading={isLoading()}
353+
isLoading={isCurrentConversationLoading}
352354
onFileRemove={handleFileRemove}
353355
onFileUpload={handleFileUpload}
354356
onSend={handleSendMessage}

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
2-
import { Trash2, Pencil, MoreHorizontal, Download } from '@lucide/svelte';
2+
import { Trash2, Pencil, MoreHorizontal, Download, Loader2 } from '@lucide/svelte';
33
import { ActionDropdown } from '$lib/components/app';
4-
import { downloadConversation } from '$lib/stores/chat.svelte';
4+
import { downloadConversation, getAllLoadingConversations } from '$lib/stores/chat.svelte';
55
import { onMount } from 'svelte';
66
77
interface Props {
@@ -25,6 +25,8 @@
2525
let renderActionsDropdown = $state(false);
2626
let dropdownOpen = $state(false);
2727
28+
let isLoading = $derived(getAllLoadingConversations().includes(conversation.id));
29+
2830
function handleEdit(event: Event) {
2931
event.stopPropagation();
3032
onEdit?.(conversation.id);
@@ -83,11 +85,16 @@
8385
onmouseover={handleMouseOver}
8486
onmouseleave={handleMouseLeave}
8587
>
86-
<!-- svelte-ignore a11y_click_events_have_key_events -->
87-
<!-- svelte-ignore a11y_no_static_element_interactions -->
88-
<span class="truncate text-sm font-medium" onclick={handleMobileSidebarItemClick}>
89-
{conversation.name}
90-
</span>
88+
<div class="flex min-w-0 flex-1 items-center gap-2">
89+
{#if isLoading}
90+
<Loader2 class="h-3.5 w-3.5 shrink-0 animate-spin text-muted-foreground" />
91+
{/if}
92+
<!-- svelte-ignore a11y_click_events_have_key_events -->
93+
<!-- svelte-ignore a11y_no_static_element_interactions -->
94+
<span class="truncate text-sm font-medium" onclick={handleMobileSidebarItemClick}>
95+
{conversation.name}
96+
</span>
97+
</div>
9198

9299
{#if renderActionsDropdown}
93100
<div class="actions flex items-center">

0 commit comments

Comments
 (0)