use-processing-state.svelte.ts 7.2 KB

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