@pmate/chat-sdk 0.1.11 → 0.1.13

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 (39) hide show
  1. package/lib/components/ChatInputMobile.js +174 -5
  2. package/lib/hooks/useVoiceInputController.d.ts +2 -0
  3. package/lib/hooks/useVoiceInputController.js +18 -2
  4. package/lib/types/voice.d.ts +1 -0
  5. package/package.json +1 -1
  6. package/dist/assets/ChatInput.stories-BABdkDrt.js +0 -1545
  7. package/dist/assets/Color-AVL7NMMY-D33umUzR.js +0 -1
  8. package/dist/assets/DocsRenderer-PQXLIZUC-BUYhEk48.js +0 -1243
  9. package/dist/assets/client-De2ACSn_.js +0 -9
  10. package/dist/assets/iframe-DVJDoCiO.js +0 -1071
  11. package/dist/assets/iframe-q28pAgAa.css +0 -1
  12. package/dist/assets/index-CkuJGuix.js +0 -1
  13. package/dist/assets/preload-helper-PPVm8Dsz.js +0 -1
  14. package/dist/assets/react-18-DfLUnvSL.js +0 -1
  15. package/dist/favicon-wrapper.svg +0 -46
  16. package/dist/favicon.svg +0 -1
  17. package/dist/iframe.html +0 -687
  18. package/dist/index.html +0 -148
  19. package/dist/index.json +0 -1
  20. package/dist/nunito-sans-bold-italic.woff2 +0 -0
  21. package/dist/nunito-sans-bold.woff2 +0 -0
  22. package/dist/nunito-sans-italic.woff2 +0 -0
  23. package/dist/nunito-sans-regular.woff2 +0 -0
  24. package/dist/project.json +0 -1
  25. package/dist/sb-addons/docs-1/manager-bundle.js +0 -151
  26. package/dist/sb-addons/docs-1/manager-bundle.js.LEGAL.txt +0 -0
  27. package/dist/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js +0 -971
  28. package/dist/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js.LEGAL.txt +0 -0
  29. package/dist/sb-common-assets/favicon-wrapper.svg +0 -46
  30. package/dist/sb-common-assets/favicon.svg +0 -1
  31. package/dist/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
  32. package/dist/sb-common-assets/nunito-sans-bold.woff2 +0 -0
  33. package/dist/sb-common-assets/nunito-sans-italic.woff2 +0 -0
  34. package/dist/sb-common-assets/nunito-sans-regular.woff2 +0 -0
  35. package/dist/sb-manager/globals-module-info.js +0 -799
  36. package/dist/sb-manager/globals-runtime.js +0 -69791
  37. package/dist/sb-manager/globals.js +0 -34
  38. package/dist/sb-manager/runtime.js +0 -13198
  39. package/dist/vite-inject-mocker-entry.js +0 -2
@@ -27,6 +27,7 @@ function MobileInner(props) {
27
27
  const [isEmojiOpen, setIsEmojiOpen] = useState(false);
28
28
  const [isMoreOpen, setIsMoreOpen] = useState(false);
29
29
  const [isUploadingImage, setIsUploadingImage] = useState(false);
30
+ const [transcriptDraft, setTranscriptDraft] = useState("");
30
31
  const cancelZoneRef = useRef(null);
31
32
  const transcribeZoneRef = useRef(null);
32
33
  const textComposerRef = useRef(null);
@@ -35,6 +36,7 @@ function MobileInner(props) {
35
36
  const fileInputRef = useRef(null);
36
37
  const voice = useVoiceInputController({
37
38
  ...props.voice,
39
+ confirmTranscription: props.voice?.confirmTranscription ?? true,
38
40
  onVoiceTranscript: props.voice?.onVoiceTranscript,
39
41
  onVoiceSend: async (blob) => {
40
42
  await props.onSend?.({
@@ -50,6 +52,11 @@ function MobileInner(props) {
50
52
  state.setText(props.value);
51
53
  }
52
54
  }, [props.value]);
55
+ useEffect(() => {
56
+ if (state.status === "transcript-ready") {
57
+ setTranscriptDraft(state.transcript);
58
+ }
59
+ }, [state.status, state.transcript]);
53
60
  const statusText = state.status === "recording"
54
61
  ? "正在录音"
55
62
  : state.status === "transcribing"
@@ -108,6 +115,10 @@ function MobileInner(props) {
108
115
  });
109
116
  };
110
117
  const isRecordingOverlayVisible = state.status === "recording" || state.status === "transcribing";
118
+ const isTranscriptConfirmVisible = state.status === "transcript-ready" && Boolean(state.transcript);
119
+ const transcriptConfirmSurface = `color-mix(in srgb, ${tokens.color.surfaceInputFocus} 86%, ${tokens.color.brandPrimary})`;
120
+ const transcriptConfirmActionSurface = `color-mix(in srgb, ${tokens.color.surfaceInputFocus} 72%, transparent)`;
121
+ const transcriptConfirmActionBorder = `color-mix(in srgb, ${tokens.color.borderDefault} 72%, transparent)`;
111
122
  const updateRecordingChoice = (clientX, clientY) => {
112
123
  const isInside = (node) => {
113
124
  if (!node) {
@@ -144,6 +155,44 @@ function MobileInner(props) {
144
155
  textarea?.setSelectionRange(caret, caret);
145
156
  });
146
157
  };
158
+ const sendConfirmedTranscript = async () => {
159
+ const transcript = transcriptDraft.trim();
160
+ if (!transcript) {
161
+ voice.clearTranscriptPreview();
162
+ return;
163
+ }
164
+ state.setStatus("sending");
165
+ if (props.voice?.onVoiceTranscript) {
166
+ await props.voice.onVoiceTranscript(transcript, {
167
+ blob: voice.pendingTranscriptBlob ?? new Blob(),
168
+ action: "transcribe",
169
+ });
170
+ }
171
+ else {
172
+ await props.onSend?.({
173
+ text: transcript,
174
+ attachments: state.attachments,
175
+ transcript,
176
+ mode: "voice",
177
+ });
178
+ }
179
+ state.setText("");
180
+ props.onChange?.("");
181
+ voice.clearTranscriptPreview();
182
+ };
183
+ const sendConfirmedOriginalVoice = async () => {
184
+ if (!voice.pendingTranscriptBlob) {
185
+ return;
186
+ }
187
+ state.setStatus("sending");
188
+ await props.onSend?.({
189
+ text: "",
190
+ attachments: state.attachments,
191
+ audio: voice.pendingTranscriptBlob,
192
+ mode: "voice",
193
+ });
194
+ voice.clearTranscriptPreview();
195
+ };
147
196
  const attachmentActionStyle = {
148
197
  display: "grid",
149
198
  justifyItems: "center",
@@ -197,14 +246,16 @@ function MobileInner(props) {
197
246
  boxShadow: "none",
198
247
  overflow: "visible",
199
248
  }, children: [isRecordingOverlayVisible ? (_jsxs("div", { style: {
200
- position: "absolute",
201
- left: 0,
202
- right: 0,
203
- bottom: 72,
249
+ position: "fixed",
250
+ left: "50%",
251
+ bottom: "calc(72px + env(safe-area-inset-bottom, 0px))",
252
+ width: "min(420px, calc(100vw - 16px))",
253
+ transform: "translateX(-50%)",
204
254
  height: 250,
205
255
  pointerEvents: "none",
206
256
  background: "linear-gradient(180deg, rgba(22, 16, 45, 0) 0%, rgba(22, 16, 45, 0.24) 34%, rgba(22, 16, 45, 0.6) 100%)",
207
257
  borderRadius: 28,
258
+ zIndex: 1000,
208
259
  }, children: [_jsxs("div", { style: {
209
260
  position: "absolute",
210
261
  left: "50%",
@@ -280,7 +331,125 @@ function MobileInner(props) {
280
331
  boxShadow: recordingChoice === "transcribe"
281
332
  ? "0 16px 28px rgba(109, 74, 255, 0.24)"
282
333
  : "none",
283
- }, children: "\u8F6C\u6587\u5B57" })] })] })) : null, _jsx(AttachmentPreview, { attachments: attachments.attachments, tokens: tokens }), _jsx("input", { ref: imageInputRef, type: "file", accept: props.image?.accept ?? "image/*", style: { display: "none" }, onChange: (event) => {
334
+ }, children: "\u8F6C\u6587\u5B57" })] })] })) : null, isTranscriptConfirmVisible ? (_jsxs("div", { style: {
335
+ position: "fixed",
336
+ inset: 0,
337
+ zIndex: 1001,
338
+ display: "grid",
339
+ gridTemplateRows: "minmax(0, 1fr) auto",
340
+ padding: "max(20px, env(safe-area-inset-top, 0px)) 16px calc(28px + env(safe-area-inset-bottom, 0px))",
341
+ background: "rgba(10, 12, 16, 0.68)",
342
+ backdropFilter: "blur(1px)",
343
+ }, children: [_jsx("div", { style: {
344
+ minHeight: 0,
345
+ display: "grid",
346
+ alignContent: "end",
347
+ paddingBottom: 72,
348
+ }, children: _jsxs("div", { style: {
349
+ justifySelf: "center",
350
+ position: "relative",
351
+ width: "min(100%, 524px)",
352
+ minHeight: 88,
353
+ padding: "24px 26px",
354
+ borderRadius: tokens.radius.inputSurface + 8,
355
+ background: transcriptConfirmSurface,
356
+ border: `1px solid ${tokens.color.borderFocus}`,
357
+ color: tokens.color.textPrimary,
358
+ fontSize: Math.max(20, tokens.typography.inputText.size + 7),
359
+ lineHeight: "34px",
360
+ fontWeight: 680,
361
+ boxShadow: tokens.shadow.inputElevated,
362
+ display: "grid",
363
+ }, children: [_jsx("textarea", { "aria-label": "\u7F16\u8F91\u8F6C\u5199\u6587\u672C", value: transcriptDraft, onChange: (event) => {
364
+ setTranscriptDraft(event.target.value);
365
+ }, style: {
366
+ width: "100%",
367
+ minHeight: 40,
368
+ maxHeight: 180,
369
+ resize: "none",
370
+ border: 0,
371
+ outline: "none",
372
+ background: "transparent",
373
+ color: "inherit",
374
+ font: "inherit",
375
+ lineHeight: "inherit",
376
+ padding: 0,
377
+ margin: 0,
378
+ overflowWrap: "anywhere",
379
+ } }), _jsx("span", { "aria-hidden": "true", style: {
380
+ position: "absolute",
381
+ right: 72,
382
+ bottom: -10,
383
+ width: 22,
384
+ height: 22,
385
+ borderRadius: 4,
386
+ background: transcriptConfirmSurface,
387
+ borderRight: `1px solid ${tokens.color.borderFocus}`,
388
+ borderBottom: `1px solid ${tokens.color.borderFocus}`,
389
+ transform: "rotate(45deg)",
390
+ } })] }) }), _jsxs("div", { style: {
391
+ display: "grid",
392
+ gridTemplateColumns: "92px 112px minmax(120px, 176px)",
393
+ justifyContent: "space-between",
394
+ alignItems: "end",
395
+ gap: 16,
396
+ width: "min(100%, 524px)",
397
+ justifySelf: "center",
398
+ }, children: [_jsxs("button", { type: "button", "aria-label": "\u53D6\u6D88\u8F6C\u6587\u5B57", onClick: () => {
399
+ voice.clearTranscriptPreview();
400
+ }, style: {
401
+ border: 0,
402
+ background: "transparent",
403
+ color: tokens.color.textSecondary,
404
+ display: "grid",
405
+ justifyItems: "center",
406
+ gap: 10,
407
+ fontSize: 14,
408
+ lineHeight: "20px",
409
+ fontWeight: 500,
410
+ }, children: [_jsx("span", { style: {
411
+ width: 64,
412
+ height: 64,
413
+ borderRadius: "50%",
414
+ display: "grid",
415
+ placeItems: "center",
416
+ background: transcriptConfirmActionSurface,
417
+ border: `1px solid ${transcriptConfirmActionBorder}`,
418
+ color: tokens.color.textPrimary,
419
+ fontSize: 34,
420
+ lineHeight: 1,
421
+ }, children: "\u00D7" }), "\u53D6\u6D88"] }), _jsxs("button", { type: "button", "aria-label": "\u53D1\u9001\u539F\u8BED\u97F3", disabled: !voice.pendingTranscriptBlob || props.disabled, onClick: () => void sendConfirmedOriginalVoice(), style: {
422
+ border: 0,
423
+ background: "transparent",
424
+ color: tokens.color.textSecondary,
425
+ display: "grid",
426
+ justifyItems: "center",
427
+ gap: 10,
428
+ fontSize: 14,
429
+ lineHeight: "20px",
430
+ fontWeight: 500,
431
+ opacity: voice.pendingTranscriptBlob && !props.disabled ? 1 : 0.45,
432
+ }, children: [_jsx("span", { style: {
433
+ width: 64,
434
+ height: 64,
435
+ borderRadius: "50%",
436
+ display: "grid",
437
+ placeItems: "center",
438
+ background: transcriptConfirmActionSurface,
439
+ border: `1px solid ${transcriptConfirmActionBorder}`,
440
+ color: tokens.color.textPrimary,
441
+ }, children: _jsx(MicOutlineIcon, { size: 25, strokeWidth: 2 }) }), "\u53D1\u9001\u539F\u8BED\u97F3"] }), _jsx("button", { type: "button", disabled: props.disabled || !transcriptDraft.trim(), onClick: () => void sendConfirmedTranscript(), style: {
442
+ border: 0,
443
+ minHeight: 64,
444
+ borderRadius: 999,
445
+ padding: "0 28px",
446
+ background: tokens.color.actionSendDefault,
447
+ color: tokens.color.actionSendTextOnPrimary,
448
+ fontSize: 17,
449
+ lineHeight: "24px",
450
+ fontWeight: 700,
451
+ opacity: props.disabled || !transcriptDraft.trim() ? 0.5 : 1,
452
+ }, children: "\u53D1\u9001" })] })] })) : null, _jsx(AttachmentPreview, { attachments: attachments.attachments, tokens: tokens }), _jsx("input", { ref: imageInputRef, type: "file", accept: props.image?.accept ?? "image/*", style: { display: "none" }, onChange: (event) => {
284
453
  const file = event.target.files?.[0];
285
454
  if (file) {
286
455
  void handleImageFile(file);
@@ -5,8 +5,10 @@ export declare function useVoiceInputController(config?: ChatVoiceConfig): {
5
5
  status: import("..").ChatInputStatus;
6
6
  waveformLevels: number[];
7
7
  durationMs: number;
8
+ pendingTranscriptBlob: Blob | null;
8
9
  beginVoiceMode: () => void;
9
10
  setMode: (args_0: import("..").ChatInputMode | ((prev: import("..").ChatInputMode) => import("..").ChatInputMode)) => void;
11
+ clearTranscriptPreview: () => void;
10
12
  startRecording: () => void;
11
13
  cancelRecording: (message?: string | null) => void;
12
14
  stopRecording: (action?: StopAction) => void;
@@ -1,5 +1,5 @@
1
1
  import { useAtom } from "jotai";
2
- import { useEffect, useRef } from "react";
2
+ import { useEffect, useRef, useState } from "react";
3
3
  import { waveformLevelsAtom, recordingDurationMsAtom } from "../atoms/voiceAtoms";
4
4
  import { insertTranscriptText } from "./useTranscriptComposer";
5
5
  import { useChatInputState } from "./useChatInputState";
@@ -12,6 +12,7 @@ export function useVoiceInputController(config) {
12
12
  const managerRef = useRef(null);
13
13
  const stopActionRef = useRef("send");
14
14
  const abortMessageRef = useRef(null);
15
+ const [pendingTranscriptBlob, setPendingTranscriptBlob] = useState(null);
15
16
  const [levels, setLevels] = useAtom(waveformLevelsAtom);
16
17
  const [durationMs, setDurationMs] = useAtom(recordingDurationMsAtom);
17
18
  const { mode, setMode, status, setStatus, text, setText, setTranscript, setError, } = useChatInputState();
@@ -83,6 +84,7 @@ export function useVoiceInputController(config) {
83
84
  return;
84
85
  }
85
86
  if (action === "send") {
87
+ setPendingTranscriptBlob(null);
86
88
  setTranscript("");
87
89
  setDurationMs(0);
88
90
  setStatus(modeRef.current === "voice" ? "voice-mode-ready" : "idle");
@@ -94,7 +96,10 @@ export function useVoiceInputController(config) {
94
96
  return;
95
97
  }
96
98
  setTranscript(nextTranscript);
97
- setText(insertTranscriptText(textRef.current, nextTranscript));
99
+ setPendingTranscriptBlob(blob);
100
+ if (!configRef.current?.confirmTranscription) {
101
+ setText(insertTranscriptText(textRef.current, nextTranscript));
102
+ }
98
103
  setDurationMs(0);
99
104
  setStatus("transcript-ready");
100
105
  setError(null);
@@ -105,6 +110,7 @@ export function useVoiceInputController(config) {
105
110
  }
106
111
  }
107
112
  function finishVoiceSend(blob) {
113
+ setPendingTranscriptBlob(null);
108
114
  setTranscript("");
109
115
  setStatus(modeRef.current === "voice" ? "voice-mode-ready" : "idle");
110
116
  setError(null);
@@ -115,12 +121,20 @@ export function useVoiceInputController(config) {
115
121
  setStatus("voice-mode-ready");
116
122
  setError(null);
117
123
  }
124
+ function clearTranscriptPreview() {
125
+ setPendingTranscriptBlob(null);
126
+ setTranscript("");
127
+ setDurationMs(0);
128
+ setStatus(modeRef.current === "voice" ? "voice-mode-ready" : "idle");
129
+ setError(null);
130
+ }
118
131
  function startRecording() {
119
132
  if (status === "recording") {
120
133
  return;
121
134
  }
122
135
  setStatus("recording");
123
136
  setError(null);
137
+ setPendingTranscriptBlob(null);
124
138
  setTranscript("");
125
139
  setDurationMs(0);
126
140
  setLevels(DEFAULT_WAVEFORM);
@@ -145,8 +159,10 @@ export function useVoiceInputController(config) {
145
159
  status,
146
160
  waveformLevels: levels,
147
161
  durationMs,
162
+ pendingTranscriptBlob,
148
163
  beginVoiceMode,
149
164
  setMode,
165
+ clearTranscriptPreview,
150
166
  startRecording,
151
167
  cancelRecording,
152
168
  stopRecording,
@@ -12,6 +12,7 @@ export type ChatVoiceConfig = {
12
12
  lang?: string;
13
13
  minDurationMs?: number;
14
14
  transcribeTimeoutMs?: number;
15
+ confirmTranscription?: boolean;
15
16
  simulatedTranscript?: string;
16
17
  shortRecordingMessage?: string;
17
18
  onVoiceSend?: (blob: Blob) => Promise<void> | void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
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",