1import { activeProcessingState } from '$lib/stores/chat.svelte';
  2import { config } from '$lib/stores/settings.svelte';
  3
  4export interface LiveProcessingStats {
  5	tokensProcessed: number;
  6	totalTokens: number;
  7	timeMs: number;
  8	tokensPerSecond: number;
  9	etaSecs?: number;
 10}
 11
 12export interface LiveGenerationStats {
 13	tokensGenerated: number;
 14	timeMs: number;
 15	tokensPerSecond: number;
 16}
 17
 18export interface UseProcessingStateReturn {
 19	readonly processingState: ApiProcessingState | null;
 20	getProcessingDetails(): string[];
 21	getProcessingMessage(): string;
 22	getPromptProgressText(): string | null;
 23	getLiveProcessingStats(): LiveProcessingStats | null;
 24	getLiveGenerationStats(): LiveGenerationStats | null;
 25	shouldShowDetails(): boolean;
 26	startMonitoring(): void;
 27	stopMonitoring(): void;
 28}
 29
 30/**
 31 * useProcessingState - Reactive processing state hook
 32 *
 33 * This hook provides reactive access to the processing state of the server.
 34 * It directly reads from chatStore's reactive state and provides
 35 * formatted processing details for UI display.
 36 *
 37 * **Features:**
 38 * - Real-time processing state via direct reactive state binding
 39 * - Context and output token tracking
 40 * - Tokens per second calculation
 41 * - Automatic updates when streaming data arrives
 42 * - Supports multiple concurrent conversations
 43 *
 44 * @returns Hook interface with processing state and control methods
 45 */
 46export function useProcessingState(): UseProcessingStateReturn {
 47	let isMonitoring = $state(false);
 48	let lastKnownState = $state<ApiProcessingState | null>(null);
 49	let lastKnownProcessingStats = $state<LiveProcessingStats | null>(null);
 50
 51	// Derive processing state reactively from chatStore's direct state
 52	const processingState = $derived.by(() => {
 53		if (!isMonitoring) {
 54			return lastKnownState;
 55		}
 56		// Read directly from the reactive state export
 57		return activeProcessingState();
 58	});
 59
 60	// Track last known state for keepStatsVisible functionality
 61	$effect(() => {
 62		if (processingState && isMonitoring) {
 63			lastKnownState = processingState;
 64		}
 65	});
 66
 67	// Track last known processing stats for when promptProgress disappears
 68	$effect(() => {
 69		if (processingState?.promptProgress) {
 70			const { processed, total, time_ms, cache } = processingState.promptProgress;
 71			const actualProcessed = processed - cache;
 72			const actualTotal = total - cache;
 73
 74			if (actualProcessed > 0 && time_ms > 0) {
 75				const tokensPerSecond = actualProcessed / (time_ms / 1000);
 76				lastKnownProcessingStats = {
 77					tokensProcessed: actualProcessed,
 78					totalTokens: actualTotal,
 79					timeMs: time_ms,
 80					tokensPerSecond
 81				};
 82			}
 83		}
 84	});
 85
 86	function getETASecs(done: number, total: number, elapsedMs: number): number | undefined {
 87		const elapsedSecs = elapsedMs / 1000;
 88		const progressETASecs =
 89			done === 0 || elapsedSecs < 0.5
 90				? undefined // can be the case for the 0% progress report
 91				: elapsedSecs * (total / done - 1);
 92		return progressETASecs;
 93	}
 94
 95	function startMonitoring(): void {
 96		if (isMonitoring) return;
 97		isMonitoring = true;
 98	}
 99
100	function stopMonitoring(): void {
101		if (!isMonitoring) return;
102		isMonitoring = false;
103
104		// Only clear last known state if keepStatsVisible is disabled
105		const currentConfig = config();
106		if (!currentConfig.keepStatsVisible) {
107			lastKnownState = null;
108			lastKnownProcessingStats = null;
109		}
110	}
111
112	function getProcessingMessage(): string {
113		if (!processingState) {
114			return 'Processing...';
115		}
116
117		switch (processingState.status) {
118			case 'initializing':
119				return 'Initializing...';
120			case 'preparing':
121				if (processingState.progressPercent !== undefined) {
122					return `Processing (${processingState.progressPercent}%)`;
123				}
124				return 'Preparing response...';
125			case 'generating':
126				return '';
127			default:
128				return 'Processing...';
129		}
130	}
131
132	function getProcessingDetails(): string[] {
133		// Use current processing state or fall back to last known state
134		const stateToUse = processingState || lastKnownState;
135		if (!stateToUse) {
136			return [];
137		}
138
139		const details: string[] = [];
140
141		// Always show context info when we have valid data
142		if (stateToUse.contextUsed >= 0 && stateToUse.contextTotal > 0) {
143			const contextPercent = Math.round((stateToUse.contextUsed / stateToUse.contextTotal) * 100);
144
145			details.push(
146				`Context: ${stateToUse.contextUsed}/${stateToUse.contextTotal} (${contextPercent}%)`
147			);
148		}
149
150		if (stateToUse.outputTokensUsed > 0) {
151			// Handle infinite max_tokens (-1) case
152			if (stateToUse.outputTokensMax <= 0) {
153				details.push(`Output: ${stateToUse.outputTokensUsed}/∞`);
154			} else {
155				const outputPercent = Math.round(
156					(stateToUse.outputTokensUsed / stateToUse.outputTokensMax) * 100
157				);
158
159				details.push(
160					`Output: ${stateToUse.outputTokensUsed}/${stateToUse.outputTokensMax} (${outputPercent}%)`
161				);
162			}
163		}
164
165		if (stateToUse.tokensPerSecond && stateToUse.tokensPerSecond > 0) {
166			details.push(`${stateToUse.tokensPerSecond.toFixed(1)} tokens/sec`);
167		}
168
169		if (stateToUse.speculative) {
170			details.push('Speculative decoding enabled');
171		}
172
173		return details;
174	}
175
176	function shouldShowDetails(): boolean {
177		return processingState !== null && processingState.status !== 'idle';
178	}
179
180	/**
181	 * Returns a short progress message with percent
182	 */
183	function getPromptProgressText(): string | null {
184		if (!processingState?.promptProgress) return null;
185
186		const { processed, total, cache } = processingState.promptProgress;
187
188		const actualProcessed = processed - cache;
189		const actualTotal = total - cache;
190		const percent = Math.round((actualProcessed / actualTotal) * 100);
191		const eta = getETASecs(actualProcessed, actualTotal, processingState.promptProgress.time_ms);
192
193		if (eta !== undefined) {
194			const etaSecs = Math.ceil(eta);
195			return `Processing ${percent}% (ETA: ${etaSecs}s)`;
196		}
197
198		return `Processing ${percent}%`;
199	}
200
201	/**
202	 * Returns live processing statistics for display (prompt processing phase)
203	 * Returns last known stats when promptProgress becomes unavailable
204	 */
205	function getLiveProcessingStats(): LiveProcessingStats | null {
206		if (processingState?.promptProgress) {
207			const { processed, total, time_ms, cache } = processingState.promptProgress;
208
209			const actualProcessed = processed - cache;
210			const actualTotal = total - cache;
211
212			if (actualProcessed > 0 && time_ms > 0) {
213				const tokensPerSecond = actualProcessed / (time_ms / 1000);
214
215				return {
216					tokensProcessed: actualProcessed,
217					totalTokens: actualTotal,
218					timeMs: time_ms,
219					tokensPerSecond
220				};
221			}
222		}
223
224		// Return last known stats if promptProgress is no longer available
225		return lastKnownProcessingStats;
226	}
227
228	/**
229	 * Returns live generation statistics for display (token generation phase)
230	 */
231	function getLiveGenerationStats(): LiveGenerationStats | null {
232		if (!processingState) return null;
233
234		const { tokensDecoded, tokensPerSecond } = processingState;
235
236		if (tokensDecoded <= 0) return null;
237
238		// Calculate time from tokens and speed
239		const timeMs =
240			tokensPerSecond && tokensPerSecond > 0 ? (tokensDecoded / tokensPerSecond) * 1000 : 0;
241
242		return {
243			tokensGenerated: tokensDecoded,
244			timeMs,
245			tokensPerSecond: tokensPerSecond || 0
246		};
247	}
248
249	return {
250		get processingState() {
251			return processingState;
252		},
253		getProcessingDetails,
254		getProcessingMessage,
255		getPromptProgressText,
256		getLiveProcessingStats,
257		getLiveGenerationStats,
258		shouldShowDetails,
259		startMonitoring,
260		stopMonitoring
261	};
262}