| | import { activeProcessingState } from '$lib/stores/chat.svelte';
|
| | import { config } from '$lib/stores/settings.svelte';
|
| | import { STATS_UNITS } from '$lib/constants/processing-info';
|
| | import type { ApiProcessingState, LiveProcessingStats, LiveGenerationStats } from '$lib/types';
|
| |
|
| | export interface UseProcessingStateReturn {
|
| | readonly processingState: ApiProcessingState | null;
|
| | getProcessingDetails(): string[];
|
| | getTechnicalDetails(): string[];
|
| | getProcessingMessage(): string;
|
| | getPromptProgressText(): string | null;
|
| | getLiveProcessingStats(): LiveProcessingStats | null;
|
| | getLiveGenerationStats(): LiveGenerationStats | null;
|
| | shouldShowDetails(): boolean;
|
| | startMonitoring(): void;
|
| | stopMonitoring(): void;
|
| | }
|
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | export function useProcessingState(): UseProcessingStateReturn {
|
| | let isMonitoring = $state(false);
|
| | let lastKnownState = $state<ApiProcessingState | null>(null);
|
| | let lastKnownProcessingStats = $state<LiveProcessingStats | null>(null);
|
| |
|
| |
|
| | const processingState = $derived.by(() => {
|
| | if (!isMonitoring) {
|
| | return lastKnownState;
|
| | }
|
| |
|
| | return activeProcessingState();
|
| | });
|
| |
|
| |
|
| | $effect(() => {
|
| | if (processingState && isMonitoring) {
|
| | lastKnownState = processingState;
|
| | }
|
| | });
|
| |
|
| |
|
| | $effect(() => {
|
| | if (processingState?.promptProgress) {
|
| | const { processed, total, time_ms, cache } = processingState.promptProgress;
|
| | const actualProcessed = processed - cache;
|
| | const actualTotal = total - cache;
|
| |
|
| | if (actualProcessed > 0 && time_ms > 0) {
|
| | const tokensPerSecond = actualProcessed / (time_ms / 1000);
|
| | lastKnownProcessingStats = {
|
| | tokensProcessed: actualProcessed,
|
| | totalTokens: actualTotal,
|
| | timeMs: time_ms,
|
| | tokensPerSecond
|
| | };
|
| | }
|
| | }
|
| | });
|
| |
|
| | function getETASecs(done: number, total: number, elapsedMs: number): number | undefined {
|
| | const elapsedSecs = elapsedMs / 1000;
|
| | const progressETASecs =
|
| | done === 0 || elapsedSecs < 0.5
|
| | ? undefined
|
| | : elapsedSecs * (total / done - 1);
|
| | return progressETASecs;
|
| | }
|
| |
|
| | function startMonitoring(): void {
|
| | if (isMonitoring) return;
|
| | isMonitoring = true;
|
| | }
|
| |
|
| | function stopMonitoring(): void {
|
| | if (!isMonitoring) return;
|
| | isMonitoring = false;
|
| |
|
| |
|
| | const currentConfig = config();
|
| | if (!currentConfig.keepStatsVisible) {
|
| | lastKnownState = null;
|
| | lastKnownProcessingStats = null;
|
| | }
|
| | }
|
| |
|
| | function getProcessingMessage(): string {
|
| | if (!processingState) {
|
| | return 'Processing...';
|
| | }
|
| |
|
| | switch (processingState.status) {
|
| | case 'initializing':
|
| | return 'Initializing...';
|
| | case 'preparing':
|
| | if (processingState.progressPercent !== undefined) {
|
| | return `Processing (${processingState.progressPercent}%)`;
|
| | }
|
| | return 'Preparing response...';
|
| | case 'generating':
|
| | return '';
|
| | default:
|
| | return 'Processing...';
|
| | }
|
| | }
|
| |
|
| | function getProcessingDetails(): string[] {
|
| |
|
| | const stateToUse = processingState || lastKnownState;
|
| | if (!stateToUse) {
|
| | return [];
|
| | }
|
| |
|
| | const details: string[] = [];
|
| |
|
| |
|
| | if (stateToUse.promptProgress) {
|
| | const { processed, total, time_ms, cache } = stateToUse.promptProgress;
|
| | const actualProcessed = processed - cache;
|
| | const actualTotal = total - cache;
|
| |
|
| | if (actualProcessed < actualTotal && actualProcessed > 0) {
|
| | const percent = Math.round((actualProcessed / actualTotal) * 100);
|
| | const eta = getETASecs(actualProcessed, actualTotal, time_ms);
|
| |
|
| | if (eta !== undefined) {
|
| | const etaSecs = Math.ceil(eta);
|
| | details.push(`Processing ${percent}% (ETA: ${etaSecs}s)`);
|
| | } else {
|
| | details.push(`Processing ${percent}%`);
|
| | }
|
| | }
|
| | }
|
| |
|
| |
|
| | if (
|
| | typeof stateToUse.contextTotal === 'number' &&
|
| | stateToUse.contextUsed >= 0 &&
|
| | stateToUse.contextTotal > 0
|
| | ) {
|
| | const contextPercent = Math.round((stateToUse.contextUsed / stateToUse.contextTotal) * 100);
|
| |
|
| | details.push(
|
| | `Context: ${stateToUse.contextUsed}/${stateToUse.contextTotal} (${contextPercent}%)`
|
| | );
|
| | }
|
| |
|
| | if (stateToUse.outputTokensUsed > 0) {
|
| |
|
| | if (stateToUse.outputTokensMax <= 0) {
|
| | details.push(`Output: ${stateToUse.outputTokensUsed}/∞`);
|
| | } else {
|
| | const outputPercent = Math.round(
|
| | (stateToUse.outputTokensUsed / stateToUse.outputTokensMax) * 100
|
| | );
|
| |
|
| | details.push(
|
| | `Output: ${stateToUse.outputTokensUsed}/${stateToUse.outputTokensMax} (${outputPercent}%)`
|
| | );
|
| | }
|
| | }
|
| |
|
| | if (stateToUse.tokensPerSecond && stateToUse.tokensPerSecond > 0) {
|
| | details.push(`${stateToUse.tokensPerSecond.toFixed(1)} ${STATS_UNITS.TOKENS_PER_SECOND}`);
|
| | }
|
| |
|
| | if (stateToUse.speculative) {
|
| | details.push('Speculative decoding enabled');
|
| | }
|
| |
|
| | return details;
|
| | }
|
| |
|
| | |
| | |
| |
|
| | function getTechnicalDetails(): string[] {
|
| | const stateToUse = processingState || lastKnownState;
|
| | if (!stateToUse) {
|
| | return [];
|
| | }
|
| |
|
| | const details: string[] = [];
|
| |
|
| |
|
| | if (
|
| | typeof stateToUse.contextTotal === 'number' &&
|
| | stateToUse.contextUsed >= 0 &&
|
| | stateToUse.contextTotal > 0
|
| | ) {
|
| | const contextPercent = Math.round((stateToUse.contextUsed / stateToUse.contextTotal) * 100);
|
| |
|
| | details.push(
|
| | `Context: ${stateToUse.contextUsed}/${stateToUse.contextTotal} (${contextPercent}%)`
|
| | );
|
| | }
|
| |
|
| | if (stateToUse.outputTokensUsed > 0) {
|
| |
|
| | if (stateToUse.outputTokensMax <= 0) {
|
| | details.push(`Output: ${stateToUse.outputTokensUsed}/∞`);
|
| | } else {
|
| | const outputPercent = Math.round(
|
| | (stateToUse.outputTokensUsed / stateToUse.outputTokensMax) * 100
|
| | );
|
| |
|
| | details.push(
|
| | `Output: ${stateToUse.outputTokensUsed}/${stateToUse.outputTokensMax} (${outputPercent}%)`
|
| | );
|
| | }
|
| | }
|
| |
|
| | if (stateToUse.tokensPerSecond && stateToUse.tokensPerSecond > 0) {
|
| | details.push(`${stateToUse.tokensPerSecond.toFixed(1)} ${STATS_UNITS.TOKENS_PER_SECOND}`);
|
| | }
|
| |
|
| | if (stateToUse.speculative) {
|
| | details.push('Speculative decoding enabled');
|
| | }
|
| |
|
| | return details;
|
| | }
|
| |
|
| | function shouldShowDetails(): boolean {
|
| | return processingState !== null && processingState.status !== 'idle';
|
| | }
|
| |
|
| | |
| | |
| |
|
| | function getPromptProgressText(): string | null {
|
| | if (!processingState?.promptProgress) return null;
|
| |
|
| | const { processed, total, cache } = processingState.promptProgress;
|
| |
|
| | const actualProcessed = processed - cache;
|
| | const actualTotal = total - cache;
|
| | const percent = Math.round((actualProcessed / actualTotal) * 100);
|
| | const eta = getETASecs(actualProcessed, actualTotal, processingState.promptProgress.time_ms);
|
| |
|
| | if (eta !== undefined) {
|
| | const etaSecs = Math.ceil(eta);
|
| | return `Processing ${percent}% (ETA: ${etaSecs}s)`;
|
| | }
|
| |
|
| | return `Processing ${percent}%`;
|
| | }
|
| |
|
| | |
| | |
| | |
| |
|
| | function getLiveProcessingStats(): LiveProcessingStats | null {
|
| | if (processingState?.promptProgress) {
|
| | const { processed, total, time_ms, cache } = processingState.promptProgress;
|
| |
|
| | const actualProcessed = processed - cache;
|
| | const actualTotal = total - cache;
|
| |
|
| | if (actualProcessed > 0 && time_ms > 0) {
|
| | const tokensPerSecond = actualProcessed / (time_ms / 1000);
|
| |
|
| | return {
|
| | tokensProcessed: actualProcessed,
|
| | totalTokens: actualTotal,
|
| | timeMs: time_ms,
|
| | tokensPerSecond
|
| | };
|
| | }
|
| | }
|
| |
|
| |
|
| | return lastKnownProcessingStats;
|
| | }
|
| |
|
| | |
| | |
| |
|
| | function getLiveGenerationStats(): LiveGenerationStats | null {
|
| | if (!processingState) return null;
|
| |
|
| | const { tokensDecoded, tokensPerSecond } = processingState;
|
| |
|
| | if (tokensDecoded <= 0) return null;
|
| |
|
| |
|
| | const timeMs =
|
| | tokensPerSecond && tokensPerSecond > 0 ? (tokensDecoded / tokensPerSecond) * 1000 : 0;
|
| |
|
| | return {
|
| | tokensGenerated: tokensDecoded,
|
| | timeMs,
|
| | tokensPerSecond: tokensPerSecond || 0
|
| | };
|
| | }
|
| |
|
| | return {
|
| | get processingState() {
|
| | return processingState;
|
| | },
|
| | getProcessingDetails,
|
| | getTechnicalDetails,
|
| | getProcessingMessage,
|
| | getPromptProgressText,
|
| | getLiveProcessingStats,
|
| | getLiveGenerationStats,
|
| | shouldShowDetails,
|
| | startMonitoring,
|
| | stopMonitoring
|
| | };
|
| | }
|
| |
|