@pmate/chat-sdk 0.2.3 → 0.3.0
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.
- package/README.md +29 -0
- package/dist/assets/{ChatInput.stories-pjQKncGw.js → ChatInput.stories-BQcIQDVE.js} +232 -24
- package/dist/assets/{Color-AVL7NMMY-CKkkQz33.js → Color-AVL7NMMY-Q9rY0eHd.js} +1 -1
- package/dist/assets/{DocsRenderer-PQXLIZUC-CVx0v9rK.js → DocsRenderer-PQXLIZUC-B5DlnesB.js} +4 -4
- package/dist/assets/{client-CMlGEQEC.js → client-DTiYOHAb.js} +1 -1
- package/dist/assets/{iframe-Bo_J3UyC.js → iframe-1QrxCBqr.js} +3 -3
- package/dist/assets/{index-D9myAQ7a.js → index-CkqBtKlT.js} +1 -1
- package/dist/assets/{react-18-CVKdgGic.js → react-18-6BKf31wF.js} +1 -1
- package/dist/iframe.html +1 -1
- package/dist/index.html +1 -1
- package/dist/index.json +1 -1
- package/dist/project.json +1 -1
- package/lib/components/ChatInputDesktop.js +146 -141
- package/lib/components/ChatInputMobile.js +12 -6
- package/lib/components/MentionMenu.d.ts +15 -0
- package/lib/components/MentionMenu.js +16 -0
- package/lib/hooks/useMentions.d.ts +38 -0
- package/lib/hooks/useMentions.js +83 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/lib/mentions/model.d.ts +28 -0
- package/lib/mentions/model.js +40 -0
- package/lib/runtime/chatClient.d.ts +2 -0
- package/lib/runtime/chatClient.js +1 -1
- package/lib/types/chatInput.d.ts +4 -0
- package/package.json +16 -17
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as s}from"./iframe-
|
|
1
|
+
import{r as s}from"./iframe-1QrxCBqr.js";import{c as l}from"./client-DTiYOHAb.js";import"./preload-helper-PPVm8Dsz.js";var n=new Map;function m(){return globalThis.IS_REACT_ACT_ENVIRONMENT}var a=({callback:e,children:r})=>{let t=s.useRef();return s.useLayoutEffect(()=>{t.current!==e&&(t.current=e,e())},[e]),r};typeof Promise.withResolvers>"u"&&(Promise.withResolvers=()=>{let e=null,r=null;return{promise:new Promise((t,o)=>{e=t,r=o}),resolve:e,reject:r}});var v=async(e,r,t)=>{let o=await c(r,t);if(m()){o.render(e);return}let{promise:i,resolve:u}=Promise.withResolvers();return o.render(s.createElement(a,{callback:u},e)),i},f=(e,r)=>{let t=n.get(e);t&&(t.unmount(),n.delete(e))},c=async(e,r)=>{let t=n.get(e);return t||(t=l.createRoot(e,r),n.set(e,t)),t};export{v as renderElement,f as unmountElement};
|
package/dist/iframe.html
CHANGED
|
@@ -529,7 +529,7 @@
|
|
|
529
529
|
}
|
|
530
530
|
</script>
|
|
531
531
|
|
|
532
|
-
<script type="module" crossorigin src="./assets/iframe-
|
|
532
|
+
<script type="module" crossorigin src="./assets/iframe-1QrxCBqr.js"></script>
|
|
533
533
|
<link rel="modulepreload" crossorigin href="./assets/preload-helper-PPVm8Dsz.js">
|
|
534
534
|
<link rel="stylesheet" crossorigin href="./assets/iframe-4FbYaDjf.css">
|
|
535
535
|
</head>
|
package/dist/index.html
CHANGED
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
|
|
128
128
|
|
|
129
129
|
|
|
130
|
-
window['STORYBOOK_FRAMEWORK'] = "/Users/ramroll/dev/pmate/pmate-mono/node_modules/.pnpm/@storybook+react-vite@9.0.17_react-dom@19.2.3_react@19.2.3__react@19.2.3_rollup@4.54.0__90095e863266ef31e7a8a2925998abad/node_modules/@storybook/react-vite";
|
|
130
|
+
window['STORYBOOK_FRAMEWORK'] = "/Users/ramroll/dev/pmate-envs/pmate-mono-9/node_modules/.pnpm/@storybook+react-vite@9.0.17_react-dom@19.2.3_react@19.2.3__react@19.2.3_rollup@4.54.0__90095e863266ef31e7a8a2925998abad/node_modules/@storybook/react-vite";
|
|
131
131
|
|
|
132
132
|
|
|
133
133
|
</script>
|
package/dist/index.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"v":5,"entries":{"pmate-chat-sdk-chatinput--playground":{"type":"story","id":"pmate-chat-sdk-chatinput--playground","name":"Playground","title":"@pmate/chat-sdk/ChatInput","importPath":"./src/stories/ChatInput.stories.tsx","tags":["dev","test"]},"pmate-chat-sdk-chatinput--desktop-multiline":{"type":"story","id":"pmate-chat-sdk-chatinput--desktop-multiline","name":"Desktop Multiline","title":"@pmate/chat-sdk/ChatInput","importPath":"./src/stories/ChatInput.stories.tsx","tags":["dev","test"]}}}
|
|
1
|
+
{"v":5,"entries":{"pmate-chat-sdk-chatinput--playground":{"type":"story","id":"pmate-chat-sdk-chatinput--playground","name":"Playground","title":"@pmate/chat-sdk/ChatInput","importPath":"./src/stories/ChatInput.stories.tsx","tags":["dev","test"]},"pmate-chat-sdk-chatinput--desktop-multiline":{"type":"story","id":"pmate-chat-sdk-chatinput--desktop-multiline","name":"Desktop Multiline","title":"@pmate/chat-sdk/ChatInput","importPath":"./src/stories/ChatInput.stories.tsx","tags":["dev","test"]},"pmate-chat-sdk-chatinput--group-mentions":{"type":"story","id":"pmate-chat-sdk-chatinput--group-mentions","name":"Group Mentions","title":"@pmate/chat-sdk/ChatInput","importPath":"./src/stories/ChatInput.stories.tsx","tags":["dev","test"]},"pmate-chat-sdk-chatinput--mobile-group-mentions":{"type":"story","id":"pmate-chat-sdk-chatinput--mobile-group-mentions","name":"Mobile Group Mentions","title":"@pmate/chat-sdk/ChatInput","importPath":"./src/stories/ChatInput.stories.tsx","tags":["dev","test"]},"pmate-chat-sdk-chatinput--dark-group-mentions":{"type":"story","id":"pmate-chat-sdk-chatinput--dark-group-mentions","name":"Dark Group Mentions","title":"@pmate/chat-sdk/ChatInput","importPath":"./src/stories/ChatInput.stories.tsx","tags":["dev","test"]}}}
|
package/dist/project.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"generatedAt":
|
|
1
|
+
{"generatedAt":1791336224560,"userSince":1774065191593,"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":false,"hasStorybookEslint":false,"refCount":0,"testPackages":{"vitest":"4.0.17"},"hasRouterPackage":false,"monorepo":"Turborepo","packageManager":{"type":"pnpm","version":"10.6.5","agent":"pnpm","nodeLinker":"undefined"},"preview":{"usesGlobals":false},"framework":{"name":"@storybook/react-vite","options":{}},"builder":"@storybook/builder-vite","renderer":"@storybook/react","portableStoriesFileCount":0,"applicationFileCount":0,"storybookVersion":"9.1.17","language":"typescript","storybookPackages":{"@storybook/addon-docs":{"version":"9.1.17"},"@storybook/react-vite":{"version":"9.0.17"},"storybook":{"version":"9.1.17"}},"addons":{"$SNIP/node_modules/.pnpm/@storybook+addon-docs@9.1.17_@types+react@19.2.8_storybook@9.1.17_@testing-library+dom@_6282815dfd5a1a048c68a92ca86a2d86/node_modules/@storybook/addon-docs":{}}}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMentions } from "../hooks/useMentions";
|
|
3
|
+
import { MentionMenu } from "./MentionMenu";
|
|
2
4
|
import React from "react";
|
|
3
5
|
import { Provider, createStore } from "jotai";
|
|
4
6
|
import { useEffect, useRef, useState } from "react";
|
|
@@ -22,6 +24,10 @@ function DesktopInner(props) {
|
|
|
22
24
|
const tokens = mergeChatInputTokens(props.tokens);
|
|
23
25
|
const state = useChatInputState();
|
|
24
26
|
const attachments = useAttachmentState();
|
|
27
|
+
const mentions = useMentions({
|
|
28
|
+
value: state.text, config: props.mentions, disabled: props.disabled || state.status === "sending" || state.mode === "voice",
|
|
29
|
+
onChange(value) { state.setText(value); state.setStatus(value.trim() ? "typing" : "idle"); props.onChange?.(value); },
|
|
30
|
+
});
|
|
25
31
|
const [isEmojiOpen, setIsEmojiOpen] = useState(false);
|
|
26
32
|
const [isMoreOpen, setIsMoreOpen] = useState(false);
|
|
27
33
|
const [isUploadingImage, setIsUploadingImage] = useState(false);
|
|
@@ -66,6 +72,7 @@ function DesktopInner(props) {
|
|
|
66
72
|
state.setStatus("sending");
|
|
67
73
|
await props.onSend({
|
|
68
74
|
text: state.text,
|
|
75
|
+
mentions: mentions.getMentions(),
|
|
69
76
|
attachments: state.attachments,
|
|
70
77
|
transcript: state.transcript || undefined,
|
|
71
78
|
});
|
|
@@ -178,153 +185,151 @@ function DesktopInner(props) {
|
|
|
178
185
|
alignSelf: "center",
|
|
179
186
|
transition: `color ${tokens.motion.stateTransition}, transform ${tokens.motion.pressFeedback}`,
|
|
180
187
|
};
|
|
181
|
-
return (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
event.
|
|
218
|
-
|
|
219
|
-
event.keyCode === 229)
|
|
220
|
-
return;
|
|
221
|
-
event.preventDefault();
|
|
222
|
-
if (state.text.trim() || state.attachments.length > 0)
|
|
223
|
-
void onSend();
|
|
224
|
-
}, onChange: (event) => {
|
|
225
|
-
state.setText(event.target.value);
|
|
226
|
-
state.setStatus(event.target.value.trim() ? "typing" : "idle");
|
|
227
|
-
props.onChange?.(event.target.value);
|
|
228
|
-
}, style: {
|
|
229
|
-
flex: 1,
|
|
230
|
-
minHeight: tokens.sizing.multilineTextMinHeight,
|
|
231
|
-
maxHeight: tokens.sizing.multilineTextMaxHeight,
|
|
232
|
-
padding: 0,
|
|
233
|
-
overflowY: "auto",
|
|
234
|
-
caretColor: tokens.color.brandPrimary,
|
|
235
|
-
} }), _jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }, children: [_jsx(MoreTrigger, { "data-icon-size": tokens.sizing.desktopIcon, onClick: () => { setIsEmojiOpen(false); setIsMoreOpen((prev) => !prev); }, disabled: props.disabled, style: { ...sideActionStyle, color: isMoreOpen ? tokens.color.iconActive : tokens.color.iconDefault, opacity: props.disabled ? 0.4 : 1 } }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("button", { type: "button", onClick: () => {
|
|
236
|
-
if (props.disabled || props.enableVoice === false)
|
|
237
|
-
return;
|
|
238
|
-
if (voice.status === "recording") {
|
|
239
|
-
voice.stopRecording("transcribe");
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
if (voice.status === "transcribing")
|
|
243
|
-
return;
|
|
244
|
-
voice.beginVoiceMode();
|
|
245
|
-
voice.startRecording();
|
|
246
|
-
}, style: {
|
|
247
|
-
...sideActionStyle,
|
|
248
|
-
color: voice.status === "recording" || voice.status === "transcribing" ? tokens.color.iconActive : tokens.color.iconDefault,
|
|
249
|
-
cursor: props.disabled || props.enableVoice === false ? "not-allowed" : "pointer",
|
|
250
|
-
opacity: props.disabled || props.enableVoice === false ? 0.4 : 1,
|
|
251
|
-
}, "aria-label": voice.status === "recording" ? "停止录音并转文字" : voice.status === "transcribing" ? "正在转写语音" : "开始语音输入", children: voice.status === "recording" ? _jsx(StopCircleOutlineIcon, { size: tokens.sizing.desktopIcon }) : _jsx(MicOutlineIcon, { size: tokens.sizing.desktopIcon, strokeWidth: 2 }) }), _jsx(EmojiTrigger, { ref: emojiAnchorRef, "data-icon-size": tokens.sizing.desktopIcon, style: sideActionStyle, onClick: () => { setIsMoreOpen(false); setIsEmojiOpen((prev) => !prev); } }), _jsx(SendAction, { tokens: tokens, disabled: props.disabled || state.status === "sending" || (!state.text.trim() && state.attachments.length === 0), "aria-label": "\u53D1\u9001\u6D88\u606F", onClick: () => void onSend(), style: { width: tokens.sizing.desktopSendAction, height: tokens.sizing.desktopSendAction, padding: 0, borderRadius: 999, fontSize: tokens.sizing.desktopSendIcon, lineHeight: `${tokens.sizing.desktopSendAction}px` }, children: "\u2191" })] })] })] })) : _jsxs("div", { style: {
|
|
252
|
-
display: "grid",
|
|
253
|
-
gridTemplateColumns: "32px minmax(0, 1fr) 32px 32px",
|
|
254
|
-
alignItems: "center",
|
|
255
|
-
gap: 6,
|
|
256
|
-
}, children: [_jsx("button", { type: "button", onClick: () => {
|
|
257
|
-
if (props.disabled || props.enableVoice === false) {
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
if (voice.status === "recording") {
|
|
261
|
-
voice.stopRecording("transcribe");
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
if (voice.status === "transcribing") {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
voice.beginVoiceMode();
|
|
268
|
-
voice.startRecording();
|
|
269
|
-
}, style: {
|
|
270
|
-
...sideActionStyle,
|
|
271
|
-
color: voice.status === "recording" || voice.status === "transcribing"
|
|
272
|
-
? tokens.color.iconActive
|
|
273
|
-
: tokens.color.iconDefault,
|
|
274
|
-
cursor: props.disabled || props.enableVoice === false ? "not-allowed" : "pointer",
|
|
275
|
-
opacity: props.disabled || props.enableVoice === false ? 0.4 : 1,
|
|
276
|
-
}, "aria-label": voice.status === "recording"
|
|
277
|
-
? "停止录音并转文字"
|
|
278
|
-
: voice.status === "transcribing"
|
|
279
|
-
? "正在转写语音"
|
|
280
|
-
: "开始语音输入", children: voice.status === "recording" ? (_jsx(StopCircleOutlineIcon, { size: 27 })) : (_jsx(MicOutlineIcon, { size: 24, strokeWidth: 2 })) }), _jsx(InputSurface, { tokens: tokens, status: state.status, styleOverride: {
|
|
281
|
-
minHeight: 52,
|
|
282
|
-
padding: "0 14px",
|
|
283
|
-
display: "flex",
|
|
284
|
-
alignItems: "center",
|
|
285
|
-
}, children: _jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, rows: 1, placeholder: "Message PMate", onPaste: handlePaste, onDrop: props.onDrop, onKeyDown: (event) => {
|
|
188
|
+
return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [_jsx(MentionMenu, { ...mentions.menuProps, tokens: tokens }), _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) => {
|
|
189
|
+
const file = event.target.files?.[0];
|
|
190
|
+
if (file) {
|
|
191
|
+
void handleImageFile(file);
|
|
192
|
+
setIsMoreOpen(false);
|
|
193
|
+
}
|
|
194
|
+
event.currentTarget.value = "";
|
|
195
|
+
} }), _jsx("input", { ref: fileInputRef, type: "file", style: { display: "none" }, onChange: (event) => {
|
|
196
|
+
const file = event.target.files?.[0];
|
|
197
|
+
if (file) {
|
|
198
|
+
handleFileAttachment(file);
|
|
199
|
+
setIsMoreOpen(false);
|
|
200
|
+
}
|
|
201
|
+
event.currentTarget.value = "";
|
|
202
|
+
} }), _jsx(EmojiPicker, { open: isEmojiOpen, isMobile: false, anchorRef: emojiAnchorRef, onSelect: insertEmoji, onClose: () => setIsEmojiOpen(false) }), _jsx(AttachmentPreview, { attachments: attachments.attachments, tokens: tokens, onRemove: attachments.removeAttachment }), isMoreOpen ? (_jsxs("div", { style: {
|
|
203
|
+
position: "absolute",
|
|
204
|
+
left: 0,
|
|
205
|
+
right: 0,
|
|
206
|
+
bottom: props.multiline ? 154 : 66,
|
|
207
|
+
padding: "12px 14px",
|
|
208
|
+
borderRadius: 24,
|
|
209
|
+
background: `color-mix(in srgb, ${tokens.color.surfaceInputFocus} 94%, transparent)`,
|
|
210
|
+
border: `1px solid ${tokens.color.borderDefault}`,
|
|
211
|
+
display: "flex",
|
|
212
|
+
gap: 12,
|
|
213
|
+
boxShadow: "0 18px 50px rgba(0, 0, 0, 0.16)",
|
|
214
|
+
backdropFilter: "blur(18px)",
|
|
215
|
+
zIndex: 3,
|
|
216
|
+
}, 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, props.multiline ? (_jsxs(InputSurface, { tokens: tokens, status: state.status, styleOverride: {
|
|
217
|
+
minHeight: tokens.sizing.multilineMinHeight,
|
|
218
|
+
padding: "16px 18px 12px",
|
|
219
|
+
display: "flex",
|
|
220
|
+
flexDirection: "column",
|
|
221
|
+
gap: 12,
|
|
222
|
+
}, children: [_jsx(TextComposer, { ref: textComposerRef, tokens: tokens, ...mentions.inputProps, disabled: props.disabled || state.status === "sending", value: state.text, rows: 3, placeholder: "Message PMate", onPaste: handlePaste, onDrop: props.onDrop, onKeyDown: (event) => {
|
|
223
|
+
mentions.inputProps.onKeyDown(event);
|
|
224
|
+
if (event.defaultPrevented)
|
|
225
|
+
return;
|
|
286
226
|
if (event.key !== "Enter" ||
|
|
227
|
+
event.shiftKey ||
|
|
287
228
|
event.nativeEvent.isComposing ||
|
|
288
|
-
event.keyCode === 229)
|
|
229
|
+
event.keyCode === 229)
|
|
230
|
+
return;
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
if (state.text.trim() || state.attachments.length > 0)
|
|
233
|
+
void onSend();
|
|
234
|
+
}, style: {
|
|
235
|
+
flex: 1,
|
|
236
|
+
minHeight: tokens.sizing.multilineTextMinHeight,
|
|
237
|
+
maxHeight: tokens.sizing.multilineTextMaxHeight,
|
|
238
|
+
padding: 0,
|
|
239
|
+
overflowY: "auto",
|
|
240
|
+
caretColor: tokens.color.brandPrimary,
|
|
241
|
+
} }), _jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }, children: [_jsx(MoreTrigger, { "data-icon-size": tokens.sizing.desktopIcon, onClick: () => { setIsEmojiOpen(false); setIsMoreOpen((prev) => !prev); }, disabled: props.disabled, style: { ...sideActionStyle, color: isMoreOpen ? tokens.color.iconActive : tokens.color.iconDefault, opacity: props.disabled ? 0.4 : 1 } }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("button", { type: "button", onClick: () => {
|
|
242
|
+
if (props.disabled || props.enableVoice === false)
|
|
243
|
+
return;
|
|
244
|
+
if (voice.status === "recording") {
|
|
245
|
+
voice.stopRecording("transcribe");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (voice.status === "transcribing")
|
|
249
|
+
return;
|
|
250
|
+
voice.beginVoiceMode();
|
|
251
|
+
voice.startRecording();
|
|
252
|
+
}, style: {
|
|
253
|
+
...sideActionStyle,
|
|
254
|
+
color: voice.status === "recording" || voice.status === "transcribing" ? tokens.color.iconActive : tokens.color.iconDefault,
|
|
255
|
+
cursor: props.disabled || props.enableVoice === false ? "not-allowed" : "pointer",
|
|
256
|
+
opacity: props.disabled || props.enableVoice === false ? 0.4 : 1,
|
|
257
|
+
}, "aria-label": voice.status === "recording" ? "停止录音并转文字" : voice.status === "transcribing" ? "正在转写语音" : "开始语音输入", children: voice.status === "recording" ? _jsx(StopCircleOutlineIcon, { size: tokens.sizing.desktopIcon }) : _jsx(MicOutlineIcon, { size: tokens.sizing.desktopIcon, strokeWidth: 2 }) }), _jsx(EmojiTrigger, { ref: emojiAnchorRef, "data-icon-size": tokens.sizing.desktopIcon, style: sideActionStyle, onClick: () => { setIsMoreOpen(false); setIsEmojiOpen((prev) => !prev); } }), _jsx(SendAction, { tokens: tokens, disabled: props.disabled || state.status === "sending" || (!state.text.trim() && state.attachments.length === 0), "aria-label": "\u53D1\u9001\u6D88\u606F", onClick: () => void onSend(), style: { width: tokens.sizing.desktopSendAction, height: tokens.sizing.desktopSendAction, padding: 0, borderRadius: 999, fontSize: tokens.sizing.desktopSendIcon, lineHeight: `${tokens.sizing.desktopSendAction}px` }, children: "\u2191" })] })] })] })) : _jsxs("div", { style: {
|
|
258
|
+
display: "grid",
|
|
259
|
+
gridTemplateColumns: "32px minmax(0, 1fr) 32px 32px",
|
|
260
|
+
alignItems: "center",
|
|
261
|
+
gap: 6,
|
|
262
|
+
}, children: [_jsx("button", { type: "button", onClick: () => {
|
|
263
|
+
if (props.disabled || props.enableVoice === false) {
|
|
289
264
|
return;
|
|
290
265
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
266
|
+
if (voice.status === "recording") {
|
|
267
|
+
voice.stopRecording("transcribe");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (voice.status === "transcribing") {
|
|
271
|
+
return;
|
|
297
272
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
state.setStatus(event.target.value.trim() ? "typing" : "idle");
|
|
301
|
-
props.onChange?.(event.target.value);
|
|
273
|
+
voice.beginVoiceMode();
|
|
274
|
+
voice.startRecording();
|
|
302
275
|
}, style: {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
276
|
+
...sideActionStyle,
|
|
277
|
+
color: voice.status === "recording" || voice.status === "transcribing"
|
|
278
|
+
? tokens.color.iconActive
|
|
279
|
+
: tokens.color.iconDefault,
|
|
280
|
+
cursor: props.disabled || props.enableVoice === false ? "not-allowed" : "pointer",
|
|
281
|
+
opacity: props.disabled || props.enableVoice === false ? 0.4 : 1,
|
|
282
|
+
}, "aria-label": voice.status === "recording"
|
|
283
|
+
? "停止录音并转文字"
|
|
284
|
+
: voice.status === "transcribing"
|
|
285
|
+
? "正在转写语音"
|
|
286
|
+
: "开始语音输入", children: voice.status === "recording" ? (_jsx(StopCircleOutlineIcon, { size: 27 })) : (_jsx(MicOutlineIcon, { size: 24, strokeWidth: 2 })) }), _jsx(InputSurface, { tokens: tokens, status: state.status, styleOverride: {
|
|
287
|
+
minHeight: 52,
|
|
288
|
+
padding: "0 14px",
|
|
289
|
+
display: "flex",
|
|
290
|
+
alignItems: "center",
|
|
291
|
+
}, children: _jsx(TextComposer, { ref: textComposerRef, tokens: tokens, ...mentions.inputProps, disabled: props.disabled || state.status === "sending", value: state.text, rows: 1, placeholder: "Message PMate", onPaste: handlePaste, onDrop: props.onDrop, onKeyDown: (event) => {
|
|
292
|
+
mentions.inputProps.onKeyDown(event);
|
|
293
|
+
if (event.defaultPrevented)
|
|
294
|
+
return;
|
|
295
|
+
if (event.key !== "Enter" ||
|
|
296
|
+
event.nativeEvent.isComposing ||
|
|
297
|
+
event.keyCode === 229) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const shouldSend = props.submitShortcut === "meta-enter" ? event.metaKey || event.ctrlKey : !event.shiftKey;
|
|
301
|
+
if (shouldSend) {
|
|
302
|
+
event.preventDefault();
|
|
303
|
+
if (state.text.trim() || state.attachments.length > 0) {
|
|
304
|
+
void onSend();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}, style: {
|
|
308
|
+
minHeight: tokens.typography.inputText.lineHeight,
|
|
309
|
+
height: tokens.typography.inputText.lineHeight,
|
|
310
|
+
maxHeight: 128,
|
|
311
|
+
paddingTop: 0,
|
|
312
|
+
paddingBottom: 0,
|
|
313
|
+
display: "block",
|
|
314
|
+
overflow: "hidden",
|
|
315
|
+
caretColor: tokens.color.brandPrimary,
|
|
316
|
+
} }) }), _jsx(EmojiTrigger, { ref: emojiAnchorRef, "data-icon-size": 24, style: {
|
|
317
|
+
...sideActionStyle,
|
|
318
|
+
width: 32,
|
|
319
|
+
height: 32,
|
|
320
|
+
}, onClick: () => {
|
|
321
|
+
setIsMoreOpen(false);
|
|
322
|
+
setIsEmojiOpen((prev) => !prev);
|
|
323
|
+
} }), _jsx(MoreTrigger, { "data-icon-size": 24, onClick: () => {
|
|
324
|
+
setIsEmojiOpen(false);
|
|
325
|
+
setIsMoreOpen((prev) => !prev);
|
|
326
|
+
}, disabled: props.disabled, style: {
|
|
327
|
+
...sideActionStyle,
|
|
328
|
+
color: isMoreOpen ? tokens.color.iconActive : tokens.color.iconDefault,
|
|
329
|
+
width: 32,
|
|
330
|
+
height: 32,
|
|
331
|
+
opacity: props.disabled ? 0.4 : 1,
|
|
332
|
+
} })] }), _jsx(StatusHint, { tokens: tokens, text: desktopStatusText, tone: state.error ? "error" : "secondary" })] })] }));
|
|
328
333
|
}
|
|
329
334
|
export function ChatInputDesktop(props) {
|
|
330
335
|
const stateKey = JSON.stringify({
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMentions } from "../hooks/useMentions";
|
|
3
|
+
import { MentionMenu } from "./MentionMenu";
|
|
2
4
|
import React from "react";
|
|
3
5
|
import { Provider, createStore } from "jotai";
|
|
4
6
|
import { useEffect, useRef, useState } from "react";
|
|
@@ -23,6 +25,10 @@ function MobileInner(props) {
|
|
|
23
25
|
const tokens = mergeChatInputTokens(props.tokens);
|
|
24
26
|
const state = useChatInputState();
|
|
25
27
|
const attachments = useAttachmentState();
|
|
28
|
+
const mentions = useMentions({
|
|
29
|
+
value: state.text, config: props.mentions, disabled: props.disabled || state.status === "sending" || state.mode === "voice",
|
|
30
|
+
onChange(value) { state.setText(value); state.setStatus(value.trim() ? "typing" : "idle"); props.onChange?.(value); },
|
|
31
|
+
});
|
|
26
32
|
const [recordingChoice, setRecordingChoice] = useState("send");
|
|
27
33
|
const [isEmojiOpen, setIsEmojiOpen] = useState(false);
|
|
28
34
|
const [isMoreOpen, setIsMoreOpen] = useState(false);
|
|
@@ -69,6 +75,7 @@ function MobileInner(props) {
|
|
|
69
75
|
state.setStatus("sending");
|
|
70
76
|
await props.onSend({
|
|
71
77
|
text: state.text,
|
|
78
|
+
mentions: mentions.getMentions(),
|
|
72
79
|
attachments: state.attachments,
|
|
73
80
|
transcript: state.transcript || undefined,
|
|
74
81
|
mode: "text",
|
|
@@ -264,7 +271,7 @@ function MobileInner(props) {
|
|
|
264
271
|
userSelect: "none",
|
|
265
272
|
WebkitUserSelect: "none",
|
|
266
273
|
WebkitTouchCallout: "none",
|
|
267
|
-
}, children: [isRecordingOverlayVisible ? (_jsxs("div", { style: {
|
|
274
|
+
}, children: [_jsx(MentionMenu, { ...mentions.menuProps, tokens: tokens }), isRecordingOverlayVisible ? (_jsxs("div", { style: {
|
|
268
275
|
position: "fixed",
|
|
269
276
|
left: "50%",
|
|
270
277
|
bottom: "calc(72px + env(safe-area-inset-bottom, 0px))",
|
|
@@ -552,7 +559,10 @@ function MobileInner(props) {
|
|
|
552
559
|
}, onPressCancel: () => {
|
|
553
560
|
setRecordingChoice("send");
|
|
554
561
|
voice.cancelRecording();
|
|
555
|
-
} }) })) : (_jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, rows: 1, wrap: "off", placeholder: "Message", enterKeyHint: "send", onPaste: handlePaste, onDrop: props.onDrop, onKeyDown: (event) => {
|
|
562
|
+
} }) })) : (_jsx(TextComposer, { ref: textComposerRef, tokens: tokens, ...mentions.inputProps, disabled: props.disabled || state.status === "sending", value: state.text, rows: 1, wrap: "off", placeholder: "Message", enterKeyHint: "send", onPaste: handlePaste, onDrop: props.onDrop, onKeyDown: (event) => {
|
|
563
|
+
mentions.inputProps.onKeyDown(event);
|
|
564
|
+
if (event.defaultPrevented)
|
|
565
|
+
return;
|
|
556
566
|
if (event.key === "Enter" &&
|
|
557
567
|
!event.shiftKey &&
|
|
558
568
|
!event.nativeEvent.isComposing &&
|
|
@@ -562,10 +572,6 @@ function MobileInner(props) {
|
|
|
562
572
|
void onSend();
|
|
563
573
|
}
|
|
564
574
|
}
|
|
565
|
-
}, onChange: (event) => {
|
|
566
|
-
state.setText(event.target.value);
|
|
567
|
-
state.setStatus(event.target.value.trim() ? "typing" : "idle");
|
|
568
|
-
props.onChange?.(event.target.value);
|
|
569
575
|
}, style: {
|
|
570
576
|
minHeight: tokens.typography.inputText.lineHeight,
|
|
571
577
|
height: tokens.typography.inputText.lineHeight,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
import type { ChatInputTokens, DeepPartial } from "../types/chatInput";
|
|
3
|
+
import type { MentionCandidate } from "../mentions/model";
|
|
4
|
+
export declare function MentionMenu({ id, open, candidates, activeIndex, onSelect, onActiveIndex, menuRef, label, emptyText, tokens: overrides }: {
|
|
5
|
+
id: string;
|
|
6
|
+
open: boolean;
|
|
7
|
+
candidates: MentionCandidate[];
|
|
8
|
+
activeIndex: number;
|
|
9
|
+
onSelect: (candidate: MentionCandidate) => void;
|
|
10
|
+
onActiveIndex: (index: number) => void;
|
|
11
|
+
menuRef: RefObject<HTMLDivElement | null>;
|
|
12
|
+
label: string;
|
|
13
|
+
emptyText: string;
|
|
14
|
+
tokens?: DeepPartial<ChatInputTokens>;
|
|
15
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect } from "react";
|
|
3
|
+
import { mergeChatInputTokens } from "../tokens/defaultTokens";
|
|
4
|
+
export function MentionMenu({ id, open, candidates, activeIndex, onSelect, onActiveIndex, menuRef, label, emptyText, tokens: overrides }) {
|
|
5
|
+
const tokens = mergeChatInputTokens(overrides);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
menuRef.current?.querySelector('[aria-selected="true"]')?.scrollIntoView({ block: "nearest" });
|
|
8
|
+
}, [activeIndex, open]);
|
|
9
|
+
if (!open)
|
|
10
|
+
return null;
|
|
11
|
+
return _jsx("div", { ref: menuRef, id: id, role: "listbox", "aria-label": label, style: {
|
|
12
|
+
position: "absolute", bottom: "calc(100% + 8px)", left: 0, width: "min(360px, 100%)", maxHeight: "min(280px, 40dvh)", overflowY: "auto", overscrollBehavior: "contain", zIndex: 20,
|
|
13
|
+
boxSizing: "border-box", padding: 6, borderRadius: tokens.radius.inputSurface, border: `1px solid ${tokens.color.borderDefault}`,
|
|
14
|
+
background: tokens.color.surfaceInputFocus, color: tokens.color.textPrimary, boxShadow: tokens.shadow.inputElevated, fontFamily: tokens.typography.familyBase,
|
|
15
|
+
}, children: candidates.length ? candidates.map((candidate, index) => _jsxs("button", { id: `${id}-${index}`, role: "option", "aria-selected": index === activeIndex, tabIndex: -1, type: "button", onMouseDown: event => event.preventDefault(), onMouseMove: () => onActiveIndex(index), onClick: () => onSelect(candidate), style: { display: "flex", alignItems: "center", gap: 10, width: "100%", minHeight: 44, padding: "8px 10px", border: 0, borderRadius: 8, textAlign: "left", cursor: "pointer", color: "inherit", background: index === activeIndex ? `color-mix(in srgb, ${tokens.color.brandPrimary} 14%, ${tokens.color.surfaceInputFocus})` : "transparent" }, children: [candidate.avatar ? _jsx("img", { src: candidate.avatar, alt: "", width: 28, height: 28, style: { borderRadius: "50%", objectFit: "cover" } }) : _jsx("span", { "aria-hidden": true, style: { width: 28, flexShrink: 0, textAlign: "center", color: tokens.color.brandPrimary }, children: "@" }), _jsxs("span", { style: { minWidth: 0, overflowWrap: "anywhere" }, children: [_jsx("span", { style: { display: "block", fontSize: tokens.typography.inputText.size }, children: candidate.name }), _jsx("small", { style: { color: tokens.color.textSecondary }, children: candidate.description ?? candidate.id })] })] }, candidate.id)) : _jsx("div", { role: "status", style: { padding: 12, color: tokens.color.textSecondary }, children: emptyText }) });
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ChangeEvent, KeyboardEvent, SyntheticEvent } from "react";
|
|
2
|
+
import type { ChatMention, MentionCandidate, MentionConfig } from "../mentions/model";
|
|
3
|
+
/** Attach inputProps to a textarea, render MentionMenu with menuProps, and pass
|
|
4
|
+
* getMentions() with the untrimmed value when sending. No business lookup is performed. */
|
|
5
|
+
export declare function useMentions({ value, onChange, config, disabled }: {
|
|
6
|
+
value: string;
|
|
7
|
+
onChange: (value: string) => void;
|
|
8
|
+
config?: MentionConfig;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}): {
|
|
11
|
+
getMentions: () => ChatMention[];
|
|
12
|
+
inputProps: {
|
|
13
|
+
onChange(event: ChangeEvent<HTMLTextAreaElement>): void;
|
|
14
|
+
onSelect: (event: SyntheticEvent<HTMLTextAreaElement>) => void;
|
|
15
|
+
onFocus(event: SyntheticEvent<HTMLTextAreaElement>): void;
|
|
16
|
+
onBlur(event: React.FocusEvent<HTMLTextAreaElement>): void;
|
|
17
|
+
onCompositionStart(): void;
|
|
18
|
+
onCompositionEnd(event: SyntheticEvent<HTMLTextAreaElement>): void;
|
|
19
|
+
onKeyDown: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
20
|
+
role?: "combobox" | undefined;
|
|
21
|
+
"aria-autocomplete"?: "list" | undefined;
|
|
22
|
+
"aria-haspopup"?: "listbox" | undefined;
|
|
23
|
+
"aria-expanded"?: boolean | undefined;
|
|
24
|
+
"aria-controls"?: string | undefined;
|
|
25
|
+
"aria-activedescendant"?: string | undefined;
|
|
26
|
+
};
|
|
27
|
+
menuProps: {
|
|
28
|
+
id: string;
|
|
29
|
+
open: boolean;
|
|
30
|
+
candidates: MentionCandidate[];
|
|
31
|
+
activeIndex: number;
|
|
32
|
+
onSelect: (candidate: MentionCandidate) => void;
|
|
33
|
+
onActiveIndex: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
34
|
+
menuRef: import("react").RefObject<HTMLDivElement | null>;
|
|
35
|
+
label: string;
|
|
36
|
+
emptyText: string;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useId, useRef, useState } from "react";
|
|
2
|
+
import { filterMentionCandidates, findMentionQuery, rebaseMentions } from "../mentions/model";
|
|
3
|
+
/** Attach inputProps to a textarea, render MentionMenu with menuProps, and pass
|
|
4
|
+
* getMentions() with the untrimmed value when sending. No business lookup is performed. */
|
|
5
|
+
export function useMentions({ value, onChange, config, disabled = false }) {
|
|
6
|
+
const id = useId();
|
|
7
|
+
const inputRef = useRef(null);
|
|
8
|
+
const menuRef = useRef(null);
|
|
9
|
+
const document = useRef({ text: value, mentions: [] });
|
|
10
|
+
const [caret, setCaret] = useState(0);
|
|
11
|
+
const [focused, setFocused] = useState(false);
|
|
12
|
+
const [composing, setComposing] = useState(false);
|
|
13
|
+
const [dismissed, setDismissed] = useState(false);
|
|
14
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
15
|
+
const mentions = rebaseMentions(document.current.text, value, document.current.mentions);
|
|
16
|
+
document.current = { text: value, mentions };
|
|
17
|
+
const query = config && focused && !composing && !disabled && !dismissed ? findMentionQuery(value, caret) : null;
|
|
18
|
+
const candidates = query ? filterMentionCandidates(config.candidates, query.query) : [];
|
|
19
|
+
const index = Math.min(activeIndex, Math.max(0, candidates.length - 1));
|
|
20
|
+
function select(candidate) {
|
|
21
|
+
if (!query)
|
|
22
|
+
return;
|
|
23
|
+
const insertion = `@${candidate.name}`;
|
|
24
|
+
const next = value.slice(0, query.start) + insertion + " " + value.slice(query.end);
|
|
25
|
+
document.current = {
|
|
26
|
+
text: next,
|
|
27
|
+
mentions: [...rebaseMentions(value, next, mentions), { id: candidate.id, name: candidate.name, start: query.start, end: query.start + insertion.length }].sort((a, b) => a.start - b.start),
|
|
28
|
+
};
|
|
29
|
+
setDismissed(true);
|
|
30
|
+
onChange(next);
|
|
31
|
+
requestAnimationFrame(() => {
|
|
32
|
+
const position = query.start + insertion.length + 1;
|
|
33
|
+
inputRef.current?.focus();
|
|
34
|
+
inputRef.current?.setSelectionRange(position, position);
|
|
35
|
+
setCaret(position);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function updateSelection(event) {
|
|
39
|
+
inputRef.current = event.currentTarget;
|
|
40
|
+
setCaret(event.currentTarget.selectionStart);
|
|
41
|
+
if (event.currentTarget.selectionStart !== event.currentTarget.selectionEnd)
|
|
42
|
+
setDismissed(true);
|
|
43
|
+
}
|
|
44
|
+
function onKeyDown(event) {
|
|
45
|
+
if (!query || event.nativeEvent.isComposing || event.keyCode === 229)
|
|
46
|
+
return;
|
|
47
|
+
if (event.key === "Escape") {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
setDismissed(true);
|
|
50
|
+
}
|
|
51
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
setActiveIndex(candidates.length ? (index + (event.key === "ArrowDown" ? 1 : candidates.length - 1)) % candidates.length : 0);
|
|
54
|
+
}
|
|
55
|
+
if (event.key === "Enter" && !event.shiftKey && !event.metaKey && !event.ctrlKey) {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
if (candidates[index])
|
|
58
|
+
select(candidates[index]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
getMentions: () => config ? document.current.mentions.filter(mention => config.candidates.some(candidate => candidate.id === mention.id)) : [],
|
|
63
|
+
inputProps: {
|
|
64
|
+
...(config ? { role: "combobox", "aria-autocomplete": "list", "aria-haspopup": "listbox", "aria-expanded": Boolean(query), "aria-controls": query ? id : undefined, "aria-activedescendant": query && candidates[index] ? `${id}-${index}` : undefined } : {}),
|
|
65
|
+
onChange(event) {
|
|
66
|
+
updateSelection(event);
|
|
67
|
+
setDismissed(false);
|
|
68
|
+
setActiveIndex(0);
|
|
69
|
+
onChange(event.currentTarget.value);
|
|
70
|
+
},
|
|
71
|
+
onSelect: updateSelection,
|
|
72
|
+
onFocus(event) { updateSelection(event); setFocused(true); },
|
|
73
|
+
onBlur(event) {
|
|
74
|
+
if (!menuRef.current?.contains(event.relatedTarget))
|
|
75
|
+
setFocused(false);
|
|
76
|
+
},
|
|
77
|
+
onCompositionStart() { setComposing(true); },
|
|
78
|
+
onCompositionEnd(event) { setComposing(false); setDismissed(false); updateSelection(event); },
|
|
79
|
+
onKeyDown,
|
|
80
|
+
},
|
|
81
|
+
menuProps: { id, open: Boolean(query), candidates, activeIndex: index, onSelect: select, onActiveIndex: setActiveIndex, menuRef, label: config?.label ?? "选择要提及的成员", emptyText: config?.emptyText ?? "没有匹配的成员" },
|
|
82
|
+
};
|
|
83
|
+
}
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED