返回 DeepSeek-Reasonix
draftModelInheritance.ts
根目录 / desktop / frontend / src / app-runtime / draftModelInheritance.ts
1 import { useCallback, useEffect, type RefObject } from "react";
2
3 import type {
4 ModelInfo,
5 SessionDraftSettings,
6 SessionDraftSubmissionView,
7 SessionDraftView,
8 } from "../generated/desktopContract.generated";
9 import { app } from "../lib/bridge";
10
11 export function canonicalJSON(value: unknown): string {
12 return JSON.stringify(value, (_key, item) => item && typeof item === "object" && !Array.isArray(item)
13 ? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item);
14 }
15
16 export function sameDraftSettings(left: SessionDraftSettings, right: SessionDraftSettings): boolean {
17 // An inherited model is a live projection, not an editor change. Keep this
18 // comparison aligned with the backend draft digest for saves and submission.
19 if (left.modelSource === "default" && right.modelSource === "default") {
20 return canonicalJSON({ ...left, model: "" }) === canonicalJSON({ ...right, model: "" });
21 }
22 return canonicalJSON(left) === canonicalJSON(right);
23 }
24
25 export function draftSubmissionLocksEditing(operation?: SessionDraftSubmissionView | null): boolean {
26 return Boolean(operation && !["cancelled", "terminal_failed"].includes(operation.phase));
27 }
28
29 export type InheritedDraftModelEntry = {
30 draft: SessionDraftView;
31 settings: SessionDraftSettings;
32 models?: ModelInfo[];
33 generation: number;
34 lifecycle: "active" | "converted" | "discarded";
35 preparingSubmission: boolean;
36 operation?: SessionDraftSubmissionView;
37 modelReadVersion: number;
38 };
39
40 export function applyInheritedModel(entry: InheritedDraftModelEntry, settings: SessionDraftSettings, readVersion: number) {
41 if (entry.modelReadVersion !== readVersion || entry.settings.modelSource !== "default"
42 || settings.modelSource !== "default") return;
43 entry.settings = { ...entry.settings, model: settings.model };
44 entry.draft = { ...entry.draft, settings: { ...entry.draft.settings, model: settings.model } };
45 }
46
47 export function useInheritedDraftModels<T extends InheritedDraftModelEntry>(
48 entriesRef: RefObject<Map<string, T>>,
49 disposed: RefObject<boolean>,
50 publish: (draftId?: string | null) => void,
51 ) {
52 const refresh = useCallback(async () => {
53 const targets = [...entriesRef.current.values()]
54 .filter((entry) => entry.lifecycle === "active" && entry.settings.modelSource === "default"
55 && !entry.preparingSubmission && !draftSubmissionLocksEditing(entry.operation))
56 .map((entry) => ({ id: entry.draft.id, entry, generation: entry.generation, readVersion: ++entry.modelReadVersion }));
57 await Promise.all(targets.map(async (target) => {
58 try {
59 const context = await app.GetDraftContext(target.id);
60 const entry = entriesRef.current.get(target.id);
61 if (disposed.current || !entry || entry !== target.entry || entry.lifecycle !== "active" || entry.generation !== target.generation
62 || entry.modelReadVersion !== target.readVersion || entry.settings.modelSource !== "default"
63 || context.draft.settings.modelSource !== "default" || entry.preparingSubmission
64 || draftSubmissionLocksEditing(entry.operation)) return;
65 entry.models = context.models ?? [];
66 applyInheritedModel(entry, context.draft.settings, target.readVersion);
67 publish(target.id);
68 } catch {
69 // Keep the last effective model until the next catalog refresh or open.
70 }
71 }));
72 }, [disposed, entriesRef, publish]);
73
74 useEffect(() => {
75 const listener = () => { void refresh(); };
76 window.addEventListener("reasonix:model-catalog-changed", listener);
77 return () => window.removeEventListener("reasonix:model-catalog-changed", listener);
78 }, [refresh]);
79 }
80
80 lines TYPESCRIPT