@pmate/chat-sdk 0.1.19 → 0.1.21

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.
@@ -1 +1 @@
1
- import{r as s}from"./iframe-DaqBxyKM.js";import{c as l}from"./client-CHe67G1Y.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};
1
+ import{r as s}from"./iframe-Bo_J3UyC.js";import{c as l}from"./client-CMlGEQEC.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,9 +529,9 @@
529
529
  }
530
530
  </script>
531
531
 
532
- <script type="module" crossorigin src="./assets/iframe-DaqBxyKM.js"></script>
532
+ <script type="module" crossorigin src="./assets/iframe-Bo_J3UyC.js"></script>
533
533
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-PPVm8Dsz.js">
534
- <link rel="stylesheet" crossorigin href="./assets/iframe-DBNeRwYQ.css">
534
+ <link rel="stylesheet" crossorigin href="./assets/iframe-4FbYaDjf.css">
535
535
  </head>
536
536
 
537
537
  <body>
package/dist/project.json CHANGED
@@ -1 +1 @@
1
- {"generatedAt":1786605509716,"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
+ {"generatedAt":1787881497340,"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,6 +1,7 @@
1
1
  import type { ChatAttachment } from "../types/attachment";
2
2
  import type { ChatInputTokens } from "../types/chatInput";
3
- export declare function AttachmentPreview({ attachments, tokens, }: {
3
+ export declare function AttachmentPreview({ attachments, tokens, onRemove, }: {
4
4
  attachments: ChatAttachment[];
5
5
  tokens: ChatInputTokens;
6
+ onRemove?: (id: string) => void;
6
7
  }): import("react/jsx-runtime").JSX.Element | null;
@@ -1,6 +1,6 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from "react";
3
- export function AttachmentPreview({ attachments, tokens, }) {
3
+ export function AttachmentPreview({ attachments, tokens, onRemove, }) {
4
4
  if (attachments.length === 0) {
5
5
  return null;
6
6
  }
@@ -12,18 +12,11 @@ export function AttachmentPreview({ attachments, tokens, }) {
12
12
  background: tokens.color.surfaceInputFocus,
13
13
  color: tokens.color.textSecondary,
14
14
  border: `1px solid ${tokens.color.borderDefault}`,
15
- padding: "6px 10px",
15
+ padding: attachment.kind === "image" ? 5 : "6px 10px",
16
16
  fontSize: 12,
17
17
  lineHeight: "16px",
18
18
  fontFamily: tokens.typography.familyBase,
19
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))) }));
20
+ position: "relative",
21
+ }, title: attachment.url ?? attachment.name, children: [attachment.kind === "image" && (attachment.previewUrl || attachment.url) ? (_jsx("img", { src: attachment.previewUrl ?? attachment.url, alt: attachment.name, style: { width: 64, height: 64, borderRadius: Math.max(6, tokens.radius.attachmentPreview - 4), objectFit: "cover" } })) : _jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", style: { width: 6, height: 6, borderRadius: 999, background: tokens.color.textSecondary, opacity: 0.9 } }), attachment.name] }), onRemove ? _jsx("button", { type: "button", "aria-label": `Remove ${attachment.name}`, onClick: () => onRemove(attachment.id), style: { position: attachment.kind === "image" ? "absolute" : "static", top: 8, right: 8, width: 20, height: 20, display: "grid", placeItems: "center", border: `1px solid ${tokens.color.borderDefault}`, borderRadius: 999, background: tokens.color.surfaceInputDefault, color: tokens.color.textPrimary, cursor: "pointer", lineHeight: 1 }, children: "\u00D7" }) : null] }, attachment.id))) }));
29
22
  }
@@ -110,6 +110,19 @@ function DesktopInner(props) {
110
110
  file,
111
111
  });
112
112
  };
113
+ const handlePaste = (event) => {
114
+ props.onPaste?.(event);
115
+ if (event.defaultPrevented || props.enableImage === false)
116
+ return;
117
+ const images = [...event.clipboardData.items]
118
+ .filter((item) => item.kind === "file" && item.type.startsWith("image/"))
119
+ .flatMap((item) => item.getAsFile() ? [item.getAsFile()] : []);
120
+ if (!images.length)
121
+ return;
122
+ event.preventDefault();
123
+ for (const image of images)
124
+ void handleImageFile(image);
125
+ };
113
126
  const insertEmoji = (emoji) => {
114
127
  const textarea = textComposerRef.current;
115
128
  const start = textarea?.selectionStart ?? state.text.length;
@@ -179,7 +192,7 @@ function DesktopInner(props) {
179
192
  setIsMoreOpen(false);
180
193
  }
181
194
  event.currentTarget.value = "";
182
- } }), _jsx(EmojiPicker, { open: isEmojiOpen, isMobile: false, anchorRef: emojiAnchorRef, onSelect: insertEmoji, onClose: () => setIsEmojiOpen(false) }), _jsx(AttachmentPreview, { attachments: attachments.attachments, tokens: tokens }), isMoreOpen ? (_jsxs("div", { style: {
195
+ } }), _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: {
183
196
  position: "absolute",
184
197
  left: 0,
185
198
  right: 0,
@@ -199,12 +212,15 @@ function DesktopInner(props) {
199
212
  display: "flex",
200
213
  flexDirection: "column",
201
214
  gap: 12,
202
- }, children: [_jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, rows: 3, placeholder: "Message PMate", onPaste: props.onPaste, onDrop: props.onDrop, onKeyDown: (event) => {
203
- if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
204
- event.preventDefault();
205
- if (state.text.trim() || state.attachments.length > 0)
206
- void onSend();
207
- }
215
+ }, children: [_jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, rows: 3, placeholder: "Message PMate", onPaste: handlePaste, onDrop: props.onDrop, onKeyDown: (event) => {
216
+ if (event.key !== "Enter" ||
217
+ event.shiftKey ||
218
+ event.nativeEvent.isComposing ||
219
+ event.keyCode === 229)
220
+ return;
221
+ event.preventDefault();
222
+ if (state.text.trim() || state.attachments.length > 0)
223
+ void onSend();
208
224
  }, onChange: (event) => {
209
225
  state.setText(event.target.value);
210
226
  state.setStatus(event.target.value.trim() ? "typing" : "idle");
@@ -266,8 +282,10 @@ function DesktopInner(props) {
266
282
  padding: "0 14px",
267
283
  display: "flex",
268
284
  alignItems: "center",
269
- }, children: _jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, rows: 1, placeholder: "Message PMate", onPaste: props.onPaste, onDrop: props.onDrop, onKeyDown: (event) => {
270
- if (event.key !== "Enter") {
285
+ }, children: _jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, rows: 1, placeholder: "Message PMate", onPaste: handlePaste, onDrop: props.onDrop, onKeyDown: (event) => {
286
+ if (event.key !== "Enter" ||
287
+ event.nativeEvent.isComposing ||
288
+ event.keyCode === 229) {
271
289
  return;
272
290
  }
273
291
  const shouldSend = props.submitShortcut === "meta-enter" ? event.metaKey || event.ctrlKey : !event.shiftKey;
@@ -114,6 +114,19 @@ function MobileInner(props) {
114
114
  file,
115
115
  });
116
116
  };
117
+ const handlePaste = (event) => {
118
+ props.onPaste?.(event);
119
+ if (event.defaultPrevented || props.enableImage === false)
120
+ return;
121
+ const images = [...event.clipboardData.items]
122
+ .filter((item) => item.kind === "file" && item.type.startsWith("image/"))
123
+ .flatMap((item) => item.getAsFile() ? [item.getAsFile()] : []);
124
+ if (!images.length)
125
+ return;
126
+ event.preventDefault();
127
+ for (const image of images)
128
+ void handleImageFile(image);
129
+ };
117
130
  const isRecordingOverlayVisible = state.status === "recording" || state.status === "transcribing";
118
131
  const isTranscriptConfirmVisible = state.status === "transcript-ready" && Boolean(state.transcript);
119
132
  const transcriptConfirmSurface = `color-mix(in srgb, ${tokens.color.surfaceInputFocus} 86%, ${tokens.color.brandPrimary})`;
@@ -455,7 +468,7 @@ function MobileInner(props) {
455
468
  lineHeight: "24px",
456
469
  fontWeight: 700,
457
470
  opacity: props.disabled || !transcriptDraft.trim() ? 0.5 : 1,
458
- }, 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) => {
471
+ }, children: "\u53D1\u9001" })] })] })) : null, _jsx(AttachmentPreview, { attachments: attachments.attachments, tokens: tokens, onRemove: attachments.removeAttachment }), _jsx("input", { ref: imageInputRef, type: "file", accept: props.image?.accept ?? "image/*", style: { display: "none" }, onChange: (event) => {
459
472
  const file = event.target.files?.[0];
460
473
  if (file) {
461
474
  void handleImageFile(file);
@@ -539,8 +552,11 @@ function MobileInner(props) {
539
552
  }, onPressCancel: () => {
540
553
  setRecordingChoice("send");
541
554
  voice.cancelRecording();
542
- } }) })) : (_jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, rows: 1, wrap: "off", placeholder: "Message", enterKeyHint: "send", onPaste: props.onPaste, onDrop: props.onDrop, onKeyDown: (event) => {
543
- if (event.key === "Enter" && !event.shiftKey) {
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) => {
556
+ if (event.key === "Enter" &&
557
+ !event.shiftKey &&
558
+ !event.nativeEvent.isComposing &&
559
+ event.keyCode !== 229) {
544
560
  event.preventDefault();
545
561
  if (state.text.trim()) {
546
562
  void onSend();
@@ -9,7 +9,8 @@ export declare function useVoiceInputController(config?: ChatVoiceConfig): {
9
9
  beginVoiceMode: () => void;
10
10
  setMode: (args_0: import("..").ChatInputMode | ((prev: import("..").ChatInputMode) => import("..").ChatInputMode)) => void;
11
11
  clearTranscriptPreview: () => void;
12
- startRecording: () => void;
12
+ prepareRecording: () => Promise<boolean>;
13
+ startRecording: () => Promise<boolean>;
13
14
  cancelRecording: (message?: string | null) => void;
14
15
  stopRecording: (action?: StopAction) => void;
15
16
  };
@@ -3,7 +3,7 @@ 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";
6
- import { MicEvents, MicStateManager } from "../voice/MicStateManager";
6
+ import { MicEvents, MicState, MicStateManager } from "../voice/MicStateManager";
7
7
  const DEFAULT_WAVEFORM = [
8
8
  0.18, 0.28, 0.42, 0.58, 0.74, 0.9, 0.74, 0.58, 0.42, 0.28, 0.18,
9
9
  ];
@@ -32,6 +32,11 @@ export function useVoiceInputController(config) {
32
32
  const unsubLevel = manager.on(MicEvents.Level, (nextLevels) => {
33
33
  setLevels(nextLevels);
34
34
  });
35
+ const unsubState = manager.on(MicEvents.StateChanged, ({ state }) => {
36
+ if (state === MicState.Recording) {
37
+ setStatus("recording");
38
+ }
39
+ });
35
40
  const unsubFinished = manager.on(MicEvents.Finished, (blob) => {
36
41
  setLevels(DEFAULT_WAVEFORM);
37
42
  setDurationMs(0);
@@ -63,6 +68,7 @@ export function useVoiceInputController(config) {
63
68
  return () => {
64
69
  unsubData();
65
70
  unsubLevel();
71
+ unsubState();
66
72
  unsubFinished();
67
73
  unsubAbort();
68
74
  unsubError();
@@ -128,11 +134,18 @@ export function useVoiceInputController(config) {
128
134
  setStatus(modeRef.current === "voice" ? "voice-mode-ready" : "idle");
129
135
  setError(null);
130
136
  }
131
- function startRecording() {
137
+ async function prepareRecording() {
138
+ setError(null);
139
+ setPendingTranscriptBlob(null);
140
+ setTranscript("");
141
+ setDurationMs(0);
142
+ abortMessageRef.current = null;
143
+ return await managerRef.current?.prepare() ?? false;
144
+ }
145
+ async function startRecording() {
132
146
  if (status === "recording") {
133
- return;
147
+ return true;
134
148
  }
135
- setStatus("recording");
136
149
  setError(null);
137
150
  setPendingTranscriptBlob(null);
138
151
  setTranscript("");
@@ -140,7 +153,7 @@ export function useVoiceInputController(config) {
140
153
  setLevels(DEFAULT_WAVEFORM);
141
154
  abortMessageRef.current = null;
142
155
  stopActionRef.current = "send";
143
- void managerRef.current?.start();
156
+ return await managerRef.current?.start() ?? false;
144
157
  }
145
158
  function cancelRecording(message = null) {
146
159
  abortMessageRef.current = message;
@@ -163,6 +176,7 @@ export function useVoiceInputController(config) {
163
176
  beginVoiceMode,
164
177
  setMode,
165
178
  clearTranscriptPreview,
179
+ prepareRecording,
166
180
  startRecording,
167
181
  cancelRecording,
168
182
  stopRecording,
@@ -115,7 +115,7 @@ export type ChatInputBaseProps = {
115
115
  value?: string;
116
116
  defaultValue?: string;
117
117
  onChange?: (value: string) => void;
118
- /** Receives paste events from the internal textarea before the SDK mutates composer state. */
118
+ /** Receives paste events before built-in image extraction. Call preventDefault() to fully own the event. */
119
119
  onPaste?: ClipboardEventHandler<HTMLTextAreaElement>;
120
120
  /** Receives drop events from the internal textarea so hosts can accept files or custom payloads. */
121
121
  onDrop?: DragEventHandler<HTMLTextAreaElement>;
@@ -141,5 +141,6 @@ export type ChatInputMobileProps = ChatInputBaseProps & {
141
141
  voiceModeDefault?: ChatInputMode;
142
142
  };
143
143
  export type ChatInputDesktopProps = ChatInputBaseProps & {
144
+ /** Defaults to Enter to send and Shift+Enter to insert a newline. */
144
145
  submitShortcut?: "enter" | "meta-enter";
145
146
  };
@@ -1,7 +1,9 @@
1
+ import { Recorder } from "./Recorder";
1
2
  export declare enum MicState {
2
3
  Idle = 0,
3
4
  Initializing = 1,
4
- Recording = 2
5
+ Recording = 2,
6
+ Ready = 3
5
7
  }
6
8
  export declare enum MicEvents {
7
9
  StateChanged = "stateChanged",
@@ -21,17 +23,22 @@ type MicEventMap = {
21
23
  [MicEvents.Abort]: () => void;
22
24
  [MicEvents.Error]: (error: Error) => void;
23
25
  };
26
+ type RecorderController = Pick<Recorder, "on" | "init" | "startRecording" | "stopRecording" | "detachStream" | "dispose">;
24
27
  export declare class MicStateManager {
25
28
  private recorder;
26
29
  private state;
27
30
  private chunks;
28
31
  private stream;
32
+ private preparePromise;
33
+ private operationId;
29
34
  private listeners;
30
35
  private initialized;
36
+ constructor(recorder?: RecorderController);
31
37
  on<K extends MicEvents>(event: K, handler: MicEventMap[K]): () => void;
32
38
  private emit;
33
39
  init(): void;
34
- start(): Promise<void>;
40
+ prepare(): Promise<boolean>;
41
+ start(): Promise<boolean>;
35
42
  stop(): void;
36
43
  abort(): void;
37
44
  dispose(): void;
@@ -4,6 +4,7 @@ export var MicState;
4
4
  MicState[MicState["Idle"] = 0] = "Idle";
5
5
  MicState[MicState["Initializing"] = 1] = "Initializing";
6
6
  MicState[MicState["Recording"] = 2] = "Recording";
7
+ MicState[MicState["Ready"] = 3] = "Ready";
7
8
  })(MicState || (MicState = {}));
8
9
  export var MicEvents;
9
10
  (function (MicEvents) {
@@ -15,10 +16,12 @@ export var MicEvents;
15
16
  MicEvents["Error"] = "error";
16
17
  })(MicEvents || (MicEvents = {}));
17
18
  export class MicStateManager {
18
- recorder = new Recorder();
19
+ recorder;
19
20
  state = MicState.Idle;
20
21
  chunks = [];
21
22
  stream;
23
+ preparePromise = null;
24
+ operationId = 0;
22
25
  listeners = {
23
26
  [MicEvents.StateChanged]: new Set(),
24
27
  [MicEvents.Data]: new Set(),
@@ -28,6 +31,9 @@ export class MicStateManager {
28
31
  [MicEvents.Error]: new Set(),
29
32
  };
30
33
  initialized = false;
34
+ constructor(recorder = new Recorder()) {
35
+ this.recorder = recorder;
36
+ }
31
37
  on(event, handler) {
32
38
  this.listeners[event].add(handler);
33
39
  return () => {
@@ -62,44 +68,93 @@ export class MicStateManager {
62
68
  this.reset();
63
69
  });
64
70
  this.recorder.on(RecorderEvents.Error, (error) => {
71
+ if (this.state === MicState.Initializing) {
72
+ return;
73
+ }
65
74
  this.abort();
66
75
  this.emit(MicEvents.Error, error);
67
76
  });
68
77
  }
69
- async start() {
70
- if (this.state === MicState.Initializing || this.state === MicState.Recording) {
71
- return;
78
+ async prepare() {
79
+ if (this.state === MicState.Ready || this.state === MicState.Recording) {
80
+ return true;
81
+ }
82
+ if (this.state === MicState.Initializing && this.preparePromise) {
83
+ return await this.preparePromise;
72
84
  }
85
+ const operationId = ++this.operationId;
73
86
  this.setState(MicState.Initializing);
74
- try {
87
+ this.releaseStream();
88
+ const preparePromise = (async () => {
75
89
  if (!navigator.mediaDevices?.getUserMedia) {
76
90
  throw new Error("Microphone access is not available in this browser.");
77
91
  }
78
- this.releaseStream();
79
- this.stream = await navigator.mediaDevices.getUserMedia({
92
+ const stream = await navigator.mediaDevices.getUserMedia({
80
93
  audio: {
81
94
  echoCancellation: true,
82
95
  noiseSuppression: true,
83
96
  autoGainControl: true,
84
97
  },
85
98
  });
86
- await this.recorder.init(this.stream);
87
- this.stream.getTracks().forEach((track) => {
88
- track.enabled = true;
89
- });
99
+ if (operationId !== this.operationId) {
100
+ stream.getTracks().forEach((track) => track.stop());
101
+ return false;
102
+ }
103
+ this.stream = stream;
104
+ await this.recorder.init(stream);
105
+ if (operationId !== this.operationId) {
106
+ this.releaseStream();
107
+ return false;
108
+ }
109
+ this.pauseStream();
110
+ this.setState(MicState.Ready);
111
+ return true;
112
+ })();
113
+ this.preparePromise = preparePromise;
114
+ try {
115
+ return await preparePromise;
116
+ }
117
+ catch (error) {
118
+ if (operationId !== this.operationId) {
119
+ return false;
120
+ }
121
+ this.releaseStream();
122
+ this.reset();
123
+ this.emit(MicEvents.Error, toError(error));
124
+ return false;
125
+ }
126
+ finally {
127
+ if (this.preparePromise === preparePromise) {
128
+ this.preparePromise = null;
129
+ }
130
+ }
131
+ }
132
+ async start() {
133
+ if (this.state === MicState.Recording) {
134
+ return true;
135
+ }
136
+ if (this.state !== MicState.Ready && !(await this.prepare())) {
137
+ return false;
138
+ }
139
+ try {
90
140
  await this.recorder.startRecording();
91
141
  this.setState(MicState.Recording);
142
+ return true;
92
143
  }
93
144
  catch (error) {
145
+ this.releaseStream();
94
146
  this.reset();
95
147
  this.emit(MicEvents.Error, toError(error));
148
+ return false;
96
149
  }
97
150
  }
98
151
  stop() {
99
- if (this.state !== MicState.Initializing && this.state !== MicState.Recording) {
152
+ if (this.state !== MicState.Initializing
153
+ && this.state !== MicState.Ready
154
+ && this.state !== MicState.Recording) {
100
155
  return;
101
156
  }
102
- if (this.state === MicState.Initializing) {
157
+ if (this.state !== MicState.Recording) {
103
158
  this.abort();
104
159
  return;
105
160
  }
@@ -112,6 +167,7 @@ export class MicStateManager {
112
167
  if (this.state === MicState.Idle) {
113
168
  return;
114
169
  }
170
+ this.operationId += 1;
115
171
  this.recorder.stopRecording();
116
172
  this.recorder.detachStream();
117
173
  this.pauseStream();
@@ -119,6 +175,7 @@ export class MicStateManager {
119
175
  this.reset();
120
176
  }
121
177
  dispose() {
178
+ this.operationId += 1;
122
179
  this.recorder.stopRecording();
123
180
  this.releaseStream();
124
181
  this.recorder.dispose();
@@ -46,6 +46,7 @@ export class Recorder {
46
46
  async init(stream) {
47
47
  await this.ensureAudioGraph();
48
48
  this.attachStream(stream);
49
+ await this.audioContext?.resume();
49
50
  this.initialized = true;
50
51
  }
51
52
  async ensureAudioGraph() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
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",