@pmate/chat-sdk 0.1.6 → 0.1.8

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 (63) hide show
  1. package/lib/components/AttachmentPreview.js +20 -4
  2. package/lib/components/ChatInputDesktop.js +149 -43
  3. package/lib/components/ChatInputMobile.js +119 -44
  4. package/lib/components/HoldToTalkButton.js +10 -9
  5. package/lib/components/ImageTrigger.d.ts +1 -1
  6. package/lib/components/ImageTrigger.js +1 -1
  7. package/lib/components/icons.d.ts +3 -0
  8. package/lib/components/icons.js +13 -4
  9. package/lib/hooks/useAttachmentState.d.ts +3 -1
  10. package/lib/hooks/useVoiceInputController.js +21 -2
  11. package/lib/index.d.ts +4 -0
  12. package/lib/index.js +4 -0
  13. package/lib/runtime/botAuth.d.ts +32 -0
  14. package/lib/runtime/botAuth.js +71 -0
  15. package/lib/runtime/botKey.d.ts +14 -0
  16. package/lib/runtime/botKey.js +25 -0
  17. package/lib/runtime/chatClient.d.ts +62 -0
  18. package/lib/runtime/chatClient.js +209 -0
  19. package/lib/runtime/index.d.ts +5 -0
  20. package/lib/runtime/index.js +5 -0
  21. package/lib/runtime/protocol.d.ts +83 -0
  22. package/lib/runtime/protocol.js +113 -0
  23. package/lib/runtime/thread.d.ts +3 -0
  24. package/lib/runtime/thread.js +18 -0
  25. package/lib/tokens/chatInputThemes.js +20 -20
  26. package/lib/tokens/defaultTokens.js +17 -17
  27. package/lib/types/attachment.d.ts +1 -1
  28. package/lib/types/voice.d.ts +1 -0
  29. package/package.json +7 -1
  30. package/dist/assets/ChatInput.stories-CSt4WoEv.js +0 -1702
  31. package/dist/assets/Color-AVL7NMMY-BspvszXC.js +0 -1
  32. package/dist/assets/DocsRenderer-PQXLIZUC-BM8CapQZ.js +0 -1243
  33. package/dist/assets/client-Bw6ZM57k.js +0 -9
  34. package/dist/assets/iframe-Dci6y3pc.js +0 -1071
  35. package/dist/assets/iframe-DnNTepBm.css +0 -1
  36. package/dist/assets/index-DQ_t8Ou6.js +0 -1
  37. package/dist/assets/preload-helper-PPVm8Dsz.js +0 -1
  38. package/dist/assets/react-18-3UzO1Bv7.js +0 -1
  39. package/dist/favicon-wrapper.svg +0 -46
  40. package/dist/favicon.svg +0 -1
  41. package/dist/iframe.html +0 -687
  42. package/dist/index.html +0 -148
  43. package/dist/index.json +0 -1
  44. package/dist/nunito-sans-bold-italic.woff2 +0 -0
  45. package/dist/nunito-sans-bold.woff2 +0 -0
  46. package/dist/nunito-sans-italic.woff2 +0 -0
  47. package/dist/nunito-sans-regular.woff2 +0 -0
  48. package/dist/project.json +0 -1
  49. package/dist/sb-addons/docs-1/manager-bundle.js +0 -151
  50. package/dist/sb-addons/docs-1/manager-bundle.js.LEGAL.txt +0 -0
  51. package/dist/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js +0 -971
  52. package/dist/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js.LEGAL.txt +0 -0
  53. package/dist/sb-common-assets/favicon-wrapper.svg +0 -46
  54. package/dist/sb-common-assets/favicon.svg +0 -1
  55. package/dist/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
  56. package/dist/sb-common-assets/nunito-sans-bold.woff2 +0 -0
  57. package/dist/sb-common-assets/nunito-sans-italic.woff2 +0 -0
  58. package/dist/sb-common-assets/nunito-sans-regular.woff2 +0 -0
  59. package/dist/sb-manager/globals-module-info.js +0 -799
  60. package/dist/sb-manager/globals-runtime.js +0 -69791
  61. package/dist/sb-manager/globals.js +0 -34
  62. package/dist/sb-manager/runtime.js +0 -13198
  63. package/dist/vite-inject-mocker-entry.js +0 -2
@@ -1,13 +1,29 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  export function AttachmentPreview({ attachments, tokens, }) {
4
4
  if (attachments.length === 0) {
5
5
  return null;
6
6
  }
7
- return (_jsx("div", { className: "flex flex-wrap", style: { gap: tokens.spacing.attachmentGap }, children: attachments.map((attachment) => (_jsx("span", { style: {
7
+ return (_jsx("div", { className: "flex flex-wrap", style: { gap: tokens.spacing.attachmentGap }, children: attachments.map((attachment) => (_jsxs("span", { style: {
8
+ display: "inline-flex",
9
+ alignItems: "center",
10
+ gap: 6,
8
11
  borderRadius: tokens.radius.attachmentPreview,
9
12
  background: tokens.color.surfaceInputFocus,
10
13
  color: tokens.color.textSecondary,
11
- padding: "4px 10px",
12
- }, title: attachment.url ?? attachment.name, children: attachment.name }, attachment.id))) }));
14
+ border: `1px solid ${tokens.color.borderDefault}`,
15
+ padding: "6px 10px",
16
+ fontSize: 12,
17
+ lineHeight: "16px",
18
+ fontFamily: tokens.typography.familyBase,
19
+ boxShadow: tokens.shadow.inputElevated,
20
+ }, title: attachment.url ?? attachment.name, children: [_jsx("span", { "aria-hidden": "true", style: {
21
+ width: 6,
22
+ height: 6,
23
+ borderRadius: 999,
24
+ background: attachment.kind === "image"
25
+ ? tokens.color.iconActive
26
+ : tokens.color.textSecondary,
27
+ opacity: 0.9,
28
+ } }), attachment.name] }, attachment.id))) }));
13
29
  }
@@ -11,21 +11,23 @@ import { useVoiceInputController } from "../hooks/useVoiceInputController";
11
11
  import { AttachmentPreview } from "./AttachmentPreview";
12
12
  import { EmojiPicker } from "./EmojiPicker";
13
13
  import { EmojiTrigger } from "./EmojiTrigger";
14
+ import { MoreTrigger } from "./ImageTrigger";
14
15
  import { InputSurface } from "./InputSurface";
15
- import { SendAction } from "./SendAction";
16
16
  import { StatusHint } from "./StatusHint";
17
17
  import { TextComposer } from "./TextComposer";
18
18
  import { WaveformFeedback } from "./WaveformFeedback";
19
- import { ImageOutlineIcon, MicOutlineIcon } from "./icons";
19
+ import { FileOutlineIcon, ImageOutlineIcon, MicOutlineIcon, StopCircleOutlineIcon, } from "./icons";
20
20
  function DesktopInner(props) {
21
21
  const tokens = mergeChatInputTokens(props.tokens);
22
22
  const state = useChatInputState();
23
23
  const attachments = useAttachmentState();
24
24
  const [isEmojiOpen, setIsEmojiOpen] = useState(false);
25
+ const [isMoreOpen, setIsMoreOpen] = useState(false);
25
26
  const [isUploadingImage, setIsUploadingImage] = useState(false);
26
27
  const textComposerRef = useRef(null);
27
28
  const emojiAnchorRef = useRef(null);
28
29
  const imageInputRef = useRef(null);
30
+ const fileInputRef = useRef(null);
29
31
  const voice = useVoiceInputController({
30
32
  ...props.voice,
31
33
  onVoiceSend: async (blob) => {
@@ -99,6 +101,13 @@ function DesktopInner(props) {
99
101
  setIsUploadingImage(false);
100
102
  }
101
103
  };
104
+ const handleFileAttachment = (file) => {
105
+ attachments.addAttachment({
106
+ kind: "file",
107
+ name: file.name,
108
+ file,
109
+ });
110
+ };
102
111
  const insertEmoji = (emoji) => {
103
112
  const textarea = textComposerRef.current;
104
113
  const start = textarea?.selectionStart ?? state.text.length;
@@ -113,53 +122,150 @@ function DesktopInner(props) {
113
122
  textarea?.setSelectionRange(caret, caret);
114
123
  });
115
124
  };
116
- return (_jsx(InputSurface, { tokens: tokens, status: state.status, children: _jsxs("div", { className: "grid gap-3", style: { position: "relative" }, children: [_jsx("input", { ref: imageInputRef, type: "file", accept: props.image?.accept ?? "image/*", style: { display: "none" }, onChange: (event) => {
125
+ const attachmentActionStyle = {
126
+ display: "grid",
127
+ justifyItems: "center",
128
+ alignContent: "center",
129
+ gap: 7,
130
+ minWidth: 72,
131
+ minHeight: 78,
132
+ border: 0,
133
+ borderRadius: 18,
134
+ background: "transparent",
135
+ color: tokens.color.textSecondary,
136
+ cursor: "pointer",
137
+ fontSize: 12,
138
+ fontFamily: tokens.typography.familyBase,
139
+ };
140
+ const attachmentIconStyle = {
141
+ width: 52,
142
+ height: 52,
143
+ borderRadius: 16,
144
+ display: "inline-flex",
145
+ alignItems: "center",
146
+ justifyContent: "center",
147
+ background: tokens.color.surfaceInputDefault,
148
+ color: tokens.color.iconDefault,
149
+ border: `1px solid ${tokens.color.borderDefault}`,
150
+ boxShadow: "0 10px 26px rgba(0, 0, 0, 0.10)",
151
+ };
152
+ const sideActionStyle = {
153
+ display: "inline-flex",
154
+ alignItems: "center",
155
+ justifyContent: "center",
156
+ width: 32,
157
+ height: 32,
158
+ border: 0,
159
+ borderRadius: 999,
160
+ background: "transparent",
161
+ color: tokens.color.iconDefault,
162
+ cursor: "pointer",
163
+ alignSelf: "center",
164
+ transition: `color ${tokens.motion.stateTransition}, transform ${tokens.motion.pressFeedback}`,
165
+ };
166
+ return (_jsx("div", { style: { position: "relative", width: "100%" }, children: _jsxs("div", { className: "grid gap-3", style: { position: "relative", width: "100%" }, children: [_jsx("input", { ref: imageInputRef, type: "file", accept: props.image?.accept ?? "image/*", style: { display: "none" }, onChange: (event) => {
117
167
  const file = event.target.files?.[0];
118
168
  if (file) {
119
169
  void handleImageFile(file);
170
+ setIsMoreOpen(false);
120
171
  }
121
172
  event.currentTarget.value = "";
122
- } }), _jsx(EmojiPicker, { open: isEmojiOpen, isMobile: false, anchorRef: emojiAnchorRef, onSelect: insertEmoji, onClose: () => setIsEmojiOpen(false) }), _jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, placeholder: "Message PMate", onChange: (event) => {
123
- state.setText(event.target.value);
124
- state.setStatus(event.target.value.trim() ? "typing" : "idle");
125
- props.onChange?.(event.target.value);
126
- } }), _jsx(AttachmentPreview, { attachments: attachments.attachments, tokens: tokens }), voice.status === "recording" ? (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(WaveformFeedback, { levels: voice.waveformLevels, tokens: tokens, compact: true }), _jsxs("span", { style: { color: tokens.color.textSecondary, fontSize: 12 }, children: [Math.max(1, Math.round(voice.durationMs / 1000)), "s"] })] })) : null, _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(EmojiTrigger, { ref: emojiAnchorRef, style: { color: tokens.color.iconDefault }, onClick: () => setIsEmojiOpen((prev) => !prev) }), _jsx("button", { type: "button", onClick: () => imageInputRef.current?.click(), style: {
127
- display: "inline-flex",
128
- alignItems: "center",
129
- justifyContent: "center",
130
- width: 32,
131
- height: 32,
132
- border: 0,
133
- background: "transparent",
134
- color: tokens.color.iconDefault,
135
- cursor: props.disabled || isUploadingImage ? "not-allowed" : "pointer",
136
- opacity: props.disabled || isUploadingImage ? 0.4 : 1,
137
- }, disabled: props.disabled || isUploadingImage, children: _jsx(ImageOutlineIcon, { size: 22 }) }), _jsx("button", { type: "button", onClick: () => {
138
- if (props.disabled || props.enableVoice === false) {
139
- return;
140
- }
141
- if (voice.status === "recording") {
142
- voice.stopRecording("transcribe");
143
- return;
173
+ } }), _jsx("input", { ref: fileInputRef, type: "file", style: { display: "none" }, onChange: (event) => {
174
+ const file = event.target.files?.[0];
175
+ if (file) {
176
+ handleFileAttachment(file);
177
+ setIsMoreOpen(false);
178
+ }
179
+ event.currentTarget.value = "";
180
+ } }), _jsx(EmojiPicker, { open: isEmojiOpen, isMobile: false, anchorRef: emojiAnchorRef, onSelect: insertEmoji, onClose: () => setIsEmojiOpen(false) }), _jsx(AttachmentPreview, { attachments: attachments.attachments, tokens: tokens }), isMoreOpen ? (_jsxs("div", { style: {
181
+ position: "absolute",
182
+ left: 0,
183
+ right: 0,
184
+ bottom: 66,
185
+ padding: "12px 14px",
186
+ borderRadius: 24,
187
+ background: `color-mix(in srgb, ${tokens.color.surfaceInputFocus} 94%, transparent)`,
188
+ border: `1px solid ${tokens.color.borderDefault}`,
189
+ display: "flex",
190
+ gap: 12,
191
+ boxShadow: "0 18px 50px rgba(0, 0, 0, 0.16)",
192
+ backdropFilter: "blur(18px)",
193
+ zIndex: 3,
194
+ }, children: [_jsxs("button", { type: "button", style: attachmentActionStyle, disabled: props.disabled || isUploadingImage, onClick: () => imageInputRef.current?.click(), children: [_jsx("span", { style: attachmentIconStyle, children: _jsx(ImageOutlineIcon, { size: 24 }) }), "\u56FE\u7247"] }), _jsxs("button", { type: "button", style: attachmentActionStyle, disabled: props.disabled, onClick: () => fileInputRef.current?.click(), children: [_jsx("span", { style: attachmentIconStyle, children: _jsx(FileOutlineIcon, { size: 24 }) }), "\u6587\u4EF6"] })] })) : null, voice.status === "recording" ? (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(WaveformFeedback, { levels: voice.waveformLevels, tokens: tokens, compact: true }), _jsxs("span", { style: { color: tokens.color.textSecondary, fontSize: 12 }, children: [Math.max(1, Math.round(voice.durationMs / 1000)), "s"] })] })) : null, _jsxs("div", { style: {
195
+ display: "grid",
196
+ gridTemplateColumns: "32px minmax(0, 1fr) 32px 32px",
197
+ alignItems: "center",
198
+ gap: 6,
199
+ }, children: [_jsx("button", { type: "button", onClick: () => {
200
+ if (props.disabled || props.enableVoice === false) {
201
+ return;
202
+ }
203
+ if (voice.status === "recording") {
204
+ voice.stopRecording("transcribe");
205
+ return;
206
+ }
207
+ if (voice.status === "transcribing") {
208
+ return;
209
+ }
210
+ voice.beginVoiceMode();
211
+ voice.startRecording();
212
+ }, style: {
213
+ ...sideActionStyle,
214
+ color: voice.status === "recording" || voice.status === "transcribing"
215
+ ? tokens.color.iconActive
216
+ : tokens.color.iconDefault,
217
+ cursor: props.disabled || props.enableVoice === false ? "not-allowed" : "pointer",
218
+ opacity: props.disabled || props.enableVoice === false ? 0.4 : 1,
219
+ }, "aria-label": voice.status === "recording"
220
+ ? "停止录音并转文字"
221
+ : voice.status === "transcribing"
222
+ ? "正在转写语音"
223
+ : "开始语音输入", children: voice.status === "recording" ? (_jsx(StopCircleOutlineIcon, { size: 27 })) : (_jsx(MicOutlineIcon, { size: 24, strokeWidth: 2 })) }), _jsx(InputSurface, { tokens: tokens, status: state.status, styleOverride: {
224
+ minHeight: 52,
225
+ padding: "0 14px",
226
+ display: "flex",
227
+ alignItems: "center",
228
+ }, children: _jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, rows: 1, placeholder: "Message PMate", onKeyDown: (event) => {
229
+ if (event.key !== "Enter") {
230
+ return;
231
+ }
232
+ const shouldSend = props.submitShortcut === "meta-enter" ? event.metaKey || event.ctrlKey : !event.shiftKey;
233
+ if (shouldSend) {
234
+ event.preventDefault();
235
+ if (state.text.trim() || state.attachments.length > 0) {
236
+ void onSend();
144
237
  }
145
- voice.beginVoiceMode();
146
- voice.startRecording();
147
- }, style: {
148
- display: "inline-flex",
149
- alignItems: "center",
150
- justifyContent: "center",
151
- width: 32,
152
- height: 32,
153
- border: 0,
154
- background: "transparent",
155
- color: voice.status === "recording" || voice.status === "transcribing"
156
- ? tokens.color.iconActive
157
- : tokens.color.iconDefault,
158
- cursor: props.disabled || props.enableVoice === false ? "not-allowed" : "pointer",
159
- opacity: props.disabled || props.enableVoice === false ? 0.4 : 1,
160
- }, "aria-label": voice.status === "recording"
161
- ? "停止录音并转文字"
162
- : "开始语音输入", children: _jsx(MicOutlineIcon, { size: 22 }) })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { style: { color: tokens.color.textSecondary, fontSize: 12 }, children: "Enter send / Shift+Enter newline" }), _jsx(SendAction, { tokens: tokens, disabled: props.disabled || (!state.text.trim() && state.attachments.length === 0), onClick: () => void onSend(), children: "Send" })] })] }), _jsx(StatusHint, { tokens: tokens, text: desktopStatusText, tone: state.error ? "error" : "secondary" })] }) }));
238
+ }
239
+ }, onChange: (event) => {
240
+ state.setText(event.target.value);
241
+ state.setStatus(event.target.value.trim() ? "typing" : "idle");
242
+ props.onChange?.(event.target.value);
243
+ }, style: {
244
+ minHeight: tokens.typography.inputText.lineHeight,
245
+ height: tokens.typography.inputText.lineHeight,
246
+ maxHeight: 128,
247
+ paddingTop: 0,
248
+ paddingBottom: 0,
249
+ display: "block",
250
+ overflow: "hidden",
251
+ caretColor: tokens.color.brandPrimary,
252
+ } }) }), _jsx(EmojiTrigger, { ref: emojiAnchorRef, "data-icon-size": 24, style: {
253
+ ...sideActionStyle,
254
+ width: 32,
255
+ height: 32,
256
+ }, onClick: () => {
257
+ setIsMoreOpen(false);
258
+ setIsEmojiOpen((prev) => !prev);
259
+ } }), _jsx(MoreTrigger, { "data-icon-size": 24, onClick: () => {
260
+ setIsEmojiOpen(false);
261
+ setIsMoreOpen((prev) => !prev);
262
+ }, disabled: props.disabled, style: {
263
+ ...sideActionStyle,
264
+ color: isMoreOpen ? tokens.color.iconActive : tokens.color.iconDefault,
265
+ width: 32,
266
+ height: 32,
267
+ opacity: props.disabled ? 0.4 : 1,
268
+ } })] }), _jsx(StatusHint, { tokens: tokens, text: desktopStatusText, tone: state.error ? "error" : "secondary" })] }) }));
163
269
  }
164
270
  export function ChatInputDesktop(props) {
165
271
  const stateKey = JSON.stringify({
@@ -13,25 +13,26 @@ import { AttachmentPreview } from "./AttachmentPreview";
13
13
  import { EmojiPicker } from "./EmojiPicker";
14
14
  import { EmojiTrigger } from "./EmojiTrigger";
15
15
  import { HoldToTalkButton } from "./HoldToTalkButton";
16
- import { ImageTrigger } from "./ImageTrigger";
16
+ import { MoreTrigger } from "./ImageTrigger";
17
17
  import { InputSurface } from "./InputSurface";
18
- import { SendAction } from "./SendAction";
19
18
  import { StatusHint } from "./StatusHint";
20
19
  import { TextComposer } from "./TextComposer";
21
20
  import { WaveformFeedback } from "./WaveformFeedback";
22
- import { KeyboardOutlineIcon, MicOutlineIcon } from "./icons";
21
+ import { FileOutlineIcon, ImageOutlineIcon, KeyboardOutlineIcon, MicOutlineIcon, } from "./icons";
23
22
  function MobileInner(props) {
24
23
  const tokens = mergeChatInputTokens(props.tokens);
25
24
  const state = useChatInputState();
26
25
  const attachments = useAttachmentState();
27
26
  const [recordingChoice, setRecordingChoice] = useState("send");
28
27
  const [isEmojiOpen, setIsEmojiOpen] = useState(false);
28
+ const [isMoreOpen, setIsMoreOpen] = useState(false);
29
29
  const [isUploadingImage, setIsUploadingImage] = useState(false);
30
30
  const cancelZoneRef = useRef(null);
31
31
  const transcribeZoneRef = useRef(null);
32
32
  const textComposerRef = useRef(null);
33
33
  const emojiAnchorRef = useRef(null);
34
34
  const imageInputRef = useRef(null);
35
+ const fileInputRef = useRef(null);
35
36
  const voice = useVoiceInputController({
36
37
  ...props.voice,
37
38
  onVoiceSend: async (blob) => {
@@ -98,6 +99,13 @@ function MobileInner(props) {
98
99
  setIsUploadingImage(false);
99
100
  }
100
101
  };
102
+ const handleFileAttachment = (file) => {
103
+ attachments.addAttachment({
104
+ kind: "file",
105
+ name: file.name,
106
+ file,
107
+ });
108
+ };
101
109
  const isRecordingOverlayVisible = state.status === "recording" || state.status === "transcribing";
102
110
  const updateRecordingChoice = (clientX, clientY) => {
103
111
  const isInside = (node) => {
@@ -135,10 +143,52 @@ function MobileInner(props) {
135
143
  textarea?.setSelectionRange(caret, caret);
136
144
  });
137
145
  };
146
+ const attachmentActionStyle = {
147
+ display: "grid",
148
+ justifyItems: "center",
149
+ alignContent: "center",
150
+ gap: 7,
151
+ minWidth: 72,
152
+ minHeight: 78,
153
+ border: 0,
154
+ borderRadius: 18,
155
+ background: "transparent",
156
+ color: tokens.color.textSecondary,
157
+ cursor: "pointer",
158
+ fontSize: 12,
159
+ fontFamily: tokens.typography.familyBase,
160
+ };
161
+ const attachmentIconStyle = {
162
+ width: 52,
163
+ height: 52,
164
+ borderRadius: 16,
165
+ display: "inline-flex",
166
+ alignItems: "center",
167
+ justifyContent: "center",
168
+ background: tokens.color.surfaceInputDefault,
169
+ color: tokens.color.iconDefault,
170
+ border: `1px solid ${tokens.color.borderDefault}`,
171
+ boxShadow: "0 10px 26px rgba(0, 0, 0, 0.10)",
172
+ };
173
+ const sideActionStyle = {
174
+ display: "inline-flex",
175
+ alignItems: "center",
176
+ justifyContent: "center",
177
+ width: 30,
178
+ height: 30,
179
+ border: 0,
180
+ borderRadius: 999,
181
+ background: "transparent",
182
+ color: tokens.color.iconDefault,
183
+ cursor: "pointer",
184
+ alignSelf: "center",
185
+ WebkitTapHighlightColor: "transparent",
186
+ transition: `color ${tokens.motion.stateTransition}, transform ${tokens.motion.pressFeedback}`,
187
+ };
138
188
  return (_jsxs("div", { style: {
139
189
  position: "relative",
140
190
  width: "100%",
141
- maxWidth: 420,
191
+ maxWidth: "min(420px, calc(100vw - 16px))",
142
192
  padding: 0,
143
193
  background: "transparent",
144
194
  borderRadius: tokens.radius.inputSurface,
@@ -233,35 +283,48 @@ function MobileInner(props) {
233
283
  const file = event.target.files?.[0];
234
284
  if (file) {
235
285
  void handleImageFile(file);
286
+ setIsMoreOpen(false);
287
+ }
288
+ event.currentTarget.value = "";
289
+ } }), _jsx("input", { ref: fileInputRef, type: "file", style: { display: "none" }, onChange: (event) => {
290
+ const file = event.target.files?.[0];
291
+ if (file) {
292
+ handleFileAttachment(file);
293
+ setIsMoreOpen(false);
236
294
  }
237
295
  event.currentTarget.value = "";
238
- } }), _jsxs("div", { style: {
296
+ } }), isMoreOpen ? (_jsxs("div", { style: {
297
+ position: "absolute",
298
+ left: 0,
299
+ right: 0,
300
+ bottom: 64,
301
+ padding: "12px 14px",
302
+ borderRadius: 24,
303
+ background: `color-mix(in srgb, ${tokens.color.surfaceInputFocus} 94%, transparent)`,
304
+ border: `1px solid ${tokens.color.borderDefault}`,
305
+ display: "flex",
306
+ gap: 12,
307
+ boxShadow: "0 18px 50px rgba(0, 0, 0, 0.16)",
308
+ backdropFilter: "blur(18px)",
309
+ zIndex: 3,
310
+ }, children: [_jsxs("button", { type: "button", style: attachmentActionStyle, disabled: props.disabled || isUploadingImage, onClick: () => imageInputRef.current?.click(), children: [_jsx("span", { style: attachmentIconStyle, children: _jsx(ImageOutlineIcon, { size: 24 }) }), "\u56FE\u7247"] }), _jsxs("button", { type: "button", style: attachmentActionStyle, disabled: props.disabled, onClick: () => fileInputRef.current?.click(), children: [_jsx("span", { style: attachmentIconStyle, children: _jsx(FileOutlineIcon, { size: 24 }) }), "\u6587\u4EF6"] })] })) : null, _jsxs("div", { style: {
239
311
  position: "relative",
240
312
  display: "grid",
241
- gridTemplateColumns: "32px minmax(0, 1fr) 32px auto",
242
- gap: tokens.spacing.actionGap,
243
- alignItems: "end",
313
+ gridTemplateColumns: "30px minmax(0, 1fr) 30px 30px",
314
+ gap: 4,
315
+ alignItems: "center",
244
316
  padding: "0 0 env(safe-area-inset-bottom, 0px)",
245
317
  }, children: [_jsx(EmojiPicker, { open: isEmojiOpen, isMobile: true, anchorRef: emojiAnchorRef, onSelect: insertEmoji, onClose: () => setIsEmojiOpen(false) }), _jsx("button", { type: "button", "aria-label": state.mode === "voice" ? "切换到文字输入" : "切换到语音输入", disabled: props.disabled || props.enableVoice === false, onClick: () => {
246
318
  const next = state.mode === "voice" ? "text" : "voice";
247
319
  state.setMode(next);
248
320
  state.setStatus(next === "voice" ? "voice-mode-ready" : "idle");
249
321
  }, style: {
250
- display: "inline-flex",
251
- alignItems: "center",
252
- justifyContent: "center",
253
- width: 28,
254
- height: 28,
255
- border: 0,
256
- borderRadius: 999,
257
- background: "transparent",
322
+ ...sideActionStyle,
258
323
  color: state.mode === "voice"
259
324
  ? tokens.color.iconActive
260
325
  : tokens.color.iconDefault,
261
- cursor: "pointer",
262
- alignSelf: "center",
263
326
  opacity: props.disabled ? 0.45 : 1,
264
- }, children: state.mode === "voice" ? _jsx(KeyboardOutlineIcon, { size: 24 }) : _jsx(MicOutlineIcon, { size: 24 }) }), _jsx(InputSurface, { tokens: tokens, status: state.status, styleOverride: {
327
+ }, children: state.mode === "voice" ? (_jsx(KeyboardOutlineIcon, { size: 27 })) : (_jsx(MicOutlineIcon, { size: 24, strokeWidth: 2 })) }), _jsx(InputSurface, { tokens: tokens, status: state.status, styleOverride: {
265
328
  borderRadius: tokens.radius.inputSurface,
266
329
  background: state.status === "error"
267
330
  ? tokens.color.surfaceInputError
@@ -276,9 +339,11 @@ function MobileInner(props) {
276
339
  boxShadow: state.status === "recording"
277
340
  ? tokens.shadow.recordingActive
278
341
  : tokens.shadow.inputElevated,
279
- minHeight: state.mode === "voice" ? 48 : 42,
280
- padding: state.mode === "voice" ? "0" : "0 10px",
281
- }, children: state.mode === "voice" ? (_jsx("div", { children: _jsx(HoldToTalkButton, { tokens: tokens, active: isRecordingOverlayVisible, disabled: props.disabled, text: "\u6309\u4F4F \u8BF4\u8BDD", activeText: "\u677E\u5F00 \u53D1\u9001", onPressStart: () => {
342
+ minHeight: 48,
343
+ display: "flex",
344
+ alignItems: "center",
345
+ padding: state.mode === "voice" ? "0" : "0 12px",
346
+ }, children: state.mode === "voice" ? (_jsx("div", { style: { minHeight: 48 }, children: _jsx(HoldToTalkButton, { tokens: tokens, active: isRecordingOverlayVisible, disabled: props.disabled, text: "\u6309\u4F4F \u8BF4\u8BDD", activeText: "\u677E\u5F00 \u53D1\u9001", onPressStart: () => {
282
347
  setRecordingChoice("send");
283
348
  voice.startRecording();
284
349
  }, onPressMove: (event) => {
@@ -297,37 +362,47 @@ function MobileInner(props) {
297
362
  }, onPressCancel: () => {
298
363
  setRecordingChoice("send");
299
364
  voice.cancelRecording();
300
- } }) })) : (_jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, rows: 1, placeholder: "Message", onChange: (event) => {
365
+ } }) })) : (_jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, rows: 1, wrap: "off", placeholder: "Message", enterKeyHint: "send", onKeyDown: (event) => {
366
+ if (event.key === "Enter" && !event.shiftKey) {
367
+ event.preventDefault();
368
+ if (state.text.trim()) {
369
+ void onSend();
370
+ }
371
+ }
372
+ }, onChange: (event) => {
301
373
  state.setText(event.target.value);
302
374
  state.setStatus(event.target.value.trim() ? "typing" : "idle");
303
375
  props.onChange?.(event.target.value);
304
376
  }, style: {
305
- minHeight: 18,
377
+ minHeight: tokens.typography.inputText.lineHeight,
378
+ height: tokens.typography.inputText.lineHeight,
306
379
  maxHeight: 92,
307
- paddingTop: 9,
308
- paddingBottom: 9,
380
+ paddingTop: 0,
381
+ paddingBottom: 0,
382
+ display: "block",
383
+ overflow: "hidden",
384
+ whiteSpace: "pre",
309
385
  color: tokens.color.textPrimary,
310
386
  fontSize: tokens.typography.inputText.size,
311
387
  lineHeight: `${tokens.typography.inputText.lineHeight}px`,
312
388
  caretColor: tokens.color.brandPrimary,
313
- } })) }), _jsx(EmojiTrigger, { ref: emojiAnchorRef, "data-icon-size": 24, onClick: () => setIsEmojiOpen((prev) => !prev), style: {
314
- color: tokens.color.iconDefault,
315
- alignSelf: "center",
316
- width: 28,
317
- height: 28,
318
- } }), state.mode === "text" && state.text.trim() ? (_jsx(SendAction, { tokens: tokens, disabled: props.disabled, onClick: () => void onSend(), style: {
319
- minWidth: 64,
320
- minHeight: 32,
321
- borderRadius: tokens.radius.actionButton,
322
- alignSelf: "center",
323
- padding: "0 14px",
324
- }, children: "Send" })) : (_jsx(ImageTrigger, { "data-icon-size": 24, onClick: () => imageInputRef.current?.click(), disabled: props.disabled || isUploadingImage, style: {
325
- color: tokens.color.iconDefault,
326
- alignSelf: "center",
327
- width: 28,
328
- height: 28,
329
- opacity: props.disabled || isUploadingImage ? 0.4 : 1,
330
- } }))] }), _jsx(StatusHint, { tokens: tokens, text: state.status === "recording" || state.status === "transcribing" ? undefined : statusText, tone: state.status === "error" ? "error" : "secondary" })] }));
389
+ } })) }), _jsx(EmojiTrigger, { ref: emojiAnchorRef, "data-icon-size": 24, onClick: () => {
390
+ setIsMoreOpen(false);
391
+ setIsEmojiOpen((prev) => !prev);
392
+ }, style: {
393
+ ...sideActionStyle,
394
+ width: 30,
395
+ height: 30,
396
+ } }), _jsx(MoreTrigger, { "data-icon-size": 24, onClick: () => {
397
+ setIsEmojiOpen(false);
398
+ setIsMoreOpen((prev) => !prev);
399
+ }, disabled: props.disabled, style: {
400
+ ...sideActionStyle,
401
+ color: isMoreOpen ? tokens.color.iconActive : tokens.color.iconDefault,
402
+ width: 30,
403
+ height: 30,
404
+ opacity: props.disabled ? 0.4 : 1,
405
+ } })] }), _jsx(StatusHint, { tokens: tokens, text: state.status === "recording" || state.status === "transcribing" ? undefined : statusText, tone: state.status === "error" ? "error" : "secondary" })] }));
331
406
  }
332
407
  export function ChatInputMobile(props) {
333
408
  const stateKey = JSON.stringify({
@@ -18,15 +18,16 @@ export function HoldToTalkButton({ tokens, active, disabled, text = "按住 说
18
18
  }, onPointerMove: onPressMove, onPointerUp: onPressEnd, onPointerCancel: onPressCancel, style: {
19
19
  width: "100%",
20
20
  borderRadius: tokens.radius.mobileHoldToTalk,
21
- background: active ? "#ECE2FF" : "#FFFFFF",
22
- color: active ? "#4E2DBF" : "#171717",
23
- border: `1px solid ${active ? "#BFA9FF" : "#E5E7EB"}`,
24
- boxShadow: active ? "0 14px 28px rgba(109, 74, 255, 0.22)" : "none",
25
- padding: "15px 18px",
26
- minHeight: 60,
27
- fontSize: 16,
28
- lineHeight: "22px",
29
- fontWeight: 600,
21
+ background: active ? tokens.color.surfaceInputRecording : "transparent",
22
+ color: active ? tokens.color.iconActive : tokens.color.textPrimary,
23
+ border: 0,
24
+ boxShadow: "none",
25
+ padding: "0 16px",
26
+ minHeight: 48,
27
+ fontFamily: tokens.typography.familyBase,
28
+ fontSize: tokens.typography.voiceHoldToTalk.size,
29
+ lineHeight: `${tokens.typography.voiceHoldToTalk.lineHeight}px`,
30
+ fontWeight: tokens.typography.voiceHoldToTalk.weight,
30
31
  opacity: disabled ? 0.55 : 1,
31
32
  userSelect: "none",
32
33
  WebkitUserSelect: "none",
@@ -1,2 +1,2 @@
1
1
  import type { ButtonHTMLAttributes } from "react";
2
- export declare function ImageTrigger(props: ButtonHTMLAttributes<HTMLButtonElement>): import("react/jsx-runtime").JSX.Element;
2
+ export declare function MoreTrigger(props: ButtonHTMLAttributes<HTMLButtonElement>): import("react/jsx-runtime").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { PlusOutlineIcon } from "./icons";
4
- export function ImageTrigger(props) {
4
+ export function MoreTrigger(props) {
5
5
  const iconSize = Number(props["data-icon-size"] ?? 26);
6
6
  return (_jsx("button", { type: "button", "aria-label": "More", ...props, style: {
7
7
  display: "inline-flex",
@@ -4,8 +4,11 @@ type IconProps = {
4
4
  strokeWidth?: number;
5
5
  };
6
6
  export declare function MicOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
7
+ export declare function VoiceInputOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
7
8
  export declare function KeyboardOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function StopCircleOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
8
10
  export declare function SmileOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
9
11
  export declare function PlusOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
10
12
  export declare function ImageOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function FileOutlineIcon({ size, stroke, strokeWidth, }: IconProps): import("react/jsx-runtime").JSX.Element;
11
14
  export {};
@@ -6,7 +6,10 @@ function BaseIcon({ size = 22, children, }) {
6
6
  export function MicOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.9, }) {
7
7
  return (_jsxs(BaseIcon, { size: size, children: [_jsx("rect", { x: "9", y: "3", width: "6", height: "11", rx: "3", stroke: stroke, strokeWidth: strokeWidth }), _jsx("path", { d: "M6.5 11.5C6.5 14.5376 8.96243 17 12 17C15.0376 17 17.5 14.5376 17.5 11.5", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" }), _jsx("path", { d: "M12 17V21", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" }), _jsx("path", { d: "M8.5 21H15.5", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" })] }));
8
8
  }
9
- export function KeyboardOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.8, }) {
9
+ export function VoiceInputOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.7, }) {
10
+ return (_jsxs(BaseIcon, { size: size, children: [_jsx("circle", { cx: "12", cy: "12", r: "9", stroke: stroke, strokeWidth: strokeWidth }), _jsx("path", { d: "M7.4 10.6H8.9L11 8.7V15.3L8.9 13.4H7.4V10.6Z", stroke: stroke, strokeWidth: strokeWidth, strokeLinejoin: "round" }), _jsx("path", { d: "M13.25 9.6C13.9 10.2 14.25 11.02 14.25 12C14.25 12.98 13.9 13.8 13.25 14.4", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" }), _jsx("path", { d: "M15.45 7.45C16.63 8.55 17.3 10.12 17.3 12C17.3 13.88 16.63 15.45 15.45 16.55", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" })] }));
11
+ }
12
+ export function KeyboardOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.7, }) {
10
13
  return (_jsxs(BaseIcon, { size: size, children: [_jsx("rect", { x: "3", y: "5", width: "18", height: "14", rx: "3", stroke: stroke, strokeWidth: strokeWidth }), [
11
14
  [6.5, 9],
12
15
  [10, 9],
@@ -18,12 +21,18 @@ export function KeyboardOutlineIcon({ size, stroke = "currentColor", strokeWidth
18
21
  [17, 12.5],
19
22
  ].map(([cx, cy], index) => (_jsx("circle", { cx: cx, cy: cy, r: "0.8", fill: stroke }, index))), _jsx("path", { d: "M7 16.2H17", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" })] }));
20
23
  }
21
- export function SmileOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.8, }) {
24
+ export function StopCircleOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.8, }) {
25
+ return (_jsxs(BaseIcon, { size: size, children: [_jsx("circle", { cx: "12", cy: "12", r: "9", stroke: stroke, strokeWidth: strokeWidth }), _jsx("rect", { x: "8.4", y: "8.4", width: "7.2", height: "7.2", rx: "1.4", stroke: stroke, strokeWidth: strokeWidth, strokeLinejoin: "round" })] }));
26
+ }
27
+ export function SmileOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.7, }) {
22
28
  return (_jsxs(BaseIcon, { size: size, children: [_jsx("circle", { cx: "12", cy: "12", r: "9", stroke: stroke, strokeWidth: strokeWidth }), _jsx("circle", { cx: "9", cy: "10", r: "1", fill: stroke }), _jsx("circle", { cx: "15", cy: "10", r: "1", fill: stroke }), _jsx("path", { d: "M8.2 14.2C9.15 15.55 10.42 16.2 12 16.2C13.58 16.2 14.85 15.55 15.8 14.2", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" })] }));
23
29
  }
24
- export function PlusOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.8, }) {
30
+ export function PlusOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.7, }) {
25
31
  return (_jsxs(BaseIcon, { size: size, children: [_jsx("circle", { cx: "12", cy: "12", r: "9", stroke: stroke, strokeWidth: strokeWidth }), _jsx("path", { d: "M12 8V16", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" }), _jsx("path", { d: "M8 12H16", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" })] }));
26
32
  }
27
- export function ImageOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.8, }) {
33
+ export function ImageOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.7, }) {
28
34
  return (_jsxs(BaseIcon, { size: size, children: [_jsx("rect", { x: "4", y: "5", width: "16", height: "14", rx: "3", stroke: stroke, strokeWidth: strokeWidth }), _jsx("circle", { cx: "9", cy: "10", r: "1.6", stroke: stroke, strokeWidth: strokeWidth }), _jsx("path", { d: "M7 16L11.2 12.1C11.75 11.58 12.63 11.58 13.18 12.1L17 15.6", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round" })] }));
29
35
  }
36
+ export function FileOutlineIcon({ size, stroke = "currentColor", strokeWidth = 1.7, }) {
37
+ return (_jsxs(BaseIcon, { size: size, children: [_jsx("path", { d: "M7 3.8H13.4L18 8.4V20.2H7V3.8Z", stroke: stroke, strokeWidth: strokeWidth, strokeLinejoin: "round" }), _jsx("path", { d: "M13.4 3.8V8.4H18", stroke: stroke, strokeWidth: strokeWidth, strokeLinejoin: "round" }), _jsx("path", { d: "M9.7 12.4H15.3", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" }), _jsx("path", { d: "M9.7 15.5H14", stroke: stroke, strokeWidth: strokeWidth, strokeLinecap: "round" })] }));
38
+ }
@@ -1,6 +1,8 @@
1
1
  import type { ChatAttachment } from "../types/attachment";
2
2
  export declare function useAttachmentState(): {
3
3
  attachments: ChatAttachment[];
4
- addAttachment(attachment: Omit<ChatAttachment, "id" | "kind">): void;
4
+ addAttachment(attachment: Omit<ChatAttachment, "id" | "kind"> & {
5
+ kind?: ChatAttachment["kind"];
6
+ }): void;
5
7
  removeAttachment(id: string): void;
6
8
  };