@pmate/chat-sdk 0.11.3 → 0.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/lib/components/ChatInputDesktop.js +26 -25
- package/lib/components/ChatInputMobile.js +6 -10
- package/lib/components/ImageAttachmentTray.d.ts +11 -0
- package/lib/components/ImageAttachmentTray.js +9 -0
- package/lib/hooks/useAttachmentState.js +3 -3
- package/lib/hooks/useImageAttachments.d.ts +19 -0
- package/lib/hooks/useImageAttachments.js +68 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/lib/types/chatInput.d.ts +4 -0
- package/lib/ux/imagePaste.d.ts +13 -0
- package/lib/ux/imagePaste.js +23 -0
- package/package.json +16 -17
package/README.md
CHANGED
|
@@ -106,3 +106,24 @@ await meetingClient.join(groupId, teamId, invitedProfileIds)
|
|
|
106
106
|
All commands for an established call/meeting carry its stored Team. Foreign-Team responses are ignored, and a mismatched join acknowledgment releases capture. Hub reach call events require Team context and are checked against the authoritative attach response before showing incoming UI. A native call ID can still be attached directly: the server supplies its authoritative Team.
|
|
107
107
|
|
|
108
108
|
This is a protocol-breaking minor release for the pre-1.0 SDK. It requires the Team-scoped call-api and updated notification consumers; coordinate deployment with the application's migration rollout. It does not migrate legacy meeting/group history itself.
|
|
109
|
+
|
|
110
|
+
## Clipboard images in custom composers
|
|
111
|
+
|
|
112
|
+
### Background
|
|
113
|
+
|
|
114
|
+
Applications with their own input UI need the same screenshot paste behavior as the SDK inputs. `handleImagePaste` reads the user's paste event without requesting clipboard access; it does not fetch HTML image URLs.
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
<textarea onPaste={event => handleImagePaste(event, {
|
|
118
|
+
disabled,
|
|
119
|
+
onImages: files => { void uploadImages(files).catch(showUploadError) },
|
|
120
|
+
})} />
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The handler leaves text-only pastes and events already handled by the host untouched. Image items take precedence over text/HTML; `clipboardData.files` is used when no readable image items exist. The host owns size/type validation, upload progress, error feedback and whether to preview or send the image. Both SDK inputs use this interface. Multiple attachments are appended without overwriting earlier selections.
|
|
124
|
+
|
|
125
|
+
### Draft preview and upload
|
|
126
|
+
|
|
127
|
+
`useImageAttachments({ uploadImage, disabled })` provides `onPaste`, `addFiles`, `attachments`, `pending`, `remove` and `retry`. Render `ImageAttachmentTray` inside the composer's surface with these attachments and the input tokens. Local previews appear immediately; uploads happen in the background. Removal/unmount revokes local object URLs, and late upload results cannot reinsert removed drafts. Upload errors remain visible and retryable.
|
|
128
|
+
|
|
129
|
+
Only send on explicit submission: block send while `pending`, deliver each ready attachment's `url`, and remove it after delivery succeeds. Keep unsuccessful drafts to allow retry without reuploading. For `ChatInputDesktop`, use `hasPendingContent` and `sendDisabled` with host-owned drafts. Unmount or key the draft owner when switching accounts, Teams or conversations.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { handleImagePaste } from "../ux/imagePaste";
|
|
2
3
|
import { useMentions } from "../hooks/useMentions";
|
|
3
4
|
import { MentionMenu } from "./MentionMenu";
|
|
4
5
|
import React from "react";
|
|
@@ -66,21 +67,26 @@ function DesktopInner(props) {
|
|
|
66
67
|
? "Transcript inserted. Edit or send."
|
|
67
68
|
: undefined));
|
|
68
69
|
const onSend = async () => {
|
|
69
|
-
if (!props.onSend) {
|
|
70
|
+
if (!props.onSend || props.disabled || props.sendDisabled || isUploadingImage || state.status === "sending") {
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
72
73
|
state.setStatus("sending");
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
state.setError(null);
|
|
75
|
+
try {
|
|
76
|
+
await props.onSend({
|
|
77
|
+
text: state.text, mentions: mentions.getMentions(), attachments: state.attachments,
|
|
78
|
+
transcript: state.transcript || undefined,
|
|
79
|
+
});
|
|
80
|
+
state.setText("");
|
|
81
|
+
state.setTranscript("");
|
|
82
|
+
state.setAttachments([]);
|
|
83
|
+
props.onChange?.("");
|
|
84
|
+
state.setStatus("idle");
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
state.setError(error instanceof Error ? error.message : "Failed to send message");
|
|
88
|
+
state.setStatus("error");
|
|
89
|
+
}
|
|
84
90
|
};
|
|
85
91
|
const handleImageFile = async (file) => {
|
|
86
92
|
if (!props.image?.uploadImage) {
|
|
@@ -119,16 +125,11 @@ function DesktopInner(props) {
|
|
|
119
125
|
};
|
|
120
126
|
const handlePaste = (event) => {
|
|
121
127
|
props.onPaste?.(event);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (!images.length)
|
|
128
|
-
return;
|
|
129
|
-
event.preventDefault();
|
|
130
|
-
for (const image of images)
|
|
131
|
-
void handleImageFile(image);
|
|
128
|
+
handleImagePaste(event, {
|
|
129
|
+
disabled: props.disabled || props.enableImage === false || state.status === "sending",
|
|
130
|
+
onImages: images => { for (const image of images)
|
|
131
|
+
void handleImageFile(image); },
|
|
132
|
+
});
|
|
132
133
|
};
|
|
133
134
|
const insertEmoji = (emoji) => {
|
|
134
135
|
const textarea = textComposerRef.current;
|
|
@@ -229,7 +230,7 @@ function DesktopInner(props) {
|
|
|
229
230
|
event.keyCode === 229)
|
|
230
231
|
return;
|
|
231
232
|
event.preventDefault();
|
|
232
|
-
if (state.text.trim() || state.attachments.length > 0)
|
|
233
|
+
if (state.text.trim() || state.attachments.length > 0 || props.hasPendingContent)
|
|
233
234
|
void onSend();
|
|
234
235
|
}, style: {
|
|
235
236
|
flex: 1,
|
|
@@ -254,7 +255,7 @@ function DesktopInner(props) {
|
|
|
254
255
|
color: voice.status === "recording" || voice.status === "transcribing" ? tokens.color.iconActive : tokens.color.iconDefault,
|
|
255
256
|
cursor: props.disabled || props.enableVoice === false ? "not-allowed" : "pointer",
|
|
256
257
|
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
|
+
}, "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 || props.sendDisabled || isUploadingImage || state.status === "sending" || (!state.text.trim() && state.attachments.length === 0 && !props.hasPendingContent), "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
259
|
display: "grid",
|
|
259
260
|
gridTemplateColumns: "32px minmax(0, 1fr) 32px 32px",
|
|
260
261
|
alignItems: "center",
|
|
@@ -300,7 +301,7 @@ function DesktopInner(props) {
|
|
|
300
301
|
const shouldSend = props.submitShortcut === "meta-enter" ? event.metaKey || event.ctrlKey : !event.shiftKey;
|
|
301
302
|
if (shouldSend) {
|
|
302
303
|
event.preventDefault();
|
|
303
|
-
if (state.text.trim() || state.attachments.length > 0) {
|
|
304
|
+
if (state.text.trim() || state.attachments.length > 0 || props.hasPendingContent) {
|
|
304
305
|
void onSend();
|
|
305
306
|
}
|
|
306
307
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { handleImagePaste } from "../ux/imagePaste";
|
|
2
3
|
import { useMentions } from "../hooks/useMentions";
|
|
3
4
|
import { MentionMenu } from "./MentionMenu";
|
|
4
5
|
import React from "react";
|
|
@@ -123,16 +124,11 @@ function MobileInner(props) {
|
|
|
123
124
|
};
|
|
124
125
|
const handlePaste = (event) => {
|
|
125
126
|
props.onPaste?.(event);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (!images.length)
|
|
132
|
-
return;
|
|
133
|
-
event.preventDefault();
|
|
134
|
-
for (const image of images)
|
|
135
|
-
void handleImageFile(image);
|
|
127
|
+
handleImagePaste(event, {
|
|
128
|
+
disabled: props.disabled || props.enableImage === false || state.status === "sending",
|
|
129
|
+
onImages: images => { for (const image of images)
|
|
130
|
+
void handleImageFile(image); },
|
|
131
|
+
});
|
|
136
132
|
};
|
|
137
133
|
const isRecordingOverlayVisible = state.status === "recording" || state.status === "transcribing";
|
|
138
134
|
const isTranscriptConfirmVisible = state.status === "transcript-ready" && Boolean(state.transcript);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ImageDraftAttachment } from "../hooks/useImageAttachments";
|
|
2
|
+
import type { ChatInputTokens } from "../types/chatInput";
|
|
3
|
+
/** Shared inline draft UI for host-owned inputs. Uses the same tokens as SDK composers. */
|
|
4
|
+
export declare function ImageAttachmentTray({ attachments, tokens, onRemove, onRetry, disabled, locale }: {
|
|
5
|
+
attachments: ImageDraftAttachment[];
|
|
6
|
+
tokens: ChatInputTokens;
|
|
7
|
+
onRemove: (id: string) => void;
|
|
8
|
+
onRetry: (id: string) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
locale?: "en" | "zh";
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
/** Shared inline draft UI for host-owned inputs. Uses the same tokens as SDK composers. */
|
|
4
|
+
export function ImageAttachmentTray({ attachments, tokens, onRemove, onRetry, disabled, locale = "en" }) {
|
|
5
|
+
if (!attachments.length)
|
|
6
|
+
return null;
|
|
7
|
+
return _jsx("div", { "aria-label": locale === "zh" ? "待发送图片" : "Image drafts", style: { display: "flex", flexWrap: "wrap", gap: 10, padding: 12, minWidth: 0 }, children: attachments.map(item => _jsxs("div", { style: { position: "relative", width: 112, maxWidth: "100%", borderRadius: 12,
|
|
8
|
+
border: `1px solid ${item.status === "error" ? tokens.color.statusError : tokens.color.borderDefault}`, overflow: "hidden", background: tokens.color.surfaceInputDefault }, children: [_jsx("img", { src: item.previewUrl, alt: item.name, style: { display: "block", width: "100%", height: 96, objectFit: "contain" } }), _jsx("button", { type: "button", disabled: disabled, "aria-label": `${locale === "zh" ? "移除" : "Remove"} ${item.name}`, onClick: () => onRemove(item.id), style: { position: "absolute", top: 4, right: 4, width: 24, height: 24, borderRadius: 99, border: 0, background: tokens.color.surfaceInputFocus, color: tokens.color.textPrimary, cursor: "pointer" }, children: "\u00D7" }), item.status === "uploading" ? _jsx("div", { role: "status", style: { fontSize: 12, padding: 6, color: tokens.color.textSecondary }, children: locale === "zh" ? "正在上传…" : "Uploading…" }) : null, item.status === "error" ? _jsxs("div", { style: { fontSize: 12, padding: 6, overflowWrap: "anywhere", color: tokens.color.statusError }, children: [_jsx("span", { role: "alert", children: item.error }), _jsx("button", { type: "button", disabled: disabled, onClick: () => onRetry(item.id), style: { display: "block", marginTop: 4, cursor: "pointer" }, children: locale === "zh" ? "重试上传" : "Retry upload" })] }) : null] }, item.id)) });
|
|
9
|
+
}
|
|
@@ -4,8 +4,8 @@ export function useAttachmentState() {
|
|
|
4
4
|
return {
|
|
5
5
|
attachments,
|
|
6
6
|
addAttachment(attachment) {
|
|
7
|
-
setAttachments([
|
|
8
|
-
...
|
|
7
|
+
setAttachments(current => [
|
|
8
|
+
...current,
|
|
9
9
|
{
|
|
10
10
|
id: crypto.randomUUID(),
|
|
11
11
|
kind: "image",
|
|
@@ -15,7 +15,7 @@ export function useAttachmentState() {
|
|
|
15
15
|
setStatus("attachment-selected");
|
|
16
16
|
},
|
|
17
17
|
removeAttachment(id) {
|
|
18
|
-
setAttachments(
|
|
18
|
+
setAttachments(current => current.filter((item) => item.id !== id));
|
|
19
19
|
},
|
|
20
20
|
};
|
|
21
21
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ClipboardEventHandler } from "react";
|
|
2
|
+
import type { ChatAttachment } from "../types/attachment";
|
|
3
|
+
import type { ChatImageUploadResult } from "../types/chatInput";
|
|
4
|
+
export type ImageDraftAttachment = ChatAttachment & {
|
|
5
|
+
status: "uploading" | "ready" | "error";
|
|
6
|
+
error?: string;
|
|
7
|
+
};
|
|
8
|
+
/** Local previews appear immediately. Uploading never sends a chat message. */
|
|
9
|
+
export declare function useImageAttachments(options: {
|
|
10
|
+
uploadImage?: (file: File) => Promise<ChatImageUploadResult>;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
}): {
|
|
13
|
+
attachments: ImageDraftAttachment[];
|
|
14
|
+
addFiles: (files: File[]) => void;
|
|
15
|
+
onPaste: ClipboardEventHandler<HTMLTextAreaElement>;
|
|
16
|
+
pending: boolean;
|
|
17
|
+
remove(id: string): void;
|
|
18
|
+
retry(id: string): void;
|
|
19
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { handleImagePaste } from "../ux/imagePaste";
|
|
3
|
+
/** Local previews appear immediately. Uploading never sends a chat message. */
|
|
4
|
+
export function useImageAttachments(options) {
|
|
5
|
+
const [attachments, setAttachments] = useState([]);
|
|
6
|
+
const entries = useRef(new Map());
|
|
7
|
+
const active = useRef(true);
|
|
8
|
+
const sync = () => { if (active.current)
|
|
9
|
+
setAttachments([...entries.current.values()]); };
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
active.current = true;
|
|
12
|
+
return () => {
|
|
13
|
+
active.current = false;
|
|
14
|
+
for (const item of entries.current.values())
|
|
15
|
+
if (item.previewUrl)
|
|
16
|
+
URL.revokeObjectURL(item.previewUrl);
|
|
17
|
+
entries.current.clear();
|
|
18
|
+
};
|
|
19
|
+
}, []);
|
|
20
|
+
async function upload(item) {
|
|
21
|
+
item.status = "uploading";
|
|
22
|
+
item.error = undefined;
|
|
23
|
+
sync();
|
|
24
|
+
try {
|
|
25
|
+
const result = options.uploadImage ? await options.uploadImage(item.file) : undefined;
|
|
26
|
+
if (entries.current.get(item.id) !== item)
|
|
27
|
+
return;
|
|
28
|
+
item.url = result?.url;
|
|
29
|
+
item.status = "ready";
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (entries.current.get(item.id) !== item)
|
|
33
|
+
return;
|
|
34
|
+
item.status = "error";
|
|
35
|
+
item.error = error instanceof Error ? error.message : "Image upload failed";
|
|
36
|
+
}
|
|
37
|
+
sync();
|
|
38
|
+
}
|
|
39
|
+
function addFiles(files) {
|
|
40
|
+
if (options.disabled)
|
|
41
|
+
return;
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
const item = { id: crypto.randomUUID(), kind: "image", name: file.name,
|
|
44
|
+
file, previewUrl: URL.createObjectURL(file), status: "uploading" };
|
|
45
|
+
entries.current.set(item.id, item);
|
|
46
|
+
void upload(item);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const onPaste = event => {
|
|
50
|
+
handleImagePaste(event, { disabled: options.disabled, onImages: addFiles });
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
attachments, addFiles, onPaste,
|
|
54
|
+
pending: attachments.some(item => item.status !== "ready"),
|
|
55
|
+
remove(id) {
|
|
56
|
+
const item = entries.current.get(id);
|
|
57
|
+
if (item?.previewUrl)
|
|
58
|
+
URL.revokeObjectURL(item.previewUrl);
|
|
59
|
+
entries.current.delete(id);
|
|
60
|
+
sync();
|
|
61
|
+
},
|
|
62
|
+
retry(id) {
|
|
63
|
+
const item = entries.current.get(id);
|
|
64
|
+
if (!options.disabled && item?.status === "error")
|
|
65
|
+
void upload(item);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -38,3 +38,6 @@ export * from "./runtime/meeting";
|
|
|
38
38
|
export * from "./runtime/teamScope";
|
|
39
39
|
export * from "./runtime/teamMigrations";
|
|
40
40
|
export * from "./runtime/socialReadClient";
|
|
41
|
+
export * from "./ux/imagePaste";
|
|
42
|
+
export * from "./hooks/useImageAttachments";
|
|
43
|
+
export * from "./components/ImageAttachmentTray";
|
package/lib/index.js
CHANGED
|
@@ -38,3 +38,6 @@ export * from "./runtime/meeting";
|
|
|
38
38
|
export * from "./runtime/teamScope";
|
|
39
39
|
export * from "./runtime/teamMigrations";
|
|
40
40
|
export * from "./runtime/socialReadClient";
|
|
41
|
+
export * from "./ux/imagePaste";
|
|
42
|
+
export * from "./hooks/useImageAttachments";
|
|
43
|
+
export * from "./components/ImageAttachmentTray";
|
package/lib/types/chatInput.d.ts
CHANGED
|
@@ -140,6 +140,10 @@ export type ChatInputBaseProps = {
|
|
|
140
140
|
audio?: Blob;
|
|
141
141
|
mode?: "text" | "voice";
|
|
142
142
|
}) => Promise<void> | void;
|
|
143
|
+
/** Host-owned drafts can enable sending even when the SDK text/attachment list is empty. */
|
|
144
|
+
hasPendingContent?: boolean;
|
|
145
|
+
/** Blocks submission while host-owned drafts are uploading or invalid. */
|
|
146
|
+
sendDisabled?: boolean;
|
|
143
147
|
enableVoice?: boolean;
|
|
144
148
|
enableEmoji?: boolean;
|
|
145
149
|
enableImage?: boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Clipboard image handling for SDK inputs and host-owned composers. No clipboard permission request. */
|
|
2
|
+
export type ImagePasteEvent = {
|
|
3
|
+
clipboardData: Pick<DataTransfer, "items" | "files"> | null;
|
|
4
|
+
defaultPrevented: boolean;
|
|
5
|
+
preventDefault(): void;
|
|
6
|
+
};
|
|
7
|
+
/** Prefer items; files is used when the browser exposes no readable image items. */
|
|
8
|
+
export declare function getClipboardImages(data: ImagePasteEvent["clipboardData"]): File[];
|
|
9
|
+
/** Consumes image pastes only. The host owns validation, upload, progress and errors. */
|
|
10
|
+
export declare function handleImagePaste(event: ImagePasteEvent, options: {
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
onImages: (files: File[]) => void;
|
|
13
|
+
}): boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Prefer items; files is used when the browser exposes no readable image items. */
|
|
2
|
+
export function getClipboardImages(data) {
|
|
3
|
+
if (!data)
|
|
4
|
+
return [];
|
|
5
|
+
const images = Array.from(data.items).flatMap(item => {
|
|
6
|
+
if (item.kind !== "file" || !item.type.startsWith("image/"))
|
|
7
|
+
return [];
|
|
8
|
+
const file = item.getAsFile();
|
|
9
|
+
return file ? [file] : [];
|
|
10
|
+
});
|
|
11
|
+
return images.length ? images : Array.from(data.files).filter(file => file.type.startsWith("image/"));
|
|
12
|
+
}
|
|
13
|
+
/** Consumes image pastes only. The host owns validation, upload, progress and errors. */
|
|
14
|
+
export function handleImagePaste(event, options) {
|
|
15
|
+
if (event.defaultPrevented || options.disabled)
|
|
16
|
+
return false;
|
|
17
|
+
const images = getClipboardImages(event.clipboardData);
|
|
18
|
+
if (!images.length)
|
|
19
|
+
return false;
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
options.onImages(images);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmate/chat-sdk",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.4",
|
|
4
4
|
"description": "Cross-platform PMate chat input components, voice input primitives, and ASR helpers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -28,15 +28,6 @@
|
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"clean": "rimraf lib",
|
|
33
|
-
"build": "pnpm run clean && tsc -p tsconfig.build.json && cp src/voice/recorder-processor.js lib/voice/recorder-processor.js",
|
|
34
|
-
"prepack": "pnpm run build",
|
|
35
|
-
"npm:publish": "pnpm run build && npm publish --access public --registry https://registry.npmjs.org/",
|
|
36
|
-
"test": "vitest run",
|
|
37
|
-
"storybook": "storybook dev -p 6009",
|
|
38
|
-
"build-storybook": "storybook build --output-dir dist"
|
|
39
|
-
},
|
|
40
31
|
"dependencies": {
|
|
41
32
|
"@emoji-mart/data": "^1.2.1",
|
|
42
33
|
"@emoji-mart/react": "^1.1.1",
|
|
@@ -51,16 +42,24 @@
|
|
|
51
42
|
"devDependencies": {
|
|
52
43
|
"@storybook/addon-docs": "^9.0.17",
|
|
53
44
|
"@storybook/react-vite": "9.0.17",
|
|
54
|
-
"@tailwindcss/vite": "
|
|
55
|
-
"@types/react": "
|
|
56
|
-
"@types/react-dom": "
|
|
45
|
+
"@tailwindcss/vite": "^4.1.3",
|
|
46
|
+
"@types/react": "^19.2.7",
|
|
47
|
+
"@types/react-dom": "^19.2.3",
|
|
57
48
|
"fake-indexeddb": "^6.2.5",
|
|
58
|
-
"react": "
|
|
59
|
-
"react-dom": "
|
|
49
|
+
"react": "^19.1.1",
|
|
50
|
+
"react-dom": "^19.1.1",
|
|
60
51
|
"rimraf": "^6.1.2",
|
|
61
52
|
"storybook": "^9.0.17",
|
|
62
|
-
"tailwindcss": "
|
|
53
|
+
"tailwindcss": "^4.0.14",
|
|
63
54
|
"typescript": "5.9.2",
|
|
64
55
|
"vitest": "^4.0.17"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"clean": "rimraf lib",
|
|
59
|
+
"build": "pnpm run clean && tsc -p tsconfig.build.json && cp src/voice/recorder-processor.js lib/voice/recorder-processor.js",
|
|
60
|
+
"npm:publish": "pnpm run build && npm publish --access public --registry https://registry.npmjs.org/",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"storybook": "storybook dev -p 6009",
|
|
63
|
+
"build-storybook": "storybook build --output-dir dist"
|
|
65
64
|
}
|
|
66
|
-
}
|
|
65
|
+
}
|