Files
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

74 lines
2.7 KiB
TypeScript

'use client';
/**
* Per-reasoning-block timing, so each thinking panel shows how long the model
* spent on THAT block. A multi-step agent reasons in several turns (read →
* reason → edit → reason → answer), producing multiple reasoning blocks; a single
* per-message timer would freeze on the first tool call while later blocks keep
* streaming. So timers are keyed per block (`${messageId}:${ordinal}`): a block
* starts on first observation and ends when a LATER part follows it (the model
* moved on). The last reasoning block stays open — ticking live — until something
* follows it or the run finalizes. The pure reducer is unit-tested; the store is
* a thin reactive wrapper the runtime drives and the panel reads.
*/
import { create } from 'zustand';
export interface ThinkTimer {
startedAt: number;
endedAt?: number;
}
/** Pure transition: start on first observation; end once a later part follows. */
export function nextThinkTimer(
prev: ThinkTimer | undefined,
opts: { end: boolean; now: number },
): ThinkTimer {
const { end, now } = opts;
if (!prev) return { startedAt: now };
if (prev.endedAt == null && end) return { ...prev, endedAt: now };
return prev;
}
/** Human-readable elapsed: one decimal under 10s, whole seconds beyond. */
export function formatThinkDuration(ms: number): string {
const clamped = ms < 0 ? 0 : ms;
const s = clamped / 1000;
return s < 10 ? `${s.toFixed(1)}s` : `${Math.round(s)}s`;
}
interface ThinkingTimersState {
timers: Record<string, ThinkTimer>;
/** Fold an observation for one block; no-op if state is unchanged. */
observe(key: string, opts: { end: boolean; now: number }): void;
/** Force-close every still-open timer whose key starts with `prefix`. */
endAll(prefix: string, now: number): void;
/** Restore a finished timer from a persisted duration (post-refresh). */
seed(key: string, durationMs: number): void;
clear(): void;
}
export const useThinkingTimers = create<ThinkingTimersState>((set, get) => ({
timers: {},
observe: (key, opts) => {
const prev = get().timers[key];
const next = nextThinkTimer(prev, opts);
if (next === prev) return;
set((s) => ({ timers: { ...s.timers, [key]: next } }));
},
endAll: (prefix, now) => {
const timers = get().timers;
let changed = false;
const updated = { ...timers };
for (const [key, t] of Object.entries(timers)) {
if (key.startsWith(prefix) && t.endedAt == null) {
updated[key] = { ...t, endedAt: now };
changed = true;
}
}
if (changed) set({ timers: updated });
},
seed: (key, durationMs) =>
set((s) => ({ timers: { ...s.timers, [key]: { startedAt: 0, endedAt: durationMs } } })),
clear: () => set({ timers: {} }),
}));