返回 DeepSeek-Reasonix
sessionExperience.ts
根目录 / desktop / frontend / src / lib / sessionExperience.ts
1 import { useMemo, useSyncExternalStore } from "react";
2
3 export type SessionExperience = "standard" | "deep";
4
5 export type WorkProcessPresentation = {
6 experience: SessionExperience;
7 showWhileRunning: true;
8 keepExpandedAfterCompletion: boolean;
9 };
10
11 const SESSION_EXPERIENCE_KEY = "reasonix-session-experience";
12 const SESSION_EXPERIENCE_EVENT = "reasonix:session-experience";
13
14 let current: SessionExperience = "standard";
15 let hydrated = false;
16 const listeners = new Set<() => void>();
17 const beforeChangeListeners = new Set<(previous: SessionExperience, next: SessionExperience) => void>();
18
19 function normalize(value: unknown): SessionExperience {
20 return value === "deep" ? "deep" : "standard";
21 }
22
23 function emit(): void {
24 for (const listener of listeners) listener();
25 if (typeof window !== "undefined") {
26 window.dispatchEvent(new CustomEvent(SESSION_EXPERIENCE_EVENT, { detail: current }));
27 }
28 }
29
30 function emitCompatibilitySignals(next: SessionExperience): void {
31 if (typeof window !== "undefined") {
32 window.dispatchEvent(new CustomEvent("reasonix:process-fold", {
33 detail: next === "deep" ? "expanded" : "auto",
34 }));
35 window.dispatchEvent(new CustomEvent("reasonix:reasoning-display-mode", {
36 detail: next === "deep" ? "expanded" : "auto",
37 }));
38 }
39 }
40
41 function writeCompatibilityMirrors(next: SessionExperience): void {
42 if (typeof localStorage === "undefined") return;
43 localStorage.setItem(SESSION_EXPERIENCE_KEY, next);
44 localStorage.setItem("reasonix-display-mode", "standard");
45 localStorage.setItem("reasonix-process-fold", next === "deep" ? "expanded" : "auto");
46 // The oldest boolean summary key cannot represent Deep. Let the mirrored
47 // backend reasoning field win instead of reviving a contradictory value.
48 localStorage.removeItem("reasonix-reasoning-summary");
49 }
50
51 export function getSessionExperience(): SessionExperience {
52 // The backend snapshot is authoritative. Before it arrives, use the safe
53 // default instead of reviving a stale value written by an older frontend.
54 if (!hydrated) return "standard";
55 return current;
56 }
57
58 export function hydrateSessionExperience(value: unknown): void {
59 const next = normalize(value);
60 hydrated = true;
61 current = next;
62 writeCompatibilityMirrors(next);
63 emit();
64 emitCompatibilitySignals(next);
65 }
66
67 export function applySessionExperience(value: SessionExperience): void {
68 const next = normalize(value);
69 hydrated = true;
70 writeCompatibilityMirrors(next);
71 if (next === current) {
72 emitCompatibilitySignals(next);
73 return;
74 }
75 for (const listener of beforeChangeListeners) listener(current, next);
76 current = next;
77 emit();
78 emitCompatibilitySignals(next);
79 }
80
81 /** Synchronously capture layout state before a mode change updates row
82 * geometry. Layout owners use this to preserve the reader's logical anchor. */
83 export function onSessionExperienceWillChange(
84 listener: (previous: SessionExperience, next: SessionExperience) => void,
85 ): () => void {
86 beforeChangeListeners.add(listener);
87 return () => beforeChangeListeners.delete(listener);
88 }
89
90 export function resolveWorkProcessPresentation(value: SessionExperience): WorkProcessPresentation {
91 return {
92 experience: value,
93 showWhileRunning: true,
94 keepExpandedAfterCompletion: value === "deep",
95 };
96 }
97
98 export function useSessionExperience(): SessionExperience {
99 return useSyncExternalStore(
100 (listener) => {
101 listeners.add(listener);
102 return () => listeners.delete(listener);
103 },
104 getSessionExperience,
105 () => "standard",
106 );
107 }
108
109 export function useWorkProcessPresentation(): WorkProcessPresentation {
110 const experience = useSessionExperience();
111 return useMemo(() => resolveWorkProcessPresentation(experience), [experience]);
112 }
113
113 lines TYPESCRIPT