@morlay/ui-conversation-message-actions 0.0.14 → 0.0.16
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 +28 -24
- package/dist/client.js +72 -109
- package/dist/index.d.mts +14 -2
- package/dist/index.mjs +1 -1
- package/dist/{plan-BdU_mnTv.d.mts → plan-Cn3TPTa0.d.mts} +7 -2
- package/dist/plan.d.mts +1 -1
- package/dist/{src-ks167MhO.mjs → src-Dq1X3rOO.mjs} +52 -1
- package/dist/testing.d.mts +2581 -41
- package/dist/testing.mjs +1060 -6
- package/package.json +21 -20
- package/src/client/chat-node/MessageItem.tsx +27 -9
- package/src/client/controller.ts +30 -2
- package/src/index.ts +72 -1
- package/src/shared.ts +7 -0
- package/src/types.ts +8 -0
- package/src/client/chat-node/MessageEditDialog.module.css +0 -24
- package/src/client/chat-node/MessageEditDialog.tsx +0 -71
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/ui-conversation-message-actions",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"description": "Rewind / retry / fork orchestration (host) plus conversation.chat.node renderer replacement (browser) — message edit / retry actions on user bubbles.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"conversation-chat-node",
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"./client": "./dist/client.js"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@morlay/session-branch": "^0.0.
|
|
38
|
+
"@morlay/session-branch": "^0.0.6"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@morlay/session-rdb": "^0.0.
|
|
41
|
+
"@morlay/session-rdb": "^0.0.18",
|
|
42
42
|
"@types/react": "^19.2.18",
|
|
43
43
|
"@types/react-dom": "^19.2.5",
|
|
44
44
|
"lightningcss": "^1.32.0",
|
|
@@ -47,22 +47,22 @@
|
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
50
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.
|
|
51
|
-
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.
|
|
52
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.5-rc.
|
|
53
|
-
"@deepseek-ai/dsh-client-store": "^0.1.5-rc.
|
|
54
|
-
"@deepseek-ai/dsh-client-ui-attachment": "^0.1.5-rc.
|
|
55
|
-
"@deepseek-ai/dsh-client-ui-chat": "^0.1.5-rc.
|
|
56
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.
|
|
57
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.
|
|
59
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.
|
|
60
|
-
"@deepseek-ai/dsh-invariants": "^0.1.5-rc.
|
|
61
|
-
"@deepseek-ai/dsh-llm": "^0.1.5-rc.
|
|
62
|
-
"@deepseek-ai/dsh-session": "^0.1.5-rc.
|
|
63
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.5-rc.
|
|
64
|
-
"@deepseek-ai/dsh-session-projection": "^0.1.5-rc.
|
|
65
|
-
"@deepseek-ai/dsh-token-meter": "^0.1.5-rc.
|
|
50
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.5-rc.2",
|
|
53
|
+
"@deepseek-ai/dsh-client-store": "^0.1.5-rc.2",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-attachment": "^0.1.5-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-chat": "^0.1.5-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.5-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.5-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-token-meter": "^0.1.5-rc.2"
|
|
66
66
|
},
|
|
67
67
|
"dsh": {
|
|
68
68
|
"bundle": {
|
|
@@ -99,7 +99,8 @@
|
|
|
99
99
|
"postgres-interval": "1.2.0",
|
|
100
100
|
"split2": "4.2.0",
|
|
101
101
|
"tinyrainbow": "3.1.1",
|
|
102
|
-
"xtend": "4.0.2"
|
|
102
|
+
"xtend": "4.0.2",
|
|
103
|
+
"zod": "4.6.1"
|
|
103
104
|
},
|
|
104
105
|
"scripts": {
|
|
105
106
|
"build": "pnpm exec tsdown"
|
|
@@ -12,7 +12,6 @@ import { JsonBlock } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
|
12
12
|
import type { ChatNodeViewProps, ChatViewSlotProps } from "@deepseek-ai/dsh-client-ui-chat/client";
|
|
13
13
|
import type { RenderMessageImages } from "@deepseek-ai/dsh-client-ui-conversation/client";
|
|
14
14
|
import { MessageIconActions } from "./MessageIconActions.tsx";
|
|
15
|
-
import { MessageEditDialog } from "./MessageEditDialog.tsx";
|
|
16
15
|
import css from "./MessageItem.module.css";
|
|
17
16
|
import type { EditableMessageBlock } from "../../shared.ts";
|
|
18
17
|
import type { SessionEditorFace } from "../controller.ts";
|
|
@@ -104,11 +103,11 @@ export const UserMessageNodeView = memo(function UserMessageNodeView({
|
|
|
104
103
|
node,
|
|
105
104
|
renderMessageImages,
|
|
106
105
|
t,
|
|
107
|
-
|
|
106
|
+
recall,
|
|
108
107
|
retry,
|
|
109
108
|
}: ChatNodeViewProps<"user" | "steering"> & InjectFace<SessionEditorFace>) {
|
|
110
109
|
const data = node.data;
|
|
111
|
-
const [
|
|
110
|
+
const [confirmingRecall, setConfirmingRecall] = useState<EditableMessageBlock | null>(null);
|
|
112
111
|
const [confirmingRetry, setConfirmingRetry] = useState(false);
|
|
113
112
|
const turnLocation =
|
|
114
113
|
node.location.kind === "turn" || node.location.kind === "step" ? node.location.turn : undefined;
|
|
@@ -125,7 +124,7 @@ export const UserMessageNodeView = memo(function UserMessageNodeView({
|
|
|
125
124
|
textBlock === undefined || turn === undefined
|
|
126
125
|
? undefined
|
|
127
126
|
: () => {
|
|
128
|
-
|
|
127
|
+
setConfirmingRecall({
|
|
129
128
|
key: `${node.anchorSeq}:${String(textBlockIndex)}`,
|
|
130
129
|
turn,
|
|
131
130
|
eventSeq: node.anchorSeq,
|
|
@@ -144,11 +143,30 @@ export const UserMessageNodeView = memo(function UserMessageNodeView({
|
|
|
144
143
|
: undefined;
|
|
145
144
|
return (
|
|
146
145
|
<>
|
|
147
|
-
{
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
146
|
+
{confirmingRecall !== null && (
|
|
147
|
+
<Modal
|
|
148
|
+
open
|
|
149
|
+
onClose={() => setConfirmingRecall(null)}
|
|
150
|
+
title="编辑消息"
|
|
151
|
+
closeLabel="关闭"
|
|
152
|
+
description="将撤回该消息及其之后的对话内容到输入框,请修改后重新发送。"
|
|
153
|
+
footer={
|
|
154
|
+
<div className={css.confirmActions}>
|
|
155
|
+
<Button variant="outline" onClick={() => setConfirmingRecall(null)}>
|
|
156
|
+
取消
|
|
157
|
+
</Button>
|
|
158
|
+
<Button
|
|
159
|
+
variant="primary"
|
|
160
|
+
onClick={() => {
|
|
161
|
+
const target = confirmingRecall;
|
|
162
|
+
setConfirmingRecall(null);
|
|
163
|
+
void recall(target);
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
撤回并编辑
|
|
167
|
+
</Button>
|
|
168
|
+
</div>
|
|
169
|
+
}
|
|
152
170
|
/>
|
|
153
171
|
)}
|
|
154
172
|
{confirmingRetry && turn !== undefined && (
|
package/src/client/controller.ts
CHANGED
|
@@ -26,7 +26,7 @@ function editorApiPath(): string {
|
|
|
26
26
|
export interface SessionEditorState {
|
|
27
27
|
status: "idle" | "loading" | "ready" | "error";
|
|
28
28
|
error: string | null;
|
|
29
|
-
pending: VersionOperation | "import" | null;
|
|
29
|
+
pending: VersionOperation | "recall" | "import" | null;
|
|
30
30
|
timeline: SessionEditorTimeline | null;
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -42,6 +42,7 @@ export interface SessionEditorFace {
|
|
|
42
42
|
retry(turn: number, cascade: "truncate" | "preserve"): Promise<boolean>;
|
|
43
43
|
reroll(): Promise<boolean>;
|
|
44
44
|
rewind(toBoundary: number): Promise<boolean>;
|
|
45
|
+
recall(message: EditableMessageBlock): Promise<boolean>;
|
|
45
46
|
|
|
46
47
|
importSession(file: File): Promise<boolean>;
|
|
47
48
|
openVersion(sessionId: string): Promise<void>;
|
|
@@ -105,6 +106,11 @@ export class SessionEditorController {
|
|
|
105
106
|
reroll: () => this.mutate({ action: "reroll", sessionId: this.sessionId }),
|
|
106
107
|
rewind: (toBoundary) =>
|
|
107
108
|
this.mutate({ action: "rewind", sessionId: this.sessionId, toBoundary }),
|
|
109
|
+
recall: (message) =>
|
|
110
|
+
this.mutate(
|
|
111
|
+
{ action: "recall", sessionId: this.sessionId, eventSeq: message.eventSeq },
|
|
112
|
+
() => this.setComposerDraft(message.text),
|
|
113
|
+
),
|
|
108
114
|
importSession: (file) => this.importSession(file),
|
|
109
115
|
openVersion: (sessionId) => this.openWhenListed(sessionId as SessionId),
|
|
110
116
|
};
|
|
@@ -196,7 +202,10 @@ export class SessionEditorController {
|
|
|
196
202
|
}
|
|
197
203
|
}
|
|
198
204
|
|
|
199
|
-
private async mutate(
|
|
205
|
+
private async mutate(
|
|
206
|
+
operation: SessionEditorOperation,
|
|
207
|
+
onApplied?: () => void,
|
|
208
|
+
): Promise<boolean> {
|
|
200
209
|
// 只拦截并发操作;不要求 status === "ready"——编辑/重试的数据来自客户端
|
|
201
210
|
// conversation 节点,与 timeline 加载无关,status 为 idle 时也可发起请求。
|
|
202
211
|
const current = this.store.getSnapshot();
|
|
@@ -222,6 +231,9 @@ export class SessionEditorController {
|
|
|
222
231
|
this.store.update((state) => {
|
|
223
232
|
state.pending = null;
|
|
224
233
|
});
|
|
234
|
+
// recall 的草稿回填必须在刷新(resync / 整页重载)之前:草稿经会话级
|
|
235
|
+
// 持久化 mirror 落 store,整页重载后仍能恢复。
|
|
236
|
+
onApplied?.();
|
|
225
237
|
const result = value as SessionEditorOperationResult;
|
|
226
238
|
// 仅 fork 产生新 id 时需要等列表发布并打开新版本;就地编辑不改 id。
|
|
227
239
|
if (String(result.sessionId) !== String(this.sessionId)) {
|
|
@@ -266,6 +278,22 @@ export class SessionEditorController {
|
|
|
266
278
|
}
|
|
267
279
|
}
|
|
268
280
|
|
|
281
|
+
/**
|
|
282
|
+
* 把撤回的消息文本回填到该会话的 composer 草稿(`conversation.input` 的
|
|
283
|
+
* `setDraft`,即主输入框的整段替换语义)。会话 scope / binding / conversation
|
|
284
|
+
* 服务缺失时静默跳过——撤回本身已成功,草稿回填失败不应回滚会话状态。
|
|
285
|
+
*/
|
|
286
|
+
private setComposerDraft(text: string): void {
|
|
287
|
+
// binding 缺失时 input.for 会抛(会话 scope 已拆除),先短路。
|
|
288
|
+
if (this.sessions.binding(this.sessionId) === undefined) return;
|
|
289
|
+
const scoped = this.sessions.scope(this.sessionId);
|
|
290
|
+
if (scoped === undefined) return;
|
|
291
|
+
const conversation = scoped.get("conversation") as
|
|
292
|
+
| { input?: { for(ctx: ClientContext): { setDraft(text: string): void } } }
|
|
293
|
+
| undefined;
|
|
294
|
+
conversation?.input?.for(scoped).setDraft(text);
|
|
295
|
+
}
|
|
296
|
+
|
|
269
297
|
private async importSession(file: File): Promise<boolean> {
|
|
270
298
|
const current = this.store.getSnapshot();
|
|
271
299
|
if (current.pending !== null) return false;
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import type {
|
|
18
18
|
EditOperation,
|
|
19
19
|
EditableMessageBlock,
|
|
20
|
+
RecallOperation,
|
|
20
21
|
RerollOperation,
|
|
21
22
|
RetryOperation,
|
|
22
23
|
RetryableTurn,
|
|
@@ -43,6 +44,7 @@ export type { CascadePolicy, EditableBlockKind } from "@morlay/session-branch";
|
|
|
43
44
|
export type {
|
|
44
45
|
EditableMessageBlock,
|
|
45
46
|
EditOperation,
|
|
47
|
+
RecallOperation,
|
|
46
48
|
RerollOperation,
|
|
47
49
|
RetryOperation,
|
|
48
50
|
RetryableTurn,
|
|
@@ -250,6 +252,10 @@ export class SessionEditor extends Service {
|
|
|
250
252
|
return this.branchOperation(operation, signal);
|
|
251
253
|
}
|
|
252
254
|
|
|
255
|
+
recall(operation: RecallOperation, signal?: AbortSignal): Promise<SessionEditorResult> {
|
|
256
|
+
return this.recallOperation(operation, signal);
|
|
257
|
+
}
|
|
258
|
+
|
|
253
259
|
async editableMessages(
|
|
254
260
|
sessionId: SessionId,
|
|
255
261
|
signal?: AbortSignal,
|
|
@@ -386,6 +392,57 @@ export class SessionEditor extends Service {
|
|
|
386
392
|
};
|
|
387
393
|
}
|
|
388
394
|
|
|
395
|
+
/**
|
|
396
|
+
* 撤回(recall):只 rewind 截断,不重写、不重放——被撤回的 user 消息文本
|
|
397
|
+
* 由客户端回填到输入框,交给用户修改后重新发送。
|
|
398
|
+
*
|
|
399
|
+
* 边界与 edit 的轮首 user 一致:轮首消息整轮截断到前一轮 `turn/end`
|
|
400
|
+
* (无前轮则 -1),轮内 followup 只截断到该消息本身(exclusive drop),
|
|
401
|
+
* 避免留下悬空 `turn/start` 让下一次发送开到错误轮号。
|
|
402
|
+
*/
|
|
403
|
+
private async recallOperation(
|
|
404
|
+
operation: RecallOperation,
|
|
405
|
+
signal?: AbortSignal,
|
|
406
|
+
): Promise<SessionEditorResult> {
|
|
407
|
+
signal?.throwIfAborted();
|
|
408
|
+
const events = await this.readEvents(operation.sessionId, signal);
|
|
409
|
+
const turns = closedTurns(events);
|
|
410
|
+
const turnIndex = turns.findIndex(
|
|
411
|
+
(turn) =>
|
|
412
|
+
operation.eventSeq > turn.startSeq &&
|
|
413
|
+
(turn.endSeq === undefined || operation.eventSeq < turn.endSeq),
|
|
414
|
+
);
|
|
415
|
+
const turn = turns[turnIndex];
|
|
416
|
+
if (turn === undefined) {
|
|
417
|
+
throw new SessionBranchError("所选消息不属于已落定回合。", "INVALID_BOUNDARY");
|
|
418
|
+
}
|
|
419
|
+
const userIndex = turn.users.findIndex((user) => user.seq === operation.eventSeq);
|
|
420
|
+
if (userIndex === -1) {
|
|
421
|
+
throw new SessionBranchError("所选消息不存在或不可撤回。", "INVALID_BOUNDARY");
|
|
422
|
+
}
|
|
423
|
+
const preceding = precedingContentIndex(turns, turnIndex);
|
|
424
|
+
const boundary =
|
|
425
|
+
userIndex === 0 ? (preceding < 0 ? -1 : turns[preceding]!.endSeq!) : operation.eventSeq;
|
|
426
|
+
// live agent 运行中时先等其停下:rewind 会截断其 session 内存 log。
|
|
427
|
+
await this.prepareIdle(operation.sessionId, signal);
|
|
428
|
+
await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);
|
|
429
|
+
return {
|
|
430
|
+
sessionId: operation.sessionId,
|
|
431
|
+
queuedTurns: 0,
|
|
432
|
+
live: this.ctx.sessions.get(operation.sessionId) !== undefined,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** rewind 前等待驻留 agent 到达 quiescence(无 agent / 无 live owner 时空操作)。 */
|
|
437
|
+
private async prepareIdle(sessionId: SessionId, signal?: AbortSignal): Promise<void> {
|
|
438
|
+
signal?.throwIfAborted();
|
|
439
|
+
const agents = this.ctx.get("agents") as EditorAgentRegistry | undefined;
|
|
440
|
+
const existing = agents?.get(sessionId);
|
|
441
|
+
if (existing === undefined) return;
|
|
442
|
+
await existing.whenIdle();
|
|
443
|
+
signal?.throwIfAborted();
|
|
444
|
+
}
|
|
445
|
+
|
|
389
446
|
private async readEvents(
|
|
390
447
|
sessionId: SessionId,
|
|
391
448
|
signal?: AbortSignal,
|
|
@@ -546,6 +603,12 @@ function decodeOperation(value: unknown): SessionEditorOperation {
|
|
|
546
603
|
sessionId,
|
|
547
604
|
toBoundary: integerOf(record["toBoundary"], "toBoundary"),
|
|
548
605
|
};
|
|
606
|
+
case "recall":
|
|
607
|
+
return {
|
|
608
|
+
action: "recall",
|
|
609
|
+
sessionId,
|
|
610
|
+
eventSeq: integerOf(record["eventSeq"], "eventSeq"),
|
|
611
|
+
};
|
|
549
612
|
case "fork":
|
|
550
613
|
return {
|
|
551
614
|
action: "fork",
|
|
@@ -556,7 +619,7 @@ function decodeOperation(value: unknown): SessionEditorOperation {
|
|
|
556
619
|
: { childSessionId: sessionIdOf(record["childSessionId"]) }),
|
|
557
620
|
};
|
|
558
621
|
default:
|
|
559
|
-
throw new TypeError("action 必须是 edit、reroll、retry、rewind 或 fork。");
|
|
622
|
+
throw new TypeError("action 必须是 edit、reroll、retry、rewind、recall 或 fork。");
|
|
560
623
|
}
|
|
561
624
|
}
|
|
562
625
|
|
|
@@ -629,6 +692,14 @@ async function runOperation(
|
|
|
629
692
|
case "rewind":
|
|
630
693
|
await editor.rewind(operation.sessionId, operation.toBoundary);
|
|
631
694
|
return { sessionId: operation.sessionId, queuedTurns: 0 };
|
|
695
|
+
case "recall": {
|
|
696
|
+
const result = await editor.recall(operation);
|
|
697
|
+
return {
|
|
698
|
+
sessionId: result.sessionId,
|
|
699
|
+
queuedTurns: result.queuedTurns,
|
|
700
|
+
...(result.live === undefined ? {} : { live: result.live }),
|
|
701
|
+
};
|
|
702
|
+
}
|
|
632
703
|
case "fork":
|
|
633
704
|
return {
|
|
634
705
|
sessionId: await editor.fork(
|
package/src/shared.ts
CHANGED
|
@@ -33,6 +33,12 @@ export interface RewindOperation {
|
|
|
33
33
|
toBoundary: number;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export interface RecallOperation {
|
|
37
|
+
action: "recall";
|
|
38
|
+
sessionId: SessionId;
|
|
39
|
+
eventSeq: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
export interface ForkOperation {
|
|
37
43
|
action: "fork";
|
|
38
44
|
sessionId: SessionId;
|
|
@@ -45,6 +51,7 @@ export type SessionEditorOperation =
|
|
|
45
51
|
| RerollOperation
|
|
46
52
|
| RetryOperation
|
|
47
53
|
| RewindOperation
|
|
54
|
+
| RecallOperation
|
|
48
55
|
| ForkOperation;
|
|
49
56
|
|
|
50
57
|
export interface SessionEditorOperationResult {
|
package/src/types.ts
CHANGED
|
@@ -31,6 +31,13 @@ export interface RewindOperation {
|
|
|
31
31
|
toBoundary: number;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export interface RecallOperation {
|
|
35
|
+
action: "recall";
|
|
36
|
+
sessionId: SessionId;
|
|
37
|
+
|
|
38
|
+
eventSeq: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
export interface ForkOperation {
|
|
35
42
|
action: "fork";
|
|
36
43
|
sessionId: SessionId;
|
|
@@ -43,6 +50,7 @@ export type SessionEditorOperation =
|
|
|
43
50
|
| RerollOperation
|
|
44
51
|
| RetryOperation
|
|
45
52
|
| RewindOperation
|
|
53
|
+
| RecallOperation
|
|
46
54
|
| ForkOperation;
|
|
47
55
|
|
|
48
56
|
export interface SessionEditorResult {
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
.input {
|
|
2
|
-
/* composer-card 同款视觉(ui-conversation InputBar.module.css 的 .card):
|
|
3
|
-
圆角胶囊 + 输入 surface token + 阴影,编辑弹窗与主输入框观感一致。 */
|
|
4
|
-
width: 100%;
|
|
5
|
-
min-height: 96px;
|
|
6
|
-
box-sizing: border-box;
|
|
7
|
-
font: inherit;
|
|
8
|
-
font-size: 16px;
|
|
9
|
-
line-height: 24px;
|
|
10
|
-
padding: 10px 16px;
|
|
11
|
-
border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
|
|
12
|
-
border-radius: 22px;
|
|
13
|
-
background: var(--dsw-specific-input-major);
|
|
14
|
-
box-shadow: var(--dsw-shadow-lv2);
|
|
15
|
-
color: var(--dsw-text, inherit);
|
|
16
|
-
resize: none;
|
|
17
|
-
overflow-y: auto;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.actions {
|
|
21
|
-
display: flex;
|
|
22
|
-
justify-content: flex-end;
|
|
23
|
-
gap: 8px;
|
|
24
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { useRef, useState } from "react";
|
|
2
|
-
import { Button, Modal } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
3
|
-
import type { EditableMessageBlock } from "../../shared.ts";
|
|
4
|
-
import css from "./MessageEditDialog.module.css";
|
|
5
|
-
|
|
6
|
-
const BLOCK_TITLE: Record<EditableMessageBlock["kind"], string> = {
|
|
7
|
-
user: "编辑用户消息",
|
|
8
|
-
"assistant.reasoning": "编辑助手思考",
|
|
9
|
-
"assistant.response": "编辑助手回复",
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export function MessageEditDialog({
|
|
13
|
-
block,
|
|
14
|
-
onSave,
|
|
15
|
-
onClose,
|
|
16
|
-
}: {
|
|
17
|
-
block: EditableMessageBlock;
|
|
18
|
-
onSave: (text: string) => Promise<boolean>;
|
|
19
|
-
onClose: () => void;
|
|
20
|
-
}) {
|
|
21
|
-
const [text, setText] = useState(block.text);
|
|
22
|
-
const [saving, setSaving] = useState(false);
|
|
23
|
-
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
24
|
-
|
|
25
|
-
const autosize = (): void => {
|
|
26
|
-
const el = inputRef.current;
|
|
27
|
-
if (el === null) return;
|
|
28
|
-
el.style.height = "auto";
|
|
29
|
-
el.style.height = `${el.scrollHeight}px`;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const save = (): void => {
|
|
33
|
-
if (saving) return;
|
|
34
|
-
setSaving(true);
|
|
35
|
-
void onSave(text).then((applied) => {
|
|
36
|
-
if (applied) onClose();
|
|
37
|
-
else setSaving(false);
|
|
38
|
-
});
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<Modal
|
|
43
|
-
open
|
|
44
|
-
onClose={onClose}
|
|
45
|
-
title={BLOCK_TITLE[block.kind]}
|
|
46
|
-
closeLabel="关闭"
|
|
47
|
-
footer={
|
|
48
|
-
<div className={css.actions}>
|
|
49
|
-
<Button variant="outline" onClick={onClose} disabled={saving}>
|
|
50
|
-
取消
|
|
51
|
-
</Button>
|
|
52
|
-
<Button variant="primary" onClick={save} disabled={saving}>
|
|
53
|
-
{saving ? "保存中…" : "保存"}
|
|
54
|
-
</Button>
|
|
55
|
-
</div>
|
|
56
|
-
}
|
|
57
|
-
>
|
|
58
|
-
<textarea
|
|
59
|
-
ref={inputRef}
|
|
60
|
-
className={css.input}
|
|
61
|
-
value={text}
|
|
62
|
-
onChange={(event) => {
|
|
63
|
-
setText(event.target.value);
|
|
64
|
-
autosize();
|
|
65
|
-
}}
|
|
66
|
-
autoFocus
|
|
67
|
-
rows={4}
|
|
68
|
-
/>
|
|
69
|
-
</Modal>
|
|
70
|
-
);
|
|
71
|
-
}
|