@morlay/ui-conversation-message-actions 0.0.11 → 0.0.13
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/README.md +0 -6
- package/dist/client.js +831 -0
- package/dist/index.d.mts +105 -0
- package/dist/index.mjs +3 -0
- package/dist/invariant.d.mts +7 -0
- package/dist/invariant.mjs +8 -0
- package/dist/plan-Bw4zoI4N.d.mts +86 -0
- package/dist/plan.d.mts +2 -0
- package/dist/plan.mjs +225 -0
- package/dist/src-CXYW0Bc0.mjs +408 -0
- package/dist/testing.d.mts +707 -0
- package/dist/testing.mjs +23690 -0
- package/package.json +60 -31
- package/src/client/chat-node/MessageEditDialog.module.css +24 -0
- package/src/client/chat-node/MessageEditDialog.tsx +71 -0
- package/src/client/chat-node/MessageIconActions.module.css +86 -0
- package/src/client/chat-node/MessageIconActions.tsx +189 -0
- package/src/client/chat-node/MessageItem.module.css +290 -0
- package/src/client/chat-node/MessageItem.tsx +198 -0
- package/src/client/chat-node/message-chrome.ts +61 -0
- package/src/client/chat-node/register.ts +33 -0
- package/src/client/chat-node/use-calendar-day.ts +23 -0
- package/src/client/controller.ts +327 -0
- package/src/client/css-modules.d.ts +4 -0
- package/src/client/import-action.tsx +74 -0
- package/src/client/index.ts +50 -0
- package/src/index.ts +650 -0
- package/src/invariant.ts +13 -0
- package/src/plan.ts +322 -0
- package/src/shared.ts +167 -0
- package/src/testing.ts +151 -0
- package/src/types.ts +73 -0
- package/lib/client.js +0 -2243
- package/lib/client.js.map +0 -1
- package/lib/index.d.mts +0 -211
- package/lib/index.d.mts.map +0 -1
- package/lib/index.mjs +0 -685
- package/lib/index.mjs.map +0 -1
- package/lib/invariant.d.mts +0 -15
- package/lib/invariant.d.mts.map +0 -1
- package/lib/invariant.mjs +0 -22
- package/lib/invariant.mjs.map +0 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/* User bubble: right-aligned column (bubble + IconActions). Figma
|
|
2
|
+
User_Bubble/message_container 659:38813 — r22 fill, actions gap 6 below. */
|
|
3
|
+
|
|
4
|
+
.userRow {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
align-items: flex-end;
|
|
8
|
+
gap: 6px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.userStack {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
align-items: flex-end;
|
|
15
|
+
gap: 8px;
|
|
16
|
+
min-width: 0;
|
|
17
|
+
max-width: min(525px, 82%);
|
|
18
|
+
}
|
|
19
|
+
.bubble {
|
|
20
|
+
/* 525px cap inside the 736 column; percentage keeps narrow windows sane. */
|
|
21
|
+
max-width: 100%;
|
|
22
|
+
background: var(--dsw-specific-bubble);
|
|
23
|
+
border-radius: 22px;
|
|
24
|
+
/* 44px single-line bubble: 24 line + 10 vertical padding each side. */
|
|
25
|
+
padding: 10px 16px;
|
|
26
|
+
font-size: 16px;
|
|
27
|
+
line-height: 24px;
|
|
28
|
+
color: var(--dsw-alias-label-primary);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.contextRow {
|
|
32
|
+
padding: 2px 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Compaction marker: one dim 24px row with a context icon at rest and a
|
|
36
|
+
hover/focus disclosure for the summary body. Dimmed title (not
|
|
37
|
+
label-primary) — the row is a boundary notice, not conversation content. */
|
|
38
|
+
.compactionRow {
|
|
39
|
+
padding: 2px 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.compactionButton {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
width: 100%;
|
|
46
|
+
height: 24px;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
padding: 0;
|
|
49
|
+
border: none;
|
|
50
|
+
border-radius: 6px;
|
|
51
|
+
background: none;
|
|
52
|
+
color: inherit;
|
|
53
|
+
font: inherit;
|
|
54
|
+
text-align: left;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.compactionButton:not(:disabled) {
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.compactionButton:not(:disabled):hover {
|
|
62
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.compactionLeading {
|
|
66
|
+
flex: none;
|
|
67
|
+
display: inline-grid;
|
|
68
|
+
place-items: center;
|
|
69
|
+
width: 16px;
|
|
70
|
+
height: 16px;
|
|
71
|
+
margin-right: 6px;
|
|
72
|
+
color: var(--dsw-alias-label-secondary);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.compactionContextIcon,
|
|
76
|
+
.compactionDisclosureIcon {
|
|
77
|
+
display: inline-flex;
|
|
78
|
+
grid-area: 1 / 1;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: center;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.compactionDisclosureIcon {
|
|
84
|
+
opacity: 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.compactionButton:not(:disabled):hover .compactionContextIcon,
|
|
88
|
+
.compactionButton:not(:disabled):focus-visible .compactionContextIcon {
|
|
89
|
+
opacity: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.compactionButton:not(:disabled):hover .compactionDisclosureIcon,
|
|
93
|
+
.compactionButton:not(:disabled):focus-visible .compactionDisclosureIcon {
|
|
94
|
+
opacity: 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.compactionTitle {
|
|
98
|
+
flex: none;
|
|
99
|
+
font-size: 14px;
|
|
100
|
+
line-height: 24px;
|
|
101
|
+
color: var(--dsw-alias-label-primary-dimmed);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.compactionSep {
|
|
105
|
+
flex: none;
|
|
106
|
+
width: 2px;
|
|
107
|
+
height: 2px;
|
|
108
|
+
margin: 0 8px;
|
|
109
|
+
border-radius: 1px;
|
|
110
|
+
background: var(--dsw-alias-label-caption);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.compactionSummary {
|
|
114
|
+
flex: 1 1 auto;
|
|
115
|
+
min-width: 0;
|
|
116
|
+
overflow: hidden;
|
|
117
|
+
color: var(--dsw-alias-label-tertiary);
|
|
118
|
+
font-size: 14px;
|
|
119
|
+
line-height: 24px;
|
|
120
|
+
text-overflow: ellipsis;
|
|
121
|
+
white-space: nowrap;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.compactionBody {
|
|
125
|
+
padding: 4px 0 4px 22px;
|
|
126
|
+
color: var(--dsw-alias-label-tertiary);
|
|
127
|
+
font-size: 14px;
|
|
128
|
+
line-height: 24px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.retryRow {
|
|
132
|
+
color: var(--dsw-alias-label-tertiary);
|
|
133
|
+
font-size: 13px;
|
|
134
|
+
line-height: 20px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.retrySummary {
|
|
138
|
+
display: inline-flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
width: fit-content;
|
|
141
|
+
padding: 2px 0;
|
|
142
|
+
gap: 7px;
|
|
143
|
+
border-radius: 3px;
|
|
144
|
+
color: inherit;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
list-style: none;
|
|
147
|
+
user-select: none;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.retrySummary::-webkit-details-marker {
|
|
151
|
+
display: none;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.retrySummary::after {
|
|
155
|
+
width: 6px;
|
|
156
|
+
height: 6px;
|
|
157
|
+
border-right: 1.5px solid currentcolor;
|
|
158
|
+
border-bottom: 1.5px solid currentcolor;
|
|
159
|
+
content: "";
|
|
160
|
+
opacity: 0.8;
|
|
161
|
+
transform: rotate(-45deg);
|
|
162
|
+
transition: transform 120ms ease;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.retrySummary:hover {
|
|
166
|
+
color: var(--dsw-alias-label-secondary);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.retrySummary:focus-visible {
|
|
170
|
+
outline: 1.5px solid var(--dsw-alias-button-info-fill);
|
|
171
|
+
outline-offset: 2px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.retryText {
|
|
175
|
+
color: inherit;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.retryRow[data-active] .retryText {
|
|
179
|
+
background: linear-gradient(
|
|
180
|
+
90deg,
|
|
181
|
+
var(--dsw-alias-label-tertiary) 0%,
|
|
182
|
+
var(--dsw-alias-label-tertiary) 40%,
|
|
183
|
+
var(--dsw-alias-label-secondary) 50%,
|
|
184
|
+
var(--dsw-alias-label-tertiary) 60%,
|
|
185
|
+
var(--dsw-alias-label-tertiary) 100%
|
|
186
|
+
);
|
|
187
|
+
background-position: 100% 50%;
|
|
188
|
+
background-size: 200% 100%;
|
|
189
|
+
background-clip: text;
|
|
190
|
+
color: transparent;
|
|
191
|
+
animation: retry-shimmer 1.6s ease-in-out infinite;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.retryRow[open] .retrySummary::after {
|
|
195
|
+
transform: rotate(45deg);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.retryDetails {
|
|
199
|
+
display: grid;
|
|
200
|
+
gap: 2px;
|
|
201
|
+
margin-top: 3px;
|
|
202
|
+
padding-left: 14px;
|
|
203
|
+
overflow-wrap: anywhere;
|
|
204
|
+
font-size: 12px;
|
|
205
|
+
line-height: 18px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.retryDetailLabel {
|
|
209
|
+
color: var(--dsw-alias-label-secondary);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.turnErrorRow {
|
|
213
|
+
display: grid;
|
|
214
|
+
grid-template-columns: 10px minmax(0, 1fr) auto;
|
|
215
|
+
gap: 8px;
|
|
216
|
+
align-items: start;
|
|
217
|
+
padding: 2px 0;
|
|
218
|
+
font-size: 13px;
|
|
219
|
+
line-height: 20px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.turnErrorDot {
|
|
223
|
+
margin-top: 5px;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.turnErrorCopy {
|
|
227
|
+
min-width: 0;
|
|
228
|
+
overflow-wrap: anywhere;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.turnErrorTitle {
|
|
232
|
+
margin-right: 6px;
|
|
233
|
+
color: var(--dsw-alias-state-error-primary);
|
|
234
|
+
font-weight: 600;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.turnErrorMessage {
|
|
238
|
+
color: var(--dsw-alias-label-secondary);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.turnErrorCode {
|
|
242
|
+
color: var(--dsw-alias-label-tertiary);
|
|
243
|
+
font: var(--dsw-font-markdown-code-block-small);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.maxTokensTitle {
|
|
247
|
+
margin-right: 6px;
|
|
248
|
+
color: var(--dsw-alias-state-warn-primary);
|
|
249
|
+
font-weight: 600;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@keyframes retry-shimmer {
|
|
253
|
+
from {
|
|
254
|
+
background-position: 100% 50%;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
to {
|
|
258
|
+
background-position: 0 50%;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
@media (prefers-reduced-motion: reduce) {
|
|
263
|
+
.retryRow[data-active] .retryText {
|
|
264
|
+
background: none;
|
|
265
|
+
color: inherit;
|
|
266
|
+
animation: none;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* Reference chip projection inside a user bubble (`<skill>name</skill>` model
|
|
271
|
+
spans render as chips; free geometry — no textarea pairing here). */
|
|
272
|
+
.refChip {
|
|
273
|
+
display: inline-block;
|
|
274
|
+
margin: 0 2px;
|
|
275
|
+
padding: 0 8px;
|
|
276
|
+
border-radius: 6px;
|
|
277
|
+
background: rgba(97, 135, 216, 0.22);
|
|
278
|
+
color: var(--dsw-alias-label-primary);
|
|
279
|
+
font-size: 0.85em;
|
|
280
|
+
line-height: 1.6;
|
|
281
|
+
white-space: nowrap;
|
|
282
|
+
vertical-align: baseline;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* Retry-confirmation modal action row. */
|
|
286
|
+
.confirmActions {
|
|
287
|
+
display: flex;
|
|
288
|
+
justify-content: flex-end;
|
|
289
|
+
gap: 8px;
|
|
290
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// MessageItem: the user / admitted-steering chat node renderer.
|
|
2
|
+
// 新版 `dsh-client-ui-chat` 已内置全部 chat-node 渲染器;本项目仅**替换**
|
|
3
|
+
// `user`/`steering` 两个 key(keyed slot reuse 即替换),在消息动作行上提供
|
|
4
|
+
// edit / retry(新版无此能力)。其余 key 由新版内置渲染器处理。
|
|
5
|
+
|
|
6
|
+
import { memo, useState } from "react";
|
|
7
|
+
import type { ReactNode } from "react";
|
|
8
|
+
import type { InjectFace } from "@deepseek-ai/dsh-client-ui-slots";
|
|
9
|
+
import { Button, Modal } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
10
|
+
import type { UserMessageNode } from "@deepseek-ai/dsh-client-ui-chat/client";
|
|
11
|
+
import { JsonBlock, MessageText } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
12
|
+
import type { ChatNodeViewProps, ChatViewSlotProps } from "@deepseek-ai/dsh-client-ui-chat/client";
|
|
13
|
+
import type { RenderMessageImages } from "@deepseek-ai/dsh-client-ui-conversation/client";
|
|
14
|
+
import { MessageIconActions } from "./MessageIconActions.tsx";
|
|
15
|
+
import { MessageEditDialog } from "./MessageEditDialog.tsx";
|
|
16
|
+
import css from "./MessageItem.module.css";
|
|
17
|
+
import type { EditableMessageBlock } from "../../shared.ts";
|
|
18
|
+
import type { SessionEditorFace } from "../controller.ts";
|
|
19
|
+
|
|
20
|
+
type UserImage = Extract<UserMessageNode["content"][number], { type: "image" }>;
|
|
21
|
+
|
|
22
|
+
function contentParts(content: readonly unknown[]): {
|
|
23
|
+
text: string;
|
|
24
|
+
images: { attachment: UserImage["attachment"] }[];
|
|
25
|
+
rest: unknown[];
|
|
26
|
+
} {
|
|
27
|
+
const texts: string[] = [];
|
|
28
|
+
const images: { attachment: UserImage["attachment"] }[] = [];
|
|
29
|
+
const rest: unknown[] = [];
|
|
30
|
+
for (const block of content) {
|
|
31
|
+
const b = block as { type?: string; text?: string; attachment?: unknown };
|
|
32
|
+
if (b.type === "text" && typeof b.text === "string") texts.push(b.text);
|
|
33
|
+
else if (b.type === "image" && b.attachment !== undefined) {
|
|
34
|
+
images.push({ attachment: (b as UserImage).attachment });
|
|
35
|
+
} else rest.push(block);
|
|
36
|
+
}
|
|
37
|
+
return { text: texts.join(""), images, rest };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function projectUserText(text: string): ReactNode {
|
|
41
|
+
const re = /(^|\s)([/@][\w-]+)(?=\s|$)/g;
|
|
42
|
+
const parts: ReactNode[] = [];
|
|
43
|
+
let cursor = 0;
|
|
44
|
+
let m: RegExpExecArray | null;
|
|
45
|
+
while ((m = re.exec(text)) !== null) {
|
|
46
|
+
const tokenStart = m.index + (m[1]?.length ?? 0);
|
|
47
|
+
const label = m[2] ?? "";
|
|
48
|
+
if (tokenStart > cursor)
|
|
49
|
+
parts.push(<MessageText key={cursor} text={text.slice(cursor, tokenStart)} />);
|
|
50
|
+
parts.push(
|
|
51
|
+
<span
|
|
52
|
+
key={tokenStart}
|
|
53
|
+
className={css.refChip}
|
|
54
|
+
data-ref-chip={label.startsWith("@") ? "subagent" : "skill"}
|
|
55
|
+
>
|
|
56
|
+
{label}
|
|
57
|
+
</span>,
|
|
58
|
+
);
|
|
59
|
+
cursor = tokenStart + label.length;
|
|
60
|
+
}
|
|
61
|
+
if (parts.length === 0) return <MessageText text={text} />;
|
|
62
|
+
if (cursor < text.length) parts.push(<MessageText key={cursor} text={text.slice(cursor)} />);
|
|
63
|
+
return <>{parts}</>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function UserStyleBubble({
|
|
67
|
+
content,
|
|
68
|
+
renderMessageImages,
|
|
69
|
+
actions,
|
|
70
|
+
t,
|
|
71
|
+
}: {
|
|
72
|
+
content: readonly unknown[];
|
|
73
|
+
renderMessageImages: RenderMessageImages;
|
|
74
|
+
|
|
75
|
+
actions?: (text: string) => ReactNode;
|
|
76
|
+
t: ChatViewSlotProps["t"];
|
|
77
|
+
}): ReactNode {
|
|
78
|
+
const { text, images, rest } = contentParts(content);
|
|
79
|
+
const truncated = (total: number): string => t("json.truncated", { total });
|
|
80
|
+
const showBubble = text !== "" || rest.length > 0;
|
|
81
|
+
return (
|
|
82
|
+
<div className={css.userRow} data-time-hover-root>
|
|
83
|
+
<div className={css.userStack}>
|
|
84
|
+
{renderMessageImages({ images, align: "end" })}
|
|
85
|
+
{showBubble && (
|
|
86
|
+
<div className={css.bubble}>
|
|
87
|
+
{projectUserText(text)}
|
|
88
|
+
{rest.map((block, i) => (
|
|
89
|
+
<JsonBlock
|
|
90
|
+
key={i}
|
|
91
|
+
label={t("message.extraBlock")}
|
|
92
|
+
payload={block}
|
|
93
|
+
truncatedLabel={truncated}
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
{actions?.(text)}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const UserMessageNodeView = memo(function UserMessageNodeView({
|
|
105
|
+
node,
|
|
106
|
+
renderMessageImages,
|
|
107
|
+
t,
|
|
108
|
+
edit,
|
|
109
|
+
retry,
|
|
110
|
+
}: ChatNodeViewProps<"user" | "steering"> & InjectFace<SessionEditorFace>) {
|
|
111
|
+
const data = node.data;
|
|
112
|
+
const [editing, setEditing] = useState<EditableMessageBlock | null>(null);
|
|
113
|
+
const [confirmingRetry, setConfirmingRetry] = useState(false);
|
|
114
|
+
const turnLocation =
|
|
115
|
+
node.location.kind === "turn" || node.location.kind === "step" ? node.location.turn : undefined;
|
|
116
|
+
const turn = turnLocation?.turn;
|
|
117
|
+
// 重试只对已闭合轮次开放(未闭合/无闭合边界的轮次服务端无法重放)。
|
|
118
|
+
const retryable = turnLocation?.status === "closed";
|
|
119
|
+
// 编辑目标:第一个文本块(与 Timeline 编辑面的 blockIndex 对齐)。
|
|
120
|
+
const textBlockIndex = data.content.findIndex(
|
|
121
|
+
(block) => (block as { type?: string }).type === "text",
|
|
122
|
+
);
|
|
123
|
+
const textBlock =
|
|
124
|
+
textBlockIndex === -1 ? undefined : (data.content[textBlockIndex] as { text?: string });
|
|
125
|
+
const onEdit =
|
|
126
|
+
textBlock === undefined || turn === undefined
|
|
127
|
+
? undefined
|
|
128
|
+
: () => {
|
|
129
|
+
setEditing({
|
|
130
|
+
key: `${node.anchorSeq}:${String(textBlockIndex)}`,
|
|
131
|
+
turn,
|
|
132
|
+
eventSeq: node.anchorSeq,
|
|
133
|
+
blockIndex: textBlockIndex,
|
|
134
|
+
kind: "user",
|
|
135
|
+
text: textBlock.text ?? "",
|
|
136
|
+
time: data.time,
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
// 重试先弹确认(就地编辑会抛弃该回合及其后的内容)。
|
|
140
|
+
const onRetry =
|
|
141
|
+
retryable && turn !== undefined
|
|
142
|
+
? () => {
|
|
143
|
+
setConfirmingRetry(true);
|
|
144
|
+
}
|
|
145
|
+
: undefined;
|
|
146
|
+
return (
|
|
147
|
+
<>
|
|
148
|
+
{editing !== null && (
|
|
149
|
+
<MessageEditDialog
|
|
150
|
+
block={editing}
|
|
151
|
+
onSave={(text) => edit(editing, text, "truncate")}
|
|
152
|
+
onClose={() => setEditing(null)}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
{confirmingRetry && turn !== undefined && (
|
|
156
|
+
<Modal
|
|
157
|
+
open
|
|
158
|
+
onClose={() => setConfirmingRetry(false)}
|
|
159
|
+
title="重试回合"
|
|
160
|
+
closeLabel="关闭"
|
|
161
|
+
description={`将重新生成第 ${turn} 轮的回复,并抛弃该回合之后的内容。`}
|
|
162
|
+
footer={
|
|
163
|
+
<div className={css.confirmActions}>
|
|
164
|
+
<Button variant="outline" onClick={() => setConfirmingRetry(false)}>
|
|
165
|
+
取消
|
|
166
|
+
</Button>
|
|
167
|
+
<Button
|
|
168
|
+
variant="primary"
|
|
169
|
+
onClick={() => {
|
|
170
|
+
setConfirmingRetry(false);
|
|
171
|
+
void retry(turn, "truncate");
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
确认重试
|
|
175
|
+
</Button>
|
|
176
|
+
</div>
|
|
177
|
+
}
|
|
178
|
+
/>
|
|
179
|
+
)}
|
|
180
|
+
<UserStyleBubble
|
|
181
|
+
content={data.content}
|
|
182
|
+
renderMessageImages={renderMessageImages}
|
|
183
|
+
t={t}
|
|
184
|
+
actions={(text) => (
|
|
185
|
+
<MessageIconActions
|
|
186
|
+
text={text}
|
|
187
|
+
time={data.time}
|
|
188
|
+
clock="start"
|
|
189
|
+
className={css.actions}
|
|
190
|
+
t={t}
|
|
191
|
+
onEdit={onEdit}
|
|
192
|
+
onRetry={onRetry}
|
|
193
|
+
/>
|
|
194
|
+
)}
|
|
195
|
+
/>
|
|
196
|
+
</>
|
|
197
|
+
);
|
|
198
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Shared time-label helpers for user/assistant IconActions rows.
|
|
2
|
+
|
|
3
|
+
import type { Translate } from "@deepseek-ai/dsh-client-ui-slots";
|
|
4
|
+
|
|
5
|
+
export type ClockTranslate = Translate<"clock.md" | "clock.ymd">;
|
|
6
|
+
|
|
7
|
+
export type RunDurationTranslate = Translate<"duration.seconds" | "duration.minutes">;
|
|
8
|
+
function pad2(n: number): string {
|
|
9
|
+
return String(n).padStart(2, "0");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function startOfLocalDay(ms: number): number {
|
|
13
|
+
const d = new Date(ms);
|
|
14
|
+
d.setHours(0, 0, 0, 0);
|
|
15
|
+
return d.getTime();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function msUntilNextLocalMidnight(ms: number): number {
|
|
19
|
+
const next = new Date(ms);
|
|
20
|
+
next.setHours(24, 0, 0, 0);
|
|
21
|
+
return Math.max(next.getTime() - ms, 1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatRunDuration(ms: number, t: RunDurationTranslate): string {
|
|
25
|
+
const total = Math.max(0, Math.floor(ms / 1000));
|
|
26
|
+
const minutes = Math.floor(total / 60);
|
|
27
|
+
const seconds = total % 60;
|
|
28
|
+
return minutes > 0
|
|
29
|
+
? t("duration.minutes", { minutes, seconds: String(seconds).padStart(2, "0") })
|
|
30
|
+
: t("duration.seconds", { seconds });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatLatencySeconds(ms: number): string {
|
|
34
|
+
const s = Math.max(0, ms) / 1000;
|
|
35
|
+
return s < 10 ? String(Math.round(s * 10) / 10) : String(Math.round(s));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatTokensPerSecond(tps: number): string {
|
|
39
|
+
const clamped = Math.max(0, tps);
|
|
40
|
+
return clamped >= 10 ? String(Math.round(clamped)) : String(Math.round(clamped * 10) / 10);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatMessageClock(
|
|
44
|
+
time: number,
|
|
45
|
+
t: ClockTranslate,
|
|
46
|
+
now: number = Date.now(),
|
|
47
|
+
): string {
|
|
48
|
+
const d = new Date(time);
|
|
49
|
+
const n = new Date(now);
|
|
50
|
+
const clock = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
51
|
+
if (
|
|
52
|
+
d.getFullYear() === n.getFullYear() &&
|
|
53
|
+
d.getMonth() === n.getMonth() &&
|
|
54
|
+
d.getDate() === n.getDate()
|
|
55
|
+
) {
|
|
56
|
+
return clock;
|
|
57
|
+
}
|
|
58
|
+
const params = { y: d.getFullYear(), m: d.getMonth() + 1, d: d.getDate() };
|
|
59
|
+
const md = d.getFullYear() === n.getFullYear() ? t("clock.md", params) : t("clock.ymd", params);
|
|
60
|
+
return `${md} ${clock}`;
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
3
|
+
import "@deepseek-ai/dsh-client-ui-conversation/client";
|
|
4
|
+
import "@deepseek-ai/dsh-client-ui-chat/client";
|
|
5
|
+
import type {} from "@deepseek-ai/dsh-client-ui-renderer/client";
|
|
6
|
+
import { UserMessageNodeView } from "./MessageItem.tsx";
|
|
7
|
+
import type { SessionEditorController } from "../controller.ts";
|
|
8
|
+
|
|
9
|
+
const NS = "conversation";
|
|
10
|
+
|
|
11
|
+
export function registerChatNodeRenderers(
|
|
12
|
+
ctx: Context,
|
|
13
|
+
controllerFor: (sessionId: SessionId) => SessionEditorController,
|
|
14
|
+
): void {
|
|
15
|
+
const injectFace = (sessionId: SessionId) => controllerFor(sessionId).face;
|
|
16
|
+
|
|
17
|
+
for (const key of ["user", "steering"] as const) {
|
|
18
|
+
ctx.slots.inject("conversation.chat.node", () =>
|
|
19
|
+
ctx.slots.register(
|
|
20
|
+
{
|
|
21
|
+
name: "conversation.chat.node",
|
|
22
|
+
key,
|
|
23
|
+
locale: NS,
|
|
24
|
+
// keyed slot 同 key 同 priority 会抛错;priority -1 让本项目
|
|
25
|
+
// 渲染器以最低优先级渲染,shadow 上游默认(priority 0)注册。
|
|
26
|
+
priority: -1,
|
|
27
|
+
inject: injectFace,
|
|
28
|
+
} as never,
|
|
29
|
+
UserMessageNodeView as never,
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Component-local calendar-day tick: memoized message rows keep stable props
|
|
2
|
+
// across midnight, so the IconActions clock needs a local day seat that
|
|
3
|
+
// re-fires at the next local midnight without reaching for framework hooks.
|
|
4
|
+
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { msUntilNextLocalMidnight, startOfLocalDay } from "./message-chrome.ts";
|
|
7
|
+
|
|
8
|
+
export function useCalendarDay(): number {
|
|
9
|
+
const [day, setDay] = useState(() => startOfLocalDay(Date.now()));
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
12
|
+
const arm = (): void => {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
setDay(startOfLocalDay(now));
|
|
15
|
+
timer = setTimeout(arm, msUntilNextLocalMidnight(now));
|
|
16
|
+
};
|
|
17
|
+
timer = setTimeout(arm, msUntilNextLocalMidnight(Date.now()));
|
|
18
|
+
return () => {
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
};
|
|
21
|
+
}, []);
|
|
22
|
+
return day;
|
|
23
|
+
}
|