@awiki/dsh-plugin 0.2.0-rc.4

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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +138 -0
  3. package/README.zh.md +114 -0
  4. package/THIRD_PARTY_NOTICES.md +21 -0
  5. package/cordis.patch.yml +26 -0
  6. package/lib/client.js +8398 -0
  7. package/lib/client.js.map +1 -0
  8. package/lib/index.js +1497 -0
  9. package/lib/invariant.js +16 -0
  10. package/lib/provider.js +22 -0
  11. package/lib/sdk-adapter-DPPZhs6p.mjs +316 -0
  12. package/lib/summary-provider.js +192 -0
  13. package/lib/typert.host.d.ts +3 -0
  14. package/lib/typert.host.js +1201 -0
  15. package/lib/typert.remote-client.d.ts +54 -0
  16. package/lib/typert.remote-client.d.ts.map +1 -0
  17. package/lib/typert.remote-client.js +817 -0
  18. package/lib/types/client/AwikiOverlay.d.ts +22 -0
  19. package/lib/types/client/AwikiOverlay.d.ts.map +1 -0
  20. package/lib/types/client/AwikiOverlay.js +689 -0
  21. package/lib/types/client/AwikiOverlay.js.map +1 -0
  22. package/lib/types/client/AwikiSettingsSection.d.ts +25 -0
  23. package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -0
  24. package/lib/types/client/AwikiSettingsSection.js +119 -0
  25. package/lib/types/client/AwikiSettingsSection.js.map +1 -0
  26. package/lib/types/client/assets.d.ts +3 -0
  27. package/lib/types/client/assets.d.ts.map +1 -0
  28. package/lib/types/client/assets.js +70 -0
  29. package/lib/types/client/assets.js.map +1 -0
  30. package/lib/types/client/controller.d.ts +194 -0
  31. package/lib/types/client/controller.d.ts.map +1 -0
  32. package/lib/types/client/controller.js +701 -0
  33. package/lib/types/client/controller.js.map +1 -0
  34. package/lib/types/client/file.d.ts +20 -0
  35. package/lib/types/client/file.d.ts.map +1 -0
  36. package/lib/types/client/file.js +41 -0
  37. package/lib/types/client/file.js.map +1 -0
  38. package/lib/types/client/index.d.ts +16 -0
  39. package/lib/types/client/index.d.ts.map +1 -0
  40. package/lib/types/client/index.js +129 -0
  41. package/lib/types/client/index.js.map +1 -0
  42. package/lib/types/client/settings-controller.d.ts +28 -0
  43. package/lib/types/client/settings-controller.d.ts.map +1 -0
  44. package/lib/types/client/settings-controller.js +139 -0
  45. package/lib/types/client/settings-controller.js.map +1 -0
  46. package/lib/types/client/settings-locales.d.ts +13 -0
  47. package/lib/types/client/settings-locales.d.ts.map +1 -0
  48. package/lib/types/client/settings-locales.js +68 -0
  49. package/lib/types/client/settings-locales.js.map +1 -0
  50. package/lib/types/client/slots.d.ts +74 -0
  51. package/lib/types/client/slots.d.ts.map +1 -0
  52. package/lib/types/client/slots.js +3 -0
  53. package/lib/types/client/slots.js.map +1 -0
  54. package/lib/types/client/store.d.ts +18 -0
  55. package/lib/types/client/store.d.ts.map +1 -0
  56. package/lib/types/client/store.js +17 -0
  57. package/lib/types/client/store.js.map +1 -0
  58. package/lib/types/client.d.ts +3 -0
  59. package/lib/types/client.d.ts.map +1 -0
  60. package/lib/types/client.js +3 -0
  61. package/lib/types/client.js.map +1 -0
  62. package/lib/types/domain.d.ts +10 -0
  63. package/lib/types/domain.d.ts.map +1 -0
  64. package/lib/types/domain.js +18 -0
  65. package/lib/types/domain.js.map +1 -0
  66. package/lib/types/index.d.ts +188 -0
  67. package/lib/types/index.d.ts.map +1 -0
  68. package/lib/types/index.js +861 -0
  69. package/lib/types/index.js.map +1 -0
  70. package/lib/types/invariant.d.ts +9 -0
  71. package/lib/types/invariant.d.ts.map +1 -0
  72. package/lib/types/invariant.js +14 -0
  73. package/lib/types/invariant.js.map +1 -0
  74. package/lib/types/provider-api.d.ts +70 -0
  75. package/lib/types/provider-api.d.ts.map +1 -0
  76. package/lib/types/provider-api.js +3 -0
  77. package/lib/types/provider-api.js.map +1 -0
  78. package/lib/types/provider.d.ts +9 -0
  79. package/lib/types/provider.d.ts.map +1 -0
  80. package/lib/types/provider.js +20 -0
  81. package/lib/types/provider.js.map +1 -0
  82. package/lib/types/sdk-adapter.d.ts +46 -0
  83. package/lib/types/sdk-adapter.d.ts.map +1 -0
  84. package/lib/types/sdk-adapter.js +327 -0
  85. package/lib/types/sdk-adapter.js.map +1 -0
  86. package/lib/types/session.d.ts +14 -0
  87. package/lib/types/session.d.ts.map +1 -0
  88. package/lib/types/session.js +76 -0
  89. package/lib/types/session.js.map +1 -0
  90. package/lib/types/settings-rpc-contract.d.ts +34 -0
  91. package/lib/types/settings-rpc-contract.d.ts.map +1 -0
  92. package/lib/types/settings-rpc-contract.js +62 -0
  93. package/lib/types/settings-rpc-contract.js.map +1 -0
  94. package/lib/types/settings-rpc.d.ts +6 -0
  95. package/lib/types/settings-rpc.d.ts.map +1 -0
  96. package/lib/types/settings-rpc.js +147 -0
  97. package/lib/types/settings-rpc.js.map +1 -0
  98. package/lib/types/settings.d.ts +13 -0
  99. package/lib/types/settings.d.ts.map +1 -0
  100. package/lib/types/settings.js +15 -0
  101. package/lib/types/settings.js.map +1 -0
  102. package/lib/types/summary-provider-api.d.ts +45 -0
  103. package/lib/types/summary-provider-api.d.ts.map +1 -0
  104. package/lib/types/summary-provider-api.js +3 -0
  105. package/lib/types/summary-provider-api.js.map +1 -0
  106. package/lib/types/summary-provider.d.ts +48 -0
  107. package/lib/types/summary-provider.d.ts.map +1 -0
  108. package/lib/types/summary-provider.js +208 -0
  109. package/lib/types/summary-provider.js.map +1 -0
  110. package/lib/types/tools.d.ts +20 -0
  111. package/lib/types/tools.d.ts.map +1 -0
  112. package/lib/types/tools.js +139 -0
  113. package/lib/types/tools.js.map +1 -0
  114. package/lib/types/types.d.ts +292 -0
  115. package/lib/types/types.d.ts.map +1 -0
  116. package/lib/types/types.js +6 -0
  117. package/lib/types/types.js.map +1 -0
  118. package/package.json +179 -0
@@ -0,0 +1,689 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /** AWiki trigger, identity registration, and direct/group messaging drawer. */
3
+ import { useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
4
+ import { IconChevronLeftOutline14, IconCheckOutline16, IconChecklistOutline14, IconChevronDownOutline14, IconCloseOutline16, IconCopyOutline16, IconDataOutline16, IconDownloadOutline16, IconEditOutline16, IconGlobeOutline14, IconGoalOutline16, IconLoadingOutline16, IconPaperclipOutline16, IconPlusOutline16, IconRefreshOutline16, IconRefreshOutline14, IconSendOutline16, IconSparkle16, IconUserOutline16, Button, Menu, Modal, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives';
5
+ import { AWIKI_ME_APP_ICON_DATA_URL } from "./assets.js";
6
+ import { createAttachmentObjectUrl, fileToBase64, saveDownloadedAttachment } from "./file.js";
7
+ import css from './AwikiOverlay.module.css';
8
+ export const AWIKI_LAUNCHER_POSITION_KEY = 'dsh-awiki-launcher-position-v1';
9
+ const LAUNCHER_SIZE = 48;
10
+ const LAUNCHER_EDGE_GAP = 8;
11
+ const LAUNCHER_RIGHT_OFFSET = 28;
12
+ const LAUNCHER_BOTTOM_CLEARANCE = 152;
13
+ const LAUNCHER_DRAG_THRESHOLD = 4;
14
+ const DRAWER_LONG_PRESS_MS = 300;
15
+ const DRAWER_ANCHOR_GAP = 8;
16
+ const DRAWER_EDGE_GAP = 8;
17
+ const DRAWER_NOMINAL_WIDTH = 720;
18
+ const DRAWER_NOMINAL_HEIGHT = 720;
19
+ const DRAWER_HORIZONTAL_RESERVE = 80;
20
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
21
+ /** Keep the floating launcher fully reachable inside the current viewport. */
22
+ export function clampAwikiLauncherPosition(position, width, height) {
23
+ return {
24
+ left: Math.min(Math.max(position.left, LAUNCHER_EDGE_GAP), Math.max(LAUNCHER_EDGE_GAP, width - LAUNCHER_SIZE - LAUNCHER_EDGE_GAP)),
25
+ top: Math.min(Math.max(position.top, LAUNCHER_EDGE_GAP), Math.max(LAUNCHER_EDGE_GAP, height - LAUNCHER_SIZE - LAUNCHER_EDGE_GAP)),
26
+ };
27
+ }
28
+ function overflowAmount(position, panelWidth, panelHeight, viewportWidth, viewportHeight) {
29
+ return Math.max(0, DRAWER_EDGE_GAP - position.left)
30
+ + Math.max(0, DRAWER_EDGE_GAP - position.top)
31
+ + Math.max(0, position.left + panelWidth + DRAWER_EDGE_GAP - viewportWidth)
32
+ + Math.max(0, position.top + panelHeight + DRAWER_EDGE_GAP - viewportHeight);
33
+ }
34
+ /** Place the chat panel in the launcher corner quadrant with the least viewport overflow. */
35
+ export function resolveAwikiDrawerPlacement(launcher, panelWidth, panelHeight, viewportWidth, viewportHeight, preferredDirection) {
36
+ const candidates = [
37
+ {
38
+ direction: 'upper-left',
39
+ left: launcher.left - panelWidth - DRAWER_ANCHOR_GAP,
40
+ top: launcher.top - panelHeight - DRAWER_ANCHOR_GAP,
41
+ },
42
+ {
43
+ direction: 'upper-right',
44
+ left: launcher.left + LAUNCHER_SIZE + DRAWER_ANCHOR_GAP,
45
+ top: launcher.top - panelHeight - DRAWER_ANCHOR_GAP,
46
+ },
47
+ {
48
+ direction: 'lower-left',
49
+ left: launcher.left - panelWidth - DRAWER_ANCHOR_GAP,
50
+ top: launcher.top + LAUNCHER_SIZE + DRAWER_ANCHOR_GAP,
51
+ },
52
+ {
53
+ direction: 'lower-right',
54
+ left: launcher.left + LAUNCHER_SIZE + DRAWER_ANCHOR_GAP,
55
+ top: launcher.top + LAUNCHER_SIZE + DRAWER_ANCHOR_GAP,
56
+ },
57
+ ];
58
+ const leastOverflow = candidates.reduce((best, candidate) => (overflowAmount(candidate, panelWidth, panelHeight, viewportWidth, viewportHeight)
59
+ < overflowAmount(best, panelWidth, panelHeight, viewportWidth, viewportHeight)
60
+ ? candidate
61
+ : best));
62
+ const selected = preferredDirection === undefined
63
+ ? leastOverflow
64
+ : (candidates.find(candidate => candidate.direction === preferredDirection) ?? leastOverflow);
65
+ return {
66
+ direction: selected.direction,
67
+ left: Math.min(Math.max(selected.left, DRAWER_EDGE_GAP), Math.max(DRAWER_EDGE_GAP, viewportWidth - panelWidth - DRAWER_EDGE_GAP)),
68
+ top: Math.min(Math.max(selected.top, DRAWER_EDGE_GAP), Math.max(DRAWER_EDGE_GAP, viewportHeight - panelHeight - DRAWER_EDGE_GAP)),
69
+ };
70
+ }
71
+ function defaultLauncherPosition() {
72
+ return clampAwikiLauncherPosition({
73
+ left: window.innerWidth - LAUNCHER_SIZE - LAUNCHER_RIGHT_OFFSET,
74
+ top: window.innerHeight - LAUNCHER_SIZE - LAUNCHER_BOTTOM_CLEARANCE,
75
+ }, window.innerWidth, window.innerHeight);
76
+ }
77
+ function readLauncherPosition() {
78
+ try {
79
+ const stored = window.sessionStorage.getItem(AWIKI_LAUNCHER_POSITION_KEY);
80
+ if (stored !== null) {
81
+ const value = JSON.parse(stored);
82
+ const { left, top } = value;
83
+ if (typeof left === 'number' && Number.isFinite(left) && typeof top === 'number' && Number.isFinite(top)) {
84
+ return clampAwikiLauncherPosition({ left, top }, window.innerWidth, window.innerHeight);
85
+ }
86
+ }
87
+ }
88
+ catch {
89
+ // Storage can be unavailable in privacy-restricted browser contexts.
90
+ }
91
+ return defaultLauncherPosition();
92
+ }
93
+ function saveLauncherPosition(position) {
94
+ try {
95
+ window.sessionStorage.setItem(AWIKI_LAUNCHER_POSITION_KEY, JSON.stringify(position));
96
+ }
97
+ catch {
98
+ // The launcher remains usable when session storage is unavailable.
99
+ }
100
+ }
101
+ function callPointerCapture(target, method, pointerId) {
102
+ const capture = Reflect.get(target, method);
103
+ if (typeof capture === 'function')
104
+ Reflect.apply(capture, target, [pointerId]);
105
+ }
106
+ /** Format one Host timestamp for compact local display. */
107
+ function time(value) {
108
+ return new Intl.DateTimeFormat('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(value);
109
+ }
110
+ /** Show a clock within 24 hours, otherwise only the local calendar date. */
111
+ function conversationTime(value, now = Date.now()) {
112
+ const age = now - value;
113
+ return age >= 0 && age < ONE_DAY_MS
114
+ ? new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false }).format(value)
115
+ : new Intl.DateTimeFormat('zh-CN', { month: 'numeric', day: 'numeric' }).format(value);
116
+ }
117
+ /** Render the identity registration form and its OTP challenge transition. */
118
+ function Registration(props) {
119
+ const [phone, setPhone] = useState('');
120
+ const [handle, setHandle] = useState('');
121
+ const [otp, setOtp] = useState('');
122
+ const [otpSent, setOtpSent] = useState(false);
123
+ const [notice, setNotice] = useState(null);
124
+ const requestOtp = async () => {
125
+ const result = await props.sendRegistrationOtp({ handle: handle.trim(), phone: phone.trim() });
126
+ if (!result.ok)
127
+ return;
128
+ setOtpSent(true);
129
+ setNotice(`验证码已发送;${result.value.retryAfterSeconds} 秒后可重新获取。`);
130
+ };
131
+ const register = async () => {
132
+ /* v8 ignore next -- the registration action is rendered only after an OTP challenge starts. */
133
+ if (!otpSent)
134
+ return;
135
+ const result = await props.registerIdentity({
136
+ phone: phone.trim(), handle: handle.trim(), otp: otp.trim(),
137
+ });
138
+ if (!result.ok)
139
+ return;
140
+ setNotice(null);
141
+ };
142
+ return (_jsxs("div", { className: css.registration, children: [_jsx("div", { className: css.registrationIcon, children: _jsx(IconUserOutline16, { size: 24 }) }), _jsx("h3", { children: "\u6CE8\u518C AWiki \u8EAB\u4EFD" }), _jsx("p", { children: "\u8BE5\u8EAB\u4EFD\u7531\u5F53\u524D Harness \u90E8\u7F72\u4E2D\u7684\u5168\u90E8 Agent \u5171\u540C\u4F7F\u7528\u3002" }), _jsxs("label", { children: ["Handle", _jsx("input", { value: handle, onChange: (event) => { setHandle(event.target.value); }, autoComplete: "username", placeholder: "\u4F8B\u5982 alice" })] }), _jsxs("label", { children: ["\u624B\u673A\u53F7", _jsx("input", { value: phone, onChange: (event) => { setPhone(event.target.value); }, autoComplete: "tel" })] }), !otpSent ? (_jsx("button", { type: "button", className: css.primary, disabled: props.pending || phone.trim() === '' || handle.trim() === '', onClick: () => { void requestOtp(); }, children: "\u83B7\u53D6\u9A8C\u8BC1\u7801" })) : (_jsxs(_Fragment, { children: [_jsxs("label", { children: ["\u9A8C\u8BC1\u7801", _jsx("input", { value: otp, onChange: (event) => { setOtp(event.target.value); }, inputMode: "numeric", autoComplete: "one-time-code" })] }), _jsx("button", { type: "button", className: css.primary, disabled: props.pending || handle.trim() === '' || otp.trim() === '', onClick: () => { void register(); }, children: "\u6CE8\u518C\u8EAB\u4EFD" }), _jsx("button", { type: "button", className: css.linkButton, disabled: props.pending, onClick: () => { setOtpSent(false); setOtp(''); setNotice(null); }, children: "\u91CD\u65B0\u83B7\u53D6\u9A8C\u8BC1\u7801" })] })), notice !== null && _jsx("p", { className: css.notice, role: "status", children: notice })] }));
143
+ }
144
+ /** Let a signed-out installation resume its preserved local identity. */
145
+ function SignedOut(props) {
146
+ const [error, setError] = useState(null);
147
+ const login = async () => {
148
+ setError(null);
149
+ const result = await props.login();
150
+ if (!result.ok)
151
+ setError(result.error);
152
+ };
153
+ return (_jsxs("div", { className: css.centerState, children: [_jsx("div", { className: css.registrationIcon, children: _jsx(IconUserOutline16, { size: 24 }) }), _jsx("h3", { children: "\u5DF2\u9000\u51FA AWiki" }), _jsx("p", { children: "\u672C\u673A\u8EAB\u4EFD\u548C\u6D88\u606F\u6570\u636E\u4ECD\u5B89\u5168\u4FDD\u7559\u3002\u91CD\u65B0\u8FDB\u5165\u540E\u4F1A\u7EE7\u7EED\u4F7F\u7528\u539F\u6765\u7684 DID \u548C Handle\u3002" }), _jsx("button", { type: "button", className: css.primary, disabled: props.pending, onClick: () => { void login(); }, children: "\u91CD\u65B0\u8FDB\u5165" }), error !== null && _jsx("small", { className: css.inlineError, role: "alert", children: error })] }));
154
+ }
155
+ /** Prefer the peer WNS display name for a direct chat; groups keep their title. */
156
+ function conversationLabel(conversation) {
157
+ return conversation.kind === 'direct'
158
+ ? (conversation.displayName ?? conversation.title)
159
+ : conversation.title;
160
+ }
161
+ /** Show only the deployment identity's WNS display name, never its routing Handle. */
162
+ function identityLabel(identity) {
163
+ return identity.displayName ?? '未设置昵称';
164
+ }
165
+ /** Editable deployment identity summary shown above the conversation roster. */
166
+ function IdentityCard(props) {
167
+ const [editing, setEditing] = useState(false);
168
+ const [draft, setDraft] = useState(props.identity.displayName ?? '');
169
+ const [error, setError] = useState(null);
170
+ useEffect(() => {
171
+ if (!editing)
172
+ setDraft(props.identity.displayName ?? '');
173
+ }, [editing, props.identity.displayName]);
174
+ const cancel = () => {
175
+ setDraft(props.identity.displayName ?? '');
176
+ setError(null);
177
+ setEditing(false);
178
+ };
179
+ const save = async () => {
180
+ const displayName = draft.trim();
181
+ const length = Array.from(displayName).length;
182
+ if (length === 0) {
183
+ setError('请输入昵称');
184
+ return;
185
+ }
186
+ if (length > 50) {
187
+ setError('昵称不能超过 50 个字符');
188
+ return;
189
+ }
190
+ setError(null);
191
+ const result = await props.updateDisplayName(displayName);
192
+ if (!result.ok) {
193
+ setError(result.error);
194
+ return;
195
+ }
196
+ setDraft(result.value.displayName ?? displayName);
197
+ setEditing(false);
198
+ };
199
+ return (_jsxs("div", { className: css.identityCard, children: [_jsx("div", { className: css.identityNameRow, children: editing ? (_jsxs("form", { className: css.identityEditor, onSubmit: (event) => { event.preventDefault(); void save(); }, children: [_jsx("input", { "aria-label": "\u6635\u79F0", autoFocus: true, disabled: props.pending, value: draft, onChange: (event) => { setDraft(event.target.value); }, onKeyDown: (event) => {
200
+ if (event.key === 'Escape') {
201
+ event.stopPropagation();
202
+ cancel();
203
+ }
204
+ } }), _jsx("button", { type: "submit", "aria-label": "\u4FDD\u5B58\u6635\u79F0", disabled: props.pending, children: _jsx(IconCheckOutline16, { size: 14 }) }), _jsx("button", { type: "button", "aria-label": "\u53D6\u6D88\u4FEE\u6539\u6635\u79F0", disabled: props.pending, onClick: cancel, children: _jsx(IconCloseOutline16, { size: 14 }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Tooltip, { label: props.identity.did, side: "bottom", children: _jsx("button", { type: "button", className: css.identityName, disabled: props.pending, onClick: () => { setError(null); setEditing(true); }, children: identityLabel(props.identity) }) }), _jsx(Tooltip, { label: "\u4FEE\u6539\u6635\u79F0", side: "right", children: _jsx("button", { type: "button", className: css.identityEdit, "aria-label": "\u4FEE\u6539\u6635\u79F0", disabled: props.pending, onClick: () => { setError(null); setEditing(true); }, children: _jsx(IconEditOutline16, { size: 14 }) }) })] })) }), _jsx("small", { className: css.identityHandle, children: props.identity.handle }), _jsxs("span", { className: css.identityStatus, children: [_jsx("i", {}), "\u5728\u7EBF"] }), error !== null && _jsx("small", { className: css.identityError, role: "alert", children: error })] }));
205
+ }
206
+ /** Incoming sender label: WNS display name, then Handle, then DID. */
207
+ function senderLabel(message, peerLabel) {
208
+ if (message.outgoing)
209
+ return '我';
210
+ return peerLabel ?? message.senderDisplayName ?? message.senderHandle ?? message.senderDid;
211
+ }
212
+ /** Render one direct or group conversation row. */
213
+ function ConversationRow(props) {
214
+ const label = conversationLabel(props.conversation);
215
+ const unreadCount = props.conversation.unreadCount ?? 0;
216
+ const unreadLabel = unreadCount > 99 ? '99+' : String(unreadCount);
217
+ const preview = props.conversation.lastMessagePreview ?? '暂无消息';
218
+ return (_jsxs("button", { type: "button", className: css.conversationRow, "data-active": props.active || undefined, "aria-label": unreadCount > 0 ? `${label},${unreadCount} 条未读消息` : undefined, onClick: props.onSelect, children: [_jsxs("span", { className: css.avatar, children: [props.conversation.kind === 'direct' ? '私' : '群', unreadCount > 0 && _jsx("span", { className: css.conversationUnreadBadge, "aria-hidden": "true", children: unreadLabel })] }), _jsxs("span", { className: css.conversationText, children: [_jsxs("span", { className: css.conversationHeader, children: [_jsx("strong", { children: label }), props.conversation.lastMessageAt !== undefined && (_jsx("time", { className: css.conversationTime, children: conversationTime(props.conversation.lastMessageAt) }))] }), _jsx("small", { children: preview })] })] }));
219
+ }
220
+ /** Render one AWiki message, including an attachment download action. */
221
+ function MessageRow(props) {
222
+ const [error, setError] = useState(null);
223
+ const [preview, setPreview] = useState(null);
224
+ const [previewLoading, setPreviewLoading] = useState(false);
225
+ const imageAttachmentId = props.message.content.kind === 'attachment' && props.message.content.attachment.mimeType.startsWith('image/')
226
+ ? props.message.content.attachment.id
227
+ : null;
228
+ useEffect(() => {
229
+ if (imageAttachmentId === null)
230
+ return;
231
+ let disposed = false;
232
+ let objectUrl = null;
233
+ setPreview(null);
234
+ setPreviewLoading(true);
235
+ setError(null);
236
+ void props.download(props.message.id, imageAttachmentId).then((result) => {
237
+ if (disposed)
238
+ return;
239
+ setPreviewLoading(false);
240
+ if (!result.ok) {
241
+ setError(result.error);
242
+ return;
243
+ }
244
+ objectUrl = createAttachmentObjectUrl(result.value);
245
+ setPreview({ url: objectUrl, value: result.value });
246
+ });
247
+ return () => {
248
+ disposed = true;
249
+ if (objectUrl !== null)
250
+ URL.revokeObjectURL(objectUrl);
251
+ };
252
+ }, [imageAttachmentId, props.download, props.message.id]);
253
+ const download = async () => {
254
+ /* v8 ignore next -- only attachment content renders the button that invokes this closure. */
255
+ if (props.message.content.kind !== 'attachment')
256
+ return;
257
+ if (preview !== null) {
258
+ saveDownloadedAttachment(preview.value);
259
+ return;
260
+ }
261
+ const result = await props.download(props.message.id, props.message.content.attachment.id);
262
+ if (!result.ok) {
263
+ setError(result.error);
264
+ return;
265
+ }
266
+ saveDownloadedAttachment(result.value);
267
+ };
268
+ return (_jsxs("div", { className: css.message, "data-message-id": props.message.id, "data-outgoing": props.message.outgoing || undefined, children: [_jsxs("div", { className: css.messageMeta, children: [_jsx("span", { children: senderLabel(props.message, props.peerLabel) }), _jsx("time", { children: time(props.message.sentAt) })] }), props.message.content.kind === 'text' ? (_jsx("p", { children: props.message.content.text })) : preview !== null ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: css.imageAttachment, "aria-label": `下载图片 ${props.message.content.attachment.fileName}`, onClick: () => { void download(); }, children: [_jsx("img", { src: preview.url, alt: props.message.content.attachment.fileName, onLoad: () => { props.onImageLoad?.(props.message.id); } }), _jsxs("span", { children: [_jsx("strong", { children: props.message.content.attachment.fileName }), _jsxs("small", { children: [props.message.content.attachment.size, " \u5B57\u8282"] }), _jsx(IconDownloadOutline16, { size: 16 })] })] }), props.message.content.caption !== undefined && _jsx("p", { className: css.caption, children: props.message.content.caption })] })) : (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: css.attachment, disabled: previewLoading, onClick: () => { void download(); }, children: [_jsxs("span", { children: [_jsx("strong", { children: props.message.content.attachment.fileName }), _jsx("small", { children: previewLoading ? '正在加载图片预览…' : `${props.message.content.attachment.size} 字节` })] }), _jsx(IconDownloadOutline16, { size: 16 })] }), props.message.content.caption !== undefined && _jsx("p", { className: css.caption, children: props.message.content.caption })] })), error !== null && _jsx("small", { className: css.inlineError, children: error })] }));
269
+ }
270
+ function summaryRangeLabel(summary) {
271
+ const scope = summary.range.kind === 'unread' ? '未读以来' : '最近消息';
272
+ const formatter = new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false });
273
+ return `${scope} · ${summary.range.messageCount} 条消息 · ${formatter.format(summary.range.startedAt)}–${formatter.format(summary.range.endedAt)}`;
274
+ }
275
+ function copiedSummary(summary) {
276
+ const sections = [
277
+ 'AI 对话总结',
278
+ `范围:${summaryRangeLabel(summary)}`,
279
+ '',
280
+ '重点',
281
+ ...summary.highlights.map(item => `- ${item}`),
282
+ '',
283
+ '结论',
284
+ ...summary.conclusions.map(item => `- ${item}`),
285
+ '',
286
+ '待办',
287
+ ...summary.todos.map(item => `- ${item.owner === undefined ? '' : `${item.owner}:`}${item.text}`),
288
+ ];
289
+ return sections.join('\n');
290
+ }
291
+ /** Render every user-visible summary state without obscuring history or the composer. */
292
+ function SummaryPanel(props) {
293
+ const [copyState, setCopyState] = useState('idle');
294
+ const result = props.summary.result;
295
+ useEffect(() => { setCopyState('idle'); }, [result]);
296
+ if (props.summary.collapsed) {
297
+ return (_jsx("div", { id: props.id, className: css.summaryPanel, "data-collapsed": true, children: _jsxs("button", { type: "button", className: css.summaryCollapsed, "aria-label": "\u5C55\u5F00 AI \u5BF9\u8BDD\u603B\u7ED3", "aria-expanded": "false", onClick: () => { props.collapse(false); }, children: [_jsxs("span", { children: [_jsx(IconSparkle16, { size: 14 }), "AI \u5BF9\u8BDD\u603B\u7ED3"] }), result !== undefined && _jsx("small", { children: summaryRangeLabel(result) }), props.summary.stale && _jsx("em", { children: "\u6709\u65B0\u6D88\u606F" }), _jsx(IconChevronDownOutline14, { size: 14 })] }) }));
298
+ }
299
+ const copy = async () => {
300
+ if (result === undefined)
301
+ return;
302
+ try {
303
+ await navigator.clipboard.writeText(copiedSummary(result));
304
+ setCopyState('copied');
305
+ }
306
+ catch {
307
+ setCopyState('error');
308
+ }
309
+ };
310
+ return (_jsxs("section", { id: props.id, className: css.summaryPanel, "aria-label": "AI \u5BF9\u8BDD\u603B\u7ED3", "aria-live": "polite", children: [_jsxs("header", { className: css.summaryHeader, children: [_jsxs("span", { children: [_jsx(IconSparkle16, { size: 15 }), _jsx("strong", { children: "AI \u5BF9\u8BDD\u603B\u7ED3" })] }), result !== undefined && _jsx("small", { children: summaryRangeLabel(result) }), _jsx("button", { type: "button", "aria-label": "\u6298\u53E0 AI \u5BF9\u8BDD\u603B\u7ED3", "aria-expanded": "true", onClick: () => { props.collapse(true); }, children: _jsx(IconChevronDownOutline14, { size: 14 }) })] }), props.summary.status === 'loading' && (_jsxs("div", { className: css.summaryLoading, role: "status", children: [_jsx(IconLoadingOutline16, { size: 18 }), _jsxs("span", { children: [_jsx("strong", { children: "\u6B63\u5728\u6574\u7406\u8FD9\u6BB5\u5BF9\u8BDD\u2026" }), _jsx("small", { children: "\u53EA\u4F1A\u5904\u7406\u672C\u6B21\u9009\u62E9\u7684\u6D88\u606F\u8303\u56F4" })] })] })), props.summary.status === 'error' && (_jsxs("div", { className: css.summaryError, role: "alert", children: [_jsx("span", { children: props.summary.error ?? '暂时无法生成 AI 总结。' }), _jsxs("button", { type: "button", "aria-label": "\u91CD\u65B0\u751F\u6210 AI \u603B\u7ED3", onClick: props.regenerate, children: [_jsx(IconRefreshOutline14, { size: 14 }), "\u91CD\u65B0\u751F\u6210"] })] })), props.summary.status === 'success' && result !== undefined && (_jsxs(_Fragment, { children: [props.summary.stale && (_jsxs("div", { className: css.summaryStale, role: "status", children: [_jsx("span", { children: "\u6709\u65B0\u6D88\u606F\uFF0C\u5F53\u524D\u603B\u7ED3\u5DF2\u8FC7\u671F" }), _jsx("button", { type: "button", "aria-label": "\u6839\u636E\u65B0\u6D88\u606F\u91CD\u65B0\u751F\u6210 AI \u603B\u7ED3", onClick: props.regenerate, children: "\u91CD\u65B0\u751F\u6210" })] })), _jsxs("div", { className: css.summaryBody, children: [_jsxs("div", { className: css.summarySection, children: [_jsxs("h4", { children: [_jsx(IconGoalOutline16, { size: 15 }), "\u91CD\u70B9"] }), result.highlights.length === 0 ? _jsx("p", { children: "\u6682\u65E0\u660E\u786E\u91CD\u70B9" }) : _jsx("ul", { children: result.highlights.map(item => _jsx("li", { children: item }, item)) })] }), _jsxs("div", { className: css.summarySection, children: [_jsxs("h4", { children: [_jsx(IconCheckOutline16, { size: 15 }), "\u7ED3\u8BBA"] }), result.conclusions.length === 0 ? _jsx("p", { children: "\u6682\u65E0\u660E\u786E\u7ED3\u8BBA" }) : _jsx("ul", { children: result.conclusions.map(item => _jsx("li", { children: item }, item)) })] }), _jsxs("div", { className: css.summarySection, children: [_jsxs("h4", { children: [_jsx(IconChecklistOutline14, { size: 15 }), "\u5F85\u529E"] }), result.todos.length === 0 ? _jsx("p", { children: "\u6682\u65E0\u5F85\u529E" }) : _jsx("ul", { children: result.todos.map(item => _jsxs("li", { children: [item.owner === undefined ? '' : _jsxs("b", { children: [item.owner, "\uFF1A"] }), item.text] }, `${item.owner ?? ''}:${item.text}`)) })] })] }), _jsxs("footer", { className: css.summaryActions, children: [_jsx("button", { type: "button", onClick: () => { props.viewSource(result.range.firstMessageId); }, children: "\u67E5\u770B\u539F\u6D88\u606F" }), _jsx("span", {}), _jsxs("button", { type: "button", "aria-label": "\u91CD\u65B0\u751F\u6210 AI \u603B\u7ED3", onClick: props.regenerate, children: [_jsx(IconRefreshOutline14, { size: 14 }), "\u91CD\u65B0\u751F\u6210"] }), _jsxs("button", { type: "button", onClick: () => { void copy(); }, children: [_jsx(IconCopyOutline16, { size: 14 }), copyState === 'copied' ? '已复制' : '复制'] })] }), copyState === 'error' && _jsx("div", { className: css.summaryCopyError, role: "alert", children: "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002" }), _jsxs("div", { className: css.summaryPrivacy, children: [_jsx(IconDataOutline16, { size: 14 }), "\u4EC5\u53D1\u9001\u6240\u9009\u8303\u56F4\u7684\u6587\u672C\u4E0E\u9644\u4EF6\u5143\u6570\u636E\uFF0C\u4E0D\u53D1\u9001\u9644\u4EF6\u6587\u4EF6"] })] }))] }));
311
+ }
312
+ /** Render the conversation roster, history, composer, and one-file picker. */
313
+ function Chat(props) {
314
+ const { view } = props;
315
+ const [text, setText] = useState('');
316
+ const [file, setFile] = useState(null);
317
+ const [previewUrl, setPreviewUrl] = useState(null);
318
+ const [fileError, setFileError] = useState(null);
319
+ const input = useRef(null);
320
+ const history = useRef(null);
321
+ const previousConversationId = useRef(null);
322
+ const conversationAwaitingBottom = useRef(null);
323
+ const pendingInitialImages = useRef(new Set());
324
+ const selected = view.conversations.find(value => value.id === view.selectedConversationId);
325
+ const summary = selected === undefined ? undefined : view.summaries[selected.id];
326
+ const summaryPanelId = useId();
327
+ useLayoutEffect(() => {
328
+ const conversationId = view.selectedConversationId;
329
+ if (conversationId !== previousConversationId.current) {
330
+ previousConversationId.current = conversationId;
331
+ conversationAwaitingBottom.current = conversationId;
332
+ pendingInitialImages.current.clear();
333
+ }
334
+ if (conversationId === null
335
+ || conversationAwaitingBottom.current !== conversationId
336
+ || view.messages.length === 0
337
+ || history.current === null)
338
+ return;
339
+ pendingInitialImages.current = new Set(view.messages.flatMap(message => (message.content.kind === 'attachment' && message.content.attachment.mimeType.startsWith('image/')
340
+ ? [message.id]
341
+ : [])));
342
+ history.current.scrollTop = history.current.scrollHeight;
343
+ if (pendingInitialImages.current.size === 0)
344
+ conversationAwaitingBottom.current = null;
345
+ }, [view.messages, view.selectedConversationId]);
346
+ const scrollAfterInitialImage = (messageId) => {
347
+ if (selected === undefined || conversationAwaitingBottom.current !== selected.id)
348
+ return;
349
+ if (!pendingInitialImages.current.delete(messageId))
350
+ return;
351
+ if (history.current !== null)
352
+ history.current.scrollTop = history.current.scrollHeight;
353
+ if (pendingInitialImages.current.size === 0)
354
+ conversationAwaitingBottom.current = null;
355
+ };
356
+ const viewSummarySource = (messageId) => {
357
+ if (selected === undefined)
358
+ return;
359
+ props.setSummaryCollapsed(selected.id, true);
360
+ requestAnimationFrame(() => {
361
+ const node = [...(history.current?.querySelectorAll('[data-message-id]') ?? [])]
362
+ .find(candidate => candidate.dataset.messageId === messageId);
363
+ if (node === undefined)
364
+ return;
365
+ node.scrollIntoView({ block: 'center' });
366
+ node.tabIndex = -1;
367
+ node.focus({ preventScroll: true });
368
+ });
369
+ };
370
+ useEffect(() => {
371
+ if (file === null || !file.type.startsWith('image/')) {
372
+ setPreviewUrl(null);
373
+ return;
374
+ }
375
+ const url = URL.createObjectURL(file);
376
+ setPreviewUrl(url);
377
+ return () => { URL.revokeObjectURL(url); };
378
+ }, [file]);
379
+ const clearFile = () => {
380
+ setFile(null);
381
+ setFileError(null);
382
+ /* v8 ignore else -- the clear action is available only while the mounted file input owns the selection. */
383
+ if (input.current !== null)
384
+ input.current.value = '';
385
+ };
386
+ const sendMessage = async () => {
387
+ const draft = text.trim();
388
+ if (file === null) {
389
+ /* v8 ignore next -- the only invocation control is disabled while both text and attachment are empty. */
390
+ if (draft === '')
391
+ return;
392
+ const result = await props.sendText(draft);
393
+ if (result.ok)
394
+ setText('');
395
+ return;
396
+ }
397
+ if (file.size > view.attachmentMaxBytes) {
398
+ setFileError(`附件不能超过 ${view.attachmentMaxBytes} 字节。`);
399
+ return;
400
+ }
401
+ setFileError(null);
402
+ const bytesBase64 = await fileToBase64(file);
403
+ const result = await props.sendAttachment({
404
+ fileName: file.name,
405
+ mimeType: file.type || 'application/octet-stream',
406
+ bytesBase64,
407
+ ...(draft === '' ? {} : { caption: draft }),
408
+ });
409
+ if (result.ok) {
410
+ clearFile();
411
+ setText('');
412
+ }
413
+ };
414
+ return (_jsxs("div", { className: css.chat, children: [_jsxs("aside", { className: css.roster, "data-hidden": selected !== undefined || undefined, children: [_jsx(IdentityCard, { identity: view.identity, pending: view.pending !== null, updateDisplayName: props.updateDisplayName }), _jsx("div", { className: css.rosterTitle, children: "\u4F1A\u8BDD" }), _jsxs("div", { className: css.conversationList, children: [view.conversations.map(conversation => (_jsx(ConversationRow, { conversation: conversation, active: conversation.id === view.selectedConversationId, onSelect: () => { void props.selectConversation(conversation.id); } }, conversation.id))), view.conversations.length === 0 && _jsx("p", { className: css.empty, children: "\u8FD8\u6CA1\u6709\u53EF\u7528\u7684\u79C1\u804A\u6216\u7FA4\u804A\u3002" })] }), view.conversationsHasMore && _jsx("button", { type: "button", className: css.more, onClick: () => { void props.loadMoreConversations(); }, children: "\u52A0\u8F7D\u66F4\u591A\u4F1A\u8BDD" })] }), _jsx("section", { className: css.thread, "data-visible": selected !== undefined || undefined, children: selected === undefined ? (_jsxs("div", { className: css.threadEmpty, children: [_jsx(IconGlobeOutline14, { size: 28 }), _jsx("p", { children: "\u9009\u62E9\u4E00\u4E2A\u79C1\u804A\u6216\u7FA4\u804A\u67E5\u770B\u6D88\u606F\u3002" })] })) : (_jsxs(_Fragment, { children: [_jsxs("header", { className: css.threadHeader, children: [_jsx("button", { type: "button", className: css.back, "aria-label": "\u8FD4\u56DE\u4F1A\u8BDD\u5217\u8868", onClick: () => { void props.selectConversation(null); }, children: _jsx(IconChevronLeftOutline14, {}) }), _jsxs("div", { className: css.threadTitle, children: [_jsx("strong", { children: conversationLabel(selected) }), _jsx("small", { children: selected.kind === 'direct' ? '私聊' : '群聊' })] }), _jsxs("button", { type: "button", className: css.summaryTrigger, "aria-controls": summaryPanelId, "aria-expanded": summary === undefined ? undefined : !summary.collapsed, "aria-label": summary?.status === 'loading' ? '正在生成 AI 总结' : summary?.collapsed === true ? '展开 AI 总结' : '生成 AI 总结', disabled: summary?.status === 'loading', onClick: () => {
415
+ if (summary !== undefined && !summary.collapsed && summary.status !== 'error') {
416
+ props.setSummaryCollapsed(selected.id, true);
417
+ }
418
+ else if (summary?.collapsed === true) {
419
+ props.setSummaryCollapsed(selected.id, false);
420
+ }
421
+ else {
422
+ void props.summarizeConversation();
423
+ }
424
+ }, children: [summary?.status === 'loading' ? _jsx(IconLoadingOutline16, { size: 14 }) : _jsx(IconSparkle16, { size: 14 }), _jsx("span", { children: summary?.status === 'loading' ? '总结中' : 'AI 总结' }), summary !== undefined && _jsx(IconChevronDownOutline14, { size: 12 })] })] }), summary !== undefined && (_jsx(SummaryPanel, { id: summaryPanelId, summary: summary, regenerate: () => { void props.summarizeConversation(); }, collapse: collapsed => { props.setSummaryCollapsed(selected.id, collapsed); }, viewSource: viewSummarySource })), _jsxs("div", { ref: history, className: css.history, role: "log", "aria-label": "\u6D88\u606F\u8BB0\u5F55", children: [view.historyHasMore && _jsx("button", { type: "button", className: css.more, onClick: () => { void props.loadOlderHistory(); }, children: "\u52A0\u8F7D\u66F4\u65E9\u6D88\u606F" }), view.messages.map(message => (_jsx(MessageRow, { message: message, peerLabel: selected.kind === 'direct' ? conversationLabel(selected) : undefined, download: props.downloadAttachment, onImageLoad: scrollAfterInitialImage }, message.id))), view.messages.length === 0 && _jsx("p", { className: css.empty, children: "\u6682\u65E0\u6D88\u606F\u3002" })] }), _jsxs("div", { className: css.composer, children: [fileError !== null && _jsx("small", { className: css.inlineError, role: "alert", children: fileError }), _jsxs("div", { className: css.composeInput, children: [file !== null && (_jsxs("div", { className: css.filePreview, "data-image": previewUrl !== null || undefined, children: [previewUrl === null
425
+ ? _jsx("span", { className: css.filePreviewIcon, children: _jsx(IconPaperclipOutline16, {}) })
426
+ : _jsx("img", { src: previewUrl, alt: file.name }), previewUrl === null && _jsx("span", { className: css.filePreviewName, children: file.name }), _jsx("button", { type: "button", className: css.removeFile, "aria-label": `移除附件 ${file.name}`, onClick: clearFile, children: _jsx(IconCloseOutline16, { size: 12 }) })] })), _jsx("textarea", { value: text, onChange: (event) => { setText(event.target.value); }, placeholder: "\u8F93\u5165\u6D88\u606F", rows: 2 }), _jsxs("div", { className: css.composeActions, children: [_jsx(Tooltip, { label: "\u6DFB\u52A0\u9644\u4EF6", side: "top", children: _jsx("button", { type: "button", className: css.filePicker, "aria-label": "\u6DFB\u52A0\u9644\u4EF6", disabled: view.pending !== null, onClick: () => { input.current?.click(); }, children: _jsx(IconPaperclipOutline16, {}) }) }), _jsx("input", { ref: input, type: "file", className: css.fileInput, "aria-label": "\u9009\u62E9\u4E00\u4E2A\u9644\u4EF6", onChange: (event) => { setFile(event.target.files?.[0] ?? null); setFileError(null); } }), _jsx("button", { type: "button", className: css.send, "aria-label": "\u53D1\u9001\u6D88\u606F", disabled: view.pending !== null || (file === null && text.trim() === ''), onClick: () => { void sendMessage(); }, children: _jsx(IconSendOutline16, {}) })] })] })] })] })) })] }));
427
+ }
428
+ /**
429
+ * Render the frame-wide AWiki trigger and right-side drawer.
430
+ * @param props - slot-derived runtime, store, and injected AWiki operations.
431
+ * @returns the persistent trigger and the conditionally mounted drawer.
432
+ */
433
+ export function AwikiOverlay(props) {
434
+ const open = props.useStore(state => state.open);
435
+ const view = props.useAwiki(state => state);
436
+ const titleId = useId();
437
+ const composeTitleId = useId();
438
+ const [accountMenuOpen, setAccountMenuOpen] = useState(false);
439
+ const [menuOpen, setMenuOpen] = useState(false);
440
+ const [composeDirect, setComposeDirect] = useState(false);
441
+ const [peerHandle, setPeerHandle] = useState('');
442
+ const [composeError, setComposeError] = useState(null);
443
+ const [logoutOpen, setLogoutOpen] = useState(false);
444
+ const [logoutPending, setLogoutPending] = useState(false);
445
+ const [logoutError, setLogoutError] = useState(null);
446
+ const [launcherPosition, setLauncherPosition] = useState(readLauncherPosition);
447
+ const [launcherDragging, setLauncherDragging] = useState(false);
448
+ const [drawerDragging, setDrawerDragging] = useState(false);
449
+ const [drawerDragDirection, setDrawerDragDirection] = useState(null);
450
+ const launcherRef = useRef(null);
451
+ const rememberedConversationId = useRef(null);
452
+ const drawerWasOpen = useRef(open);
453
+ const suppressLauncherClick = useRef(false);
454
+ const launcherDrag = useRef(null);
455
+ const drawerDrag = useRef(null);
456
+ const registered = view.status === 'ready' && view.identity !== null;
457
+ const unreadCount = view.conversations.reduce((total, conversation) => total + (conversation.unreadCount ?? 0), 0);
458
+ const unreadLabel = unreadCount > 99 ? '99+' : String(unreadCount);
459
+ const drawerWidth = Math.min(DRAWER_NOMINAL_WIDTH, Math.max(1, window.innerWidth - DRAWER_HORIZONTAL_RESERVE));
460
+ const drawerHeight = Math.min(DRAWER_NOMINAL_HEIGHT, Math.max(1, window.innerHeight - DRAWER_EDGE_GAP * 2));
461
+ const drawerPlacement = resolveAwikiDrawerPlacement(launcherPosition, drawerWidth, drawerHeight, window.innerWidth, window.innerHeight, drawerDragDirection ?? undefined);
462
+ useEffect(() => {
463
+ void props.open();
464
+ return props.close;
465
+ }, [props.close, props.open]);
466
+ useEffect(() => () => {
467
+ const drag = drawerDrag.current;
468
+ if (drag !== null)
469
+ clearTimeout(drag.timer);
470
+ }, []);
471
+ useEffect(() => {
472
+ const wasOpen = drawerWasOpen.current;
473
+ drawerWasOpen.current = open;
474
+ if (open) {
475
+ if (view.selectedConversationId !== null) {
476
+ rememberedConversationId.current = view.selectedConversationId;
477
+ }
478
+ else if (!wasOpen && rememberedConversationId.current !== null) {
479
+ const remembered = rememberedConversationId.current;
480
+ if (view.conversations.some(conversation => conversation.id === remembered)) {
481
+ void props.selectConversation(remembered);
482
+ }
483
+ else {
484
+ rememberedConversationId.current = null;
485
+ }
486
+ }
487
+ return;
488
+ }
489
+ setAccountMenuOpen(false);
490
+ setMenuOpen(false);
491
+ setComposeDirect(false);
492
+ setPeerHandle('');
493
+ setComposeError(null);
494
+ const drag = drawerDrag.current;
495
+ if (drag !== null)
496
+ clearTimeout(drag.timer);
497
+ drawerDrag.current = null;
498
+ setDrawerDragging(false);
499
+ setDrawerDragDirection(null);
500
+ if (wasOpen && view.selectedConversationId !== null) {
501
+ rememberedConversationId.current = view.selectedConversationId;
502
+ void props.selectConversation(null);
503
+ }
504
+ }, [open, props.selectConversation, view.conversations, view.selectedConversationId]);
505
+ const selectConversation = (conversationId) => {
506
+ rememberedConversationId.current = conversationId;
507
+ return props.selectConversation(conversationId);
508
+ };
509
+ useEffect(() => {
510
+ if (!open)
511
+ return;
512
+ const onKeyDown = (event) => {
513
+ if (event.key !== 'Escape')
514
+ return;
515
+ if (logoutOpen) {
516
+ if (!logoutPending)
517
+ setLogoutOpen(false);
518
+ return;
519
+ }
520
+ if (composeDirect) {
521
+ setComposeDirect(false);
522
+ return;
523
+ }
524
+ if (menuOpen) {
525
+ setMenuOpen(false);
526
+ return;
527
+ }
528
+ if (accountMenuOpen) {
529
+ setAccountMenuOpen(false);
530
+ return;
531
+ }
532
+ props.actions.close();
533
+ launcherRef.current?.focus();
534
+ };
535
+ document.addEventListener('keydown', onKeyDown);
536
+ return () => { document.removeEventListener('keydown', onKeyDown); };
537
+ }, [open, accountMenuOpen, composeDirect, logoutOpen, logoutPending, menuOpen, props.actions]);
538
+ useEffect(() => {
539
+ const onResize = () => {
540
+ setLauncherPosition((current) => {
541
+ const next = clampAwikiLauncherPosition(current, window.innerWidth, window.innerHeight);
542
+ saveLauncherPosition(next);
543
+ return next;
544
+ });
545
+ };
546
+ window.addEventListener('resize', onResize);
547
+ return () => { window.removeEventListener('resize', onResize); };
548
+ }, []);
549
+ const onLauncherPointerDown = (event) => {
550
+ if (event.button !== 0)
551
+ return;
552
+ launcherDrag.current = {
553
+ pointerId: event.pointerId,
554
+ startX: event.clientX,
555
+ startY: event.clientY,
556
+ origin: launcherPosition,
557
+ moved: false,
558
+ current: launcherPosition,
559
+ };
560
+ callPointerCapture(event.currentTarget, 'setPointerCapture', event.pointerId);
561
+ };
562
+ const onLauncherPointerMove = (event) => {
563
+ const drag = launcherDrag.current;
564
+ if (drag === null || drag.pointerId !== event.pointerId)
565
+ return;
566
+ const deltaX = event.clientX - drag.startX;
567
+ const deltaY = event.clientY - drag.startY;
568
+ if (!drag.moved && Math.hypot(deltaX, deltaY) < LAUNCHER_DRAG_THRESHOLD)
569
+ return;
570
+ drag.moved = true;
571
+ setLauncherDragging(true);
572
+ drag.current = clampAwikiLauncherPosition({
573
+ left: drag.origin.left + deltaX,
574
+ top: drag.origin.top + deltaY,
575
+ }, window.innerWidth, window.innerHeight);
576
+ setLauncherPosition(drag.current);
577
+ };
578
+ const finishLauncherDrag = (event) => {
579
+ const drag = launcherDrag.current;
580
+ if (drag === null || drag.pointerId !== event.pointerId)
581
+ return;
582
+ if (drag.moved) {
583
+ suppressLauncherClick.current = true;
584
+ saveLauncherPosition(drag.current);
585
+ }
586
+ launcherDrag.current = null;
587
+ setLauncherDragging(false);
588
+ callPointerCapture(event.currentTarget, 'releasePointerCapture', event.pointerId);
589
+ };
590
+ const onDrawerPointerDown = (event) => {
591
+ if (event.button !== 0
592
+ || event.target.closest('button, input, textarea, a, [role="button"]') !== null)
593
+ return;
594
+ const drag = {
595
+ pointerId: event.pointerId,
596
+ startX: event.clientX,
597
+ startY: event.clientY,
598
+ origin: launcherPosition,
599
+ timer: undefined,
600
+ armed: false,
601
+ moved: false,
602
+ current: launcherPosition,
603
+ };
604
+ drag.timer = setTimeout(() => {
605
+ if (drawerDrag.current !== drag)
606
+ return;
607
+ drag.armed = true;
608
+ setDrawerDragging(true);
609
+ setDrawerDragDirection(drawerPlacement.direction);
610
+ }, DRAWER_LONG_PRESS_MS);
611
+ drawerDrag.current = drag;
612
+ callPointerCapture(event.currentTarget, 'setPointerCapture', event.pointerId);
613
+ };
614
+ const onDrawerPointerMove = (event) => {
615
+ const drag = drawerDrag.current;
616
+ if (drag === null || drag.pointerId !== event.pointerId)
617
+ return;
618
+ const deltaX = event.clientX - drag.startX;
619
+ const deltaY = event.clientY - drag.startY;
620
+ if (!drag.armed) {
621
+ if (Math.hypot(deltaX, deltaY) < LAUNCHER_DRAG_THRESHOLD)
622
+ return;
623
+ clearTimeout(drag.timer);
624
+ drawerDrag.current = null;
625
+ callPointerCapture(event.currentTarget, 'releasePointerCapture', event.pointerId);
626
+ return;
627
+ }
628
+ event.preventDefault();
629
+ drag.moved = true;
630
+ drag.current = clampAwikiLauncherPosition({
631
+ left: drag.origin.left + deltaX,
632
+ top: drag.origin.top + deltaY,
633
+ }, window.innerWidth, window.innerHeight);
634
+ setLauncherPosition(drag.current);
635
+ };
636
+ const finishDrawerDrag = (event) => {
637
+ const drag = drawerDrag.current;
638
+ if (drag === null || drag.pointerId !== event.pointerId)
639
+ return;
640
+ clearTimeout(drag.timer);
641
+ if (drag.moved)
642
+ saveLauncherPosition(drag.current);
643
+ drawerDrag.current = null;
644
+ setDrawerDragging(false);
645
+ setDrawerDragDirection(null);
646
+ callPointerCapture(event.currentTarget, 'releasePointerCapture', event.pointerId);
647
+ };
648
+ const toggleLauncher = () => {
649
+ if (suppressLauncherClick.current) {
650
+ suppressLauncherClick.current = false;
651
+ return;
652
+ }
653
+ props.actions.toggle();
654
+ };
655
+ const startDirect = async () => {
656
+ setComposeError(null);
657
+ const result = await props.startDirectChat(peerHandle);
658
+ if (!result.ok) {
659
+ setComposeError(result.error);
660
+ return;
661
+ }
662
+ setComposeDirect(false);
663
+ setPeerHandle('');
664
+ };
665
+ const logout = async () => {
666
+ setLogoutPending(true);
667
+ setLogoutError(null);
668
+ const result = await props.logout();
669
+ setLogoutPending(false);
670
+ if (!result.ok) {
671
+ setLogoutError(result.error);
672
+ return;
673
+ }
674
+ rememberedConversationId.current = null;
675
+ setLogoutOpen(false);
676
+ };
677
+ return (_jsxs(_Fragment, { children: [_jsxs("button", { ref: launcherRef, type: "button", className: css.trigger, style: { left: launcherPosition.left, top: launcherPosition.top }, "data-dragging": launcherDragging || undefined, "aria-label": open
678
+ ? '收起 AWiki'
679
+ : unreadCount > 0 ? `打开 AWiki,${unreadCount} 条未读消息` : '打开 AWiki', "aria-expanded": open, "aria-haspopup": "dialog", title: "AWiki", onClick: toggleLauncher, onPointerDown: onLauncherPointerDown, onPointerMove: onLauncherPointerMove, onPointerUp: finishLauncherDrag, onPointerCancel: finishLauncherDrag, children: [_jsx("img", { className: css.launcherIcon, src: AWIKI_ME_APP_ICON_DATA_URL, alt: "", "aria-hidden": "true", draggable: "false" }), unreadCount > 0 && _jsx("span", { className: css.unreadBadge, "aria-hidden": "true", children: unreadLabel })] }), open && (_jsxs("div", { className: css.drawer, style: { left: drawerPlacement.left, top: drawerPlacement.top }, "data-placement": drawerPlacement.direction, role: "dialog", "aria-modal": "false", "aria-labelledby": titleId, children: [_jsxs("header", { className: css.drawerHeader, "data-dragging": drawerDragging || undefined, title: "\u957F\u6309\u62D6\u52A8 AWiki", onPointerDown: onDrawerPointerDown, onPointerMove: onDrawerPointerMove, onPointerUp: finishDrawerDrag, onPointerCancel: finishDrawerDrag, children: [_jsxs("div", { children: [registered ? (_jsx(Menu, { open: accountMenuOpen, onClose: () => { setAccountMenuOpen(false); }, align: "start", portal: true, compact: true, items: [{ id: 'logout', label: '退出登录', danger: true }], onSelect: () => {
680
+ setAccountMenuOpen(false);
681
+ setLogoutError(null);
682
+ setLogoutOpen(true);
683
+ }, anchor: (_jsx("button", { type: "button", "aria-label": "AWiki \u8D26\u6237\u83DC\u5355", "aria-expanded": accountMenuOpen, "aria-haspopup": "menu", onClick: () => { setAccountMenuOpen(value => !value); }, children: _jsx(IconGlobeOutline14, { size: 18 }) })) })) : _jsx(IconGlobeOutline14, { size: 18 }), _jsx("h2", { id: titleId, children: "AWiki" })] }), registered && (_jsx(Menu, { open: menuOpen, onClose: () => { setMenuOpen(false); }, align: "end", portal: true, compact: true, items: [{ id: 'direct', label: '发起私聊' }], onSelect: () => {
684
+ setMenuOpen(false);
685
+ setComposeDirect(true);
686
+ }, anchor: (_jsx("button", { type: "button", "aria-label": "\u53D1\u8D77\u4F1A\u8BDD", "aria-expanded": menuOpen, "aria-haspopup": "menu", onClick: () => { setMenuOpen(value => !value); }, children: _jsx(IconPlusOutline16, {}) })) })), _jsx("button", { type: "button", "aria-label": "\u5237\u65B0 AWiki", disabled: view.pending !== null, onClick: () => { void props.open(); }, children: _jsx(IconRefreshOutline16, {}) }), _jsx("button", { type: "button", "aria-label": "\u5173\u95ED AWiki", onClick: props.actions.close, children: _jsx(IconCloseOutline16, {}) })] }), view.status === 'loading' && _jsx("div", { className: css.centerState, role: "status", children: "\u6B63\u5728\u8FDE\u63A5 AWiki\u2026" }), view.status === 'error' && _jsxs("div", { className: css.centerState, children: [_jsx("p", { children: view.error }), _jsx("button", { type: "button", className: css.primary, onClick: () => { void props.open(); }, children: "\u91CD\u8BD5" })] }), view.status === 'ready' && view.sessionStatus === 'unregistered' && _jsx(Registration, { ...props, pending: view.pending !== null }), view.status === 'ready' && view.sessionStatus === 'signed-out' && _jsx(SignedOut, { login: props.login, pending: view.pending !== null }), view.status === 'ready' && view.sessionStatus === 'active' && view.identity !== null && (_jsx(Chat, { ...props, selectConversation: selectConversation, view: { ...view, identity: view.identity } })), composeDirect && (_jsx("div", { className: css.composeBackdrop, children: _jsxs("form", { className: css.composeCard, role: "dialog", "aria-modal": "true", "aria-labelledby": composeTitleId, onSubmit: (event) => { event.preventDefault(); void startDirect(); }, children: [_jsx("h3", { id: composeTitleId, children: "\u53D1\u8D77\u79C1\u804A" }), _jsx("p", { children: "\u8F93\u5165\u5BF9\u65B9 Handle\u3002\u6253\u5F00\u4F1A\u8BDD\u524D\u4F1A\u5148\u786E\u8BA4\u8BE5\u7528\u6237\u5B58\u5728\u3002" }), _jsxs("label", { children: ["Handle", _jsx("input", { value: peerHandle, onChange: (event) => { setPeerHandle(event.target.value); setComposeError(null); }, autoComplete: "off", placeholder: "\u4F8B\u5982 alice", autoFocus: true })] }), view.pending === '查找用户' && _jsx("p", { role: "status", children: "\u6B63\u5728\u67E5\u627E\u7528\u6237\u2026" }), composeError !== null && _jsx("p", { className: css.inlineError, role: "alert", children: composeError }), _jsxs("div", { className: css.composeActions, children: [_jsx("button", { type: "button", className: css.secondary, onClick: () => { setComposeDirect(false); setComposeError(null); }, children: "\u53D6\u6D88" }), _jsx("button", { type: "submit", className: css.primary, disabled: view.pending !== null || peerHandle.trim() === '', children: "\u6253\u5F00\u4F1A\u8BDD" })] })] }) })), _jsx(Modal, { open: logoutOpen, onClose: () => { if (!logoutPending)
687
+ setLogoutOpen(false); }, title: "\u9000\u51FA\u767B\u5F55", closeLabel: "\u53D6\u6D88", description: "\u9000\u51FA\u540E\uFF0C\u672C\u673A\u5C06\u6682\u505C\u4F7F\u7528 AWiki\uFF1B\u8EAB\u4EFD\u548C\u672C\u5730\u6570\u636E\u90FD\u4F1A\u4FDD\u7559\u3002", footer: (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", variant: "outline", disabled: logoutPending, onClick: () => { setLogoutOpen(false); }, children: "\u53D6\u6D88" }), _jsx(Button, { type: "button", variant: "outline", className: css.logoutConfirm, disabled: logoutPending, onClick: () => { void logout(); }, children: logoutPending ? '正在退出…' : '确认退出' })] })), children: _jsxs("div", { className: css.logoutWarning, children: [_jsx("p", { children: "\u9000\u51FA\u671F\u95F4\uFF0CWeb UI \u548C Agent \u90FD\u4E0D\u80FD\u8BFB\u53D6\u4F1A\u8BDD\u6216\u4F7F\u7528\u8BE5\u8EAB\u4EFD\u53D1\u9001\u6D88\u606F\u3002" }), _jsx("p", { children: "\u7A0D\u540E\u70B9\u51FB\u201C\u91CD\u65B0\u8FDB\u5165\u201D\u5373\u53EF\u7531\u672C\u673A Rust SDK \u6062\u590D\u540C\u4E00\u4E2A DID\u3001Handle \u548C\u6D88\u606F\u6570\u636E\u5E93\u3002" }), logoutError !== null && _jsx("p", { className: css.inlineError, role: "alert", children: logoutError })] }) }), view.error !== null && view.status !== 'error' && _jsx("div", { className: css.error, role: "alert", children: view.error }), view.pending !== null && _jsxs("div", { className: css.pending, role: "status", children: [view.pending, "\u2026"] })] }))] }));
688
+ }
689
+ //# sourceMappingURL=AwikiOverlay.js.map