@morlay/ui-conversation-message-actions 0.0.10 → 0.0.12

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.
@@ -0,0 +1,74 @@
1
+ import { useCallback, useRef, useState, type ReactElement } from "react";
2
+ import { Button, IconProps } from "@deepseek-ai/dsh-client-ui-primitives";
3
+ import type { SessionEditorState } from "./controller.ts";
4
+
5
+ export interface SessionImportActionProps {
6
+ useSessionEditor: <S>(sel: (s: SessionEditorState) => S) => S;
7
+
8
+ importSession: (file: File) => Promise<boolean>;
9
+ }
10
+
11
+ export const IconUpload = ({ size = 14, className }: IconProps) => (
12
+ <svg
13
+ width={size}
14
+ height={size}
15
+ className={className}
16
+ viewBox="0 0 24 24"
17
+ fill="none"
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ stroke="currentColor"
20
+ stroke-width="2"
21
+ stroke-linecap="round"
22
+ stroke-linejoin="round"
23
+ >
24
+ <path d="M12 3v12" />
25
+ <path d="m17 8-5-5-5 5" />
26
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
27
+ </svg>
28
+ );
29
+
30
+ export function SessionImportAction({
31
+ useSessionEditor,
32
+ importSession,
33
+ }: SessionImportActionProps): ReactElement {
34
+ const inputRef = useRef<HTMLInputElement>(null);
35
+ const importing = useSessionEditor((s) => s.pending === "import");
36
+ const error = useSessionEditor((s) => s.error);
37
+ const [busy, setBusy] = useState(false);
38
+
39
+ const onPick = useCallback(
40
+ (file: File | undefined) => {
41
+ if (file === undefined) return;
42
+ setBusy(true);
43
+ void importSession(file).finally(() => setBusy(false));
44
+ },
45
+ [importSession],
46
+ );
47
+
48
+ const working = busy || importing;
49
+ return (
50
+ <>
51
+ <Button
52
+ size="sm"
53
+ icon={<IconUpload />}
54
+ disabled={working}
55
+ aria-busy={working}
56
+ aria-label={working ? "导入中…" : "导入会话"}
57
+ title={error ?? "导入会话:用导出的 zip 覆盖当前会话内容"}
58
+ onClick={() => inputRef.current?.click()}
59
+ >
60
+ <input
61
+ ref={inputRef}
62
+ type="file"
63
+ accept=".zip,application/zip"
64
+ style={{ display: "none" }}
65
+ onChange={(e) => {
66
+ const file = e.target.files?.[0];
67
+ e.target.value = "";
68
+ onPick(file);
69
+ }}
70
+ />
71
+ </Button>
72
+ </>
73
+ );
74
+ }
@@ -0,0 +1,50 @@
1
+ import type { Context } from "@deepseek-ai/cordis";
2
+ import type {} from "@deepseek-ai/dsh-client-ui-conversation/client";
3
+ import type {} from "@deepseek-ai/dsh-client-ui-chat/client";
4
+ import type {} from "@deepseek-ai/dsh-client-ui-renderer/client";
5
+ import type { SessionId } from "@deepseek-ai/dsh-session";
6
+ import { SessionEditorController } from "./controller.ts";
7
+ import { registerChatNodeRenderers } from "./chat-node/register.ts";
8
+ import { SessionImportAction } from "./import-action.tsx";
9
+
10
+ export const inject = ["slots", "conversation", "connection", "sessions"];
11
+
12
+ export function apply(ctx: Context): void {
13
+ const controllers = new Map<SessionId, SessionEditorController>();
14
+ const controllerFor = (sessionId: SessionId): SessionEditorController => {
15
+ let controller = controllers.get(sessionId);
16
+ if (controller === undefined) {
17
+ controller = new SessionEditorController(ctx, sessionId);
18
+ controllers.set(sessionId, controller);
19
+ }
20
+ return controller;
21
+ };
22
+
23
+ ctx.on("connection/reset", () => {
24
+ for (const controller of controllers.values()) void controller.load();
25
+ });
26
+
27
+ // 新版 `conversation.chat.node` 是 keyed slot(reuse key 即替换该节点的
28
+ // 渲染器);register 内部经 ctx.slots.inject 等待声明。
29
+ registerChatNodeRenderers(ctx, controllerFor);
30
+
31
+ // 会话头部「导入会话」入口:导出按钮旁(header.utilities list 槽),
32
+ // 用导出的 zip 覆盖当前会话内容(host 端 `/api/session.import`)。
33
+ ctx.slots.inject("conversation.session.header.utilities", () =>
34
+ ctx.slots.register(
35
+ {
36
+ name: "conversation.session.header.utilities",
37
+ id: "session-editor.import",
38
+ inject: (sessionId: SessionId) => {
39
+ const face = controllerFor(sessionId).face;
40
+ return {
41
+ hooks: face.hooks,
42
+ // face 方法是闭包(已绑定 this),直接透传。
43
+ importSession: (file: File) => face.importSession(file),
44
+ };
45
+ },
46
+ } as never,
47
+ SessionImportAction as never,
48
+ ),
49
+ );
50
+ }