refactor: Processing state reactivity
This commit is contained in:
parent
2a5922b1f6
commit
6b95118abc
|
|
@ -4,9 +4,10 @@
|
|||
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
||||
import {
|
||||
isLoading,
|
||||
isChatStreaming,
|
||||
clearProcessingState,
|
||||
updateProcessingStateFromTimings,
|
||||
setActiveProcessingConversation
|
||||
setActiveProcessingConversation,
|
||||
restoreProcessingStateFromMessages
|
||||
} from '$lib/stores/chat.svelte';
|
||||
import { activeMessages, activeConversation } from '$lib/stores/conversations.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
|
|
@ -14,34 +15,39 @@
|
|||
const processingState = useProcessingState();
|
||||
|
||||
let isCurrentConversationLoading = $derived(isLoading());
|
||||
let isStreaming = $derived(isChatStreaming());
|
||||
let hasProcessingData = $derived(processingState.processingState !== null);
|
||||
let processingDetails = $derived(processingState.getProcessingDetails());
|
||||
let showSlotsInfo = $derived(isCurrentConversationLoading || config().keepStatsVisible);
|
||||
|
||||
// Sync active processing conversation with currently viewed conversation
|
||||
let showProcessingInfo = $derived(
|
||||
isCurrentConversationLoading || isStreaming || config().keepStatsVisible || hasProcessingData
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
const conversation = activeConversation();
|
||||
// Use untrack to prevent creating reactive dependencies on state updates
|
||||
|
||||
untrack(() => setActiveProcessingConversation(conversation?.id ?? null));
|
||||
});
|
||||
|
||||
// Track loading state reactively by checking if conversation ID is in loading conversations array
|
||||
$effect(() => {
|
||||
const keepStatsVisible = config().keepStatsVisible;
|
||||
const shouldMonitor = keepStatsVisible || isCurrentConversationLoading || isStreaming;
|
||||
|
||||
if (keepStatsVisible || isCurrentConversationLoading) {
|
||||
untrack(() => processingState.startMonitoring());
|
||||
if (shouldMonitor) {
|
||||
processingState.startMonitoring();
|
||||
}
|
||||
|
||||
if (!isCurrentConversationLoading && !keepStatsVisible) {
|
||||
setTimeout(() => {
|
||||
if (!config().keepStatsVisible) {
|
||||
if (!isCurrentConversationLoading && !isStreaming && !keepStatsVisible) {
|
||||
const timeout = setTimeout(() => {
|
||||
if (!config().keepStatsVisible && !isChatStreaming()) {
|
||||
processingState.stopMonitoring();
|
||||
}
|
||||
}, PROCESSING_INFO_TIMEOUT);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
});
|
||||
|
||||
// Update processing state from stored timings
|
||||
$effect(() => {
|
||||
const conversation = activeConversation();
|
||||
const messages = activeMessages() as DatabaseMessage[];
|
||||
|
|
@ -49,47 +55,18 @@
|
|||
|
||||
if (keepStatsVisible && conversation) {
|
||||
if (messages.length === 0) {
|
||||
// Use untrack to prevent creating reactive dependencies on state updates
|
||||
untrack(() => clearProcessingState(conversation.id));
|
||||
return;
|
||||
}
|
||||
|
||||
// Search backwards through messages to find most recent assistant message with timing data
|
||||
// Using reverse iteration for performance - avoids array copy and stops at first match
|
||||
let foundTimingData = false;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
if (message.role === 'assistant' && message.timings) {
|
||||
foundTimingData = true;
|
||||
|
||||
// Use untrack to prevent creating reactive dependencies on state updates
|
||||
untrack(() =>
|
||||
updateProcessingStateFromTimings(
|
||||
{
|
||||
prompt_n: message.timings!.prompt_n || 0,
|
||||
predicted_n: message.timings!.predicted_n || 0,
|
||||
predicted_per_second:
|
||||
message.timings!.predicted_n && message.timings!.predicted_ms
|
||||
? (message.timings!.predicted_n / message.timings!.predicted_ms) * 1000
|
||||
: 0,
|
||||
cache_n: message.timings!.cache_n || 0
|
||||
},
|
||||
conversation.id
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundTimingData) {
|
||||
untrack(() => clearProcessingState(conversation.id));
|
||||
if (!isCurrentConversationLoading && !isStreaming) {
|
||||
untrack(() => restoreProcessingStateFromMessages(messages, conversation.id));
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="chat-processing-info-container pointer-events-none" class:visible={showSlotsInfo}>
|
||||
<div class="chat-processing-info-container pointer-events-none" class:visible={showProcessingInfo}>
|
||||
<div class="chat-processing-info-content">
|
||||
{#each processingDetails as detail (detail)}
|
||||
<span class="chat-processing-info-detail pointer-events-auto">{detail}</span>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
import {
|
||||
subscribeToProcessingState,
|
||||
getCurrentProcessingState,
|
||||
isChatStreaming
|
||||
} from '$lib/stores/chat.svelte';
|
||||
import { activeProcessingState } from '$lib/stores/chat.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
|
||||
export interface UseProcessingStateReturn {
|
||||
|
|
@ -10,7 +6,7 @@ export interface UseProcessingStateReturn {
|
|||
getProcessingDetails(): string[];
|
||||
getProcessingMessage(): string;
|
||||
shouldShowDetails(): boolean;
|
||||
startMonitoring(): Promise<void>;
|
||||
startMonitoring(): void;
|
||||
stopMonitoring(): void;
|
||||
}
|
||||
|
||||
|
|
@ -18,93 +14,71 @@ export interface UseProcessingStateReturn {
|
|||
* useProcessingState - Reactive processing state hook
|
||||
*
|
||||
* This hook provides reactive access to the processing state of the server.
|
||||
* It subscribes to timing data updates from ChatStore and provides
|
||||
* It directly reads from ChatStore's reactive state and provides
|
||||
* formatted processing details for UI display.
|
||||
*
|
||||
* **Features:**
|
||||
* - Real-time processing state monitoring
|
||||
* - Real-time processing state via direct reactive state binding
|
||||
* - Context and output token tracking
|
||||
* - Tokens per second calculation
|
||||
* - Graceful degradation when slots endpoint unavailable
|
||||
* - Automatic cleanup on component unmount
|
||||
* - Automatic updates when streaming data arrives
|
||||
* - Supports multiple concurrent conversations
|
||||
*
|
||||
* @returns Hook interface with processing state and control methods
|
||||
*/
|
||||
export function useProcessingState(): UseProcessingStateReturn {
|
||||
let isMonitoring = $state(false);
|
||||
let processingState = $state<ApiProcessingState | null>(null);
|
||||
let lastKnownState = $state<ApiProcessingState | null>(null);
|
||||
let unsubscribe: (() => void) | null = null;
|
||||
|
||||
async function startMonitoring(): Promise<void> {
|
||||
if (isMonitoring) return;
|
||||
|
||||
isMonitoring = true;
|
||||
|
||||
unsubscribe = subscribeToProcessingState((state) => {
|
||||
processingState = state;
|
||||
if (state) {
|
||||
lastKnownState = state;
|
||||
} else {
|
||||
lastKnownState = null;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const currentState = await getCurrentProcessingState();
|
||||
|
||||
if (currentState) {
|
||||
processingState = currentState;
|
||||
lastKnownState = currentState;
|
||||
}
|
||||
|
||||
// Check if streaming is active for UI purposes
|
||||
if (isChatStreaming()) {
|
||||
// Streaming is active, state will be updated via subscription
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to start processing state monitoring:', error);
|
||||
// Continue without monitoring - graceful degradation
|
||||
// Derive processing state reactively from ChatStore's direct state
|
||||
const processingState = $derived.by(() => {
|
||||
if (!isMonitoring) {
|
||||
return lastKnownState;
|
||||
}
|
||||
// Read directly from the reactive state export
|
||||
return activeProcessingState();
|
||||
});
|
||||
|
||||
// Track last known state for keepStatsVisible functionality
|
||||
$effect(() => {
|
||||
if (processingState && isMonitoring) {
|
||||
lastKnownState = processingState;
|
||||
}
|
||||
});
|
||||
|
||||
function startMonitoring(): void {
|
||||
if (isMonitoring) return;
|
||||
isMonitoring = true;
|
||||
}
|
||||
|
||||
function stopMonitoring(): void {
|
||||
if (!isMonitoring) return;
|
||||
|
||||
isMonitoring = false;
|
||||
|
||||
// Only clear processing state if keepStatsVisible is disabled
|
||||
// This preserves the last known state for display when stats should remain visible
|
||||
// Only clear last known state if keepStatsVisible is disabled
|
||||
const currentConfig = config();
|
||||
if (!currentConfig.keepStatsVisible) {
|
||||
processingState = null;
|
||||
} else if (lastKnownState) {
|
||||
// Keep the last known state visible when keepStatsVisible is enabled
|
||||
processingState = lastKnownState;
|
||||
}
|
||||
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
unsubscribe = null;
|
||||
lastKnownState = null;
|
||||
}
|
||||
}
|
||||
|
||||
function getProcessingMessage(): string {
|
||||
if (!processingState) {
|
||||
const state = processingState;
|
||||
if (!state) {
|
||||
return 'Processing...';
|
||||
}
|
||||
|
||||
switch (processingState.status) {
|
||||
switch (state.status) {
|
||||
case 'initializing':
|
||||
return 'Initializing...';
|
||||
case 'preparing':
|
||||
if (processingState.progressPercent !== undefined) {
|
||||
return `Processing (${processingState.progressPercent}%)`;
|
||||
if (state.progressPercent !== undefined) {
|
||||
return `Processing (${state.progressPercent}%)`;
|
||||
}
|
||||
return 'Preparing response...';
|
||||
case 'generating':
|
||||
if (processingState.tokensDecoded > 0) {
|
||||
return `Generating... (${processingState.tokensDecoded} tokens)`;
|
||||
if (state.tokensDecoded > 0) {
|
||||
return `Generating... (${state.tokensDecoded} tokens)`;
|
||||
}
|
||||
return 'Generating...';
|
||||
default:
|
||||
|
|
@ -157,7 +131,8 @@ export function useProcessingState(): UseProcessingStateReturn {
|
|||
}
|
||||
|
||||
function shouldShowDetails(): boolean {
|
||||
return processingState !== null && processingState.status !== 'idle';
|
||||
const state = processingState;
|
||||
return state !== null && state.status !== 'idle';
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { config } from '$lib/stores/settings.svelte';
|
|||
import { contextSize } from '$lib/stores/server.svelte';
|
||||
import { normalizeModelName } from '$lib/utils/model-names';
|
||||
import { filterByLeafNodeId, findDescendantMessages, findLeafNode } from '$lib/utils/branching';
|
||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
import { DEFAULT_CONTEXT } from '$lib/constants/default-context';
|
||||
import type {
|
||||
ChatMessageTimings,
|
||||
|
|
@ -55,6 +55,7 @@ import type { DatabaseMessage, DatabaseMessageExtra } from '$lib/types/database'
|
|||
* - Automatic state sync when switching between conversations
|
||||
*/
|
||||
class ChatStore {
|
||||
activeProcessingState = $state<ApiProcessingState | null>(null);
|
||||
currentResponse = $state('');
|
||||
errorDialogState = $state<{ type: 'timeout' | 'server'; message: string } | null>(null);
|
||||
isLoading = $state(false);
|
||||
|
|
@ -66,10 +67,8 @@ class ChatStore {
|
|||
|
||||
// Processing state tracking - per-conversation timing/context info
|
||||
private processingStates = new SvelteMap<string, ApiProcessingState | null>();
|
||||
private processingCallbacks = new SvelteSet<(state: ApiProcessingState | null) => void>();
|
||||
private activeConversationId = $state<string | null>(null);
|
||||
private isStreamingActive = $state(false);
|
||||
private lastKnownProcessingState = $state<ApiProcessingState | null>(null);
|
||||
|
||||
// ============ API Options ============
|
||||
|
||||
|
|
@ -190,7 +189,12 @@ class ChatStore {
|
|||
*/
|
||||
setActiveProcessingConversation(conversationId: string | null): void {
|
||||
this.activeConversationId = conversationId;
|
||||
this.notifyProcessingCallbacks();
|
||||
|
||||
if (conversationId) {
|
||||
this.activeProcessingState = this.processingStates.get(conversationId) || null;
|
||||
} else {
|
||||
this.activeProcessingState = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -207,24 +211,16 @@ class ChatStore {
|
|||
this.processingStates.delete(conversationId);
|
||||
|
||||
if (conversationId === this.activeConversationId) {
|
||||
this.lastKnownProcessingState = null;
|
||||
this.notifyProcessingCallbacks();
|
||||
this.activeProcessingState = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to processing state changes
|
||||
* Get the current processing state for the active conversation (reactive)
|
||||
* Returns the direct reactive state for UI binding
|
||||
*/
|
||||
subscribeToProcessingState(callback: (state: ApiProcessingState | null) => void): () => void {
|
||||
this.processingCallbacks.add(callback);
|
||||
|
||||
if (this.lastKnownProcessingState) {
|
||||
callback(this.lastKnownProcessingState);
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.processingCallbacks.delete(callback);
|
||||
};
|
||||
getActiveProcessingState(): ApiProcessingState | null {
|
||||
return this.activeProcessingState;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -247,36 +243,28 @@ class ChatStore {
|
|||
return;
|
||||
}
|
||||
|
||||
if (conversationId) {
|
||||
this.processingStates.set(conversationId, processingState);
|
||||
const targetId = conversationId || this.activeConversationId;
|
||||
if (targetId) {
|
||||
this.processingStates.set(targetId, processingState);
|
||||
|
||||
if (conversationId === this.activeConversationId) {
|
||||
this.lastKnownProcessingState = processingState;
|
||||
this.notifyProcessingCallbacks();
|
||||
if (targetId === this.activeConversationId) {
|
||||
this.activeProcessingState = processingState;
|
||||
}
|
||||
} else {
|
||||
this.lastKnownProcessingState = processingState;
|
||||
this.notifyProcessingCallbacks();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current processing state
|
||||
* Get current processing state (sync version for reactive access)
|
||||
*/
|
||||
async getCurrentProcessingState(): Promise<ApiProcessingState | null> {
|
||||
if (this.activeConversationId) {
|
||||
const conversationState = this.processingStates.get(this.activeConversationId);
|
||||
if (conversationState) {
|
||||
return conversationState;
|
||||
}
|
||||
}
|
||||
getCurrentProcessingStateSync(): ApiProcessingState | null {
|
||||
return this.activeProcessingState;
|
||||
}
|
||||
|
||||
if (this.lastKnownProcessingState) {
|
||||
return this.lastKnownProcessingState;
|
||||
}
|
||||
|
||||
// Try to restore from last assistant message
|
||||
const messages = conversationsStore.activeMessages;
|
||||
/**
|
||||
* Restore processing state from last assistant message timings
|
||||
* Call this when keepStatsVisible is enabled and we need to show last known stats
|
||||
*/
|
||||
restoreProcessingStateFromMessages(messages: DatabaseMessage[], conversationId: string): void {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
if (message.role === 'assistant' && message.timings) {
|
||||
|
|
@ -291,32 +279,23 @@ class ChatStore {
|
|||
});
|
||||
|
||||
if (restoredState) {
|
||||
this.lastKnownProcessingState = restoredState;
|
||||
return restoredState;
|
||||
this.processingStates.set(conversationId, restoredState);
|
||||
|
||||
if (conversationId === this.activeConversationId) {
|
||||
this.activeProcessingState = restoredState;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private notifyProcessingCallbacks(): void {
|
||||
const currentState = this.activeConversationId
|
||||
? this.processingStates.get(this.activeConversationId) || null
|
||||
: this.lastKnownProcessingState;
|
||||
|
||||
for (const callback of this.processingCallbacks) {
|
||||
try {
|
||||
callback(currentState);
|
||||
} catch (error) {
|
||||
console.error('Error in processing state callback:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getContextTotal(): number {
|
||||
if (this.lastKnownProcessingState && this.lastKnownProcessingState.contextTotal > 0) {
|
||||
return this.lastKnownProcessingState.contextTotal;
|
||||
const activeState = this.getActiveProcessingState();
|
||||
|
||||
if (activeState && activeState.contextTotal > 0) {
|
||||
return activeState.contextTotal;
|
||||
}
|
||||
|
||||
const propsContextSize = contextSize();
|
||||
|
|
@ -734,7 +713,7 @@ class ChatStore {
|
|||
content: streamingState.response
|
||||
};
|
||||
if (lastMessage.thinking?.trim()) updateData.thinking = lastMessage.thinking;
|
||||
const lastKnownState = await this.getCurrentProcessingState();
|
||||
const lastKnownState = this.getCurrentProcessingStateSync();
|
||||
if (lastKnownState) {
|
||||
updateData.timings = {
|
||||
prompt_n: lastKnownState.promptTokens || 0,
|
||||
|
|
@ -1323,9 +1302,13 @@ export const syncLoadingStateForChat = chatStore.syncLoadingStateForChat.bind(ch
|
|||
export const clearUIState = chatStore.clearUIState.bind(chatStore);
|
||||
|
||||
// Processing state (timing/context info)
|
||||
export const subscribeToProcessingState = chatStore.subscribeToProcessingState.bind(chatStore);
|
||||
export const getProcessingState = chatStore.getProcessingState.bind(chatStore);
|
||||
export const getCurrentProcessingState = chatStore.getCurrentProcessingState.bind(chatStore);
|
||||
export const getActiveProcessingState = chatStore.getActiveProcessingState.bind(chatStore);
|
||||
export const activeProcessingState = () => chatStore.activeProcessingState;
|
||||
export const getCurrentProcessingStateSync =
|
||||
chatStore.getCurrentProcessingStateSync.bind(chatStore);
|
||||
export const restoreProcessingStateFromMessages =
|
||||
chatStore.restoreProcessingStateFromMessages.bind(chatStore);
|
||||
export const clearProcessingState = chatStore.clearProcessingState.bind(chatStore);
|
||||
export const updateProcessingStateFromTimings =
|
||||
chatStore.updateProcessingStateFromTimings.bind(chatStore);
|
||||
|
|
|
|||
Loading…
Reference in New Issue