返回 DeepSeek-Reasonix
transcriptGrouping.ts
根目录 / desktop / frontend / src / lib / transcriptGrouping.ts
1 import { replaceAttachmentRefsForDisplay } from "./attachmentDisplay";
2 import type { Item } from "./useController";
3
4 export type QuestionAnchor = { id: string; text: string; turn: number; checkpointTurn?: number };
5
6 export interface TurnGroup {
7 userItem: Item;
8 assistantPreview: string;
9 toolCount: number;
10 startIdx: number;
11 endIdx: number;
12 }
13
14 export interface StepGroup {
15 items: Item[];
16 isFinal: boolean;
17 isComplete: boolean;
18 }
19
20 export type WarmLayerState = {
21 sessionKey: string;
22 expandedWarmTurns: ReadonlySet<number>;
23 coldPage: number;
24 };
25
26 export function createWarmLayerState(sessionKey: string): WarmLayerState {
27 return { sessionKey, expandedWarmTurns: new Set(), coldPage: 0 };
28 }
29
30 export function warmLayerForSession(state: WarmLayerState, sessionKey: string): WarmLayerState {
31 return state.sessionKey === sessionKey ? state : createWarmLayerState(sessionKey);
32 }
33
34 export function warmLayerWithNextColdPage(state: WarmLayerState, sessionKey: string): WarmLayerState {
35 const current = warmLayerForSession(state, sessionKey);
36 return { ...current, coldPage: current.coldPage + 1 };
37 }
38
39 export function warmLayerWithColdPageAtLeast(state: WarmLayerState, sessionKey: string, coldPage: number): WarmLayerState {
40 const current = warmLayerForSession(state, sessionKey);
41 const safeColdPage = Math.max(0, Math.floor(coldPage));
42 if (current.coldPage >= safeColdPage) return current;
43 return { ...current, coldPage: safeColdPage };
44 }
45
46 export function warmLayerWithExpandedTurn(state: WarmLayerState, sessionKey: string, turn: number, expand: boolean): WarmLayerState {
47 const current = warmLayerForSession(state, sessionKey);
48 const expandedWarmTurns = new Set(current.expandedWarmTurns);
49 if (expand) expandedWarmTurns.add(turn);
50 else expandedWarmTurns.delete(turn);
51 return { ...current, expandedWarmTurns };
52 }
53
54 export function questionAnchorId(id: string): string {
55 return `question-anchor-${id}`;
56 }
57
58 export function compactQuestionText(text: string): string {
59 const cleaned = replaceAttachmentRefsForDisplay(text).replace(/\s+/g, " ").trim();
60 if (cleaned.length <= 80) return cleaned;
61 return cleaned.slice(0, 80);
62 }
63
64 export function questionTurnsById(questions: QuestionAnchor[]): Map<string, number> {
65 const hasCheckpointTurns = questions.some((question) => question.checkpointTurn != null);
66 const turns = new Map<string, number>();
67 for (const question of questions) {
68 if (question.checkpointTurn != null) {
69 turns.set(question.id, question.checkpointTurn);
70 } else if (!hasCheckpointTurns) {
71 turns.set(question.id, question.turn);
72 }
73 }
74 return turns;
75 }
76
77 export function lastQuestionTurn(questions: readonly QuestionAnchor[], turns: ReadonlyMap<string, number>): number | undefined {
78 for (let i = questions.length - 1; i >= 0; i -= 1) {
79 const turn = turns.get(questions[i].id);
80 if (turn != null) return turn;
81 }
82 return undefined;
83 }
84
85 export function scrollVersion(items: Item[]): string {
86 return items
87 .map((it) => {
88 switch (it.kind) {
89 case "assistant":
90 return `${it.id}:a:${it.streaming ? 1 : 0}`;
91 case "tool":
92 return `${it.id}:t:${it.status}`;
93 default:
94 return `${it.id}:${it.kind}`;
95 }
96 })
97 .join("|");
98 }
99
100 export function warmUserPreview(text: string): string {
101 const cleaned = replaceAttachmentRefsForDisplay(text).replace(/\s+/g, " ").trim();
102 return cleaned.length <= 80 ? cleaned : cleaned.slice(0, 77) + "...";
103 }
104
105 export function buildTurnGroups(items: Item[]): TurnGroup[] {
106 const groups: TurnGroup[] = [];
107 let start = -1;
108 for (let i = 0; i < items.length; i += 1) {
109 if (items[i].kind === "user") {
110 if (start >= 0) {
111 groups[groups.length - 1].endIdx = i;
112 }
113 start = i;
114 groups.push({
115 userItem: items[i],
116 assistantPreview: "",
117 toolCount: 0,
118 startIdx: i,
119 endIdx: items.length,
120 });
121 } else if (start >= 0 && groups.length > 0) {
122 const group = groups[groups.length - 1];
123 const item = items[i];
124 if (item.kind === "assistant" && !item.streaming) {
125 const previewText = item.text?.trim() || "";
126 if (previewText) {
127 group.assistantPreview = warmUserPreview(previewText);
128 }
129 }
130 if (item.kind === "tool" && !item.parentId) {
131 group.toolCount += 1;
132 }
133 }
134 }
135 return groups;
136 }
137
138 export function buildStepGroups(items: Item[], startIdx = 0): StepGroup[] {
139 const groups: StepGroup[] = [];
140 let current: Item[] = [];
141
142 const flush = (isComplete: boolean) => {
143 if (current.length === 0) return;
144 groups.push({ items: current, isFinal: hasVisibleAssistantText(current), isComplete });
145 current = [];
146 };
147
148 for (let i = startIdx; i < items.length; i++) {
149 const it = items[i];
150 if (it.kind === "user") {
151 flush(true);
152 groups.push({ items: [it], isFinal: false, isComplete: true });
153 continue;
154 }
155 if (it.kind === "assistant") {
156 flush(true);
157 }
158 current.push(it);
159 }
160 flush(false);
161 return groups;
162 }
163
164 function hasVisibleAssistantText(items: Item[]): boolean {
165 return items.some((it) => it.kind === "assistant" && !it.streaming && it.text.trim() !== "");
166 }
167
168 export function warmPagination({ turnCount, hotTurns, pageSize, coldPage }: {
169 turnCount: number;
170 hotTurns: number;
171 pageSize: number;
172 coldPage: number;
173 }): { warmStartTurn: number; warmEndTurn: number; coldTurnCount: number } {
174 const safeTurnCount = Math.max(0, turnCount);
175 const safeHotTurns = Math.max(0, hotTurns);
176 const warmEndTurn = Math.max(0, safeTurnCount - Math.min(safeTurnCount, safeHotTurns));
177 if (warmEndTurn === 0) return { warmStartTurn: 0, warmEndTurn: 0, coldTurnCount: 0 };
178
179 const safePageSize = Math.max(0, pageSize);
180 const safeColdPage = Math.max(0, Math.floor(coldPage));
181 const shownWarmCount = Math.min(warmEndTurn, safePageSize * (safeColdPage + 1));
182 return {
183 warmStartTurn: warmEndTurn - shownWarmCount,
184 warmEndTurn,
185 coldTurnCount: warmEndTurn - shownWarmCount,
186 };
187 }
188
189 export function warmColdPageForTurn({ turn, turnCount, hotTurns, pageSize }: {
190 turn: number;
191 turnCount: number;
192 hotTurns: number;
193 pageSize: number;
194 }): number {
195 const safeTurnCount = Math.max(0, turnCount);
196 const safeHotTurns = Math.max(0, hotTurns);
197 const warmEndTurn = Math.max(0, safeTurnCount - Math.min(safeTurnCount, safeHotTurns));
198 if (warmEndTurn === 0 || turn >= warmEndTurn) return 0;
199
200 const safePageSize = Math.max(1, pageSize);
201 const targetTurn = Math.max(0, Math.floor(turn));
202 const shownTurnsNeeded = warmEndTurn - targetTurn;
203 return Math.max(0, Math.ceil(shownTurnsNeeded / safePageSize) - 1);
204 }
205
205 lines TYPESCRIPT