@awiki/dsh-plugin 0.2.4 → 0.2.5

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 (115) hide show
  1. package/README.md +80 -11
  2. package/README.zh.md +67 -12
  3. package/THIRD_PARTY_NOTICES.md +8 -1
  4. package/cordis.patch.yml +13 -0
  5. package/lib/client.js +8223 -1939
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +1486 -59
  8. package/lib/model-proxy-contract.js +89 -0
  9. package/lib/model-proxy.js +383 -0
  10. package/lib/provider.js +2 -1
  11. package/lib/sdk-adapter-DKgkyApB.mjs +669 -0
  12. package/lib/typert.host.js +470 -482
  13. package/lib/typert.remote-client.d.ts +15 -1
  14. package/lib/typert.remote-client.d.ts.map +1 -1
  15. package/lib/typert.remote-client.js +469 -103
  16. package/lib/types/attachment-cache.d.ts +28 -0
  17. package/lib/types/attachment-cache.d.ts.map +1 -0
  18. package/lib/types/attachment-cache.js +194 -0
  19. package/lib/types/attachment-cache.js.map +1 -0
  20. package/lib/types/client/AwikiMail.d.ts +14 -0
  21. package/lib/types/client/AwikiMail.d.ts.map +1 -0
  22. package/lib/types/client/AwikiMail.js +260 -0
  23. package/lib/types/client/AwikiMail.js.map +1 -0
  24. package/lib/types/client/AwikiOnboarding.d.ts +22 -0
  25. package/lib/types/client/AwikiOnboarding.d.ts.map +1 -0
  26. package/lib/types/client/AwikiOnboarding.js +87 -0
  27. package/lib/types/client/AwikiOnboarding.js.map +1 -0
  28. package/lib/types/client/AwikiOverlay.d.ts +15 -0
  29. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  30. package/lib/types/client/AwikiOverlay.js +251 -20
  31. package/lib/types/client/AwikiOverlay.js.map +1 -1
  32. package/lib/types/client/AwikiSettingsSection.d.ts +20 -4
  33. package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
  34. package/lib/types/client/AwikiSettingsSection.js +279 -23
  35. package/lib/types/client/AwikiSettingsSection.js.map +1 -1
  36. package/lib/types/client/RechargeComingSoonDialog.d.ts +11 -0
  37. package/lib/types/client/RechargeComingSoonDialog.d.ts.map +1 -0
  38. package/lib/types/client/RechargeComingSoonDialog.js +8 -0
  39. package/lib/types/client/RechargeComingSoonDialog.js.map +1 -0
  40. package/lib/types/client/controller.d.ts +82 -4
  41. package/lib/types/client/controller.d.ts.map +1 -1
  42. package/lib/types/client/controller.js +517 -74
  43. package/lib/types/client/controller.js.map +1 -1
  44. package/lib/types/client/image-cache.d.ts +20 -0
  45. package/lib/types/client/image-cache.d.ts.map +1 -0
  46. package/lib/types/client/image-cache.js +177 -0
  47. package/lib/types/client/image-cache.js.map +1 -0
  48. package/lib/types/client/index.d.ts +2 -0
  49. package/lib/types/client/index.d.ts.map +1 -1
  50. package/lib/types/client/index.js +84 -41
  51. package/lib/types/client/index.js.map +1 -1
  52. package/lib/types/client/model-account-settings-availability.d.ts +4 -0
  53. package/lib/types/client/model-account-settings-availability.d.ts.map +1 -0
  54. package/lib/types/client/model-account-settings-availability.js +4 -0
  55. package/lib/types/client/model-account-settings-availability.js.map +1 -0
  56. package/lib/types/client/model-availability-controller.d.ts +27 -0
  57. package/lib/types/client/model-availability-controller.d.ts.map +1 -0
  58. package/lib/types/client/model-availability-controller.js +111 -0
  59. package/lib/types/client/model-availability-controller.js.map +1 -0
  60. package/lib/types/client/model-proxy-controller.d.ts +41 -0
  61. package/lib/types/client/model-proxy-controller.d.ts.map +1 -0
  62. package/lib/types/client/model-proxy-controller.js +241 -0
  63. package/lib/types/client/model-proxy-controller.js.map +1 -0
  64. package/lib/types/client/recharge-availability.d.ts +6 -0
  65. package/lib/types/client/recharge-availability.d.ts.map +1 -0
  66. package/lib/types/client/recharge-availability.js +6 -0
  67. package/lib/types/client/recharge-availability.js.map +1 -0
  68. package/lib/types/client/settings-locales.d.ts +1 -1
  69. package/lib/types/client/settings-locales.d.ts.map +1 -1
  70. package/lib/types/client/settings-locales.js +180 -2
  71. package/lib/types/client/settings-locales.js.map +1 -1
  72. package/lib/types/client/slots.d.ts +15 -2
  73. package/lib/types/client/slots.d.ts.map +1 -1
  74. package/lib/types/index.d.ts +56 -2
  75. package/lib/types/index.d.ts.map +1 -1
  76. package/lib/types/index.js +462 -31
  77. package/lib/types/index.js.map +1 -1
  78. package/lib/types/listener-state.d.ts +23 -0
  79. package/lib/types/listener-state.d.ts.map +1 -0
  80. package/lib/types/listener-state.js +124 -0
  81. package/lib/types/listener-state.js.map +1 -0
  82. package/lib/types/listener.d.ts +96 -0
  83. package/lib/types/listener.d.ts.map +1 -0
  84. package/lib/types/listener.js +575 -0
  85. package/lib/types/listener.js.map +1 -0
  86. package/lib/types/mail.d.ts +8 -0
  87. package/lib/types/mail.d.ts.map +1 -0
  88. package/lib/types/mail.js +75 -0
  89. package/lib/types/mail.js.map +1 -0
  90. package/lib/types/model-proxy-contract.d.ts +61 -0
  91. package/lib/types/model-proxy-contract.d.ts.map +1 -0
  92. package/lib/types/model-proxy-contract.js +129 -0
  93. package/lib/types/model-proxy-contract.js.map +1 -0
  94. package/lib/types/model-proxy.d.ts +14 -0
  95. package/lib/types/model-proxy.d.ts.map +1 -0
  96. package/lib/types/model-proxy.js +371 -0
  97. package/lib/types/model-proxy.js.map +1 -0
  98. package/lib/types/provider-api.d.ts +78 -2
  99. package/lib/types/provider-api.d.ts.map +1 -1
  100. package/lib/types/provider.d.ts.map +1 -1
  101. package/lib/types/provider.js +1 -0
  102. package/lib/types/provider.js.map +1 -1
  103. package/lib/types/sdk-adapter.d.ts +26 -2
  104. package/lib/types/sdk-adapter.d.ts.map +1 -1
  105. package/lib/types/sdk-adapter.js +380 -24
  106. package/lib/types/sdk-adapter.js.map +1 -1
  107. package/lib/types/tools.d.ts +10 -0
  108. package/lib/types/tools.d.ts.map +1 -1
  109. package/lib/types/tools.js +98 -0
  110. package/lib/types/tools.js.map +1 -1
  111. package/lib/types/types.d.ts +103 -1
  112. package/lib/types/types.d.ts.map +1 -1
  113. package/lib/types/types.js.map +1 -1
  114. package/package.json +30 -5
  115. package/lib/sdk-adapter-DK6RqV-c.mjs +0 -355
@@ -4,8 +4,10 @@ import { useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
4
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
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";
7
8
  import css from './AwikiOverlay.module.css';
8
9
  export const AWIKI_LAUNCHER_POSITION_KEY = 'dsh-awiki-launcher-position-v1';
10
+ export const AWIKI_DRAWER_FRAME_KEY = 'dsh-awiki-drawer-frame-v1';
9
11
  const LAUNCHER_SIZE = 48;
10
12
  const LAUNCHER_EDGE_GAP = 8;
11
13
  const LAUNCHER_RIGHT_OFFSET = 28;
@@ -15,10 +17,15 @@ const DRAWER_LONG_PRESS_MS = 300;
15
17
  const DRAWER_ANCHOR_GAP = 8;
16
18
  const DRAWER_EDGE_GAP = 8;
17
19
  const DRAWER_NOMINAL_WIDTH = 720;
20
+ const MAIL_DRAWER_NOMINAL_WIDTH = 1040;
18
21
  const DRAWER_NOMINAL_HEIGHT = 720;
19
22
  const DRAWER_HORIZONTAL_RESERVE = 80;
23
+ const DRAWER_COMPACT_HORIZONTAL_RESERVE = 56;
24
+ const DRAWER_MIN_WIDTH = 360;
25
+ const DRAWER_MIN_HEIGHT = 360;
20
26
  const ONE_DAY_MS = 24 * 60 * 60 * 1000;
21
27
  const HISTORY_BOTTOM_THRESHOLD = 24;
28
+ const DRAWER_RESIZE_DIRECTIONS = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
22
29
  /** Keep the floating launcher fully reachable inside the current viewport. */
23
30
  export function clampAwikiLauncherPosition(position, width, height) {
24
31
  return {
@@ -69,6 +76,40 @@ export function resolveAwikiDrawerPlacement(launcher, panelWidth, panelHeight, v
69
76
  top: Math.min(Math.max(selected.top, DRAWER_EDGE_GAP), Math.max(DRAWER_EDGE_GAP, viewportHeight - panelHeight - DRAWER_EDGE_GAP)),
70
77
  };
71
78
  }
79
+ /** Keep a user-sized AWiki drawer reachable and within the current viewport. */
80
+ export function clampAwikiDrawerFrame(frame, viewportWidth, viewportHeight) {
81
+ const maxWidth = Math.max(1, viewportWidth - DRAWER_EDGE_GAP * 2);
82
+ const maxHeight = Math.max(1, viewportHeight - DRAWER_EDGE_GAP * 2);
83
+ const minWidth = Math.min(DRAWER_MIN_WIDTH, maxWidth);
84
+ const minHeight = Math.min(DRAWER_MIN_HEIGHT, maxHeight);
85
+ const width = Math.min(Math.max(frame.width, minWidth), maxWidth);
86
+ const height = Math.min(Math.max(frame.height, minHeight), maxHeight);
87
+ return {
88
+ left: Math.min(Math.max(frame.left, DRAWER_EDGE_GAP), Math.max(DRAWER_EDGE_GAP, viewportWidth - width - DRAWER_EDGE_GAP)),
89
+ top: Math.min(Math.max(frame.top, DRAWER_EDGE_GAP), Math.max(DRAWER_EDGE_GAP, viewportHeight - height - DRAWER_EDGE_GAP)),
90
+ width,
91
+ height,
92
+ };
93
+ }
94
+ /** Resize one or two drawer boundaries while keeping the opposite boundaries fixed. */
95
+ export function resizeAwikiDrawerFrame(frame, direction, deltaX, deltaY, viewportWidth, viewportHeight) {
96
+ const origin = clampAwikiDrawerFrame(frame, viewportWidth, viewportHeight);
97
+ const minWidth = Math.min(DRAWER_MIN_WIDTH, Math.max(1, viewportWidth - DRAWER_EDGE_GAP * 2));
98
+ const minHeight = Math.min(DRAWER_MIN_HEIGHT, Math.max(1, viewportHeight - DRAWER_EDGE_GAP * 2));
99
+ let left = origin.left;
100
+ let top = origin.top;
101
+ let right = origin.left + origin.width;
102
+ let bottom = origin.top + origin.height;
103
+ if (direction.includes('w'))
104
+ left = Math.min(Math.max(left + deltaX, DRAWER_EDGE_GAP), right - minWidth);
105
+ if (direction.includes('e'))
106
+ right = Math.max(Math.min(right + deltaX, viewportWidth - DRAWER_EDGE_GAP), left + minWidth);
107
+ if (direction.includes('n'))
108
+ top = Math.min(Math.max(top + deltaY, DRAWER_EDGE_GAP), bottom - minHeight);
109
+ if (direction.includes('s'))
110
+ bottom = Math.max(Math.min(bottom + deltaY, viewportHeight - DRAWER_EDGE_GAP), top + minHeight);
111
+ return clampAwikiDrawerFrame({ left, top, width: right - left, height: bottom - top }, viewportWidth, viewportHeight);
112
+ }
72
113
  function defaultLauncherPosition() {
73
114
  return clampAwikiLauncherPosition({
74
115
  left: window.innerWidth - LAUNCHER_SIZE - LAUNCHER_RIGHT_OFFSET,
@@ -99,6 +140,30 @@ function saveLauncherPosition(position) {
99
140
  // The launcher remains usable when session storage is unavailable.
100
141
  }
101
142
  }
143
+ function readDrawerFrame() {
144
+ try {
145
+ const stored = window.sessionStorage.getItem(AWIKI_DRAWER_FRAME_KEY);
146
+ if (stored !== null) {
147
+ const value = JSON.parse(stored);
148
+ const { left, top, width, height } = value;
149
+ if ([left, top, width, height].every(item => typeof item === 'number' && Number.isFinite(item))) {
150
+ return clampAwikiDrawerFrame({ left: left, top: top, width: width, height: height }, window.innerWidth, window.innerHeight);
151
+ }
152
+ }
153
+ }
154
+ catch {
155
+ // The default drawer remains usable when session storage is unavailable.
156
+ }
157
+ return null;
158
+ }
159
+ function saveDrawerFrame(frame) {
160
+ try {
161
+ window.sessionStorage.setItem(AWIKI_DRAWER_FRAME_KEY, JSON.stringify(frame));
162
+ }
163
+ catch {
164
+ // Resizing remains usable when session storage is unavailable.
165
+ }
166
+ }
102
167
  function callPointerCapture(target, method, pointerId) {
103
168
  const capture = Reflect.get(target, method);
104
169
  if (typeof capture === 'function')
@@ -116,7 +181,7 @@ function conversationTime(value, now = Date.now()) {
116
181
  : new Intl.DateTimeFormat('zh-CN', { month: 'numeric', day: 'numeric' }).format(value);
117
182
  }
118
183
  /** Render the identity registration form and its OTP challenge transition. */
119
- function Registration(props) {
184
+ export function AwikiRegistrationForm(props) {
120
185
  const [phone, setPhone] = useState('');
121
186
  const [handle, setHandle] = useState('');
122
187
  const [otp, setOtp] = useState('');
@@ -158,7 +223,7 @@ function Registration(props) {
158
223
  return;
159
224
  setNotice(null);
160
225
  };
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 })] }));
226
+ 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", autoFocus: props.autoFocusHandle })] }), _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
227
  }
163
228
  /** Let a signed-out installation resume its preserved local identity. */
164
229
  function SignedOut(props) {
@@ -222,6 +287,10 @@ function IdentityCard(props) {
222
287
  }
223
288
  } }), _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 })] }));
224
289
  }
290
+ /** Switch the shared identity between messaging and on-demand mail. */
291
+ function ModeTabs(props) {
292
+ 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 })] })] }));
293
+ }
225
294
  /** Incoming sender label: WNS display name, then Handle, then DID. */
226
295
  function senderLabel(message, peerLabel) {
227
296
  if (message.outgoing)
@@ -354,7 +423,10 @@ function Chat(props) {
354
423
  const summary = selected === undefined ? undefined : view.summaries[selected.id];
355
424
  const summaryPanelId = useId();
356
425
  selectedConversationId.current = view.selectedConversationId;
357
- const visibleSendingDraft = sendingDraft?.conversationId === view.selectedConversationId ? sendingDraft : null;
426
+ const visibleSendingDraft = sendingDraft?.conversationId === view.selectedConversationId
427
+ && !view.messages.some(message => message.id === sendingDraft.messageId)
428
+ ? sendingDraft
429
+ : null;
358
430
  const markSelectedConversationReadAtBottom = () => {
359
431
  const node = history.current;
360
432
  const newestRendered = view.messages.at(-1);
@@ -362,7 +434,7 @@ function Chat(props) {
362
434
  || selected === undefined
363
435
  || newestRendered === undefined
364
436
  || (selected.unreadCount ?? 0) <= 0
365
- || view.pending === '加载消息'
437
+ || view.localPending
366
438
  || (selected.lastMessageAt !== undefined && newestRendered.sentAt < selected.lastMessageAt)
367
439
  || node.scrollHeight - node.scrollTop - node.clientHeight > HISTORY_BOTTOM_THRESHOLD)
368
440
  return;
@@ -400,7 +472,7 @@ function Chat(props) {
400
472
  };
401
473
  useLayoutEffect(() => {
402
474
  markSelectedConversationReadAtBottom();
403
- }, [selected?.id, selected?.lastMessageAt, selected?.unreadCount, view.messages, view.pending]);
475
+ }, [selected?.id, selected?.lastMessageAt, selected?.unreadCount, view.localPending, view.messages]);
404
476
  useLayoutEffect(() => {
405
477
  const conversationId = view.selectedConversationId;
406
478
  if (conversationId !== previousConversationId.current) {
@@ -416,7 +488,7 @@ function Chat(props) {
416
488
  }
417
489
  if (conversationId === null || history.current === null)
418
490
  return;
419
- if (view.pending === '加载消息')
491
+ if (view.localPending)
420
492
  return;
421
493
  if (conversationAwaitingBottom.current === conversationId) {
422
494
  if (view.messages.length === 0)
@@ -447,7 +519,7 @@ function Chat(props) {
447
519
  setHistoryAwayFromBottom(true);
448
520
  setUnseenMessageCount(current => current + appendedMessageCount);
449
521
  }
450
- }, [view.messages, view.pending, view.selectedConversationId]);
522
+ }, [view.localPending, view.messages, view.selectedConversationId]);
451
523
  useLayoutEffect(() => {
452
524
  if (visibleSendingDraft === null || history.current === null)
453
525
  return;
@@ -504,9 +576,10 @@ function Chat(props) {
504
576
  /* v8 ignore next -- the only invocation control is disabled while both text and attachment are empty. */
505
577
  if (draft === '')
506
578
  return;
507
- setSendingDraft({ conversationId, startedAt: Date.now(), content: { kind: 'text', text: draft } });
579
+ const messageId = `msg-${crypto.randomUUID()}`;
580
+ setSendingDraft({ conversationId, messageId, startedAt: Date.now(), content: { kind: 'text', text: draft } });
508
581
  setText('');
509
- const result = await props.sendText(draft);
582
+ const result = await props.sendText(draft, messageId);
510
583
  setSendingDraft(null);
511
584
  if (!result.ok && selectedConversationId.current === conversationId)
512
585
  setText(draft);
@@ -519,8 +592,10 @@ function Chat(props) {
519
592
  setFileError(null);
520
593
  const selectedFile = file;
521
594
  const bytesBase64 = await fileToBase64(selectedFile);
595
+ const messageId = `msg-${crypto.randomUUID()}`;
522
596
  setSendingDraft({
523
597
  conversationId,
598
+ messageId,
524
599
  startedAt: Date.now(),
525
600
  content: {
526
601
  kind: 'attachment',
@@ -536,6 +611,7 @@ function Chat(props) {
536
611
  mimeType: selectedFile.type || 'application/octet-stream',
537
612
  bytesBase64,
538
613
  ...(draft === '' ? {} : { caption: draft }),
614
+ clientMessageId: messageId,
539
615
  });
540
616
  setSendingDraft(null);
541
617
  if (!result.ok && selectedConversationId.current === conversationId) {
@@ -543,7 +619,7 @@ function Chat(props) {
543
619
  setText(draft);
544
620
  }
545
621
  };
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: () => {
622
+ return (_jsxs("div", { className: css.chat, children: [_jsxs("aside", { className: css.roster, "data-hidden": selected !== undefined || undefined, "aria-label": "\u4F1A\u8BDD", children: [_jsx(IdentityCard, { identity: view.identity, pending: view.pending !== null, updateDisplayName: props.updateDisplayName }), props.modeTabs, _jsxs("div", { className: css.rosterHeader, children: [_jsx("div", { className: css.rosterTitle, children: "\u4F1A\u8BDD" }), props.composeMenu] }), _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: () => {
547
623
  if (summary !== undefined && !summary.collapsed && summary.status !== 'error') {
548
624
  props.setSummaryCollapsed(selected.id, true);
549
625
  }
@@ -553,7 +629,7 @@ function Chat(props) {
553
629
  else {
554
630
  void props.summarizeConversation();
555
631
  }
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
632
+ }, 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.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
633
  ? _jsx("span", { className: css.filePreviewIcon, children: _jsx(IconPaperclipOutline16, {}) })
558
634
  : _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) => {
559
635
  if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing)
@@ -573,17 +649,27 @@ export function AwikiOverlay(props) {
573
649
  const view = props.useAwiki(state => state);
574
650
  const titleId = useId();
575
651
  const composeTitleId = useId();
652
+ const groupComposeTitleId = useId();
576
653
  const [accountMenuOpen, setAccountMenuOpen] = useState(false);
577
654
  const [menuOpen, setMenuOpen] = useState(false);
578
655
  const [composeDirect, setComposeDirect] = useState(false);
579
656
  const [peerHandle, setPeerHandle] = useState('');
580
657
  const [composeError, setComposeError] = useState(null);
658
+ const [composeGroup, setComposeGroup] = useState(false);
659
+ const [groupName, setGroupName] = useState('');
660
+ const [groupMembers, setGroupMembers] = useState('');
661
+ const [groupComposeError, setGroupComposeError] = useState(null);
581
662
  const [logoutOpen, setLogoutOpen] = useState(false);
582
663
  const [logoutPending, setLogoutPending] = useState(false);
583
664
  const [logoutError, setLogoutError] = useState(null);
665
+ const [mode, setMode] = useState('chat');
666
+ const [mailUnreadCount, setMailUnreadCount] = useState(0);
667
+ const [mailRefreshRevision, setMailRefreshRevision] = useState(0);
584
668
  const [launcherPosition, setLauncherPosition] = useState(readLauncherPosition);
669
+ const [drawerFrame, setDrawerFrame] = useState(readDrawerFrame);
585
670
  const [launcherDragging, setLauncherDragging] = useState(false);
586
671
  const [drawerDragging, setDrawerDragging] = useState(false);
672
+ const [drawerResizing, setDrawerResizing] = useState(null);
587
673
  const [drawerDragDirection, setDrawerDragDirection] = useState(null);
588
674
  const launcherRef = useRef(null);
589
675
  const rememberedConversationId = useRef(null);
@@ -591,12 +677,21 @@ export function AwikiOverlay(props) {
591
677
  const suppressLauncherClick = useRef(false);
592
678
  const launcherDrag = useRef(null);
593
679
  const drawerDrag = useRef(null);
680
+ const drawerResize = useRef(null);
594
681
  const registered = view.status === 'ready' && view.identity !== null;
595
682
  const unreadCount = view.conversations.reduce((total, conversation) => total + (conversation.unreadCount ?? 0), 0);
596
683
  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));
684
+ 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)));
685
+ const nominalDrawerHeight = Math.min(DRAWER_NOMINAL_HEIGHT, Math.max(1, window.innerHeight - DRAWER_EDGE_GAP * 2));
686
+ const drawerWidth = drawerFrame?.width ?? nominalDrawerWidth;
687
+ const drawerHeight = drawerFrame?.height ?? nominalDrawerHeight;
599
688
  const drawerPlacement = resolveAwikiDrawerPlacement(launcherPosition, drawerWidth, drawerHeight, window.innerWidth, window.innerHeight, drawerDragDirection ?? undefined);
689
+ const renderedDrawerFrame = drawerFrame ?? {
690
+ left: drawerPlacement.left,
691
+ top: drawerPlacement.top,
692
+ width: drawerWidth,
693
+ height: drawerHeight,
694
+ };
600
695
  useEffect(() => {
601
696
  void props.open();
602
697
  return props.close;
@@ -629,11 +724,18 @@ export function AwikiOverlay(props) {
629
724
  setComposeDirect(false);
630
725
  setPeerHandle('');
631
726
  setComposeError(null);
727
+ setComposeGroup(false);
728
+ setGroupName('');
729
+ setGroupMembers('');
730
+ setGroupComposeError(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,10 @@ export function AwikiOverlay(props) {
659
761
  setComposeDirect(false);
660
762
  return;
661
763
  }
764
+ if (composeGroup) {
765
+ setComposeGroup(false);
766
+ return;
767
+ }
662
768
  if (menuOpen) {
663
769
  setMenuOpen(false);
664
770
  return;
@@ -672,7 +778,7 @@ export function AwikiOverlay(props) {
672
778
  };
673
779
  document.addEventListener('keydown', onKeyDown);
674
780
  return () => { document.removeEventListener('keydown', onKeyDown); };
675
- }, [open, accountMenuOpen, composeDirect, logoutOpen, logoutPending, menuOpen, props.actions]);
781
+ }, [open, accountMenuOpen, composeDirect, composeGroup, logoutOpen, logoutPending, menuOpen, props.actions]);
676
782
  useEffect(() => {
677
783
  const onResize = () => {
678
784
  setLauncherPosition((current) => {
@@ -680,6 +786,13 @@ export function AwikiOverlay(props) {
680
786
  saveLauncherPosition(next);
681
787
  return next;
682
788
  });
789
+ setDrawerFrame((current) => {
790
+ if (current === null)
791
+ return null;
792
+ const next = clampAwikiDrawerFrame(current, window.innerWidth, window.innerHeight);
793
+ saveDrawerFrame(next);
794
+ return next;
795
+ });
683
796
  };
684
797
  window.addEventListener('resize', onResize);
685
798
  return () => { window.removeEventListener('resize', onResize); };
@@ -694,6 +807,8 @@ export function AwikiOverlay(props) {
694
807
  origin: launcherPosition,
695
808
  moved: false,
696
809
  current: launcherPosition,
810
+ originFrame: drawerFrame,
811
+ currentFrame: drawerFrame,
697
812
  };
698
813
  callPointerCapture(event.currentTarget, 'setPointerCapture', event.pointerId);
699
814
  };
@@ -712,6 +827,14 @@ export function AwikiOverlay(props) {
712
827
  top: drag.origin.top + deltaY,
713
828
  }, window.innerWidth, window.innerHeight);
714
829
  setLauncherPosition(drag.current);
830
+ if (drag.originFrame !== null) {
831
+ drag.currentFrame = clampAwikiDrawerFrame({
832
+ ...drag.originFrame,
833
+ left: drag.originFrame.left + deltaX,
834
+ top: drag.originFrame.top + deltaY,
835
+ }, window.innerWidth, window.innerHeight);
836
+ setDrawerFrame(drag.currentFrame);
837
+ }
715
838
  };
716
839
  const finishLauncherDrag = (event) => {
717
840
  const drag = launcherDrag.current;
@@ -721,6 +844,8 @@ export function AwikiOverlay(props) {
721
844
  suppressLauncherClick.current = true;
722
845
  saveLauncherPosition(drag.current);
723
846
  }
847
+ if (drag.currentFrame !== null)
848
+ saveDrawerFrame(drag.currentFrame);
724
849
  launcherDrag.current = null;
725
850
  setLauncherDragging(false);
726
851
  callPointerCapture(event.currentTarget, 'releasePointerCapture', event.pointerId);
@@ -738,6 +863,8 @@ export function AwikiOverlay(props) {
738
863
  armed: false,
739
864
  moved: false,
740
865
  current: launcherPosition,
866
+ originFrame: drawerFrame,
867
+ currentFrame: drawerFrame,
741
868
  };
742
869
  drag.timer = setTimeout(() => {
743
870
  if (drawerDrag.current !== drag)
@@ -770,6 +897,14 @@ export function AwikiOverlay(props) {
770
897
  top: drag.origin.top + deltaY,
771
898
  }, window.innerWidth, window.innerHeight);
772
899
  setLauncherPosition(drag.current);
900
+ if (drag.originFrame !== null) {
901
+ drag.currentFrame = clampAwikiDrawerFrame({
902
+ ...drag.originFrame,
903
+ left: drag.originFrame.left + deltaX,
904
+ top: drag.originFrame.top + deltaY,
905
+ }, window.innerWidth, window.innerHeight);
906
+ setDrawerFrame(drag.currentFrame);
907
+ }
773
908
  };
774
909
  const finishDrawerDrag = (event) => {
775
910
  const drag = drawerDrag.current;
@@ -778,11 +913,76 @@ export function AwikiOverlay(props) {
778
913
  clearTimeout(drag.timer);
779
914
  if (drag.moved)
780
915
  saveLauncherPosition(drag.current);
916
+ if (drag.currentFrame !== null)
917
+ saveDrawerFrame(drag.currentFrame);
781
918
  drawerDrag.current = null;
782
919
  setDrawerDragging(false);
783
920
  setDrawerDragDirection(null);
784
921
  callPointerCapture(event.currentTarget, 'releasePointerCapture', event.pointerId);
785
922
  };
923
+ const onDrawerResizePointerDown = (event, direction) => {
924
+ if (event.button !== 0)
925
+ return;
926
+ event.preventDefault();
927
+ event.stopPropagation();
928
+ const origin = clampAwikiDrawerFrame(renderedDrawerFrame, window.innerWidth, window.innerHeight);
929
+ drawerResize.current = {
930
+ pointerId: event.pointerId,
931
+ direction,
932
+ startX: event.clientX,
933
+ startY: event.clientY,
934
+ origin,
935
+ current: origin,
936
+ };
937
+ setDrawerFrame(origin);
938
+ setDrawerResizing(direction);
939
+ callPointerCapture(event.currentTarget, 'setPointerCapture', event.pointerId);
940
+ };
941
+ const onDrawerResizePointerMove = (event) => {
942
+ const resize = drawerResize.current;
943
+ if (resize === null || resize.pointerId !== event.pointerId)
944
+ return;
945
+ event.preventDefault();
946
+ resize.current = resizeAwikiDrawerFrame(resize.origin, resize.direction, event.clientX - resize.startX, event.clientY - resize.startY, window.innerWidth, window.innerHeight);
947
+ setDrawerFrame(resize.current);
948
+ };
949
+ const finishDrawerResize = (event) => {
950
+ const resize = drawerResize.current;
951
+ if (resize === null || resize.pointerId !== event.pointerId)
952
+ return;
953
+ saveDrawerFrame(resize.current);
954
+ drawerResize.current = null;
955
+ setDrawerResizing(null);
956
+ callPointerCapture(event.currentTarget, 'releasePointerCapture', event.pointerId);
957
+ };
958
+ useEffect(() => {
959
+ if (drawerResizing === null)
960
+ return;
961
+ const onPointerMove = (event) => {
962
+ const resize = drawerResize.current;
963
+ if (resize === null || resize.pointerId !== event.pointerId)
964
+ return;
965
+ event.preventDefault();
966
+ resize.current = resizeAwikiDrawerFrame(resize.origin, resize.direction, event.clientX - resize.startX, event.clientY - resize.startY, window.innerWidth, window.innerHeight);
967
+ setDrawerFrame(resize.current);
968
+ };
969
+ const onPointerEnd = (event) => {
970
+ const resize = drawerResize.current;
971
+ if (resize === null || resize.pointerId !== event.pointerId)
972
+ return;
973
+ saveDrawerFrame(resize.current);
974
+ drawerResize.current = null;
975
+ setDrawerResizing(null);
976
+ };
977
+ window.addEventListener('pointermove', onPointerMove, { passive: false });
978
+ window.addEventListener('pointerup', onPointerEnd);
979
+ window.addEventListener('pointercancel', onPointerEnd);
980
+ return () => {
981
+ window.removeEventListener('pointermove', onPointerMove);
982
+ window.removeEventListener('pointerup', onPointerEnd);
983
+ window.removeEventListener('pointercancel', onPointerEnd);
984
+ };
985
+ }, [drawerResizing]);
786
986
  const toggleLauncher = () => {
787
987
  if (suppressLauncherClick.current) {
788
988
  suppressLauncherClick.current = false;
@@ -800,6 +1000,21 @@ export function AwikiOverlay(props) {
800
1000
  setComposeDirect(false);
801
1001
  setPeerHandle('');
802
1002
  };
1003
+ const createGroup = async () => {
1004
+ setGroupComposeError(null);
1005
+ const members = groupMembers
1006
+ .split(/[\n,,]+/u)
1007
+ .map(member => member.trim())
1008
+ .filter(member => member !== '');
1009
+ const result = await props.createGroup(groupName, members);
1010
+ if (!result.ok) {
1011
+ setGroupComposeError(result.error);
1012
+ return;
1013
+ }
1014
+ setComposeGroup(false);
1015
+ setGroupName('');
1016
+ setGroupMembers('');
1017
+ };
803
1018
  const logout = async () => {
804
1019
  setLogoutPending(true);
805
1020
  setLogoutError(null);
@@ -814,14 +1029,30 @@ export function AwikiOverlay(props) {
814
1029
  };
815
1030
  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
1031
  ? '收起 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: () => {
1032
+ : 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: {
1033
+ left: renderedDrawerFrame.left,
1034
+ top: renderedDrawerFrame.top,
1035
+ width: renderedDrawerFrame.width,
1036
+ height: renderedDrawerFrame.height,
1037
+ }, "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
1038
  setAccountMenuOpen(false);
819
1039
  setLogoutError(null);
820
1040
  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"] })] }))] }));
1041
+ }, 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" })] }), _jsx("button", { type: "button", "aria-label": mode === 'mail' ? '刷新邮箱' : '刷新 AWiki', disabled: view.pending !== null, onClick: () => {
1042
+ if (mode === 'mail')
1043
+ setMailRefreshRevision(value => value + 1);
1044
+ else
1045
+ void props.open();
1046
+ }, 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(AwikiRegistrationForm, { ...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 && (_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: [
1047
+ { id: 'direct', label: '发起私聊' },
1048
+ { id: 'group', label: '发起群聊' },
1049
+ ], onSelect: (id) => {
1050
+ setMenuOpen(false);
1051
+ if (id === 'direct')
1052
+ setComposeDirect(true);
1053
+ if (id === 'group')
1054
+ setComposeGroup(true);
1055
+ }, 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', identityCard: mode === 'mail' ? _jsx(IdentityCard, { identity: view.identity, pending: view.pending !== null, updateDisplayName: props.updateDisplayName }) : null, modeTabs: _jsx(ModeTabs, { mode: mode, mailUnreadCount: mailUnreadCount, onChange: setMode }), refreshRevision: mailRefreshRevision, onUnreadCountChange: setMailUnreadCount, getMailAccount: props.getMailAccount, listMailInbox: props.listMailInbox, readMail: props.readMail, markMailRead: props.markMailRead, sendMail: props.sendMail }) })] })), 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\u548C\u9996\u6279\u6210\u5458\u3002\u6210\u5458\u652F\u6301 Handle \u6216 DID\uFF0C\u6BCF\u884C\u4E00\u4E2A\uFF0C\u4E5F\u53EF\u4EE5\u7528\u9017\u53F7\u5206\u9694\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: ["\u7FA4\u6210\u5458", _jsx("textarea", { value: groupMembers, onChange: (event) => { setGroupMembers(event.target.value); setGroupComposeError(null); }, rows: 4, placeholder: '例如 alice.awiki.info\nbob.awiki.info' })] }), 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() === '' || groupMembers.trim() === '', children: "\u521B\u5EFA\u7FA4\u804A" })] })] }) })), _jsx(Modal, { open: logoutOpen, onClose: () => { if (!logoutPending)
1056
+ 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"] }), 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
1057
  }
827
1058
  //# sourceMappingURL=AwikiOverlay.js.map