@eka-care/medassist-core 1.0.14 → 1.0.16
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/dist/Synapse.d.ts.map +1 -1
- package/dist/Synapse.js +1 -0
- package/dist/media/audio/Audio.copy.d.ts +44 -0
- package/dist/media/audio/Audio.copy.d.ts.map +1 -0
- package/dist/media/audio/Audio.copy.js +374 -0
- package/dist/media/audio/Audio.d.ts.map +1 -1
- package/dist/media/audio/Audio.js +29 -8
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/Synapse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Synapse.d.ts","sourceRoot":"","sources":["../src/Synapse.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAeH,OAAO,EACL,uBAAuB,EAExB,MAAM,kBAAkB,CAAC;AAmB1B,OAAO,EACL,KAAK,eAAe,EACrB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAE,sBAAsB,EAAE,kBAAkB,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAI3G,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,cAAc,CAAkB;IACxC,OAAO,CAAC,eAAe,CAAkB;IAEzC,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,cAAc,CAAiB;gBAE3B,MAAM,EAAE,gBAAgB;
|
|
1
|
+
{"version":3,"file":"Synapse.d.ts","sourceRoot":"","sources":["../src/Synapse.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAeH,OAAO,EACL,uBAAuB,EAExB,MAAM,kBAAkB,CAAC;AAmB1B,OAAO,EACL,KAAK,eAAe,EACrB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAE,sBAAsB,EAAE,kBAAkB,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAI3G,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,cAAc,CAAkB;IACxC,OAAO,CAAC,eAAe,CAAkB;IAEzC,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,cAAc,CAAiB;gBAE3B,MAAM,EAAE,gBAAgB;IAcpC;;;;;;OAMG;IACU,YAAY,CACvB,eAAe,CAAC,EAAE,sBAAsB,GACvC,OAAO,CAAC,eAAe,CAAC;IAwB3B;;OAEG;IACU,WAAW,CAAC,EACvB,OAAO,EACP,KAAK,EACL,KAAK,EACL,aAAa,EACb,OAAO,EACP,eAAe,EAChB,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmCrC;;OAEG;IACI,EAAE,CACP,KAAK,EAAE,uBAAuB,EAC9B,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GACrC,IAAI;IAIP;;OAEG;IACI,GAAG,CACR,KAAK,EAAE,uBAAuB,EAC9B,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GACrC,IAAI;IAIP;;OAEG;IACI,gBAAgB,IAAI,eAAe;IAU1C;;OAEG;IACU,QAAQ,CAAC,CAAC,SAAS,gBAAgB,GAAG,gBAAgB,EACjE,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAAC,CAAC,CAAC;IA+Cb;;SAEK;IACQ,cAAc,CAAC,EAC1B,QAAQ,EACR,OAAO,GACR,EAAE;QACD,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;QAC1C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;KAClC,GAAG,OAAO,CAAC,IAAI,CAAC;IAgCjB;;OAEG;IACI,YAAY,IAAI,IAAI;IAmCpB,WAAW,IAAI,OAAO;IAG7B;;OAEG;IACI,UAAU,IAAI,IAAI;IAUzB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,cAAc;IAYtB;;;OAGG;YACW,oBAAoB;IAwClC;;;OAGG;YACW,aAAa;IA2C3B;;OAEG;YACW,cAAc;IAwB5B;;OAEG;YACW,gBAAgB;IAO9B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAwC1B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAW/B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAe9B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAgD/B;;OAEG;YACW,mBAAmB;IAsCjC,OAAO,CAAC,iBAAiB;CAW1B"}
|
package/dist/Synapse.js
CHANGED
|
@@ -33,6 +33,7 @@ class SynapseSDK {
|
|
|
33
33
|
this.resourceManager = new resources_1.ResourceManager({
|
|
34
34
|
agentId: this.config.agentId,
|
|
35
35
|
serverUrl: this.config.serverUrl || API_ORIGIN, //based on environment
|
|
36
|
+
...(this.config.auth && { authorization: this.config.auth }),
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type AudioConfig, type AudioDataCallback, type AudioErrorCallback } from "./types";
|
|
2
|
+
export declare class AudioManager {
|
|
3
|
+
private mediaRecorder;
|
|
4
|
+
private mediaStream;
|
|
5
|
+
private recordingStartTime;
|
|
6
|
+
private autoPauseTimer;
|
|
7
|
+
private config;
|
|
8
|
+
private onAudioData;
|
|
9
|
+
private onAudioError;
|
|
10
|
+
private suppressCallbacks;
|
|
11
|
+
constructor(config?: Partial<AudioConfig>);
|
|
12
|
+
/**
|
|
13
|
+
* Best-effort permission check.
|
|
14
|
+
*
|
|
15
|
+
* NOTE: On some environments (especially iOS Safari / WKWebView),
|
|
16
|
+
* the Permissions API is missing or incomplete for "microphone".
|
|
17
|
+
* In those cases we **skip** the explicit check and rely on
|
|
18
|
+
* `getUserMedia` to trigger the OS permission prompt.
|
|
19
|
+
*/
|
|
20
|
+
private checkCurrentPermissionState;
|
|
21
|
+
start(onAudioData: AudioDataCallback, onAudioError?: AudioErrorCallback): Promise<void>;
|
|
22
|
+
private processAudioChunk;
|
|
23
|
+
private toRecordingError;
|
|
24
|
+
private handleError;
|
|
25
|
+
/**
|
|
26
|
+
* Set up auto-pause timer for 15-minute limit
|
|
27
|
+
*/
|
|
28
|
+
private setupAutoPauseTimer;
|
|
29
|
+
private setupMediaRecorderEvents;
|
|
30
|
+
/**
|
|
31
|
+
* Convert blob to base64 string
|
|
32
|
+
*/
|
|
33
|
+
private blobToBase64;
|
|
34
|
+
/**
|
|
35
|
+
* Stop recording manually
|
|
36
|
+
*/
|
|
37
|
+
stop(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Cancel recording (discard data and suppress callbacks)
|
|
40
|
+
*/
|
|
41
|
+
cancel(): void;
|
|
42
|
+
destroy(): void;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=Audio.copy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Audio.copy.d.ts","sourceRoot":"","sources":["../../../src/media/audio/Audio.copy.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EAExB,MAAM,SAAS,CAAC;AAGjB,qBAAa,YAAY;IACvB,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,cAAc,CAA8C;IAEpE,OAAO,CAAC,MAAM,CAAc;IAE5B,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,YAAY,CAAmC;IACvD,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM;IAU7C;;;;;;;OAOG;YACW,2BAA2B;IAkD5B,KAAK,CAChB,WAAW,EAAE,iBAAiB,EAC9B,YAAY,CAAC,EAAE,kBAAkB,GAChC,OAAO,CAAC,IAAI,CAAC;YAoIF,iBAAiB;IAuB/B,OAAO,CAAC,gBAAgB;IA0BxB,OAAO,CAAC,WAAW;IAmBnB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,wBAAwB;IAsChC;;OAEG;IACH,OAAO,CAAC,YAAY;IA8BpB;;OAEG;IACH,IAAI,IAAI,IAAI;IA6BZ;;OAEG;IACH,MAAM,IAAI,IAAI;IAgCd,OAAO,IAAI,IAAI;CAiBhB"}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AudioManager = void 0;
|
|
4
|
+
const Error_1 = require("../../internal/Error/Error");
|
|
5
|
+
class AudioManager {
|
|
6
|
+
mediaRecorder = null;
|
|
7
|
+
mediaStream = null;
|
|
8
|
+
recordingStartTime = 0;
|
|
9
|
+
autoPauseTimer = null;
|
|
10
|
+
config;
|
|
11
|
+
onAudioData = null;
|
|
12
|
+
onAudioError = null;
|
|
13
|
+
suppressCallbacks = false; //gurad against late recorder events like ondatavaailable after cancel () or cleanup() is called
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
this.config = {
|
|
16
|
+
mimeType: "audio/webm;codecs=opus",
|
|
17
|
+
audioBitsPerSecond: 128000,
|
|
18
|
+
maxRecordingDuration: 900000, // 15 minutes
|
|
19
|
+
autoPauseEnabled: true,
|
|
20
|
+
...config,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Best-effort permission check.
|
|
25
|
+
*
|
|
26
|
+
* NOTE: On some environments (especially iOS Safari / WKWebView),
|
|
27
|
+
* the Permissions API is missing or incomplete for "microphone".
|
|
28
|
+
* In those cases we **skip** the explicit check and rely on
|
|
29
|
+
* `getUserMedia` to trigger the OS permission prompt.
|
|
30
|
+
*/
|
|
31
|
+
async checkCurrentPermissionState() {
|
|
32
|
+
try {
|
|
33
|
+
// Guard: Permissions API not supported or not fully implemented
|
|
34
|
+
if (typeof navigator === "undefined" ||
|
|
35
|
+
!("permissions" in navigator) ||
|
|
36
|
+
!navigator.permissions ||
|
|
37
|
+
typeof navigator.permissions.query !== "function") {
|
|
38
|
+
// Treat as "prompt" – let getUserMedia drive the permission flow.
|
|
39
|
+
return "prompt";
|
|
40
|
+
}
|
|
41
|
+
// Try to query microphone permission
|
|
42
|
+
// On iOS WebView, this may throw "query does not support this api"
|
|
43
|
+
const permission = await navigator.permissions.query({
|
|
44
|
+
name: "microphone",
|
|
45
|
+
});
|
|
46
|
+
if (permission.state === "denied") {
|
|
47
|
+
throw new Error_1.RecordingError("Microphone permission denied", {
|
|
48
|
+
context: { permissionState: permission.state },
|
|
49
|
+
hint: "Ensure the browser or device has microphone access enabled for this site/app.",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return permission.state;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
// Some browsers (notably iOS WebKit/WKWebView) throw for unsupported permission names
|
|
56
|
+
// or return errors like "Permission :: query doesnot support" or "query does not support this api".
|
|
57
|
+
// Fall back to letting getUserMedia handle prompting.
|
|
58
|
+
const errorMessage = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
59
|
+
if (errorMessage.includes("does not support") ||
|
|
60
|
+
errorMessage.includes("doesnot support") ||
|
|
61
|
+
errorMessage.includes("not supported") ||
|
|
62
|
+
errorMessage.includes("query does not support") ||
|
|
63
|
+
errorMessage.includes("permission::query") ||
|
|
64
|
+
errorMessage.includes("permission :: query doesnot support")) {
|
|
65
|
+
// Silently skip permission check for unsupported APIs - this is expected on iOS WebView
|
|
66
|
+
return "prompt";
|
|
67
|
+
}
|
|
68
|
+
// For other errors, log a warning but still proceed
|
|
69
|
+
console.warn("Permissions API for microphone is unavailable, falling back to getUserMedia-only flow:", error);
|
|
70
|
+
return "prompt";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async start(onAudioData, onAudioError //Clearly handles the error inside the callback without unhandled rejections
|
|
74
|
+
) {
|
|
75
|
+
try {
|
|
76
|
+
// Check if mediaDevices is available
|
|
77
|
+
if (!navigator.mediaDevices) {
|
|
78
|
+
throw new Error_1.RecordingError("Media devices API is not available. This usually means the page is not served over HTTPS or there are browser restrictions.", {
|
|
79
|
+
context: { feature: "mediaDevices" },
|
|
80
|
+
hint: "Serve the app over HTTPS and verify browser/device microphone support.",
|
|
81
|
+
displayMessage: "Microphone access is unavailable."
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Check if MediaRecorder is supported
|
|
85
|
+
if (!window.MediaRecorder) {
|
|
86
|
+
throw new Error_1.RecordingError("MediaRecorder is not supported in this browser", {
|
|
87
|
+
context: { feature: "MediaRecorder" },
|
|
88
|
+
hint: "Use a browser that supports the MediaRecorder API.",
|
|
89
|
+
displayMessage: "Microphone access is unavailable."
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// Check for supported audio formats in order of preference
|
|
93
|
+
const supportedTypes = [
|
|
94
|
+
"audio/mp3",
|
|
95
|
+
"audio/ogg;codecs=opus",
|
|
96
|
+
"audio/mp4",
|
|
97
|
+
"audio/ogg",
|
|
98
|
+
"audio/wav",
|
|
99
|
+
].filter((type) => MediaRecorder.isTypeSupported(type));
|
|
100
|
+
if (supportedTypes.length === 0) {
|
|
101
|
+
throw new Error_1.RecordingError("No supported audio formats found in this browser", {
|
|
102
|
+
context: { requestedTypes: supportedTypes },
|
|
103
|
+
hint: "Verify codec support or adjust the requested MIME types.",
|
|
104
|
+
displayMessage: "Microphone access is unavailable."
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Update config with best supported mime type
|
|
108
|
+
this.config.mimeType = supportedTypes[0];
|
|
109
|
+
await this.checkCurrentPermissionState();
|
|
110
|
+
this.onAudioData = onAudioData;
|
|
111
|
+
this.onAudioError = onAudioError ?? null;
|
|
112
|
+
this.suppressCallbacks = false; //reset the guard against late recorder events
|
|
113
|
+
// Get user media with fallback for iOS WebView compatibility
|
|
114
|
+
// iOS WebView may throw "query does not support this api" for specific audio constraints
|
|
115
|
+
try {
|
|
116
|
+
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
117
|
+
audio: {
|
|
118
|
+
echoCancellation: false,
|
|
119
|
+
noiseSuppression: false,
|
|
120
|
+
autoGainControl: false,
|
|
121
|
+
sampleRate: 44100,
|
|
122
|
+
channelCount: 1,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (constraintError) {
|
|
127
|
+
const errorMessage = (constraintError instanceof Error ? constraintError.message : String(constraintError)).toLowerCase();
|
|
128
|
+
// If the error indicates unsupported constraints (common on iOS WebView),
|
|
129
|
+
// fall back to minimal audio constraints
|
|
130
|
+
// Error formats: "Permission :: query doesnot support", "query does not support", etc.
|
|
131
|
+
if (errorMessage.includes("does not support") ||
|
|
132
|
+
errorMessage.includes("doesnot support") ||
|
|
133
|
+
errorMessage.includes("not supported") ||
|
|
134
|
+
errorMessage.includes("query does not support") ||
|
|
135
|
+
errorMessage.includes("permission :: query") ||
|
|
136
|
+
errorMessage.includes("permission::query") ||
|
|
137
|
+
errorMessage.includes("constraintnotsatisfiederror")) {
|
|
138
|
+
// Fall back to simplest audio constraint for iOS WebView compatibility
|
|
139
|
+
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
140
|
+
audio: true,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
// Re-throw other errors (permission denied, etc.)
|
|
145
|
+
if (errorMessage.includes("permission::query") || errorMessage.includes("permission :: query")) {
|
|
146
|
+
throw new Error_1.RecordingError("Microphone permission denied", {
|
|
147
|
+
context: { permissionState: "denied" },
|
|
148
|
+
hint: `Please grant microphone access to the website. You can change this in the browser settings.`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
throw constraintError;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Create MediaRecorder with fallback if the selected format fails
|
|
155
|
+
this.mediaRecorder = new MediaRecorder(this.mediaStream, {
|
|
156
|
+
mimeType: this.config.mimeType,
|
|
157
|
+
audioBitsPerSecond: this.config.audioBitsPerSecond,
|
|
158
|
+
});
|
|
159
|
+
// Verify MediaRecorder is in a valid state
|
|
160
|
+
if (this.mediaRecorder.state !== "inactive") {
|
|
161
|
+
throw new Error_1.RecordingError("MediaRecorder is in an invalid state for start()", {
|
|
162
|
+
context: { recorderState: this.mediaRecorder.state },
|
|
163
|
+
displayMessage: "Failed to start recording"
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Set up event handlers
|
|
167
|
+
this.setupMediaRecorderEvents();
|
|
168
|
+
// Start recording
|
|
169
|
+
this.mediaRecorder.start();
|
|
170
|
+
this.recordingStartTime = Date.now();
|
|
171
|
+
// Set up auto-pause timer if enabled
|
|
172
|
+
if (this.config.autoPauseEnabled) {
|
|
173
|
+
this.setupAutoPauseTimer();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
throw this.toRecordingError(error, "Failed to start audio recording", {
|
|
178
|
+
stage: "start",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async processAudioChunk(blob) {
|
|
183
|
+
try {
|
|
184
|
+
const base64Audio = await this.blobToBase64(blob);
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
if (!this.onAudioData) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const audioData = {
|
|
190
|
+
audio: base64Audio,
|
|
191
|
+
format: this.config.mimeType,
|
|
192
|
+
duration: this.recordingStartTime > 0 ? now - this.recordingStartTime : 0,
|
|
193
|
+
timestamp: this.recordingStartTime > 0 ? this.recordingStartTime : now,
|
|
194
|
+
};
|
|
195
|
+
this.onAudioData(audioData);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
this.handleError(error, { stage: "process_audio_chunk" });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
toRecordingError(error, fallbackMessage, context) {
|
|
202
|
+
if (error instanceof Error_1.RecordingError) {
|
|
203
|
+
return error;
|
|
204
|
+
}
|
|
205
|
+
if (error instanceof Error_1.SynapseError) {
|
|
206
|
+
return new Error_1.RecordingError(error.message, {
|
|
207
|
+
cause: error,
|
|
208
|
+
context: { ...error.context, ...context },
|
|
209
|
+
hint: error.hint,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const message = error instanceof Error && error.message ? error.message : fallbackMessage;
|
|
213
|
+
return new Error_1.RecordingError(message, {
|
|
214
|
+
cause: error,
|
|
215
|
+
context,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
handleError(error, context) {
|
|
219
|
+
const recordingError = this.toRecordingError(error, "Unexpected recording error", context);
|
|
220
|
+
if (this.onAudioError) {
|
|
221
|
+
try {
|
|
222
|
+
this.onAudioError(recordingError);
|
|
223
|
+
}
|
|
224
|
+
catch (callbackError) {
|
|
225
|
+
console.error("Audio error callback failed:", callbackError);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
console.error("AudioManager error:", recordingError);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Set up auto-pause timer for 15-minute limit
|
|
234
|
+
*/
|
|
235
|
+
setupAutoPauseTimer() {
|
|
236
|
+
if (this.autoPauseTimer) {
|
|
237
|
+
clearTimeout(this.autoPauseTimer);
|
|
238
|
+
}
|
|
239
|
+
this.autoPauseTimer = setTimeout(() => {
|
|
240
|
+
console.log("Auto-pause timer triggered after 15 minutes");
|
|
241
|
+
this.stop();
|
|
242
|
+
}, this.config.maxRecordingDuration);
|
|
243
|
+
}
|
|
244
|
+
setupMediaRecorderEvents() {
|
|
245
|
+
if (!this.mediaRecorder)
|
|
246
|
+
return;
|
|
247
|
+
this.mediaRecorder.ondataavailable = (event) => {
|
|
248
|
+
if (this.suppressCallbacks || event.data.size <= 0) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
this.processAudioChunk(event.data).catch((error) => this.handleError(error, {
|
|
252
|
+
stage: "media_recorder_ondataavailable",
|
|
253
|
+
hasData: event.data.size > 0,
|
|
254
|
+
}));
|
|
255
|
+
};
|
|
256
|
+
this.mediaRecorder.onstop = async () => {
|
|
257
|
+
this.recordingStartTime = 0;
|
|
258
|
+
};
|
|
259
|
+
this.mediaRecorder.onerror = (event) => {
|
|
260
|
+
this.handleError(event.error instanceof Error
|
|
261
|
+
? event.error
|
|
262
|
+
: new Error("MediaRecorder encountered an unknown error"), { stage: "media_recorder_onerror" });
|
|
263
|
+
};
|
|
264
|
+
this.mediaRecorder.onstart = () => {
|
|
265
|
+
console.log("MediaRecorder started recording");
|
|
266
|
+
if (this.mediaRecorder) {
|
|
267
|
+
console.log("MediaRecorder state:", this.mediaRecorder.state);
|
|
268
|
+
console.log("MediaRecorder MIME type:", this.mediaRecorder.mimeType);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Convert blob to base64 string
|
|
274
|
+
*/
|
|
275
|
+
blobToBase64(blob) {
|
|
276
|
+
return new Promise((resolve, reject) => {
|
|
277
|
+
const reader = new FileReader();
|
|
278
|
+
reader.onload = () => {
|
|
279
|
+
if (typeof reader.result === "string") {
|
|
280
|
+
// Remove data URL prefix (e.g., "data:audio/mp3;base64,")
|
|
281
|
+
const base64 = reader.result.split(",")[1];
|
|
282
|
+
resolve(base64);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
reject(new Error_1.RecordingError("Failed to convert blob to base64", {
|
|
286
|
+
context: { stage: "blob_to_base64" },
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
reader.onerror = () => reject(new Error_1.RecordingError("FileReader error while converting audio chunk to base64", {
|
|
291
|
+
cause: reader.error ?? undefined,
|
|
292
|
+
context: { stage: "blob_to_base64" },
|
|
293
|
+
}));
|
|
294
|
+
reader.readAsDataURL(blob);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Stop recording manually
|
|
299
|
+
*/
|
|
300
|
+
stop() {
|
|
301
|
+
if (!this.mediaRecorder)
|
|
302
|
+
return;
|
|
303
|
+
try {
|
|
304
|
+
// Clear auto-pause timer
|
|
305
|
+
if (this.autoPauseTimer) {
|
|
306
|
+
clearTimeout(this.autoPauseTimer);
|
|
307
|
+
this.autoPauseTimer = null;
|
|
308
|
+
}
|
|
309
|
+
// Stop MediaRecorder
|
|
310
|
+
if (this.mediaRecorder.state === "recording") {
|
|
311
|
+
this.mediaRecorder.stop();
|
|
312
|
+
}
|
|
313
|
+
// Stop media stream
|
|
314
|
+
if (this.mediaStream) {
|
|
315
|
+
this.mediaStream.getTracks().forEach((track) => track.stop());
|
|
316
|
+
this.mediaStream = null;
|
|
317
|
+
}
|
|
318
|
+
console.log("Audio recording stopped manually");
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
throw this.toRecordingError(error, "Failed to stop audio recording", {
|
|
322
|
+
stage: "stop",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Cancel recording (discard data and suppress callbacks)
|
|
328
|
+
*/
|
|
329
|
+
cancel() {
|
|
330
|
+
if (!this.mediaRecorder)
|
|
331
|
+
return;
|
|
332
|
+
try {
|
|
333
|
+
this.suppressCallbacks = true;
|
|
334
|
+
if (this.autoPauseTimer) {
|
|
335
|
+
clearTimeout(this.autoPauseTimer);
|
|
336
|
+
this.autoPauseTimer = null;
|
|
337
|
+
}
|
|
338
|
+
// Prevent ondataavailable from pushing chunks
|
|
339
|
+
this.mediaRecorder.ondataavailable = null;
|
|
340
|
+
// Stop MediaRecorder
|
|
341
|
+
if (this.mediaRecorder.state === "recording") {
|
|
342
|
+
this.mediaRecorder.stop();
|
|
343
|
+
}
|
|
344
|
+
// Stop media stream
|
|
345
|
+
if (this.mediaStream) {
|
|
346
|
+
this.mediaStream.getTracks().forEach((track) => track.stop());
|
|
347
|
+
this.mediaStream = null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
throw this.toRecordingError(error, "Failed to cancel audio recording", {
|
|
352
|
+
stage: "cancel",
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
destroy() {
|
|
357
|
+
try {
|
|
358
|
+
this.cancel();
|
|
359
|
+
if (this.mediaRecorder) {
|
|
360
|
+
this.mediaRecorder = null;
|
|
361
|
+
}
|
|
362
|
+
if (this.mediaStream) {
|
|
363
|
+
this.mediaStream.getTracks().forEach((track) => track.stop());
|
|
364
|
+
this.mediaStream = null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
throw this.toRecordingError(error, "Failed to destroy audio resources", {
|
|
369
|
+
stage: "destroy",
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
exports.AudioManager = AudioManager;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Audio.d.ts","sourceRoot":"","sources":["../../../src/media/audio/Audio.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EAExB,MAAM,SAAS,CAAC;AAGjB,qBAAa,YAAY;IACvB,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,cAAc,CAA8C;IAEpE,OAAO,CAAC,MAAM,CAAc;IAE5B,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,YAAY,CAAmC;IACvD,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM;YAU/B,2BAA2B;
|
|
1
|
+
{"version":3,"file":"Audio.d.ts","sourceRoot":"","sources":["../../../src/media/audio/Audio.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EAExB,MAAM,SAAS,CAAC;AAGjB,qBAAa,YAAY;IACvB,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,cAAc,CAA8C;IAEpE,OAAO,CAAC,MAAM,CAAc;IAE5B,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,YAAY,CAAmC;IACvD,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM;YAU/B,2BAA2B;IA0B5B,KAAK,CAChB,WAAW,EAAE,iBAAiB,EAC9B,YAAY,CAAC,EAAE,kBAAkB,GAChC,OAAO,CAAC,IAAI,CAAC;YA6GF,iBAAiB;IAuB/B,OAAO,CAAC,gBAAgB;IA0BxB,OAAO,CAAC,WAAW;IAmBnB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,wBAAwB;IAsChC;;OAEG;IACH,OAAO,CAAC,YAAY;IA8BpB;;OAEG;IACH,IAAI,IAAI,IAAI;IA6BZ;;OAEG;IACH,MAAM,IAAI,IAAI;IAgCd,OAAO,IAAI,IAAI;CAiBhB"}
|
|
@@ -21,16 +21,28 @@ class AudioManager {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
async checkCurrentPermissionState() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
hint: "Ensure the browser has microphone access enabled for this site.",
|
|
24
|
+
if (!navigator.permissions || !navigator.permissions.query) {
|
|
25
|
+
return "prompt";
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const permission = await navigator.permissions.query({
|
|
29
|
+
name: "microphone",
|
|
31
30
|
});
|
|
31
|
+
if (permission.state === "denied") {
|
|
32
|
+
throw new Error_1.RecordingError("Microphone permission denied", {
|
|
33
|
+
context: { permissionState: permission.state },
|
|
34
|
+
hint: "Ensure the browser has microphone access enabled for this site.",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
console.log("Microphone permission state:", permission.state);
|
|
38
|
+
return permission.state;
|
|
32
39
|
}
|
|
33
|
-
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.error("Error checking microphone permission:", error);
|
|
42
|
+
if (error instanceof Error && error.name === "NotSupportedError")
|
|
43
|
+
return "prompt";
|
|
44
|
+
}
|
|
45
|
+
return "prompt";
|
|
34
46
|
}
|
|
35
47
|
async start(onAudioData, onAudioError //Clearly handles the error inside the callback without unhandled rejections
|
|
36
48
|
) {
|
|
@@ -72,6 +84,7 @@ class AudioManager {
|
|
|
72
84
|
this.onAudioData = onAudioData;
|
|
73
85
|
this.onAudioError = onAudioError ?? null;
|
|
74
86
|
this.suppressCallbacks = false; //reset the guard against late recorder events
|
|
87
|
+
console.log("Getting user media");
|
|
75
88
|
// Get user media
|
|
76
89
|
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
77
90
|
audio: {
|
|
@@ -82,6 +95,7 @@ class AudioManager {
|
|
|
82
95
|
channelCount: 1,
|
|
83
96
|
},
|
|
84
97
|
});
|
|
98
|
+
console.log("Got user media");
|
|
85
99
|
// Create MediaRecorder with fallback if the selected format fails
|
|
86
100
|
this.mediaRecorder = new MediaRecorder(this.mediaStream, {
|
|
87
101
|
mimeType: this.config.mimeType,
|
|
@@ -105,6 +119,13 @@ class AudioManager {
|
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
121
|
catch (error) {
|
|
122
|
+
console.error("Error starting audio recording in media devices:", error);
|
|
123
|
+
if (error instanceof Error && error.name === "NotAllowedError") {
|
|
124
|
+
throw new Error_1.RecordingError("Microphone permission denied", {
|
|
125
|
+
context: { permissionState: "denied" },
|
|
126
|
+
hint: "Ensure the browser or device has microphone access enabled for this site/app.",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
108
129
|
throw this.toRecordingError(error, "Failed to start audio recording", {
|
|
109
130
|
stage: "start",
|
|
110
131
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resources/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAG9C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAG3E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EAAwB,QAAQ,EAAE,MAAM,UAAU,CAAC;AAE/D,qBAAa,eAAe;IAC1B,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,QAAQ,CAAyB;gBAE7B,MAAM,EAAE,cAAc;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resources/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAG9C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAG3E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EAAwB,QAAQ,EAAE,MAAM,UAAU,CAAC;AAE/D,qBAAa,eAAe;IAC1B,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,QAAQ,CAAyB;gBAE7B,MAAM,EAAE,cAAc;IAiBlC;;OAEG;IACU,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,eAAe,CAAC;IAsCxF;;;OAGG;IACU,eAAe,CAC1B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;IAgDjD;;;OAGG;IACU,cAAc,CACzB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,eAAe,CAAC;IAI3B;;;OAGG;IACU,YAAY,CACvB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,aAAa,EACvB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC;IAShB;;;OAGG;IACU,QAAQ,CAAC,CAAC,SAAS,gBAAgB,GAAG,gBAAgB,EACjE,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,OAAO,CAAC,CAAC,CAAC;CAMd"}
|
package/dist/resources/index.js
CHANGED
|
@@ -20,6 +20,7 @@ class ResourceManager {
|
|
|
20
20
|
baseUrl: this.resourceConfig.serverUrl,
|
|
21
21
|
headers: {
|
|
22
22
|
"X-agent-id": this.resourceConfig.agentId, // This will be included in ALL requests
|
|
23
|
+
...(this.resourceConfig.authorization && { Authorization: this.resourceConfig.authorization }),
|
|
23
24
|
},
|
|
24
25
|
// authorization: config.authorization,
|
|
25
26
|
});
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE,cAAc,8BAA8B,CAAC;AAC7C,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAG3C,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,aAAa,EAAE,CAAA;CAClC;AACD,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,KAAK,CAAC;AACnD,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,CAAC,EAAE,CAAC,eAAe,EAAE,eAAe,KAAK,IAAI,CAAC;IAGhE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;CAC5C;AACD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,OAAO,CAAC,EAAE,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE,cAAc,8BAA8B,CAAC;AAC7C,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAG3C,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,aAAa,EAAE,CAAA;CAClC;AACD,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,KAAK,CAAC;AACnD,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,CAAC,EAAE,CAAC,eAAe,EAAE,eAAe,KAAK,IAAI,CAAC;IAGhE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;CAC5C;AACD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,OAAO,CAAC,EAAE,QAAQ,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AACD,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eka-care/medassist-core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "TypeScript SDK for real-time medical chatbot experiences with session management, WebSocket connectivity, and media handling",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|