@awiki/dsh-plugin 0.2.4 → 0.3.0-rc.1

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 (142) hide show
  1. package/README.md +131 -22
  2. package/README.zh.md +110 -20
  3. package/THIRD_PARTY_NOTICES.md +8 -1
  4. package/cordis.patch.yml +5 -0
  5. package/lib/client.js +12534 -2647
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +2431 -92
  8. package/lib/model-proxy-contract.js +96 -0
  9. package/lib/provider.js +4 -1
  10. package/lib/sdk-adapter-DLhLrLI6.mjs +886 -0
  11. package/lib/typert.host.js +1269 -534
  12. package/lib/typert.remote-client.d.ts +47 -1
  13. package/lib/typert.remote-client.d.ts.map +1 -1
  14. package/lib/typert.remote-client.js +1254 -141
  15. package/lib/types/attachment-cache.d.ts +28 -0
  16. package/lib/types/attachment-cache.d.ts.map +1 -0
  17. package/lib/types/attachment-cache.js +194 -0
  18. package/lib/types/attachment-cache.js.map +1 -0
  19. package/lib/types/client/AwikiGroupDetails.d.ts +16 -0
  20. package/lib/types/client/AwikiGroupDetails.d.ts.map +1 -0
  21. package/lib/types/client/AwikiGroupDetails.js +108 -0
  22. package/lib/types/client/AwikiGroupDetails.js.map +1 -0
  23. package/lib/types/client/AwikiIdentityAccess.d.ts +21 -0
  24. package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -0
  25. package/lib/types/client/AwikiIdentityAccess.js +148 -0
  26. package/lib/types/client/AwikiIdentityAccess.js.map +1 -0
  27. package/lib/types/client/AwikiIdentityPage.d.ts +11 -0
  28. package/lib/types/client/AwikiIdentityPage.d.ts.map +1 -0
  29. package/lib/types/client/AwikiIdentityPage.js +8 -0
  30. package/lib/types/client/AwikiIdentityPage.js.map +1 -0
  31. package/lib/types/client/AwikiMail.d.ts +15 -0
  32. package/lib/types/client/AwikiMail.d.ts.map +1 -0
  33. package/lib/types/client/AwikiMail.js +380 -0
  34. package/lib/types/client/AwikiMail.js.map +1 -0
  35. package/lib/types/client/AwikiOnboarding.d.ts +22 -0
  36. package/lib/types/client/AwikiOnboarding.d.ts.map +1 -0
  37. package/lib/types/client/AwikiOnboarding.js +104 -0
  38. package/lib/types/client/AwikiOnboarding.js.map +1 -0
  39. package/lib/types/client/AwikiOverlay.d.ts +10 -0
  40. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  41. package/lib/types/client/AwikiOverlay.js +370 -127
  42. package/lib/types/client/AwikiOverlay.js.map +1 -1
  43. package/lib/types/client/AwikiProfileCard.d.ts +9 -0
  44. package/lib/types/client/AwikiProfileCard.d.ts.map +1 -0
  45. package/lib/types/client/AwikiProfileCard.js +90 -0
  46. package/lib/types/client/AwikiProfileCard.js.map +1 -0
  47. package/lib/types/client/AwikiRecoveryForm.d.ts +24 -0
  48. package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -0
  49. package/lib/types/client/AwikiRecoveryForm.js +148 -0
  50. package/lib/types/client/AwikiRecoveryForm.js.map +1 -0
  51. package/lib/types/client/AwikiSettingsSection.d.ts +18 -4
  52. package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
  53. package/lib/types/client/AwikiSettingsSection.js +277 -23
  54. package/lib/types/client/AwikiSettingsSection.js.map +1 -1
  55. package/lib/types/client/MentionText.d.ts +7 -0
  56. package/lib/types/client/MentionText.d.ts.map +1 -0
  57. package/lib/types/client/MentionText.js +10 -0
  58. package/lib/types/client/MentionText.js.map +1 -0
  59. package/lib/types/client/RechargeComingSoonDialog.d.ts +11 -0
  60. package/lib/types/client/RechargeComingSoonDialog.d.ts.map +1 -0
  61. package/lib/types/client/RechargeComingSoonDialog.js +8 -0
  62. package/lib/types/client/RechargeComingSoonDialog.js.map +1 -0
  63. package/lib/types/client/controller.d.ts +174 -4
  64. package/lib/types/client/controller.d.ts.map +1 -1
  65. package/lib/types/client/controller.js +975 -79
  66. package/lib/types/client/controller.js.map +1 -1
  67. package/lib/types/client/image-cache.d.ts +20 -0
  68. package/lib/types/client/image-cache.d.ts.map +1 -0
  69. package/lib/types/client/image-cache.js +177 -0
  70. package/lib/types/client/image-cache.js.map +1 -0
  71. package/lib/types/client/index.d.ts +2 -0
  72. package/lib/types/client/index.d.ts.map +1 -1
  73. package/lib/types/client/index.js +102 -42
  74. package/lib/types/client/index.js.map +1 -1
  75. package/lib/types/client/mail-list-cache.d.ts +15 -0
  76. package/lib/types/client/mail-list-cache.d.ts.map +1 -0
  77. package/lib/types/client/mail-list-cache.js +212 -0
  78. package/lib/types/client/mail-list-cache.js.map +1 -0
  79. package/lib/types/client/mentions.d.ts +51 -0
  80. package/lib/types/client/mentions.d.ts.map +1 -0
  81. package/lib/types/client/mentions.js +165 -0
  82. package/lib/types/client/mentions.js.map +1 -0
  83. package/lib/types/client/model-availability-controller.d.ts +27 -0
  84. package/lib/types/client/model-availability-controller.d.ts.map +1 -0
  85. package/lib/types/client/model-availability-controller.js +111 -0
  86. package/lib/types/client/model-availability-controller.js.map +1 -0
  87. package/lib/types/client/model-proxy-controller.d.ts +45 -0
  88. package/lib/types/client/model-proxy-controller.d.ts.map +1 -0
  89. package/lib/types/client/model-proxy-controller.js +306 -0
  90. package/lib/types/client/model-proxy-controller.js.map +1 -0
  91. package/lib/types/client/recharge-availability.d.ts +6 -0
  92. package/lib/types/client/recharge-availability.d.ts.map +1 -0
  93. package/lib/types/client/recharge-availability.js +6 -0
  94. package/lib/types/client/recharge-availability.js.map +1 -0
  95. package/lib/types/client/settings-locales.d.ts +1 -1
  96. package/lib/types/client/settings-locales.d.ts.map +1 -1
  97. package/lib/types/client/settings-locales.js +186 -8
  98. package/lib/types/client/settings-locales.js.map +1 -1
  99. package/lib/types/client/slots.d.ts +34 -2
  100. package/lib/types/client/slots.d.ts.map +1 -1
  101. package/lib/types/index.d.ts +95 -7
  102. package/lib/types/index.d.ts.map +1 -1
  103. package/lib/types/index.js +968 -37
  104. package/lib/types/index.js.map +1 -1
  105. package/lib/types/listener-state.d.ts +23 -0
  106. package/lib/types/listener-state.d.ts.map +1 -0
  107. package/lib/types/listener-state.js +124 -0
  108. package/lib/types/listener-state.js.map +1 -0
  109. package/lib/types/listener.d.ts +96 -0
  110. package/lib/types/listener.d.ts.map +1 -0
  111. package/lib/types/listener.js +575 -0
  112. package/lib/types/listener.js.map +1 -0
  113. package/lib/types/mail.d.ts +8 -0
  114. package/lib/types/mail.d.ts.map +1 -0
  115. package/lib/types/mail.js +75 -0
  116. package/lib/types/mail.js.map +1 -0
  117. package/lib/types/model-proxy-contract.d.ts +67 -0
  118. package/lib/types/model-proxy-contract.d.ts.map +1 -0
  119. package/lib/types/model-proxy-contract.js +135 -0
  120. package/lib/types/model-proxy-contract.js.map +1 -0
  121. package/lib/types/provider-api.d.ts +106 -2
  122. package/lib/types/provider-api.d.ts.map +1 -1
  123. package/lib/types/provider.d.ts.map +1 -1
  124. package/lib/types/provider.js +3 -0
  125. package/lib/types/provider.js.map +1 -1
  126. package/lib/types/sdk-adapter.d.ts +43 -2
  127. package/lib/types/sdk-adapter.d.ts.map +1 -1
  128. package/lib/types/sdk-adapter.js +622 -24
  129. package/lib/types/sdk-adapter.js.map +1 -1
  130. package/lib/types/sent-mail-store.d.ts +22 -0
  131. package/lib/types/sent-mail-store.d.ts.map +1 -0
  132. package/lib/types/sent-mail-store.js +246 -0
  133. package/lib/types/sent-mail-store.js.map +1 -0
  134. package/lib/types/tools.d.ts +10 -0
  135. package/lib/types/tools.d.ts.map +1 -1
  136. package/lib/types/tools.js +98 -0
  137. package/lib/types/tools.js.map +1 -1
  138. package/lib/types/types.d.ts +254 -2
  139. package/lib/types/types.d.ts.map +1 -1
  140. package/lib/types/types.js.map +1 -1
  141. package/package.json +31 -17
  142. package/lib/sdk-adapter-DK6RqV-c.mjs +0 -355
@@ -1,11 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /** AWiki trigger, identity registration, and direct/group messaging drawer. */
3
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';
4
+ import { IconChevronLeftOutline14, IconCheckOutline16, IconChecklistOutline14, IconChevronDownOutline14, IconCloseOutline16, IconCopyOutline16, IconDataOutline16, IconDownloadOutline16, IconGlobeOutline14, IconGoalOutline16, IconLoadingOutline16, IconPaperclipOutline16, IconPlusOutline16, IconRefreshOutline16, IconRefreshOutline14, IconSendOutline16, IconSettingsOutline16, IconSparkle16, Button, Menu, Modal, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives';
5
5
  import { AWIKI_ME_APP_ICON_DATA_URL } from "./assets.js";
6
6
  import { createAttachmentObjectUrl, fileToBase64, saveDownloadedAttachment } from "./file.js";
7
+ import { AwikiMail } from "./AwikiMail.js";
8
+ import { AwikiGroupDetails } from "./AwikiGroupDetails.js";
9
+ import { AwikiIdentityAccess } from "./AwikiIdentityAccess.js";
10
+ import { AwikiProfileCard } from "./AwikiProfileCard.js";
11
+ import { MentionText } from "./MentionText.js";
12
+ import { activeMentionQuery, codePointIndexToUtf16Index, insertMention, mentionCandidates, protocolMentions, transformMentionDrafts, utf16IndexToCodePointIndex, } from "./mentions.js";
7
13
  import css from './AwikiOverlay.module.css';
8
14
  export const AWIKI_LAUNCHER_POSITION_KEY = 'dsh-awiki-launcher-position-v1';
15
+ export const AWIKI_DRAWER_FRAME_KEY = 'dsh-awiki-drawer-frame-v1';
9
16
  const LAUNCHER_SIZE = 48;
10
17
  const LAUNCHER_EDGE_GAP = 8;
11
18
  const LAUNCHER_RIGHT_OFFSET = 28;
@@ -15,10 +22,15 @@ const DRAWER_LONG_PRESS_MS = 300;
15
22
  const DRAWER_ANCHOR_GAP = 8;
16
23
  const DRAWER_EDGE_GAP = 8;
17
24
  const DRAWER_NOMINAL_WIDTH = 720;
25
+ const MAIL_DRAWER_NOMINAL_WIDTH = 1040;
18
26
  const DRAWER_NOMINAL_HEIGHT = 720;
19
27
  const DRAWER_HORIZONTAL_RESERVE = 80;
28
+ const DRAWER_COMPACT_HORIZONTAL_RESERVE = 56;
29
+ const DRAWER_MIN_WIDTH = 360;
30
+ const DRAWER_MIN_HEIGHT = 360;
20
31
  const ONE_DAY_MS = 24 * 60 * 60 * 1000;
21
32
  const HISTORY_BOTTOM_THRESHOLD = 24;
33
+ const DRAWER_RESIZE_DIRECTIONS = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
22
34
  /** Keep the floating launcher fully reachable inside the current viewport. */
23
35
  export function clampAwikiLauncherPosition(position, width, height) {
24
36
  return {
@@ -69,6 +81,40 @@ export function resolveAwikiDrawerPlacement(launcher, panelWidth, panelHeight, v
69
81
  top: Math.min(Math.max(selected.top, DRAWER_EDGE_GAP), Math.max(DRAWER_EDGE_GAP, viewportHeight - panelHeight - DRAWER_EDGE_GAP)),
70
82
  };
71
83
  }
84
+ /** Keep a user-sized AWiki drawer reachable and within the current viewport. */
85
+ export function clampAwikiDrawerFrame(frame, viewportWidth, viewportHeight) {
86
+ const maxWidth = Math.max(1, viewportWidth - DRAWER_EDGE_GAP * 2);
87
+ const maxHeight = Math.max(1, viewportHeight - DRAWER_EDGE_GAP * 2);
88
+ const minWidth = Math.min(DRAWER_MIN_WIDTH, maxWidth);
89
+ const minHeight = Math.min(DRAWER_MIN_HEIGHT, maxHeight);
90
+ const width = Math.min(Math.max(frame.width, minWidth), maxWidth);
91
+ const height = Math.min(Math.max(frame.height, minHeight), maxHeight);
92
+ return {
93
+ left: Math.min(Math.max(frame.left, DRAWER_EDGE_GAP), Math.max(DRAWER_EDGE_GAP, viewportWidth - width - DRAWER_EDGE_GAP)),
94
+ top: Math.min(Math.max(frame.top, DRAWER_EDGE_GAP), Math.max(DRAWER_EDGE_GAP, viewportHeight - height - DRAWER_EDGE_GAP)),
95
+ width,
96
+ height,
97
+ };
98
+ }
99
+ /** Resize one or two drawer boundaries while keeping the opposite boundaries fixed. */
100
+ export function resizeAwikiDrawerFrame(frame, direction, deltaX, deltaY, viewportWidth, viewportHeight) {
101
+ const origin = clampAwikiDrawerFrame(frame, viewportWidth, viewportHeight);
102
+ const minWidth = Math.min(DRAWER_MIN_WIDTH, Math.max(1, viewportWidth - DRAWER_EDGE_GAP * 2));
103
+ const minHeight = Math.min(DRAWER_MIN_HEIGHT, Math.max(1, viewportHeight - DRAWER_EDGE_GAP * 2));
104
+ let left = origin.left;
105
+ let top = origin.top;
106
+ let right = origin.left + origin.width;
107
+ let bottom = origin.top + origin.height;
108
+ if (direction.includes('w'))
109
+ left = Math.min(Math.max(left + deltaX, DRAWER_EDGE_GAP), right - minWidth);
110
+ if (direction.includes('e'))
111
+ right = Math.max(Math.min(right + deltaX, viewportWidth - DRAWER_EDGE_GAP), left + minWidth);
112
+ if (direction.includes('n'))
113
+ top = Math.min(Math.max(top + deltaY, DRAWER_EDGE_GAP), bottom - minHeight);
114
+ if (direction.includes('s'))
115
+ bottom = Math.max(Math.min(bottom + deltaY, viewportHeight - DRAWER_EDGE_GAP), top + minHeight);
116
+ return clampAwikiDrawerFrame({ left, top, width: right - left, height: bottom - top }, viewportWidth, viewportHeight);
117
+ }
72
118
  function defaultLauncherPosition() {
73
119
  return clampAwikiLauncherPosition({
74
120
  left: window.innerWidth - LAUNCHER_SIZE - LAUNCHER_RIGHT_OFFSET,
@@ -99,6 +145,30 @@ function saveLauncherPosition(position) {
99
145
  // The launcher remains usable when session storage is unavailable.
100
146
  }
101
147
  }
148
+ function readDrawerFrame() {
149
+ try {
150
+ const stored = window.sessionStorage.getItem(AWIKI_DRAWER_FRAME_KEY);
151
+ if (stored !== null) {
152
+ const value = JSON.parse(stored);
153
+ const { left, top, width, height } = value;
154
+ if ([left, top, width, height].every(item => typeof item === 'number' && Number.isFinite(item))) {
155
+ return clampAwikiDrawerFrame({ left: left, top: top, width: width, height: height }, window.innerWidth, window.innerHeight);
156
+ }
157
+ }
158
+ }
159
+ catch {
160
+ // The default drawer remains usable when session storage is unavailable.
161
+ }
162
+ return null;
163
+ }
164
+ function saveDrawerFrame(frame) {
165
+ try {
166
+ window.sessionStorage.setItem(AWIKI_DRAWER_FRAME_KEY, JSON.stringify(frame));
167
+ }
168
+ catch {
169
+ // Resizing remains usable when session storage is unavailable.
170
+ }
171
+ }
102
172
  function callPointerCapture(target, method, pointerId) {
103
173
  const capture = Reflect.get(target, method);
104
174
  if (typeof capture === 'function')
@@ -115,112 +185,15 @@ function conversationTime(value, now = Date.now()) {
115
185
  ? new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false }).format(value)
116
186
  : new Intl.DateTimeFormat('zh-CN', { month: 'numeric', day: 'numeric' }).format(value);
117
187
  }
118
- /** Render the identity registration form and its OTP challenge transition. */
119
- function Registration(props) {
120
- const [phone, setPhone] = useState('');
121
- const [handle, setHandle] = useState('');
122
- const [otp, setOtp] = useState('');
123
- const [otpSent, setOtpSent] = useState(false);
124
- const [notice, setNotice] = useState(null);
125
- const [retryDeadline, setRetryDeadline] = useState(null);
126
- const [retrySeconds, setRetrySeconds] = useState(0);
127
- useEffect(() => {
128
- if (retryDeadline === null)
129
- return;
130
- const update = () => {
131
- const remaining = Math.max(0, Math.ceil((retryDeadline - Date.now()) / 1000));
132
- setRetrySeconds(remaining);
133
- if (remaining === 0)
134
- setRetryDeadline(null);
135
- };
136
- update();
137
- const timer = setInterval(update, 250);
138
- return () => { clearInterval(timer); };
139
- }, [retryDeadline]);
140
- const requestOtp = async () => {
141
- const result = await props.sendRegistrationOtp({ handle: handle.trim(), phone: phone.trim() });
142
- if (!result.ok)
143
- return;
144
- const cooldownSeconds = Math.max(0, Math.ceil(result.value.retryAfterSeconds));
145
- setOtpSent(true);
146
- setRetryDeadline(Date.now() + cooldownSeconds * 1000);
147
- setRetrySeconds(cooldownSeconds);
148
- setNotice(`验证码已发送;${cooldownSeconds} 秒后可重新获取。`);
149
- };
150
- const register = async () => {
151
- /* v8 ignore next -- the registration action is rendered only after an OTP challenge starts. */
152
- if (!otpSent)
153
- return;
154
- const result = await props.registerIdentity({
155
- phone: phone.trim(), handle: handle.trim(), otp: otp.trim(),
156
- });
157
- if (!result.ok)
158
- return;
159
- setNotice(null);
160
- };
161
- 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 || retrySeconds > 0, onClick: () => { void requestOtp(); }, children: retrySeconds > 0 ? `${retrySeconds} 秒后重新获取` : '重新获取验证码' })] })), notice !== null && _jsx("p", { className: css.notice, role: "status", children: notice })] }));
162
- }
163
- /** Let a signed-out installation resume its preserved local identity. */
164
- function SignedOut(props) {
165
- const [error, setError] = useState(null);
166
- const login = async () => {
167
- setError(null);
168
- const result = await props.login();
169
- if (!result.ok)
170
- setError(result.error);
171
- };
172
- 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 })] }));
173
- }
174
188
  /** Prefer the peer WNS display name for a direct chat; groups keep their title. */
175
189
  function conversationLabel(conversation) {
176
190
  return conversation.kind === 'direct'
177
191
  ? (conversation.displayName ?? conversation.title)
178
192
  : conversation.title;
179
193
  }
180
- /** Show only the deployment identity's WNS display name, never its routing Handle. */
181
- function identityLabel(identity) {
182
- return identity.displayName ?? '未设置昵称';
183
- }
184
- /** Editable deployment identity summary shown above the conversation roster. */
185
- function IdentityCard(props) {
186
- const [editing, setEditing] = useState(false);
187
- const [draft, setDraft] = useState(props.identity.displayName ?? '');
188
- const [error, setError] = useState(null);
189
- useEffect(() => {
190
- if (!editing)
191
- setDraft(props.identity.displayName ?? '');
192
- }, [editing, props.identity.displayName]);
193
- const cancel = () => {
194
- setDraft(props.identity.displayName ?? '');
195
- setError(null);
196
- setEditing(false);
197
- };
198
- const save = async () => {
199
- const displayName = draft.trim();
200
- const length = Array.from(displayName).length;
201
- if (length === 0) {
202
- setError('请输入昵称');
203
- return;
204
- }
205
- if (length > 50) {
206
- setError('昵称不能超过 50 个字符');
207
- return;
208
- }
209
- setError(null);
210
- const result = await props.updateDisplayName(displayName);
211
- if (!result.ok) {
212
- setError(result.error);
213
- return;
214
- }
215
- setDraft(result.value.displayName ?? displayName);
216
- setEditing(false);
217
- };
218
- 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) => {
219
- if (event.key === 'Escape') {
220
- event.stopPropagation();
221
- cancel();
222
- }
223
- } }), _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 })] }));
194
+ /** Switch the shared identity between messaging and on-demand mail. */
195
+ function ModeTabs(props) {
196
+ return (_jsxs("div", { className: css.modeTabs, role: "tablist", "aria-label": "AWiki \u529F\u80FD", children: [_jsx("button", { type: "button", role: "tab", "aria-selected": props.mode === 'chat', onClick: () => { props.onChange('chat'); }, children: "\u4F1A\u8BDD" }), _jsxs("button", { type: "button", role: "tab", "aria-selected": props.mode === 'mail', onClick: () => { props.onChange('mail'); }, children: ["\u90AE\u4EF6", props.mailUnreadCount > 0 && _jsx("small", { children: props.mailUnreadCount > 99 ? '99+' : props.mailUnreadCount })] })] }));
224
197
  }
225
198
  /** Incoming sender label: WNS display name, then Handle, then DID. */
226
199
  function senderLabel(message, peerLabel) {
@@ -284,11 +257,11 @@ function MessageRow(props) {
284
257
  }
285
258
  saveDownloadedAttachment(result.value);
286
259
  };
287
- 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 })] }));
260
+ 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: _jsx(MentionText, { text: props.message.content.text, ...props.message.content.mentions === undefined ? {} : { mentions: props.message.content.mentions } }) })) : 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 })] }));
288
261
  }
289
262
  /** Render one optimistic outgoing bubble while the Host confirms delivery. */
290
263
  function PendingMessageRow(props) {
291
- return (_jsxs("div", { className: css.pendingMessage, role: "status", "aria-live": "polite", "aria-label": "\u6D88\u606F\u53D1\u9001\u4E2D", children: [_jsx(IconLoadingOutline16, { className: css.pendingMessageSpinner, size: 14 }), _jsxs("div", { className: css.pendingMessageContent, children: [_jsxs("div", { className: css.messageMeta, children: [_jsx("span", { children: "\u6211" }), _jsx("time", { children: time(props.draft.startedAt) })] }), props.draft.content.kind === 'text' ? (_jsx("p", { children: props.draft.content.text })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.pendingAttachment, children: [_jsx(IconPaperclipOutline16, { size: 16 }), _jsxs("span", { children: [_jsx("strong", { children: props.draft.content.fileName }), _jsxs("small", { children: [props.draft.content.size, " \u5B57\u8282"] })] })] }), props.draft.content.caption !== undefined && _jsx("p", { className: css.pendingCaption, children: props.draft.content.caption })] }))] })] }));
264
+ return (_jsxs("div", { className: css.pendingMessage, role: "status", "aria-live": "polite", "aria-label": "\u6D88\u606F\u53D1\u9001\u4E2D", children: [_jsx(IconLoadingOutline16, { className: css.pendingMessageSpinner, size: 14 }), _jsxs("div", { className: css.pendingMessageContent, children: [_jsxs("div", { className: css.messageMeta, children: [_jsx("span", { children: "\u6211" }), _jsx("time", { children: time(props.draft.startedAt) })] }), props.draft.content.kind === 'text' ? (_jsx("p", { children: _jsx(MentionText, { text: props.draft.content.text, mentions: protocolMentions(props.draft.content.text, props.draft.content.mentions) }) })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.pendingAttachment, children: [_jsx(IconPaperclipOutline16, { size: 16 }), _jsxs("span", { children: [_jsx("strong", { children: props.draft.content.fileName }), _jsxs("small", { children: [props.draft.content.size, " \u5B57\u8282"] })] })] }), props.draft.content.caption !== undefined && _jsx("p", { className: css.pendingCaption, children: props.draft.content.caption })] }))] })] }));
292
265
  }
293
266
  function summaryRangeLabel(summary) {
294
267
  const scope = summary.range.kind === 'unread' ? '未读以来' : '最近消息';
@@ -336,11 +309,16 @@ function SummaryPanel(props) {
336
309
  function Chat(props) {
337
310
  const { view } = props;
338
311
  const [text, setText] = useState('');
312
+ const [mentionDrafts, setMentionDrafts] = useState([]);
313
+ const [mentionQuery, setMentionQuery] = useState(null);
314
+ const [mentionCandidateIndex, setMentionCandidateIndex] = useState(0);
339
315
  const [file, setFile] = useState(null);
340
316
  const [sendingDraft, setSendingDraft] = useState(null);
317
+ const [groupDetailsOpen, setGroupDetailsOpen] = useState(false);
341
318
  const [previewUrl, setPreviewUrl] = useState(null);
342
319
  const [fileError, setFileError] = useState(null);
343
320
  const input = useRef(null);
321
+ const composer = useRef(null);
344
322
  const history = useRef(null);
345
323
  const previousConversationId = useRef(null);
346
324
  const previousMessageTail = useRef(null);
@@ -351,10 +329,55 @@ function Chat(props) {
351
329
  const [historyAwayFromBottom, setHistoryAwayFromBottom] = useState(false);
352
330
  const [unseenMessageCount, setUnseenMessageCount] = useState(0);
353
331
  const selected = view.conversations.find(value => value.id === view.selectedConversationId);
332
+ const candidates = mentionQuery === null || selected?.kind !== 'group'
333
+ ? []
334
+ : mentionCandidates(view.groupMembers, view.identity.did, mentionQuery.query);
354
335
  const summary = selected === undefined ? undefined : view.summaries[selected.id];
355
336
  const summaryPanelId = useId();
337
+ const mentionListId = useId();
356
338
  selectedConversationId.current = view.selectedConversationId;
357
- const visibleSendingDraft = sendingDraft?.conversationId === view.selectedConversationId ? sendingDraft : null;
339
+ const visibleSendingDraft = sendingDraft?.conversationId === view.selectedConversationId
340
+ && !view.messages.some(message => message.id === sendingDraft.messageId)
341
+ ? sendingDraft
342
+ : null;
343
+ useEffect(() => {
344
+ setMentionDrafts([]);
345
+ setMentionQuery(null);
346
+ setMentionCandidateIndex(0);
347
+ setGroupDetailsOpen(false);
348
+ }, [view.selectedConversationId]);
349
+ useEffect(() => {
350
+ if (mentionCandidateIndex >= candidates.length)
351
+ setMentionCandidateIndex(0);
352
+ }, [candidates.length, mentionCandidateIndex]);
353
+ const syncMentionQuery = (value, selectionStart) => {
354
+ if (selected?.kind !== 'group' || selectionStart === null) {
355
+ setMentionQuery(null);
356
+ return;
357
+ }
358
+ setMentionQuery(activeMentionQuery(value, utf16IndexToCodePointIndex(value, selectionStart)));
359
+ setMentionCandidateIndex(0);
360
+ };
361
+ const chooseMention = (index) => {
362
+ const query = mentionQuery;
363
+ const candidate = candidates[index];
364
+ if (query === null || candidate === undefined)
365
+ return;
366
+ const insertion = insertMention(text, query, candidate, `mention-${crypto.randomUUID()}`);
367
+ const retained = transformMentionDrafts(text, insertion.text, mentionDrafts);
368
+ setText(insertion.text);
369
+ setMentionDrafts([...retained, insertion.mention]);
370
+ setMentionQuery(null);
371
+ setMentionCandidateIndex(0);
372
+ requestAnimationFrame(() => {
373
+ const textarea = composer.current;
374
+ if (textarea === null)
375
+ return;
376
+ const caret = codePointIndexToUtf16Index(insertion.text, insertion.caret);
377
+ textarea.focus();
378
+ textarea.setSelectionRange(caret, caret);
379
+ });
380
+ };
358
381
  const markSelectedConversationReadAtBottom = () => {
359
382
  const node = history.current;
360
383
  const newestRendered = view.messages.at(-1);
@@ -362,7 +385,7 @@ function Chat(props) {
362
385
  || selected === undefined
363
386
  || newestRendered === undefined
364
387
  || (selected.unreadCount ?? 0) <= 0
365
- || view.pending === '加载消息'
388
+ || view.localPending
366
389
  || (selected.lastMessageAt !== undefined && newestRendered.sentAt < selected.lastMessageAt)
367
390
  || node.scrollHeight - node.scrollTop - node.clientHeight > HISTORY_BOTTOM_THRESHOLD)
368
391
  return;
@@ -400,7 +423,7 @@ function Chat(props) {
400
423
  };
401
424
  useLayoutEffect(() => {
402
425
  markSelectedConversationReadAtBottom();
403
- }, [selected?.id, selected?.lastMessageAt, selected?.unreadCount, view.messages, view.pending]);
426
+ }, [selected?.id, selected?.lastMessageAt, selected?.unreadCount, view.localPending, view.messages]);
404
427
  useLayoutEffect(() => {
405
428
  const conversationId = view.selectedConversationId;
406
429
  if (conversationId !== previousConversationId.current) {
@@ -416,7 +439,7 @@ function Chat(props) {
416
439
  }
417
440
  if (conversationId === null || history.current === null)
418
441
  return;
419
- if (view.pending === '加载消息')
442
+ if (view.localPending)
420
443
  return;
421
444
  if (conversationAwaitingBottom.current === conversationId) {
422
445
  if (view.messages.length === 0)
@@ -447,7 +470,7 @@ function Chat(props) {
447
470
  setHistoryAwayFromBottom(true);
448
471
  setUnseenMessageCount(current => current + appendedMessageCount);
449
472
  }
450
- }, [view.messages, view.pending, view.selectedConversationId]);
473
+ }, [view.localPending, view.messages, view.selectedConversationId]);
451
474
  useLayoutEffect(() => {
452
475
  if (visibleSendingDraft === null || history.current === null)
453
476
  return;
@@ -498,18 +521,24 @@ function Chat(props) {
498
521
  const sendMessage = async () => {
499
522
  if (sendingDraft !== null || view.selectedConversationId === null)
500
523
  return;
501
- const draft = text.trim();
524
+ const draft = text;
502
525
  const conversationId = view.selectedConversationId;
503
526
  if (file === null) {
504
527
  /* v8 ignore next -- the only invocation control is disabled while both text and attachment are empty. */
505
- if (draft === '')
528
+ if (draft.trim() === '')
506
529
  return;
507
- setSendingDraft({ conversationId, startedAt: Date.now(), content: { kind: 'text', text: draft } });
530
+ const mentions = selected?.kind === 'group' ? protocolMentions(draft, mentionDrafts) : [];
531
+ const messageId = `msg-${crypto.randomUUID()}`;
532
+ setSendingDraft({ conversationId, messageId, startedAt: Date.now(), content: { kind: 'text', text: draft, mentions: mentionDrafts } });
508
533
  setText('');
509
- const result = await props.sendText(draft);
534
+ setMentionDrafts([]);
535
+ setMentionQuery(null);
536
+ const result = await props.sendText(draft, messageId, mentions);
510
537
  setSendingDraft(null);
511
- if (!result.ok && selectedConversationId.current === conversationId)
538
+ if (!result.ok && selectedConversationId.current === conversationId) {
512
539
  setText(draft);
540
+ setMentionDrafts(mentionDrafts);
541
+ }
513
542
  return;
514
543
  }
515
544
  if (file.size > view.attachmentMaxBytes) {
@@ -519,31 +548,45 @@ function Chat(props) {
519
548
  setFileError(null);
520
549
  const selectedFile = file;
521
550
  const bytesBase64 = await fileToBase64(selectedFile);
551
+ const messageId = `msg-${crypto.randomUUID()}`;
522
552
  setSendingDraft({
523
553
  conversationId,
554
+ messageId,
524
555
  startedAt: Date.now(),
525
556
  content: {
526
557
  kind: 'attachment',
527
558
  fileName: selectedFile.name,
528
559
  size: selectedFile.size,
529
- ...(draft === '' ? {} : { caption: draft }),
560
+ ...(draft.trim() === '' ? {} : { caption: draft.trim() }),
530
561
  },
531
562
  });
532
563
  clearFile();
533
564
  setText('');
565
+ setMentionDrafts([]);
566
+ setMentionQuery(null);
534
567
  const result = await props.sendAttachment({
535
568
  fileName: selectedFile.name,
536
569
  mimeType: selectedFile.type || 'application/octet-stream',
537
570
  bytesBase64,
538
- ...(draft === '' ? {} : { caption: draft }),
571
+ ...(draft.trim() === '' ? {} : { caption: draft.trim() }),
572
+ clientMessageId: messageId,
539
573
  });
540
574
  setSendingDraft(null);
541
575
  if (!result.ok && selectedConversationId.current === conversationId) {
542
576
  setFile(selectedFile);
543
577
  setText(draft);
578
+ setMentionDrafts(mentionDrafts);
544
579
  }
545
580
  };
546
- 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: () => {
581
+ return (_jsxs("div", { className: css.chat, children: [_jsxs("aside", { className: css.roster, "data-hidden": selected !== undefined || undefined, "aria-label": "\u4F1A\u8BDD", children: [_jsx(AwikiProfileCard, { identity: view.identity, profile: view.profile, pending: view.pending !== null, updateProfile: props.updateProfile }), props.modeTabs, _jsxs("div", { className: css.rosterHeader, children: [_jsx("div", { className: css.rosterTitle, children: "\u4F1A\u8BDD" }), props.composeMenu] }), view.groupRecovery !== null && (_jsxs("div", { className: css.groupRecoveryNotice, "data-status": view.groupRecovery.status, role: view.groupRecovery.status === 'blocked' ? 'alert' : 'status', children: [_jsxs("span", { children: [_jsx("strong", { children: view.groupRecovery.status === 'pending'
582
+ ? '正在恢复旧群聊身份'
583
+ : view.groupRecovery.status === 'blocked'
584
+ ? '部分旧群聊需要处理'
585
+ : '暂时无法检查旧群聊身份' }), _jsx("small", { children: view.groupRecovery.status === 'pending'
586
+ ? `${view.groupRecovery.pending} 个群聊尚未完成,其他会话不受影响。`
587
+ : view.groupRecovery.status === 'blocked'
588
+ ? `${view.groupRecovery.blocked} 个群聊未能自动恢复,其他会话不受影响。`
589
+ : '私聊和新群聊不受影响,可稍后重试。' })] }), _jsx(Tooltip, { label: "\u91CD\u8BD5\u7FA4\u804A\u8EAB\u4EFD\u6062\u590D", side: "right", children: _jsx("button", { type: "button", "aria-label": "\u91CD\u8BD5\u7FA4\u804A\u8EAB\u4EFD\u6062\u590D", disabled: view.pending !== null, onClick: () => { void props.retryGroupRebindRecovery(); }, children: _jsx(IconRefreshOutline14, { size: 14 }) }) })] })), _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' ? '私聊' : '群聊' })] }), selected.kind === 'group' && (_jsx(Tooltip, { label: "\u7FA4\u804A\u8BE6\u60C5", side: "bottom", children: _jsx("button", { type: "button", className: css.threadAction, "aria-label": "\u6253\u5F00\u7FA4\u804A\u8BE6\u60C5", "aria-expanded": groupDetailsOpen, onClick: () => { setGroupDetailsOpen(value => !value); }, children: _jsx(IconSettingsOutline16, { size: 15 }) }) })), _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: () => {
547
590
  if (summary !== undefined && !summary.collapsed && summary.status !== 'error') {
548
591
  props.setSummaryCollapsed(selected.id, true);
549
592
  }
@@ -553,9 +596,36 @@ function Chat(props) {
553
596
  else {
554
597
  void props.summarizeConversation();
555
598
  }
556
- }, 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", { className: css.historyShell, children: [_jsxs("div", { ref: history, className: css.history, role: "log", "aria-label": "\u6D88\u606F\u8BB0\u5F55", onScroll: syncHistoryPosition, children: [view.historyHasMore && _jsx("button", { type: "button", className: css.more, onClick: () => { void props.loadOlderHistory(); }, children: "\u52A0\u8F7D\u66F4\u65E9\u6D88\u606F" }), view.pending === '加载消息' && (_jsxs("div", { className: css.historyLoading, role: "status", "aria-live": "polite", "aria-label": "\u6B63\u5728\u52A0\u8F7D\u6D88\u606F", children: [_jsx(IconLoadingOutline16, { size: 18 }), _jsx("span", { children: "\u6B63\u5728\u52A0\u8F7D\u6D88\u606F\u2026" })] })), view.messages.map(message => (_jsx(MessageRow, { message: message, peerLabel: selected.kind === 'direct' ? conversationLabel(selected) : undefined, download: props.downloadAttachment, onImageLoad: scrollAfterInitialImage }, message.id))), visibleSendingDraft !== null && _jsx(PendingMessageRow, { draft: visibleSendingDraft }), view.pending !== '加载消息' && view.messages.length === 0 && visibleSendingDraft === null && _jsx("p", { className: css.empty, children: "\u6682\u65E0\u6D88\u606F\u3002" })] }), historyAwayFromBottom && (_jsxs("button", { type: "button", className: css.latestMessages, "aria-label": unseenMessageCount === 0 ? '下滑到最新消息' : `有 ${unseenMessageCount} 条新消息,下滑到最新消息`, onClick: () => { scrollHistoryToLatest(true); }, children: [_jsx(IconChevronDownOutline14, { size: 14 }), unseenMessageCount > 0 && _jsxs("span", { children: ["\u65B0\u6D88\u606F\uFF08", unseenMessageCount, "\uFF09"] })] }))] }), _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
599
+ }, 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 })] })] }), selected.kind === 'group' && groupDetailsOpen && (_jsx(AwikiGroupDetails, { group: view.selectedGroup, members: view.groupMembers, hasMore: view.groupMembersHasMore, identity: view.identity, pending: view.pending !== null, refreshSelectedGroup: props.refreshSelectedGroup, loadMoreGroupMembers: props.loadMoreGroupMembers, addSelectedGroupMember: props.addSelectedGroupMember, removeSelectedGroupMember: props.removeSelectedGroupMember, leaveSelectedGroup: props.leaveSelectedGroup, onClose: () => { setGroupDetailsOpen(false); } })), summary !== undefined && (_jsx(SummaryPanel, { id: summaryPanelId, summary: summary, regenerate: () => { void props.summarizeConversation(); }, collapse: collapsed => { props.setSummaryCollapsed(selected.id, collapsed); }, viewSource: viewSummarySource })), _jsxs("div", { className: css.historyShell, children: [_jsxs("div", { ref: history, className: css.history, role: "log", "aria-label": "\u6D88\u606F\u8BB0\u5F55", onScroll: syncHistoryPosition, children: [view.historyHasMore && _jsx("button", { type: "button", className: css.more, onClick: () => { void props.loadOlderHistory(); }, children: "\u52A0\u8F7D\u66F4\u65E9\u6D88\u606F" }), view.localPending && (_jsxs("div", { className: css.historyLoading, role: "status", "aria-live": "polite", "aria-label": "\u6B63\u5728\u8BFB\u53D6\u672C\u5730\u6D88\u606F", children: [_jsx(IconLoadingOutline16, { size: 18 }), _jsx("span", { children: "\u6B63\u5728\u8BFB\u53D6\u672C\u5730\u6D88\u606F\u2026" })] })), !view.localPending && view.refreshing && view.messages.length === 0 && (_jsxs("div", { className: css.historyLoading, role: "status", "aria-live": "polite", "aria-label": "\u6B63\u5728\u540C\u6B65\u6D88\u606F", children: [_jsx(IconLoadingOutline16, { size: 18 }), _jsx("span", { children: "\u6B63\u5728\u540C\u6B65\u6D88\u606F\u2026" })] })), view.messages.map(message => (_jsx(MessageRow, { message: message, peerLabel: selected.kind === 'direct' ? conversationLabel(selected) : undefined, download: props.downloadAttachment, onImageLoad: scrollAfterInitialImage }, message.id))), visibleSendingDraft !== null && _jsx(PendingMessageRow, { draft: visibleSendingDraft }), !view.localPending && !view.refreshing && view.messages.length === 0 && visibleSendingDraft === null && _jsx("p", { className: css.empty, children: "\u6682\u65E0\u6D88\u606F\u3002" })] }), historyAwayFromBottom && (_jsxs("button", { type: "button", className: css.latestMessages, "aria-label": unseenMessageCount === 0 ? '下滑到最新消息' : `有 ${unseenMessageCount} 条新消息,下滑到最新消息`, onClick: () => { scrollHistoryToLatest(true); }, children: [_jsx(IconChevronDownOutline14, { size: 14 }), unseenMessageCount > 0 && _jsxs("span", { children: ["\u65B0\u6D88\u606F\uFF08", unseenMessageCount, "\uFF09"] })] }))] }), _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
557
600
  ? _jsx("span", { className: css.filePreviewIcon, children: _jsx(IconPaperclipOutline16, {}) })
558
- : _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); }, onKeyDown: (event) => {
601
+ : _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 }) })] })), mentionQuery !== null && candidates.length > 0 && (_jsx("div", { id: mentionListId, className: css.mentionCandidates, role: "listbox", "aria-label": "\u53EF\u63D0\u53CA\u7684\u7FA4\u6210\u5458", children: candidates.map((candidate, index) => (_jsxs("button", { type: "button", role: "option", "aria-selected": index === mentionCandidateIndex, onMouseDown: (event) => { event.preventDefault(); chooseMention(index); }, children: [_jsx("span", { children: candidate.label.slice(0, 1).toLocaleUpperCase() }), _jsx("strong", { children: candidate.label }), _jsx("small", { children: candidate.member.handle ?? candidate.member.did })] }, candidate.member.did))) })), _jsx("textarea", { ref: composer, value: text, "aria-controls": mentionQuery !== null && candidates.length > 0 ? mentionListId : undefined, "aria-expanded": mentionQuery !== null && candidates.length > 0, "aria-autocomplete": "list", onChange: (event) => {
602
+ const next = event.target.value;
603
+ setMentionDrafts(current => transformMentionDrafts(text, next, current));
604
+ setText(next);
605
+ syncMentionQuery(next, event.target.selectionStart);
606
+ }, onSelect: (event) => { syncMentionQuery(text, event.currentTarget.selectionStart); }, onKeyDown: (event) => {
607
+ if (mentionQuery !== null && candidates.length > 0) {
608
+ if (event.key === 'ArrowDown') {
609
+ event.preventDefault();
610
+ setMentionCandidateIndex(index => (index + 1) % candidates.length);
611
+ return;
612
+ }
613
+ if (event.key === 'ArrowUp') {
614
+ event.preventDefault();
615
+ setMentionCandidateIndex(index => (index - 1 + candidates.length) % candidates.length);
616
+ return;
617
+ }
618
+ if ((event.key === 'Enter' || event.key === 'Tab') && !event.nativeEvent.isComposing) {
619
+ event.preventDefault();
620
+ chooseMention(mentionCandidateIndex);
621
+ return;
622
+ }
623
+ if (event.key === 'Escape') {
624
+ event.preventDefault();
625
+ setMentionQuery(null);
626
+ return;
627
+ }
628
+ }
559
629
  if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing)
560
630
  return;
561
631
  event.preventDefault();
@@ -573,17 +643,30 @@ export function AwikiOverlay(props) {
573
643
  const view = props.useAwiki(state => state);
574
644
  const titleId = useId();
575
645
  const composeTitleId = useId();
646
+ const groupComposeTitleId = useId();
647
+ const groupJoinTitleId = useId();
576
648
  const [accountMenuOpen, setAccountMenuOpen] = useState(false);
577
649
  const [menuOpen, setMenuOpen] = useState(false);
578
650
  const [composeDirect, setComposeDirect] = useState(false);
579
651
  const [peerHandle, setPeerHandle] = useState('');
580
652
  const [composeError, setComposeError] = useState(null);
653
+ const [composeGroup, setComposeGroup] = useState(false);
654
+ const [groupName, setGroupName] = useState('');
655
+ const [groupMembers, setGroupMembers] = useState('');
656
+ const [groupComposeError, setGroupComposeError] = useState(null);
657
+ const [joinGroupOpen, setJoinGroupOpen] = useState(false);
658
+ const [joinGroupDid, setJoinGroupDid] = useState('');
659
+ const [joinGroupError, setJoinGroupError] = useState(null);
581
660
  const [logoutOpen, setLogoutOpen] = useState(false);
582
661
  const [logoutPending, setLogoutPending] = useState(false);
583
662
  const [logoutError, setLogoutError] = useState(null);
663
+ const [mode, setMode] = useState('chat');
664
+ const [mailUnreadCount, setMailUnreadCount] = useState(0);
584
665
  const [launcherPosition, setLauncherPosition] = useState(readLauncherPosition);
666
+ const [drawerFrame, setDrawerFrame] = useState(readDrawerFrame);
585
667
  const [launcherDragging, setLauncherDragging] = useState(false);
586
668
  const [drawerDragging, setDrawerDragging] = useState(false);
669
+ const [drawerResizing, setDrawerResizing] = useState(null);
587
670
  const [drawerDragDirection, setDrawerDragDirection] = useState(null);
588
671
  const launcherRef = useRef(null);
589
672
  const rememberedConversationId = useRef(null);
@@ -591,12 +674,21 @@ export function AwikiOverlay(props) {
591
674
  const suppressLauncherClick = useRef(false);
592
675
  const launcherDrag = useRef(null);
593
676
  const drawerDrag = useRef(null);
677
+ const drawerResize = useRef(null);
594
678
  const registered = view.status === 'ready' && view.identity !== null;
595
679
  const unreadCount = view.conversations.reduce((total, conversation) => total + (conversation.unreadCount ?? 0), 0);
596
680
  const unreadLabel = unreadCount > 99 ? '99+' : String(unreadCount);
597
- const drawerWidth = Math.min(DRAWER_NOMINAL_WIDTH, Math.max(1, window.innerWidth - DRAWER_HORIZONTAL_RESERVE));
598
- const drawerHeight = Math.min(DRAWER_NOMINAL_HEIGHT, Math.max(1, window.innerHeight - DRAWER_EDGE_GAP * 2));
681
+ const nominalDrawerWidth = Math.min(mode === 'mail' ? MAIL_DRAWER_NOMINAL_WIDTH : DRAWER_NOMINAL_WIDTH, Math.max(1, window.innerWidth - (window.innerWidth <= 620 ? DRAWER_COMPACT_HORIZONTAL_RESERVE : DRAWER_HORIZONTAL_RESERVE)));
682
+ const nominalDrawerHeight = Math.min(DRAWER_NOMINAL_HEIGHT, Math.max(1, window.innerHeight - DRAWER_EDGE_GAP * 2));
683
+ const drawerWidth = drawerFrame?.width ?? nominalDrawerWidth;
684
+ const drawerHeight = drawerFrame?.height ?? nominalDrawerHeight;
599
685
  const drawerPlacement = resolveAwikiDrawerPlacement(launcherPosition, drawerWidth, drawerHeight, window.innerWidth, window.innerHeight, drawerDragDirection ?? undefined);
686
+ const renderedDrawerFrame = drawerFrame ?? {
687
+ left: drawerPlacement.left,
688
+ top: drawerPlacement.top,
689
+ width: drawerWidth,
690
+ height: drawerHeight,
691
+ };
600
692
  useEffect(() => {
601
693
  void props.open();
602
694
  return props.close;
@@ -629,11 +721,21 @@ export function AwikiOverlay(props) {
629
721
  setComposeDirect(false);
630
722
  setPeerHandle('');
631
723
  setComposeError(null);
724
+ setComposeGroup(false);
725
+ setGroupName('');
726
+ setGroupMembers('');
727
+ setGroupComposeError(null);
728
+ setJoinGroupOpen(false);
729
+ setJoinGroupDid('');
730
+ setJoinGroupError(null);
731
+ setMode('chat');
632
732
  const drag = drawerDrag.current;
633
733
  if (drag !== null)
634
734
  clearTimeout(drag.timer);
635
735
  drawerDrag.current = null;
736
+ drawerResize.current = null;
636
737
  setDrawerDragging(false);
738
+ setDrawerResizing(null);
637
739
  setDrawerDragDirection(null);
638
740
  if (wasOpen && view.selectedConversationId !== null) {
639
741
  rememberedConversationId.current = view.selectedConversationId;
@@ -659,6 +761,14 @@ export function AwikiOverlay(props) {
659
761
  setComposeDirect(false);
660
762
  return;
661
763
  }
764
+ if (composeGroup) {
765
+ setComposeGroup(false);
766
+ return;
767
+ }
768
+ if (joinGroupOpen) {
769
+ setJoinGroupOpen(false);
770
+ return;
771
+ }
662
772
  if (menuOpen) {
663
773
  setMenuOpen(false);
664
774
  return;
@@ -672,7 +782,7 @@ export function AwikiOverlay(props) {
672
782
  };
673
783
  document.addEventListener('keydown', onKeyDown);
674
784
  return () => { document.removeEventListener('keydown', onKeyDown); };
675
- }, [open, accountMenuOpen, composeDirect, logoutOpen, logoutPending, menuOpen, props.actions]);
785
+ }, [open, accountMenuOpen, composeDirect, composeGroup, joinGroupOpen, logoutOpen, logoutPending, menuOpen, props.actions]);
676
786
  useEffect(() => {
677
787
  const onResize = () => {
678
788
  setLauncherPosition((current) => {
@@ -680,6 +790,13 @@ export function AwikiOverlay(props) {
680
790
  saveLauncherPosition(next);
681
791
  return next;
682
792
  });
793
+ setDrawerFrame((current) => {
794
+ if (current === null)
795
+ return null;
796
+ const next = clampAwikiDrawerFrame(current, window.innerWidth, window.innerHeight);
797
+ saveDrawerFrame(next);
798
+ return next;
799
+ });
683
800
  };
684
801
  window.addEventListener('resize', onResize);
685
802
  return () => { window.removeEventListener('resize', onResize); };
@@ -694,6 +811,8 @@ export function AwikiOverlay(props) {
694
811
  origin: launcherPosition,
695
812
  moved: false,
696
813
  current: launcherPosition,
814
+ originFrame: drawerFrame,
815
+ currentFrame: drawerFrame,
697
816
  };
698
817
  callPointerCapture(event.currentTarget, 'setPointerCapture', event.pointerId);
699
818
  };
@@ -712,6 +831,14 @@ export function AwikiOverlay(props) {
712
831
  top: drag.origin.top + deltaY,
713
832
  }, window.innerWidth, window.innerHeight);
714
833
  setLauncherPosition(drag.current);
834
+ if (drag.originFrame !== null) {
835
+ drag.currentFrame = clampAwikiDrawerFrame({
836
+ ...drag.originFrame,
837
+ left: drag.originFrame.left + deltaX,
838
+ top: drag.originFrame.top + deltaY,
839
+ }, window.innerWidth, window.innerHeight);
840
+ setDrawerFrame(drag.currentFrame);
841
+ }
715
842
  };
716
843
  const finishLauncherDrag = (event) => {
717
844
  const drag = launcherDrag.current;
@@ -721,6 +848,8 @@ export function AwikiOverlay(props) {
721
848
  suppressLauncherClick.current = true;
722
849
  saveLauncherPosition(drag.current);
723
850
  }
851
+ if (drag.currentFrame !== null)
852
+ saveDrawerFrame(drag.currentFrame);
724
853
  launcherDrag.current = null;
725
854
  setLauncherDragging(false);
726
855
  callPointerCapture(event.currentTarget, 'releasePointerCapture', event.pointerId);
@@ -738,6 +867,8 @@ export function AwikiOverlay(props) {
738
867
  armed: false,
739
868
  moved: false,
740
869
  current: launcherPosition,
870
+ originFrame: drawerFrame,
871
+ currentFrame: drawerFrame,
741
872
  };
742
873
  drag.timer = setTimeout(() => {
743
874
  if (drawerDrag.current !== drag)
@@ -770,6 +901,14 @@ export function AwikiOverlay(props) {
770
901
  top: drag.origin.top + deltaY,
771
902
  }, window.innerWidth, window.innerHeight);
772
903
  setLauncherPosition(drag.current);
904
+ if (drag.originFrame !== null) {
905
+ drag.currentFrame = clampAwikiDrawerFrame({
906
+ ...drag.originFrame,
907
+ left: drag.originFrame.left + deltaX,
908
+ top: drag.originFrame.top + deltaY,
909
+ }, window.innerWidth, window.innerHeight);
910
+ setDrawerFrame(drag.currentFrame);
911
+ }
773
912
  };
774
913
  const finishDrawerDrag = (event) => {
775
914
  const drag = drawerDrag.current;
@@ -778,11 +917,76 @@ export function AwikiOverlay(props) {
778
917
  clearTimeout(drag.timer);
779
918
  if (drag.moved)
780
919
  saveLauncherPosition(drag.current);
920
+ if (drag.currentFrame !== null)
921
+ saveDrawerFrame(drag.currentFrame);
781
922
  drawerDrag.current = null;
782
923
  setDrawerDragging(false);
783
924
  setDrawerDragDirection(null);
784
925
  callPointerCapture(event.currentTarget, 'releasePointerCapture', event.pointerId);
785
926
  };
927
+ const onDrawerResizePointerDown = (event, direction) => {
928
+ if (event.button !== 0)
929
+ return;
930
+ event.preventDefault();
931
+ event.stopPropagation();
932
+ const origin = clampAwikiDrawerFrame(renderedDrawerFrame, window.innerWidth, window.innerHeight);
933
+ drawerResize.current = {
934
+ pointerId: event.pointerId,
935
+ direction,
936
+ startX: event.clientX,
937
+ startY: event.clientY,
938
+ origin,
939
+ current: origin,
940
+ };
941
+ setDrawerFrame(origin);
942
+ setDrawerResizing(direction);
943
+ callPointerCapture(event.currentTarget, 'setPointerCapture', event.pointerId);
944
+ };
945
+ const onDrawerResizePointerMove = (event) => {
946
+ const resize = drawerResize.current;
947
+ if (resize === null || resize.pointerId !== event.pointerId)
948
+ return;
949
+ event.preventDefault();
950
+ resize.current = resizeAwikiDrawerFrame(resize.origin, resize.direction, event.clientX - resize.startX, event.clientY - resize.startY, window.innerWidth, window.innerHeight);
951
+ setDrawerFrame(resize.current);
952
+ };
953
+ const finishDrawerResize = (event) => {
954
+ const resize = drawerResize.current;
955
+ if (resize === null || resize.pointerId !== event.pointerId)
956
+ return;
957
+ saveDrawerFrame(resize.current);
958
+ drawerResize.current = null;
959
+ setDrawerResizing(null);
960
+ callPointerCapture(event.currentTarget, 'releasePointerCapture', event.pointerId);
961
+ };
962
+ useEffect(() => {
963
+ if (drawerResizing === null)
964
+ return;
965
+ const onPointerMove = (event) => {
966
+ const resize = drawerResize.current;
967
+ if (resize === null || resize.pointerId !== event.pointerId)
968
+ return;
969
+ event.preventDefault();
970
+ resize.current = resizeAwikiDrawerFrame(resize.origin, resize.direction, event.clientX - resize.startX, event.clientY - resize.startY, window.innerWidth, window.innerHeight);
971
+ setDrawerFrame(resize.current);
972
+ };
973
+ const onPointerEnd = (event) => {
974
+ const resize = drawerResize.current;
975
+ if (resize === null || resize.pointerId !== event.pointerId)
976
+ return;
977
+ saveDrawerFrame(resize.current);
978
+ drawerResize.current = null;
979
+ setDrawerResizing(null);
980
+ };
981
+ window.addEventListener('pointermove', onPointerMove, { passive: false });
982
+ window.addEventListener('pointerup', onPointerEnd);
983
+ window.addEventListener('pointercancel', onPointerEnd);
984
+ return () => {
985
+ window.removeEventListener('pointermove', onPointerMove);
986
+ window.removeEventListener('pointerup', onPointerEnd);
987
+ window.removeEventListener('pointercancel', onPointerEnd);
988
+ };
989
+ }, [drawerResizing]);
786
990
  const toggleLauncher = () => {
787
991
  if (suppressLauncherClick.current) {
788
992
  suppressLauncherClick.current = false;
@@ -800,6 +1004,31 @@ export function AwikiOverlay(props) {
800
1004
  setComposeDirect(false);
801
1005
  setPeerHandle('');
802
1006
  };
1007
+ const createGroup = async () => {
1008
+ setGroupComposeError(null);
1009
+ const members = groupMembers
1010
+ .split(/[\n,,]+/u)
1011
+ .map(member => member.trim())
1012
+ .filter(member => member !== '');
1013
+ const result = await props.createGroup(groupName, members);
1014
+ if (!result.ok) {
1015
+ setGroupComposeError(result.error);
1016
+ return;
1017
+ }
1018
+ setComposeGroup(false);
1019
+ setGroupName('');
1020
+ setGroupMembers('');
1021
+ };
1022
+ const joinGroup = async () => {
1023
+ setJoinGroupError(null);
1024
+ const result = await props.joinGroup(joinGroupDid);
1025
+ if (!result.ok) {
1026
+ setJoinGroupError(result.error);
1027
+ return;
1028
+ }
1029
+ setJoinGroupOpen(false);
1030
+ setJoinGroupDid('');
1031
+ };
803
1032
  const logout = async () => {
804
1033
  setLogoutPending(true);
805
1034
  setLogoutError(null);
@@ -814,14 +1043,28 @@ export function AwikiOverlay(props) {
814
1043
  };
815
1044
  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
816
1045
  ? '收起 AWiki'
817
- : 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: () => {
1046
+ : 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: {
1047
+ left: renderedDrawerFrame.left,
1048
+ top: renderedDrawerFrame.top,
1049
+ width: renderedDrawerFrame.width,
1050
+ height: renderedDrawerFrame.height,
1051
+ }, "data-placement": drawerPlacement.direction, "data-mode": mode, "data-resizing": drawerResizing ?? undefined, 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: () => {
818
1052
  setAccountMenuOpen(false);
819
1053
  setLogoutError(null);
820
1054
  setLogoutOpen(true);
821
- }, 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: () => {
822
- setMenuOpen(false);
823
- setComposeDirect(true);
824
- }, 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)
825
- 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 && view.pending !== '发送消息' && view.pending !== '发送附件' && view.pending !== '加载消息' && _jsxs("div", { className: css.pending, role: "status", children: [view.pending, "\u2026"] })] }))] }));
1055
+ }, 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" })] }), mode === 'chat' && (_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' || view.sessionStatus === 'signed-out') && (_jsx("div", { className: css.identityAccess, children: _jsx(AwikiIdentityAccess, { ...props, sessionStatus: view.sessionStatus, recoveryOperationId: view.recoveryOperationId, recoveryProgress: view.recoveryProgress, pending: view.pending !== null }) })), view.status === 'ready' && view.sessionStatus === 'active' && view.identity !== null && (_jsxs(_Fragment, { children: [_jsx("div", { className: css.modePanel, "data-active": mode === 'chat' || undefined, hidden: mode !== 'chat', children: _jsx(Chat, { ...props, selectConversation: selectConversation, view: { ...view, identity: view.identity }, modeTabs: _jsx(ModeTabs, { mode: mode, mailUnreadCount: mailUnreadCount, onChange: setMode }), composeMenu: (_jsx(Menu, { open: menuOpen, onClose: () => { setMenuOpen(false); }, align: "end", portal: true, compact: true, items: [
1056
+ { id: 'direct', label: '发起私聊' },
1057
+ { id: 'group', label: '发起群聊' },
1058
+ { id: 'join-group', label: '加入群聊' },
1059
+ ], onSelect: (id) => {
1060
+ setMenuOpen(false);
1061
+ if (id === 'direct')
1062
+ setComposeDirect(true);
1063
+ if (id === 'group')
1064
+ setComposeGroup(true);
1065
+ if (id === 'join-group')
1066
+ setJoinGroupOpen(true);
1067
+ }, anchor: (_jsx("button", { type: "button", className: css.rosterAction, "aria-label": "\u53D1\u8D77\u4F1A\u8BDD", "aria-expanded": menuOpen, "aria-haspopup": "menu", onClick: () => { setMenuOpen(value => !value); }, children: _jsx(IconPlusOutline16, {}) })) })) }) }), _jsx("div", { className: css.modePanel, "data-active": mode === 'mail' || undefined, hidden: mode !== 'mail', children: _jsx(AwikiMail, { active: mode === 'mail', cacheOwner: view.identity.did, identityCard: mode === 'mail' ? _jsx(AwikiProfileCard, { identity: view.identity, profile: view.profile, pending: view.pending !== null, updateProfile: props.updateProfile }) : null, modeTabs: _jsx(ModeTabs, { mode: mode, mailUnreadCount: mailUnreadCount, onChange: setMode }), onUnreadCountChange: setMailUnreadCount, getMailAccount: props.getMailAccount, listMailInbox: props.listMailInbox, readMail: props.readMail, markMailRead: props.markMailRead, sendMail: props.sendMail }, view.identity.did) })] })), 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" })] })] }) })), composeGroup && (_jsx("div", { className: css.composeBackdrop, children: _jsxs("form", { className: css.composeCard, role: "dialog", "aria-modal": "true", "aria-labelledby": groupComposeTitleId, onSubmit: (event) => { event.preventDefault(); void createGroup(); }, children: [_jsx("h3", { id: groupComposeTitleId, children: "\u53D1\u8D77\u7FA4\u804A" }), _jsx("p", { children: "\u586B\u5199\u7FA4\u540D\u5373\u53EF\u521B\u5EFA\u3002\u4E5F\u53EF\u4EE5\u73B0\u5728\u9080\u8BF7\u9996\u6279\u6210\u5458\uFF0C\u652F\u6301 Handle \u6216 DID\u3002" }), _jsxs("label", { children: ["\u7FA4\u804A\u540D\u79F0", _jsx("input", { value: groupName, onChange: (event) => { setGroupName(event.target.value); setGroupComposeError(null); }, autoComplete: "off", placeholder: "\u4F8B\u5982 \u53D1\u5E03\u534F\u4F5C\u7FA4", autoFocus: true })] }), _jsxs("label", { children: ["\u9996\u6279\u7FA4\u6210\u5458\uFF08\u53EF\u9009\uFF09", _jsx("textarea", { "aria-label": "\u7FA4\u6210\u5458", value: groupMembers, onChange: (event) => { setGroupMembers(event.target.value); setGroupComposeError(null); }, rows: 4, placeholder: '例如 alice.awiki.ai\nbob.awiki.ai' })] }), view.pending === '创建群聊' && _jsx("p", { role: "status", children: "\u6B63\u5728\u521B\u5EFA\u7FA4\u804A\u5E76\u9080\u8BF7\u6210\u5458\u2026" }), groupComposeError !== null && _jsx("p", { className: css.inlineError, role: "alert", children: groupComposeError }), _jsxs("div", { className: css.composeActions, children: [_jsx("button", { type: "button", className: css.secondary, onClick: () => { setComposeGroup(false); setGroupComposeError(null); }, children: "\u53D6\u6D88" }), _jsx("button", { type: "submit", className: css.primary, disabled: view.pending !== null || groupName.trim() === '', children: "\u521B\u5EFA\u7FA4\u804A" })] })] }) })), joinGroupOpen && (_jsx("div", { className: css.composeBackdrop, children: _jsxs("form", { className: css.composeCard, role: "dialog", "aria-modal": "true", "aria-labelledby": groupJoinTitleId, onSubmit: (event) => { event.preventDefault(); void joinGroup(); }, children: [_jsx("h3", { id: groupJoinTitleId, children: "\u52A0\u5165\u7FA4\u804A" }), _jsx("p", { children: "\u8F93\u5165\u7FA4\u804A\u7684\u5B8C\u6574 DID\u3002\u52A0\u5165\u6210\u529F\u540E\u4F1A\u6253\u5F00\u8BE5\u7FA4\u804A\u5E76\u8BFB\u53D6\u6700\u65B0\u6210\u5458\u5217\u8868\u3002" }), _jsxs("label", { children: ["\u7FA4 DID", _jsx("input", { value: joinGroupDid, onChange: (event) => { setJoinGroupDid(event.target.value); setJoinGroupError(null); }, autoComplete: "off", placeholder: "did:wba:...", autoFocus: true })] }), joinGroupError !== null && _jsx("p", { className: css.inlineError, role: "alert", children: joinGroupError }), _jsxs("div", { className: css.composeActions, children: [_jsx("button", { type: "button", className: css.secondary, onClick: () => { setJoinGroupOpen(false); setJoinGroupError(null); }, children: "\u53D6\u6D88" }), _jsx("button", { type: "submit", className: css.primary, disabled: view.pending !== null || joinGroupDid.trim() === '', children: "\u52A0\u5165\u7FA4\u804A" })] })] }) })), _jsx(Modal, { open: logoutOpen, onClose: () => { if (!logoutPending)
1068
+ setLogoutOpen(false); }, title: "\u9000\u51FA\u767B\u5F55", closeLabel: "\u53D6\u6D88", className: css.compactModal ?? '', contentClassName: css.compactModalContent ?? '', 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\u672C\u673A\u8EAB\u4EFD\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 && view.pending !== '发送消息' && view.pending !== '发送附件' && view.pending !== '加载消息' && _jsxs("div", { className: css.pending, role: "status", children: [view.pending, "\u2026"] }), DRAWER_RESIZE_DIRECTIONS.map(direction => (_jsx("div", { className: css.resizeHandle, "data-resize-handle": direction, "aria-hidden": "true", onPointerDown: (event) => { onDrawerResizePointerDown(event, direction); }, onPointerMove: onDrawerResizePointerMove, onPointerUp: finishDrawerResize, onPointerCancel: finishDrawerResize }, direction)))] }))] }));
826
1069
  }
827
1070
  //# sourceMappingURL=AwikiOverlay.js.map