@morlay/ui-conversation-message-actions 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.cjs +1216 -2337
- package/dist/{rolldown-runtime-BMI-E3GI.mjs → rolldown-runtime-Dr7-SnC6.mjs} +2 -1
- package/dist/testing.mjs +1 -1
- package/package.json +4 -4
- package/src/client/chat-node/MessageItem.tsx +36 -17
- package/src/client/index.ts +1 -6
- package/src/client/chat-node/MessageIconActions.styles.ts +0 -76
- package/src/client/chat-node/MessageIconActions.tsx +0 -166
- package/src/client/chat-node/MessageItem.styles.ts +0 -248
- package/src/client/chat-node/message-chrome.ts +0 -44
- package/src/client/chat-node/use-calendar-day.ts +0 -19
- package/src/client/composer-stats/StatsPills.styles.ts +0 -57
- package/src/client/composer-stats/StatsPills.tsx +0 -362
- package/src/client/composer-stats/register.ts +0 -17
- package/src/client/composer-stats/stat-dialog.styles.ts +0 -70
- package/src/client/composer-stats/stat-dialog.ts +0 -52
- package/src/client/composer-stats/token-format.ts +0 -84
- package/src/client/composer-stats/turn-reading.ts +0 -35
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { Translate } from "@deepseek-ai/dsh-client-ui-slots";
|
|
2
|
-
|
|
3
|
-
export type ClockTranslate = Translate<"clock.md" | "clock.ymd">;
|
|
4
|
-
|
|
5
|
-
function pad2(n: number): string {
|
|
6
|
-
return String(n).padStart(2, "0");
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function startOfLocalDay(ms: number): number {
|
|
10
|
-
const d = new Date(ms);
|
|
11
|
-
d.setHours(0, 0, 0, 0);
|
|
12
|
-
return d.getTime();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function msUntilNextLocalMidnight(ms: number): number {
|
|
16
|
-
const next = new Date(ms);
|
|
17
|
-
next.setHours(24, 0, 0, 0);
|
|
18
|
-
return Math.max(next.getTime() - ms, 1);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function formatTokensPerSecond(tps: number): string {
|
|
22
|
-
const clamped = Math.max(0, tps);
|
|
23
|
-
return clamped >= 10 ? String(Math.round(clamped)) : String(Math.round(clamped * 10) / 10);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function formatMessageClock(
|
|
27
|
-
time: number,
|
|
28
|
-
t: ClockTranslate,
|
|
29
|
-
now: number = Date.now(),
|
|
30
|
-
): string {
|
|
31
|
-
const d = new Date(time);
|
|
32
|
-
const n = new Date(now);
|
|
33
|
-
const clock = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
34
|
-
if (
|
|
35
|
-
d.getFullYear() === n.getFullYear() &&
|
|
36
|
-
d.getMonth() === n.getMonth() &&
|
|
37
|
-
d.getDate() === n.getDate()
|
|
38
|
-
) {
|
|
39
|
-
return clock;
|
|
40
|
-
}
|
|
41
|
-
const params = { y: d.getFullYear(), m: d.getMonth() + 1, d: d.getDate() };
|
|
42
|
-
const md = d.getFullYear() === n.getFullYear() ? t("clock.md", params) : t("clock.ymd", params);
|
|
43
|
-
return `${md} ${clock}`;
|
|
44
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
import { msUntilNextLocalMidnight, startOfLocalDay } from "./message-chrome.ts";
|
|
3
|
-
|
|
4
|
-
export function useCalendarDay(): number {
|
|
5
|
-
const [day, setDay] = useState(() => startOfLocalDay(Date.now()));
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
let timer: ReturnType<typeof setTimeout>;
|
|
8
|
-
const arm = (): void => {
|
|
9
|
-
const now = Date.now();
|
|
10
|
-
setDay(startOfLocalDay(now));
|
|
11
|
-
timer = setTimeout(arm, msUntilNextLocalMidnight(now));
|
|
12
|
-
};
|
|
13
|
-
timer = setTimeout(arm, msUntilNextLocalMidnight(Date.now()));
|
|
14
|
-
return () => {
|
|
15
|
-
clearTimeout(timer);
|
|
16
|
-
};
|
|
17
|
-
}, []);
|
|
18
|
-
return day;
|
|
19
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type { CSSProps } from "@morlay/dsh-client-ui-primitives/client";
|
|
2
|
-
|
|
3
|
-
export const styles = {
|
|
4
|
-
// 上游 `StatsPills.module.css` 的 `.root` 原样:统计行在 composer dock 里是 shrink-wrap 的
|
|
5
|
-
// 一个条目,与同为 dock 子项的上下文占用按钮并排——行内间距由 dock 的 `gap: 12px` 统一,
|
|
6
|
-
// 统计自己不再撑满整行(撑满会把占用按钮挤到行尾,两块之间的间距就跟行内间距对不上)。
|
|
7
|
-
root: {
|
|
8
|
-
display: "flex",
|
|
9
|
-
justifyContent: "center",
|
|
10
|
-
gap: "12px",
|
|
11
|
-
minWidth: "0",
|
|
12
|
-
maxWidth: "100%",
|
|
13
|
-
boxSizing: "border-box",
|
|
14
|
-
fontSize: "var(--dsh-content-font-size-secondary, 13px)",
|
|
15
|
-
lineHeight: "calc(20px + var(--dsh-content-font-delta-secondary, 0px))",
|
|
16
|
-
},
|
|
17
|
-
anchor: {
|
|
18
|
-
display: "inline-flex",
|
|
19
|
-
minWidth: "0",
|
|
20
|
-
},
|
|
21
|
-
pill: {
|
|
22
|
-
cursor: "pointer",
|
|
23
|
-
|
|
24
|
-
"&:hover, &[aria-expanded='true']": {
|
|
25
|
-
background: "var(--dsw-alias-interactive-bg-hover)",
|
|
26
|
-
color: "var(--dsw-alias-label-secondary)",
|
|
27
|
-
},
|
|
28
|
-
display: "inline-flex",
|
|
29
|
-
alignItems: "center",
|
|
30
|
-
gap: "6px",
|
|
31
|
-
boxSizing: "border-box",
|
|
32
|
-
maxWidth: "100%",
|
|
33
|
-
padding: "1px 8px",
|
|
34
|
-
border: "none",
|
|
35
|
-
borderRadius: "24px",
|
|
36
|
-
background: "transparent",
|
|
37
|
-
color: "var(--dsw-alias-label-tertiary)",
|
|
38
|
-
font: "inherit",
|
|
39
|
-
fontVariantNumeric: "tabular-nums",
|
|
40
|
-
lineHeight: "inherit",
|
|
41
|
-
whiteSpace: "nowrap",
|
|
42
|
-
"& svg": {
|
|
43
|
-
width: "14px",
|
|
44
|
-
height: "14px",
|
|
45
|
-
flex: "none",
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
label: {
|
|
49
|
-
minWidth: "0",
|
|
50
|
-
overflow: "hidden",
|
|
51
|
-
textOverflow: "ellipsis",
|
|
52
|
-
},
|
|
53
|
-
sep: {
|
|
54
|
-
color: "var(--dsw-alias-separator-primary)",
|
|
55
|
-
margin: "0 6px",
|
|
56
|
-
},
|
|
57
|
-
} satisfies Record<string, CSSProps>;
|
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
// Session stats under the composer, split into two icon pills: a gauge pill (turn/step counts + output speed)
|
|
2
|
-
// opening the time-and-speed dialog, and a database pill (total tokens + cache hit) opening the token-usage
|
|
3
|
-
// dialog. Settled-node identity prevents stream-delta updates from rerendering the row; this row shadows
|
|
4
|
-
// upstream ui-chat's `composer.dock` id `stats`(priority −1),`data-composer-stats` is its stable anchor
|
|
5
|
-
// (style / test hooks)。
|
|
6
|
-
|
|
7
|
-
import { memo, useMemo, useState } from "react";
|
|
8
|
-
import { createPortal } from "react-dom";
|
|
9
|
-
import {
|
|
10
|
-
IconDatabaseOutlineRegular,
|
|
11
|
-
IconGaugeOutlineRegular,
|
|
12
|
-
} from "@deepseek-ai/dsh-client-ui-primitives";
|
|
13
|
-
import type { UseProjection } from "@deepseek-ai/dsh-api-session-controller/client";
|
|
14
|
-
import type { SnapshotSelectorHook } from "@deepseek-ai/dsh-client-ui-slots";
|
|
15
|
-
// Type-only: merges the sessionStats key into SessionProjectionMap for useProjection.
|
|
16
|
-
import type {} from "@deepseek-ai/dsh-session-stats/client";
|
|
17
|
-
import type { TokenUsageProjection } from "@deepseek-ai/dsh-token-meter/client";
|
|
18
|
-
import type { ChatSnapshot, ChatViewSlotProps } from "@deepseek-ai/dsh-client-ui-chat/client";
|
|
19
|
-
import { formatTokensPerSecond } from "../chat-node/message-chrome.ts";
|
|
20
|
-
import { assistantStepReading } from "./turn-reading.ts";
|
|
21
|
-
import { formatCacheHitPercent, formatExactTokens, formatTokens } from "./token-format.ts";
|
|
22
|
-
import { MEASURE_STYLE, useStatDialog } from "./stat-dialog.ts";
|
|
23
|
-
import { styling } from "@morlay/dsh-client-ui-primitives/client";
|
|
24
|
-
import { styles } from "./StatsPills.styles.ts";
|
|
25
|
-
import { styles as dialogStyles } from "./stat-dialog.styles.ts";
|
|
26
|
-
|
|
27
|
-
interface WindowStats {
|
|
28
|
-
turns: number;
|
|
29
|
-
steps: number;
|
|
30
|
-
// Summed request wall time (step/start → assistant/message); 0 when no node carries timing.
|
|
31
|
-
llmMs: number;
|
|
32
|
-
// Summed tool wall time (tool/call → tool/result); 0 when no pair is in-window.
|
|
33
|
-
toolMs: number;
|
|
34
|
-
// Summed first-token latency over `ttftSteps`; 0 when no step records it.
|
|
35
|
-
ttftMs: number;
|
|
36
|
-
// Steps carrying a recorded TTFT.
|
|
37
|
-
ttftSteps: number;
|
|
38
|
-
// Summed decode wall time over steps that also report output tokens.
|
|
39
|
-
decodeMs: number;
|
|
40
|
-
// Summed output tokens over the same decode-timed steps.
|
|
41
|
-
decodeTokens: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Fold assistant and tool-result nodes into window-scoped display totals — the fallback for assemblies
|
|
45
|
-
// without the `sessionStats` projection. Every displayed figure otherwise rides that durable whole-log
|
|
46
|
-
// projection (token accounting rides `tokenUsage`), because the window is paged and compaction rewrites it;
|
|
47
|
-
// field names deliberately mirror the projection's so the two swap wholesale.
|
|
48
|
-
export function deriveStats(nodes: ChatSnapshot["legacy"]["nodes"]): WindowStats {
|
|
49
|
-
const turns = new Set<number>();
|
|
50
|
-
let steps = 0;
|
|
51
|
-
let llmMs = 0;
|
|
52
|
-
let toolMs = 0;
|
|
53
|
-
let ttftMs = 0;
|
|
54
|
-
let ttftSteps = 0;
|
|
55
|
-
let decodeMs = 0;
|
|
56
|
-
let decodeTokens = 0;
|
|
57
|
-
for (const node of nodes) {
|
|
58
|
-
if (node.kind === "tool-result") {
|
|
59
|
-
if (node.callTime !== null) toolMs += Math.max(0, node.time - node.callTime);
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (node.kind !== "assistant") continue;
|
|
63
|
-
turns.add(node.turn);
|
|
64
|
-
steps += 1;
|
|
65
|
-
if (node.timing !== undefined && node.timing.stepStartTime !== null) {
|
|
66
|
-
llmMs += Math.max(0, node.timing.completedTime - node.timing.stepStartTime);
|
|
67
|
-
}
|
|
68
|
-
const reading = assistantStepReading(node);
|
|
69
|
-
if (reading.ttftMs !== null) {
|
|
70
|
-
ttftMs += reading.ttftMs;
|
|
71
|
-
ttftSteps += 1;
|
|
72
|
-
}
|
|
73
|
-
if (reading.decodeMs !== null && reading.outputTokens !== null) {
|
|
74
|
-
decodeMs += reading.decodeMs;
|
|
75
|
-
decodeTokens += reading.outputTokens;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return { turns: turns.size, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Compact duration: 45.2s under a minute, 2m42s from there on.
|
|
82
|
-
export function formatDuration(ms: number, t: ChatViewSlotProps["t"]): string {
|
|
83
|
-
const s = ms / 1_000;
|
|
84
|
-
if (s < 60) return t("duration.compactSeconds", { seconds: Math.round(s * 10) / 10 });
|
|
85
|
-
const whole = Math.round(s);
|
|
86
|
-
return t("duration.compactMinutes", {
|
|
87
|
-
minutes: Math.floor(whole / 60),
|
|
88
|
-
seconds: whole % 60,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Display-ready cache-hit share of prompt-side input over the whole durable log: integer text while integer
|
|
93
|
-
// rounding stays below 100, otherwise the minimum decimal precision that still rounds below 100; a full hit
|
|
94
|
-
// returns 100, and no billed input returns null.
|
|
95
|
-
export function cacheHitPercent(usage: TokenUsageProjection): string | null {
|
|
96
|
-
const denominator = billedInputTokens(usage);
|
|
97
|
-
return formatCacheHitPercent(usage.cacheReadTokens, denominator);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Sum the three disjoint prompt-side billing buckets.
|
|
101
|
-
export function billedInputTokens(usage: TokenUsageProjection): number {
|
|
102
|
-
return usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Props: the conversation-snapshot selector plus the projection read seat.
|
|
106
|
-
export interface StatsPillsProps {
|
|
107
|
-
useChat: SnapshotSelectorHook<ChatSnapshot>;
|
|
108
|
-
useProjection: UseProjection;
|
|
109
|
-
// The owning dock's locale seat.
|
|
110
|
-
t: ChatViewSlotProps["t"];
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function exactCount(value: number, t: ChatViewSlotProps["t"]): string {
|
|
114
|
-
return t("message.turnUsage.count", { count: formatExactTokens(value, t) });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// External open state one pill's dialog reads and writes (the row's exclusive slot).
|
|
118
|
-
type PillDialog = Pick<ReturnType<typeof useStatDialog>, "open" | "setOpen">;
|
|
119
|
-
|
|
120
|
-
function TimePill({
|
|
121
|
-
stats,
|
|
122
|
-
t,
|
|
123
|
-
dialog,
|
|
124
|
-
}: {
|
|
125
|
-
stats: WindowStats;
|
|
126
|
-
t: ChatViewSlotProps["t"];
|
|
127
|
-
dialog: PillDialog;
|
|
128
|
-
}) {
|
|
129
|
-
const { open, setOpen, rootRef, panelRef, pos } = useStatDialog(dialog);
|
|
130
|
-
const counts = t("stats.counts", { turns: stats.turns, steps: stats.steps });
|
|
131
|
-
const tps =
|
|
132
|
-
stats.decodeMs > 0
|
|
133
|
-
? t("message.tokensPerSecond", {
|
|
134
|
-
tps: formatTokensPerSecond(stats.decodeTokens / (stats.decodeMs / 1_000)),
|
|
135
|
-
})
|
|
136
|
-
: null;
|
|
137
|
-
const label = (
|
|
138
|
-
<span {...styling.props(styles.label)}>
|
|
139
|
-
{counts}
|
|
140
|
-
{tps !== null && (
|
|
141
|
-
<>
|
|
142
|
-
<span {...styling.props(styles.sep)} aria-hidden>
|
|
143
|
-
·
|
|
144
|
-
</span>
|
|
145
|
-
{tps}
|
|
146
|
-
</>
|
|
147
|
-
)}
|
|
148
|
-
</span>
|
|
149
|
-
);
|
|
150
|
-
// A window without one timed figure has no dialog rows to show, so the pill
|
|
151
|
-
// stays a plain reading instead of a button opening an empty dialog.
|
|
152
|
-
if (stats.llmMs <= 0 && stats.toolMs <= 0 && stats.ttftSteps <= 0 && stats.decodeMs <= 0) {
|
|
153
|
-
return (
|
|
154
|
-
<span {...styling.props(styles.anchor)}>
|
|
155
|
-
<span {...styling.props(styles.pill)}>
|
|
156
|
-
<IconGaugeOutlineRegular />
|
|
157
|
-
{label}
|
|
158
|
-
</span>
|
|
159
|
-
</span>
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
return (
|
|
163
|
-
<span ref={rootRef} {...styling.props(styles.anchor)}>
|
|
164
|
-
<button
|
|
165
|
-
type="button"
|
|
166
|
-
{...styling.props(styles.pill)}
|
|
167
|
-
aria-haspopup="dialog"
|
|
168
|
-
aria-expanded={open}
|
|
169
|
-
aria-label={tps === null ? counts : `${counts} · ${tps}`}
|
|
170
|
-
onClick={() => {
|
|
171
|
-
setOpen(!open);
|
|
172
|
-
}}
|
|
173
|
-
>
|
|
174
|
-
<IconGaugeOutlineRegular />
|
|
175
|
-
{label}
|
|
176
|
-
</button>
|
|
177
|
-
{open &&
|
|
178
|
-
createPortal(
|
|
179
|
-
<div
|
|
180
|
-
ref={panelRef}
|
|
181
|
-
{...styling.props(dialogStyles.panel)}
|
|
182
|
-
role="dialog"
|
|
183
|
-
aria-label={t("stats.dialog.title")}
|
|
184
|
-
style={pos ?? MEASURE_STYLE}
|
|
185
|
-
>
|
|
186
|
-
<div {...styling.props(dialogStyles.title)}>
|
|
187
|
-
<span {...styling.props(dialogStyles.titleLabel)}>
|
|
188
|
-
<IconGaugeOutlineRegular />
|
|
189
|
-
{t("stats.dialog.title")}
|
|
190
|
-
</span>
|
|
191
|
-
</div>
|
|
192
|
-
<div {...styling.props(dialogStyles.titleRule)} aria-hidden />
|
|
193
|
-
<dl {...styling.props(dialogStyles.details)} data-session-stats-details>
|
|
194
|
-
{stats.llmMs > 0 && (
|
|
195
|
-
<>
|
|
196
|
-
<dt>{t("stats.dialog.llmTime")}</dt>
|
|
197
|
-
<dd>{formatDuration(stats.llmMs, t)}</dd>
|
|
198
|
-
</>
|
|
199
|
-
)}
|
|
200
|
-
{stats.toolMs > 0 && (
|
|
201
|
-
<>
|
|
202
|
-
<dt>{t("stats.dialog.toolTime")}</dt>
|
|
203
|
-
<dd>{formatDuration(stats.toolMs, t)}</dd>
|
|
204
|
-
</>
|
|
205
|
-
)}
|
|
206
|
-
{stats.ttftSteps > 0 && (
|
|
207
|
-
<>
|
|
208
|
-
<dt>{t("stats.dialog.ttft")}</dt>
|
|
209
|
-
<dd>{formatDuration(stats.ttftMs / stats.ttftSteps, t)}</dd>
|
|
210
|
-
</>
|
|
211
|
-
)}
|
|
212
|
-
{stats.decodeMs > 0 && (
|
|
213
|
-
<>
|
|
214
|
-
<dt>{t("stats.dialog.speed")}</dt>
|
|
215
|
-
<dd>
|
|
216
|
-
{t("message.tokensPerSecond", {
|
|
217
|
-
tps: formatTokensPerSecond(stats.decodeTokens / (stats.decodeMs / 1_000)),
|
|
218
|
-
})}
|
|
219
|
-
</dd>
|
|
220
|
-
</>
|
|
221
|
-
)}
|
|
222
|
-
</dl>
|
|
223
|
-
</div>,
|
|
224
|
-
document.body,
|
|
225
|
-
)}
|
|
226
|
-
</span>
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function UsagePill({
|
|
231
|
-
usage,
|
|
232
|
-
t,
|
|
233
|
-
dialog,
|
|
234
|
-
}: {
|
|
235
|
-
usage: TokenUsageProjection;
|
|
236
|
-
t: ChatViewSlotProps["t"];
|
|
237
|
-
dialog: PillDialog;
|
|
238
|
-
}) {
|
|
239
|
-
const { open, setOpen, rootRef, panelRef, pos } = useStatDialog(dialog);
|
|
240
|
-
// Same aggregate as the Turn pill's totalTokens: every prompt-side billing bucket plus output.
|
|
241
|
-
const total = billedInputTokens(usage) + usage.outputTokens;
|
|
242
|
-
const totalText = t("message.turnUsage.count", { count: formatTokens(total, t) });
|
|
243
|
-
const cacheHit = cacheHitPercent(usage);
|
|
244
|
-
const cacheHitText = cacheHit !== null ? t("stats.cacheHit", { percent: cacheHit }) : null;
|
|
245
|
-
return (
|
|
246
|
-
<span ref={rootRef} {...styling.props(styles.anchor)}>
|
|
247
|
-
<button
|
|
248
|
-
type="button"
|
|
249
|
-
{...styling.props(styles.pill)}
|
|
250
|
-
aria-haspopup="dialog"
|
|
251
|
-
aria-expanded={open}
|
|
252
|
-
aria-label={cacheHitText === null ? totalText : `${totalText} · ${cacheHitText}`}
|
|
253
|
-
onClick={() => {
|
|
254
|
-
setOpen(!open);
|
|
255
|
-
}}
|
|
256
|
-
>
|
|
257
|
-
<IconDatabaseOutlineRegular />
|
|
258
|
-
<span {...styling.props(styles.label)}>
|
|
259
|
-
{totalText}
|
|
260
|
-
{cacheHitText !== null && (
|
|
261
|
-
<>
|
|
262
|
-
<span {...styling.props(styles.sep)} aria-hidden>
|
|
263
|
-
·
|
|
264
|
-
</span>
|
|
265
|
-
{cacheHitText}
|
|
266
|
-
</>
|
|
267
|
-
)}
|
|
268
|
-
</span>
|
|
269
|
-
</button>
|
|
270
|
-
{open &&
|
|
271
|
-
createPortal(
|
|
272
|
-
<div
|
|
273
|
-
ref={panelRef}
|
|
274
|
-
{...styling.props(dialogStyles.panel)}
|
|
275
|
-
role="dialog"
|
|
276
|
-
aria-label={t("stats.dialog.usageTitle")}
|
|
277
|
-
style={pos ?? MEASURE_STYLE}
|
|
278
|
-
>
|
|
279
|
-
<div {...styling.props(dialogStyles.title)}>
|
|
280
|
-
<span {...styling.props(dialogStyles.titleLabel)}>
|
|
281
|
-
<IconDatabaseOutlineRegular />
|
|
282
|
-
{t("stats.dialog.usageTitle")}
|
|
283
|
-
</span>
|
|
284
|
-
<span {...styling.props(dialogStyles.titleValue)}>{exactCount(total, t)}</span>
|
|
285
|
-
</div>
|
|
286
|
-
<div {...styling.props(dialogStyles.titleRule)} aria-hidden />
|
|
287
|
-
{/* jscpd:ignore-start -- the session-total bucket rows deliberately mirror
|
|
288
|
-
TurnUsagePanel's per-turn dl: same skin, different data contract (the
|
|
289
|
-
buckets are always present here; per-turn fields are optional). A
|
|
290
|
-
session that never wrote cache drops the row, as the per-turn panel
|
|
291
|
-
drops its absent fields. */}
|
|
292
|
-
<dl {...styling.props(dialogStyles.details)} data-session-stats-usage>
|
|
293
|
-
{cacheHit !== null && (
|
|
294
|
-
<>
|
|
295
|
-
<dt>{t("message.turnUsage.cacheHit")}</dt>
|
|
296
|
-
<dd>{`${cacheHit}%`}</dd>
|
|
297
|
-
</>
|
|
298
|
-
)}
|
|
299
|
-
<dt>{t("message.turnUsage.input")}</dt>
|
|
300
|
-
<dd>{exactCount(usage.uncachedInputTokens, t)}</dd>
|
|
301
|
-
<dt>{t("message.turnUsage.cacheRead")}</dt>
|
|
302
|
-
<dd>{exactCount(usage.cacheReadTokens, t)}</dd>
|
|
303
|
-
{usage.cacheWriteTokens !== 0 && (
|
|
304
|
-
<>
|
|
305
|
-
<dt>{t("message.turnUsage.cacheWrite")}</dt>
|
|
306
|
-
<dd>{exactCount(usage.cacheWriteTokens, t)}</dd>
|
|
307
|
-
</>
|
|
308
|
-
)}
|
|
309
|
-
<dt>{t("message.turnUsage.output")}</dt>
|
|
310
|
-
<dd>{exactCount(usage.outputTokens, t)}</dd>
|
|
311
|
-
</dl>
|
|
312
|
-
{/* jscpd:ignore-end */}
|
|
313
|
-
</div>,
|
|
314
|
-
document.body,
|
|
315
|
-
)}
|
|
316
|
-
</span>
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export const StatsPills = memo(function StatsPills({ useChat, useProjection, t }: StatsPillsProps) {
|
|
321
|
-
const settledNodes = useChat((s) => s.legacy.nodes);
|
|
322
|
-
const usage = useProjection("tokenUsage");
|
|
323
|
-
// One exclusive slot for both dialogs: opening either pill closes the other.
|
|
324
|
-
const [openPill, setOpenPill] = useState<"time" | "usage" | null>(null);
|
|
325
|
-
// Every figure rides the durable sessionStats projection, so paging and compaction cannot change any of
|
|
326
|
-
// them; an assembly without the unit falls back to the window-scoped fold wholesale (same field names).
|
|
327
|
-
const projected = useProjection("sessionStats");
|
|
328
|
-
const stats = useMemo(() => projected ?? deriveStats(settledNodes), [projected, settledNodes]);
|
|
329
|
-
// Gated on actual token activity: a session whose steps all settled without
|
|
330
|
-
// billing (e.g. every request failed) shows its counts without a usage pill.
|
|
331
|
-
const hasTokens = usage !== undefined && (billedInputTokens(usage) > 0 || usage.outputTokens > 0);
|
|
332
|
-
if (stats.steps === 0 && !hasTokens) return null;
|
|
333
|
-
// data-composer-stats: 这一行的稳定锚点(样式 / 测试定位用)。
|
|
334
|
-
return (
|
|
335
|
-
<div {...styling.props(styles.root)} data-composer-stats>
|
|
336
|
-
{stats.steps > 0 && (
|
|
337
|
-
<TimePill
|
|
338
|
-
stats={stats}
|
|
339
|
-
t={t}
|
|
340
|
-
dialog={{
|
|
341
|
-
open: openPill === "time",
|
|
342
|
-
setOpen: (open) => {
|
|
343
|
-
setOpenPill(open ? "time" : null);
|
|
344
|
-
},
|
|
345
|
-
}}
|
|
346
|
-
/>
|
|
347
|
-
)}
|
|
348
|
-
{hasTokens && (
|
|
349
|
-
<UsagePill
|
|
350
|
-
usage={usage}
|
|
351
|
-
t={t}
|
|
352
|
-
dialog={{
|
|
353
|
-
open: openPill === "usage",
|
|
354
|
-
setOpen: (open) => {
|
|
355
|
-
setOpenPill(open ? "usage" : null);
|
|
356
|
-
},
|
|
357
|
-
}}
|
|
358
|
-
/>
|
|
359
|
-
)}
|
|
360
|
-
</div>
|
|
361
|
-
);
|
|
362
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
-
import type {} from "@deepseek-ai/dsh-client-ui-chat/client";
|
|
3
|
-
import type {} from "@deepseek-ai/dsh-client-ui-conversation/client";
|
|
4
|
-
import { StatsPills } from "./StatsPills.tsx";
|
|
5
|
-
|
|
6
|
-
// 官方 ui-chat 的字典命名空间:文案沿用上游(`stats.*` / `message.turnUsage.*`)。
|
|
7
|
-
const NS = "chat";
|
|
8
|
-
|
|
9
|
-
// 覆盖注册:官方 ui-chat 在 `conversation.composer.dock` 注册 id `stats`(优先级 0),
|
|
10
|
-
// 我们以 priority -1 同 id 重新注册——最低优先级渲染,shadow 官方那一格。
|
|
11
|
-
// 覆盖是 shadow 不是删除:官方 stats 行仍在注册表里。
|
|
12
|
-
export function registerComposerStats(slots: Context["slots"]): () => void {
|
|
13
|
-
return slots.register(
|
|
14
|
-
{ name: "conversation.composer.dock", id: "stats", order: 0, priority: -1, locale: NS },
|
|
15
|
-
StatsPills,
|
|
16
|
-
);
|
|
17
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { CSSProps } from "@morlay/dsh-client-ui-primitives/client";
|
|
2
|
-
|
|
3
|
-
export const styles = {
|
|
4
|
-
panel: {
|
|
5
|
-
position: "fixed",
|
|
6
|
-
zIndex: "1100",
|
|
7
|
-
boxSizing: "border-box",
|
|
8
|
-
width: "max-content",
|
|
9
|
-
minWidth: "min(300px, calc(100vw - 24px))",
|
|
10
|
-
maxWidth: "min(440px, calc(100vw - 24px))",
|
|
11
|
-
padding: "16px",
|
|
12
|
-
border: "0",
|
|
13
|
-
borderRadius: "12px",
|
|
14
|
-
background: "var(--dsw-specific-menu)",
|
|
15
|
-
"--dsw-elevation-stroke-color": "var(--dsw-alias-border-l1)",
|
|
16
|
-
boxShadow: "var(--dsw-elevation-prominent)",
|
|
17
|
-
fontSize: "12px",
|
|
18
|
-
lineHeight: "18px",
|
|
19
|
-
color: "var(--dsw-alias-label-secondary)",
|
|
20
|
-
cursor: "default",
|
|
21
|
-
},
|
|
22
|
-
title: {
|
|
23
|
-
display: "flex",
|
|
24
|
-
justifyContent: "space-between",
|
|
25
|
-
gap: "16px",
|
|
26
|
-
marginBottom: "8px",
|
|
27
|
-
color: "var(--dsw-alias-label-primary)",
|
|
28
|
-
fontWeight: "500",
|
|
29
|
-
},
|
|
30
|
-
titleRule: {
|
|
31
|
-
marginBottom: "10px",
|
|
32
|
-
borderTop: "0.5px solid var(--dsw-alias-border-l2)",
|
|
33
|
-
},
|
|
34
|
-
titleValue: {
|
|
35
|
-
fontVariantNumeric: "tabular-nums",
|
|
36
|
-
},
|
|
37
|
-
titleLabel: {
|
|
38
|
-
display: "inline-flex",
|
|
39
|
-
alignItems: "center",
|
|
40
|
-
gap: "6px",
|
|
41
|
-
minWidth: "0",
|
|
42
|
-
"& svg": {
|
|
43
|
-
width: "14px",
|
|
44
|
-
height: "14px",
|
|
45
|
-
flex: "none",
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
details: {
|
|
49
|
-
display: "grid",
|
|
50
|
-
gridTemplateColumns: "minmax(76px, auto) minmax(0, 1fr)",
|
|
51
|
-
gap: "6px 16px",
|
|
52
|
-
margin: "0",
|
|
53
|
-
color: "var(--dsw-alias-label-tertiary)",
|
|
54
|
-
"& dt": {
|
|
55
|
-
minWidth: "0",
|
|
56
|
-
margin: "0",
|
|
57
|
-
},
|
|
58
|
-
"& dd": {
|
|
59
|
-
minWidth: "0",
|
|
60
|
-
margin: "0",
|
|
61
|
-
color: "var(--dsw-alias-label-secondary)",
|
|
62
|
-
fontVariantNumeric: "tabular-nums",
|
|
63
|
-
textAlign: "right",
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
reasoning: {
|
|
67
|
-
color: "var(--dsw-alias-label-tertiary)",
|
|
68
|
-
whiteSpace: "nowrap",
|
|
69
|
-
},
|
|
70
|
-
} satisfies Record<string, CSSProps>;
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, type CSSProperties, type MutableRefObject } from "react";
|
|
2
|
-
import {
|
|
3
|
-
useAnchoredPosition,
|
|
4
|
-
useDismissOnOutsidePointer,
|
|
5
|
-
} from "@deepseek-ai/dsh-client-ui-primitives";
|
|
6
|
-
|
|
7
|
-
const PANEL_MARGIN = 12;
|
|
8
|
-
|
|
9
|
-
const PANEL_GAP = 8;
|
|
10
|
-
|
|
11
|
-
export const MEASURE_STYLE: CSSProperties = { visibility: "hidden", left: 0, top: 0 };
|
|
12
|
-
|
|
13
|
-
export interface StatDialogSeat {
|
|
14
|
-
open: boolean;
|
|
15
|
-
setOpen: (open: boolean) => void;
|
|
16
|
-
rootRef: MutableRefObject<HTMLSpanElement | null>;
|
|
17
|
-
panelRef: MutableRefObject<HTMLDivElement | null>;
|
|
18
|
-
pos: CSSProperties | null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function useStatDialog(
|
|
22
|
-
controlled?: Pick<StatDialogSeat, "open" | "setOpen">,
|
|
23
|
-
): StatDialogSeat {
|
|
24
|
-
const [ownOpen, setOwnOpen] = useState(false);
|
|
25
|
-
const open = controlled?.open ?? ownOpen;
|
|
26
|
-
const setOpen = controlled?.setOpen ?? setOwnOpen;
|
|
27
|
-
const rootRef = useRef<HTMLSpanElement | null>(null);
|
|
28
|
-
const panelRef = useRef<HTMLDivElement | null>(null);
|
|
29
|
-
|
|
30
|
-
const pos = useAnchoredPosition({
|
|
31
|
-
open,
|
|
32
|
-
anchorRef: rootRef,
|
|
33
|
-
panelRef,
|
|
34
|
-
side: "top",
|
|
35
|
-
gap: PANEL_GAP,
|
|
36
|
-
margin: PANEL_MARGIN,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
useDismissOnOutsidePointer(rootRef, open, setOpen, panelRef);
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
if (!open) return;
|
|
42
|
-
const onKeyDown = (e: KeyboardEvent): void => {
|
|
43
|
-
if (e.key === "Escape") setOpen(false);
|
|
44
|
-
};
|
|
45
|
-
document.addEventListener("keydown", onKeyDown);
|
|
46
|
-
return () => {
|
|
47
|
-
document.removeEventListener("keydown", onKeyDown);
|
|
48
|
-
};
|
|
49
|
-
}, [open, setOpen]);
|
|
50
|
-
|
|
51
|
-
return { open, setOpen, rootRef, panelRef, pos };
|
|
52
|
-
}
|