返回 DeepSeek-Reasonix
reasoningSummary.ts
根目录 / desktop / frontend / src / lib / reasoningSummary.ts
1 const DEFAULT_SUMMARY_CHARS = 180;
2 const WHITESPACE_RE = /\s/u;
3
4 export type ReasoningSummaryOptions = {
5 /** While streaming, the tail line is the live signal; once done, the head line reads best. */
6 streaming: boolean;
7 maxChars?: number;
8 };
9
10 type LineBounds = { start: number; end: number };
11
12 function hasNonWhitespace(text: string, start: number, end: number): boolean {
13 for (let index = start; index < end; index += 1) {
14 if (!WHITESPACE_RE.test(text[index])) return true;
15 }
16 return false;
17 }
18
19 // Scans forward for the first non-blank line without splitting or copying the
20 // whole string — long streaming reasoning must not allocate a full line per
21 // token.
22 function firstNonBlankLine(text: string): LineBounds | null {
23 let start = 0;
24 while (start < text.length) {
25 let end = start;
26 while (end < text.length && text[end] !== "\n" && text[end] !== "\r") end += 1;
27 if (hasNonWhitespace(text, start, end)) return { start, end };
28 start = text[end] === "\r" && text[end + 1] === "\n" ? end + 2 : end + 1;
29 }
30 return null;
31 }
32
33 // Scans backward for the last non-blank line without copying its content.
34 // When the current line is longer than the preview budget, it stops at a
35 // bounded tail window instead of walking all the way back to the line start.
36 function lastNonBlankLine(text: string, initialWindow: number): LineBounds | null {
37 let end = text.length;
38 while (end > 0 && (text[end - 1] === "\n" || text[end - 1] === "\r")) end -= 1;
39 const minimumWindow = Math.max(32, initialWindow);
40 let window = minimumWindow;
41
42 while (end > 0) {
43 const windowStart = Math.max(0, end - window);
44 let separator = -1;
45 for (let index = end - 1; index >= windowStart; index -= 1) {
46 if (text[index] === "\n" || text[index] === "\r") {
47 separator = index;
48 break;
49 }
50 }
51 const start = separator >= 0 ? separator + 1 : windowStart;
52 if (hasNonWhitespace(text, start, end)) return { start, end };
53 if (separator >= 0) {
54 end = separator;
55 while (end > 0 && (text[end - 1] === "\n" || text[end - 1] === "\r")) end -= 1;
56 window = minimumWindow;
57 continue;
58 }
59 if (windowStart === 0) return null;
60 window *= 2;
61 }
62 return null;
63 }
64
65 function normalizedMaxChars(maxChars: number): number {
66 if (!Number.isFinite(maxChars)) return DEFAULT_SUMMARY_CHARS;
67 return Math.max(0, Math.floor(maxChars));
68 }
69
70 // Code-point-safe truncation so a surrogate pair is never split. Streaming
71 // keeps the tail because that is where newly arrived text appears.
72 function truncateSummary(text: string, maxChars: number, fromEnd: boolean): string {
73 const limit = normalizedMaxChars(maxChars);
74 if (limit === 0) return "";
75 const chars = Array.from(text);
76 if (chars.length <= limit) return text;
77 if (limit === 1) return "…";
78 return fromEnd
79 ? `…${chars.slice(-(limit - 1)).join("")}`
80 : `${chars.slice(0, limit - 1).join("")}…`;
81 }
82
83 function normalizeLinePreview(text: string, bounds: LineBounds, maxChars: number, fromEnd: boolean): string {
84 const limit = normalizedMaxChars(maxChars);
85 if (limit === 0) return "";
86 // A code point occupies at most two UTF-16 code units. The small cushion
87 // covers whitespace that will be collapsed before the character budget is
88 // applied, while keeping the per-token allocation bounded.
89 const rawWindow = limit * 2 + 16;
90 const start = fromEnd ? Math.max(bounds.start, bounds.end - rawWindow) : bounds.start;
91 const end = fromEnd ? bounds.end : Math.min(bounds.end, bounds.start + rawWindow);
92 const normalized = text.slice(start, end).trim().replace(/\s+/g, " ");
93 return truncateSummary(normalized, limit, fromEnd);
94 }
95
96 /** Builds a single-line plain-text preview without invoking another model. */
97 export function reasoningSummaryText(reasoning: string, { streaming, maxChars = DEFAULT_SUMMARY_CHARS }: ReasoningSummaryOptions): string {
98 if (!reasoning) return "";
99 const limit = normalizedMaxChars(maxChars);
100 if (limit === 0) return "";
101 const bounds = streaming ? lastNonBlankLine(reasoning, limit * 2 + 16) : firstNonBlankLine(reasoning);
102 return bounds ? normalizeLinePreview(reasoning, bounds, limit, streaming) : "";
103 }
104
104 lines TYPESCRIPT