@rexai/pulse-react 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,850 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ PulseClient: () => PulseClient,
34
+ PulseScanner: () => PulseScanner,
35
+ VERSION: () => VERSION,
36
+ VitalsCard: () => VitalsCard,
37
+ VitalsCardFromResult: () => VitalsCardFromResult,
38
+ useCamera: () => useCamera,
39
+ useFaceDetection: () => useFaceDetection,
40
+ useRecording: () => useRecording
41
+ });
42
+ module.exports = __toCommonJS(index_exports);
43
+
44
+ // src/components/PulseScanner.tsx
45
+ var import_react4 = require("react");
46
+
47
+ // src/hooks/useCamera.ts
48
+ var import_react = require("react");
49
+ function useCamera(options = {}) {
50
+ const {
51
+ width = 640,
52
+ height = 480,
53
+ facingMode = "user",
54
+ autoStart = false
55
+ } = options;
56
+ const videoRef = (0, import_react.useRef)(null);
57
+ const streamRef = (0, import_react.useRef)(null);
58
+ const [isActive, setIsActive] = (0, import_react.useState)(false);
59
+ const [error, setError] = (0, import_react.useState)(null);
60
+ const start = (0, import_react.useCallback)(async () => {
61
+ setError(null);
62
+ try {
63
+ const mediaStream = await navigator.mediaDevices.getUserMedia({
64
+ video: {
65
+ width: { ideal: width },
66
+ height: { ideal: height },
67
+ facingMode
68
+ },
69
+ audio: false
70
+ });
71
+ streamRef.current = mediaStream;
72
+ if (videoRef.current) {
73
+ videoRef.current.srcObject = mediaStream;
74
+ await new Promise((resolve) => {
75
+ if (videoRef.current) {
76
+ videoRef.current.onloadedmetadata = () => resolve();
77
+ }
78
+ });
79
+ }
80
+ setIsActive(true);
81
+ } catch (err) {
82
+ const errorMessage = err.name === "NotAllowedError" ? "Camera access denied. Please allow camera permissions." : err.name === "NotFoundError" ? "No camera found on this device." : err.message || "Failed to access camera.";
83
+ setError(errorMessage);
84
+ setIsActive(false);
85
+ }
86
+ }, [width, height, facingMode]);
87
+ const stop = (0, import_react.useCallback)(() => {
88
+ if (streamRef.current) {
89
+ streamRef.current.getTracks().forEach((track) => track.stop());
90
+ streamRef.current = null;
91
+ }
92
+ if (videoRef.current) {
93
+ videoRef.current.srcObject = null;
94
+ }
95
+ setIsActive(false);
96
+ }, []);
97
+ (0, import_react.useEffect)(() => {
98
+ if (autoStart) {
99
+ start();
100
+ }
101
+ return () => {
102
+ stop();
103
+ };
104
+ }, [autoStart]);
105
+ return {
106
+ videoRef,
107
+ stream: streamRef.current,
108
+ isActive,
109
+ error,
110
+ start,
111
+ stop
112
+ };
113
+ }
114
+
115
+ // src/hooks/useFaceDetection.ts
116
+ var import_react2 = require("react");
117
+ var FACE_LANDMARKS_TESSELATION = null;
118
+ var FACE_LANDMARKS_RIGHT_IRIS = null;
119
+ var FACE_LANDMARKS_LEFT_IRIS = null;
120
+ var DrawingUtils = null;
121
+ function useFaceDetection(videoElement, options = {}) {
122
+ const { enabled = true, meshCanvas = null, drawMesh = false } = options;
123
+ const [isLoading, setIsLoading] = (0, import_react2.useState)(true);
124
+ const [landmarker, setLandmarker] = (0, import_react2.useState)(null);
125
+ const [result, setResult] = (0, import_react2.useState)({
126
+ detected: false,
127
+ landmarks: null,
128
+ qualityMessage: "Initializing face detection...",
129
+ isQualityGood: false,
130
+ brightness: 0
131
+ });
132
+ const latestLandmarksRef = (0, import_react2.useRef)(null);
133
+ const brightnessCanvasRef = (0, import_react2.useRef)(null);
134
+ (0, import_react2.useEffect)(() => {
135
+ if (!enabled) {
136
+ setIsLoading(false);
137
+ return;
138
+ }
139
+ const loadMediaPipe = async () => {
140
+ try {
141
+ const mediapipe = await import("@mediapipe/tasks-vision");
142
+ const { FilesetResolver, FaceLandmarker } = mediapipe;
143
+ FACE_LANDMARKS_TESSELATION = FaceLandmarker.FACE_LANDMARKS_TESSELATION;
144
+ FACE_LANDMARKS_RIGHT_IRIS = FaceLandmarker.FACE_LANDMARKS_RIGHT_IRIS;
145
+ FACE_LANDMARKS_LEFT_IRIS = FaceLandmarker.FACE_LANDMARKS_LEFT_IRIS;
146
+ DrawingUtils = mediapipe.DrawingUtils;
147
+ const vision = await FilesetResolver.forVisionTasks(
148
+ "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3/wasm"
149
+ );
150
+ const fl = await FaceLandmarker.createFromOptions(vision, {
151
+ baseOptions: {
152
+ modelAssetPath: `https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task`,
153
+ delegate: "GPU"
154
+ },
155
+ outputFaceBlendshapes: false,
156
+ runningMode: "VIDEO",
157
+ numFaces: 1
158
+ });
159
+ setLandmarker(fl);
160
+ setIsLoading(false);
161
+ setResult((prev) => ({
162
+ ...prev,
163
+ qualityMessage: "Waiting for camera..."
164
+ }));
165
+ } catch (err) {
166
+ console.error("Failed to load MediaPipe:", err);
167
+ setIsLoading(false);
168
+ setResult((prev) => ({
169
+ ...prev,
170
+ qualityMessage: "Face detection unavailable"
171
+ }));
172
+ }
173
+ };
174
+ loadMediaPipe();
175
+ }, [enabled]);
176
+ (0, import_react2.useEffect)(() => {
177
+ brightnessCanvasRef.current = document.createElement("canvas");
178
+ brightnessCanvasRef.current.width = 64;
179
+ brightnessCanvasRef.current.height = 48;
180
+ }, []);
181
+ const calculateBrightness = (0, import_react2.useCallback)((video) => {
182
+ const canvas = brightnessCanvasRef.current;
183
+ if (!canvas) return 100;
184
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
185
+ if (!ctx) return 100;
186
+ ctx.drawImage(video, 0, 0, 64, 48);
187
+ const frame = ctx.getImageData(0, 0, 64, 48);
188
+ let sum = 0;
189
+ for (let i = 0; i < frame.data.length; i += 4) {
190
+ sum += (frame.data[i] + frame.data[i + 1] + frame.data[i + 2]) / 3;
191
+ }
192
+ return sum / (64 * 48);
193
+ }, []);
194
+ (0, import_react2.useEffect)(() => {
195
+ if (!videoElement || !landmarker || !enabled) return;
196
+ let lastTime = -1;
197
+ let frameId;
198
+ let brightnessCheckCounter = 0;
199
+ const detect = () => {
200
+ if (videoElement.paused || videoElement.ended) {
201
+ frameId = requestAnimationFrame(detect);
202
+ return;
203
+ }
204
+ if (videoElement.currentTime !== lastTime && videoElement.readyState >= 2) {
205
+ lastTime = videoElement.currentTime;
206
+ try {
207
+ const results = landmarker.detectForVideo(videoElement, performance.now());
208
+ let brightness = result.brightness;
209
+ brightnessCheckCounter++;
210
+ if (brightnessCheckCounter >= 15) {
211
+ brightness = calculateBrightness(videoElement);
212
+ brightnessCheckCounter = 0;
213
+ }
214
+ if (drawMesh && meshCanvas && DrawingUtils) {
215
+ const ctx = meshCanvas.getContext("2d");
216
+ if (ctx) {
217
+ meshCanvas.width = videoElement.videoWidth;
218
+ meshCanvas.height = videoElement.videoHeight;
219
+ ctx.clearRect(0, 0, meshCanvas.width, meshCanvas.height);
220
+ if (results.faceLandmarks && results.faceLandmarks.length > 0) {
221
+ const drawingUtils = new DrawingUtils(ctx);
222
+ for (const landmarks of results.faceLandmarks) {
223
+ drawingUtils.drawConnectors(
224
+ landmarks,
225
+ FACE_LANDMARKS_TESSELATION,
226
+ { color: "#30d5c8aa", lineWidth: 1 }
227
+ );
228
+ drawingUtils.drawConnectors(
229
+ landmarks,
230
+ FACE_LANDMARKS_RIGHT_IRIS,
231
+ { color: "#00FF00", lineWidth: 2 }
232
+ );
233
+ drawingUtils.drawConnectors(
234
+ landmarks,
235
+ FACE_LANDMARKS_LEFT_IRIS,
236
+ { color: "#00FF00", lineWidth: 2 }
237
+ );
238
+ }
239
+ }
240
+ }
241
+ }
242
+ if (results.faceLandmarks && results.faceLandmarks.length > 0) {
243
+ latestLandmarksRef.current = results.faceLandmarks[0];
244
+ let qualityMessage = "Face detected";
245
+ let isQualityGood = true;
246
+ if (brightness < 60) {
247
+ qualityMessage = "Too dark - add more light";
248
+ isQualityGood = false;
249
+ } else if (brightness > 200) {
250
+ qualityMessage = "Too bright - reduce light";
251
+ isQualityGood = false;
252
+ } else {
253
+ qualityMessage = "Face locked. Ready to scan.";
254
+ }
255
+ setResult({
256
+ detected: true,
257
+ landmarks: results.faceLandmarks[0],
258
+ qualityMessage,
259
+ isQualityGood,
260
+ brightness
261
+ });
262
+ } else {
263
+ latestLandmarksRef.current = null;
264
+ setResult({
265
+ detected: false,
266
+ landmarks: null,
267
+ qualityMessage: "Position face in frame",
268
+ isQualityGood: false,
269
+ brightness
270
+ });
271
+ }
272
+ } catch (err) {
273
+ }
274
+ }
275
+ frameId = requestAnimationFrame(detect);
276
+ };
277
+ detect();
278
+ return () => {
279
+ if (frameId) {
280
+ cancelAnimationFrame(frameId);
281
+ }
282
+ };
283
+ }, [videoElement, landmarker, enabled, calculateBrightness, result.brightness, drawMesh, meshCanvas]);
284
+ return {
285
+ ...result,
286
+ isLoading,
287
+ latestLandmarks: latestLandmarksRef
288
+ };
289
+ }
290
+
291
+ // src/hooks/useRecording.ts
292
+ var import_react3 = require("react");
293
+ function useRecording(stream, options = {}) {
294
+ const {
295
+ duration = 10,
296
+ mimeType = "video/webm;codecs=vp9",
297
+ onComplete
298
+ } = options;
299
+ const mediaRecorderRef = (0, import_react3.useRef)(null);
300
+ const timerRef = (0, import_react3.useRef)(null);
301
+ const chunksRef = (0, import_react3.useRef)([]);
302
+ const [isRecording, setIsRecording] = (0, import_react3.useState)(false);
303
+ const [timeLeft, setTimeLeft] = (0, import_react3.useState)(duration);
304
+ const [blob, setBlob] = (0, import_react3.useState)(null);
305
+ const cleanup = (0, import_react3.useCallback)(() => {
306
+ if (timerRef.current) {
307
+ clearInterval(timerRef.current);
308
+ timerRef.current = null;
309
+ }
310
+ }, []);
311
+ const start = (0, import_react3.useCallback)(() => {
312
+ if (!stream || isRecording) return;
313
+ chunksRef.current = [];
314
+ setBlob(null);
315
+ setTimeLeft(duration);
316
+ let actualMimeType = mimeType;
317
+ if (!MediaRecorder.isTypeSupported(mimeType)) {
318
+ actualMimeType = "video/webm";
319
+ if (!MediaRecorder.isTypeSupported(actualMimeType)) {
320
+ actualMimeType = "video/mp4";
321
+ }
322
+ }
323
+ try {
324
+ const recorder = new MediaRecorder(stream, { mimeType: actualMimeType });
325
+ recorder.ondataavailable = (e) => {
326
+ if (e.data.size > 0) {
327
+ chunksRef.current.push(e.data);
328
+ }
329
+ };
330
+ recorder.onstop = () => {
331
+ const videoBlob = new Blob(chunksRef.current, { type: actualMimeType });
332
+ setBlob(videoBlob);
333
+ setIsRecording(false);
334
+ cleanup();
335
+ onComplete?.(videoBlob);
336
+ };
337
+ recorder.onerror = (e) => {
338
+ console.error("MediaRecorder error:", e);
339
+ setIsRecording(false);
340
+ cleanup();
341
+ };
342
+ mediaRecorderRef.current = recorder;
343
+ recorder.start(100);
344
+ setIsRecording(true);
345
+ timerRef.current = setInterval(() => {
346
+ setTimeLeft((prev) => {
347
+ if (prev <= 1) {
348
+ recorder.stop();
349
+ return 0;
350
+ }
351
+ return prev - 1;
352
+ });
353
+ }, 1e3);
354
+ } catch (err) {
355
+ console.error("Failed to start recording:", err);
356
+ }
357
+ }, [stream, isRecording, duration, mimeType, onComplete, cleanup]);
358
+ const stop = (0, import_react3.useCallback)(() => {
359
+ if (mediaRecorderRef.current && isRecording) {
360
+ mediaRecorderRef.current.stop();
361
+ }
362
+ }, [isRecording]);
363
+ const reset = (0, import_react3.useCallback)(() => {
364
+ cleanup();
365
+ setIsRecording(false);
366
+ setTimeLeft(duration);
367
+ setBlob(null);
368
+ chunksRef.current = [];
369
+ }, [duration, cleanup]);
370
+ return {
371
+ isRecording,
372
+ timeLeft,
373
+ blob,
374
+ start,
375
+ stop,
376
+ reset
377
+ };
378
+ }
379
+
380
+ // src/client.ts
381
+ var import_meta = {};
382
+ var PulseClient = class {
383
+ constructor(config) {
384
+ if (!config.apiKey) {
385
+ throw new Error("PulseAI SDK: apiKey is required");
386
+ }
387
+ this.apiKey = config.apiKey;
388
+ this.baseUrl = config.baseUrl || typeof import_meta !== "undefined" && import_meta.env?.VITE_API_URL || "http://localhost:8123";
389
+ this.timeout = config.timeout || 6e4;
390
+ }
391
+ /**
392
+ * Analyze video for medical vital signs
393
+ * Returns heart rate, HRV, SpO2, stress index, and more.
394
+ */
395
+ async analyze(options) {
396
+ const formData = new FormData();
397
+ formData.append("video", options.video, "scan.webm");
398
+ if (options.age !== void 0) {
399
+ formData.append("age", options.age.toString());
400
+ }
401
+ if (options.sex) {
402
+ formData.append("sex", options.sex);
403
+ }
404
+ if (options.weight !== void 0) {
405
+ formData.append("weight", options.weight.toString());
406
+ }
407
+ if (options.height !== void 0) {
408
+ formData.append("height", options.height.toString());
409
+ }
410
+ const response = await this.request("/analyze", formData);
411
+ return response;
412
+ }
413
+ /**
414
+ * Analyze video for beauty/skin metrics
415
+ * Returns skin age, texture, hydration, symmetry, and recommendations.
416
+ */
417
+ async analyzeBeauty(options) {
418
+ const formData = new FormData();
419
+ formData.append("video", options.video, "beauty_scan.webm");
420
+ if (options.age !== void 0) {
421
+ formData.append("age", options.age.toString());
422
+ }
423
+ if (options.gender) {
424
+ formData.append("gender", options.gender);
425
+ }
426
+ const response = await this.request("/analyze/beauty", formData);
427
+ return response;
428
+ }
429
+ /**
430
+ * Check API health status
431
+ */
432
+ async health() {
433
+ const response = await fetch(`${this.baseUrl}/health`, {
434
+ method: "GET",
435
+ headers: { "X-API-Key": this.apiKey }
436
+ });
437
+ if (!response.ok) {
438
+ throw new Error("Health check failed");
439
+ }
440
+ return response.json();
441
+ }
442
+ /**
443
+ * Internal request handler with timeout and error handling
444
+ */
445
+ async request(endpoint, formData) {
446
+ const controller = new AbortController();
447
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
448
+ try {
449
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
450
+ method: "POST",
451
+ headers: {
452
+ "X-API-Key": this.apiKey
453
+ // Don't set Content-Type - browser sets it with boundary for FormData
454
+ },
455
+ body: formData,
456
+ signal: controller.signal
457
+ });
458
+ clearTimeout(timeoutId);
459
+ if (!response.ok) {
460
+ const errorData = await response.json().catch(() => ({}));
461
+ const message = errorData.detail || `Request failed with status ${response.status}`;
462
+ if (response.status === 401) {
463
+ throw new Error("Invalid API key. Please check your credentials.");
464
+ }
465
+ if (response.status === 402) {
466
+ throw new Error("API quota exceeded. Please upgrade your plan.");
467
+ }
468
+ if (response.status === 403) {
469
+ throw new Error("API key missing. Include X-API-Key header.");
470
+ }
471
+ if (response.status >= 500) {
472
+ throw new Error("Server error. Please try again later.");
473
+ }
474
+ throw new Error(message);
475
+ }
476
+ const result = await response.json();
477
+ if (result.status !== "success") {
478
+ throw new Error(result.message || "Analysis failed");
479
+ }
480
+ return result.data;
481
+ } catch (err) {
482
+ clearTimeout(timeoutId);
483
+ if (err.name === "AbortError") {
484
+ throw new Error("Request timed out. The video may be too long or the connection too slow.");
485
+ }
486
+ throw err;
487
+ }
488
+ }
489
+ };
490
+
491
+ // src/components/PulseScanner.tsx
492
+ var import_jsx_runtime = require("react/jsx-runtime");
493
+ var PulseScanner = ({
494
+ apiKey,
495
+ apiUrl,
496
+ duration = 10,
497
+ onResult,
498
+ onError,
499
+ onStatusChange,
500
+ showPreview = true,
501
+ showOverlay = true,
502
+ showMesh = false,
503
+ showCountdown = true,
504
+ className = "",
505
+ theme = "light",
506
+ userData,
507
+ autoStart = false
508
+ }) => {
509
+ const clientRef = (0, import_react4.useRef)(null);
510
+ const meshCanvasRef = (0, import_react4.useRef)(null);
511
+ (0, import_react4.useEffect)(() => {
512
+ clientRef.current = new PulseClient({
513
+ apiKey,
514
+ baseUrl: apiUrl
515
+ // undefined is fine - client will use env var
516
+ });
517
+ }, [apiKey, apiUrl]);
518
+ const {
519
+ videoRef,
520
+ stream,
521
+ isActive: isCameraActive,
522
+ error: cameraError,
523
+ start: startCamera,
524
+ stop: stopCamera
525
+ } = useCamera({ autoStart });
526
+ const {
527
+ detected: faceDetected,
528
+ qualityMessage,
529
+ isQualityGood,
530
+ isLoading: isFaceDetectionLoading
531
+ } = useFaceDetection(videoRef.current, {
532
+ enabled: isCameraActive,
533
+ meshCanvas: meshCanvasRef.current,
534
+ drawMesh: showMesh
535
+ });
536
+ const {
537
+ isRecording,
538
+ timeLeft,
539
+ blob: recordedBlob,
540
+ start: startRecording,
541
+ reset: resetRecording
542
+ } = useRecording(stream, { duration });
543
+ const [status, setStatus] = (0, import_react4.useState)("idle");
544
+ const [error, setError] = (0, import_react4.useState)(null);
545
+ (0, import_react4.useEffect)(() => {
546
+ onStatusChange?.(status);
547
+ }, [status, onStatusChange]);
548
+ (0, import_react4.useEffect)(() => {
549
+ if (cameraError) {
550
+ setError(cameraError);
551
+ setStatus("error");
552
+ onError?.(new Error(cameraError));
553
+ }
554
+ }, [cameraError, onError]);
555
+ (0, import_react4.useEffect)(() => {
556
+ if (isFaceDetectionLoading) {
557
+ setStatus("loading");
558
+ } else if (isCameraActive && !isRecording) {
559
+ setStatus("ready");
560
+ }
561
+ }, [isFaceDetectionLoading, isCameraActive, isRecording]);
562
+ (0, import_react4.useEffect)(() => {
563
+ if (isRecording) {
564
+ setStatus("recording");
565
+ }
566
+ }, [isRecording]);
567
+ (0, import_react4.useEffect)(() => {
568
+ if (recordedBlob && !isRecording) {
569
+ analyzeVideo(recordedBlob);
570
+ }
571
+ }, [recordedBlob, isRecording]);
572
+ const analyzeVideo = (0, import_react4.useCallback)(async (videoBlob) => {
573
+ if (!clientRef.current) {
574
+ setError("Client not initialized");
575
+ setStatus("error");
576
+ return;
577
+ }
578
+ setStatus("uploading");
579
+ setError(null);
580
+ try {
581
+ setStatus("analyzing");
582
+ const result = await clientRef.current.analyze({
583
+ video: videoBlob,
584
+ age: userData?.age,
585
+ sex: userData?.sex,
586
+ weight: userData?.weight,
587
+ height: userData?.height
588
+ });
589
+ setStatus("complete");
590
+ onResult(result);
591
+ } catch (err) {
592
+ setError(err.message);
593
+ setStatus("error");
594
+ onError?.(err);
595
+ }
596
+ }, [userData, onResult, onError]);
597
+ const handleStartScan = (0, import_react4.useCallback)(() => {
598
+ setError(null);
599
+ startRecording();
600
+ }, [startRecording]);
601
+ const handleReset = (0, import_react4.useCallback)(() => {
602
+ resetRecording();
603
+ setError(null);
604
+ setStatus(isCameraActive ? "ready" : "idle");
605
+ }, [resetRecording, isCameraActive]);
606
+ const handleEnableCamera = (0, import_react4.useCallback)(async () => {
607
+ setError(null);
608
+ setStatus("loading");
609
+ await startCamera();
610
+ }, [startCamera]);
611
+ (0, import_react4.useEffect)(() => {
612
+ return () => {
613
+ stopCamera();
614
+ };
615
+ }, [stopCamera]);
616
+ const themeClass = `pulse-scanner--${theme}`;
617
+ const statusClass = `pulse-scanner--${status}`;
618
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `pulse-scanner ${themeClass} ${statusClass} ${className}`, children: [
619
+ showPreview && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pulse-scanner__preview", children: [
620
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
621
+ "video",
622
+ {
623
+ ref: videoRef,
624
+ autoPlay: true,
625
+ playsInline: true,
626
+ muted: true,
627
+ className: "pulse-scanner__video"
628
+ }
629
+ ),
630
+ showMesh && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
631
+ "canvas",
632
+ {
633
+ ref: meshCanvasRef,
634
+ className: "pulse-scanner__mesh-canvas"
635
+ }
636
+ ),
637
+ showOverlay && isCameraActive && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pulse-scanner__overlay", children: [
638
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `pulse-scanner__face-guide ${faceDetected ? "pulse-scanner__face-guide--detected" : ""}` }),
639
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `pulse-scanner__quality ${isQualityGood ? "pulse-scanner__quality--good" : ""}`, children: isRecording ? "Hold still..." : qualityMessage })
640
+ ] }),
641
+ showCountdown && isRecording && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pulse-scanner__countdown", children: [
642
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pulse-scanner__countdown-ring", children: [
643
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 100 100", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
644
+ "circle",
645
+ {
646
+ cx: "50",
647
+ cy: "50",
648
+ r: "45",
649
+ fill: "none",
650
+ stroke: "currentColor",
651
+ strokeWidth: "4",
652
+ strokeDasharray: `${timeLeft / duration * 283} 283`,
653
+ transform: "rotate(-90 50 50)"
654
+ }
655
+ ) }),
656
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pulse-scanner__countdown-number", children: timeLeft })
657
+ ] }),
658
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pulse-scanner__countdown-label", children: "seconds" })
659
+ ] }),
660
+ (status === "uploading" || status === "analyzing") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pulse-scanner__processing", children: [
661
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pulse-scanner__spinner" }),
662
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pulse-scanner__processing-text", children: status === "uploading" ? "Uploading..." : "Analyzing vitals..." })
663
+ ] })
664
+ ] }),
665
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pulse-scanner__controls", children: !isCameraActive ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
666
+ "button",
667
+ {
668
+ onClick: handleEnableCamera,
669
+ disabled: status === "loading",
670
+ className: "pulse-scanner__btn pulse-scanner__btn--primary",
671
+ children: status === "loading" ? "Loading..." : "Enable Camera"
672
+ }
673
+ ) : status === "complete" || status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
674
+ "button",
675
+ {
676
+ onClick: handleReset,
677
+ className: "pulse-scanner__btn pulse-scanner__btn--secondary",
678
+ children: "Scan Again"
679
+ }
680
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
681
+ "button",
682
+ {
683
+ onClick: handleStartScan,
684
+ disabled: isRecording || status === "uploading" || status === "analyzing",
685
+ className: "pulse-scanner__btn pulse-scanner__btn--primary",
686
+ children: isRecording ? `Recording ${timeLeft}s` : status === "uploading" || status === "analyzing" ? "Processing..." : `Start ${duration}s Scan`
687
+ }
688
+ ) }),
689
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pulse-scanner__error", children: [
690
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pulse-scanner__error-icon", children: "!" }),
691
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pulse-scanner__error-text", children: error })
692
+ ] })
693
+ ] });
694
+ };
695
+
696
+ // src/components/VitalsCard.tsx
697
+ var import_jsx_runtime2 = require("react/jsx-runtime");
698
+ function getHeartRateStatus(hr) {
699
+ if (hr < 55) return { label: "Low", color: "blue" };
700
+ if (hr > 100) return { label: "Elevated", color: "orange" };
701
+ return { label: "Normal", color: "green" };
702
+ }
703
+ function getSpo2Status(spo2) {
704
+ if (spo2 < 95) return { label: "Low", color: "orange" };
705
+ return { label: "Optimal", color: "green" };
706
+ }
707
+ function getStressStatus(stress) {
708
+ if (stress > 70) return { label: "High", color: "red" };
709
+ if (stress < 40) return { label: "Relaxed", color: "green" };
710
+ return { label: "Moderate", color: "blue" };
711
+ }
712
+ function getHrvStatus(hrv) {
713
+ if (hrv < 30) return { label: "Low", color: "orange" };
714
+ if (hrv > 60) return { label: "Excellent", color: "green" };
715
+ return { label: "Balanced", color: "blue" };
716
+ }
717
+ var VitalsCard = ({
718
+ heartRate,
719
+ spo2,
720
+ stressIndex,
721
+ hrvSdnn,
722
+ hrvRmssd,
723
+ respiratoryRate,
724
+ bloodPressure,
725
+ recoveryScore,
726
+ loading = false,
727
+ error = null,
728
+ compact = false,
729
+ showAll = false,
730
+ className = "",
731
+ theme = "light"
732
+ }) => {
733
+ if (loading) {
734
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `vitals-card vitals-card--loading vitals-card--${theme} ${className}`, children: [
735
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "vitals-card__spinner" }),
736
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vitals-card__loading-text", children: "Analyzing vitals..." })
737
+ ] });
738
+ }
739
+ if (error) {
740
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `vitals-card vitals-card--error vitals-card--${theme} ${className}`, children: [
741
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vitals-card__error-icon", children: "!" }),
742
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vitals-card__error-text", children: error })
743
+ ] });
744
+ }
745
+ const hrStatus = heartRate !== void 0 ? getHeartRateStatus(heartRate) : null;
746
+ const spo2Status = spo2 !== void 0 ? getSpo2Status(spo2) : null;
747
+ const stressStatus = stressIndex !== void 0 ? getStressStatus(stressIndex) : null;
748
+ const hrvStatus = hrvSdnn !== void 0 ? getHrvStatus(hrvSdnn) : null;
749
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `vitals-card vitals-card--${theme} ${compact ? "vitals-card--compact" : ""} ${className}`, children: [
750
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vitals-card__grid", children: [
751
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vitals-card__item", children: [
752
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vitals-card__label", children: "Heart Rate" }),
753
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "vitals-card__value", children: [
754
+ heartRate !== void 0 ? heartRate : "--",
755
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("small", { className: "vitals-card__unit", children: "BPM" })
756
+ ] }),
757
+ hrStatus && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: `vitals-card__status vitals-card__status--${hrStatus.color}`, children: hrStatus.label })
758
+ ] }),
759
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vitals-card__item", children: [
760
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vitals-card__label", children: "SpO2" }),
761
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "vitals-card__value", children: [
762
+ spo2 !== void 0 ? spo2 : "--",
763
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("small", { className: "vitals-card__unit", children: "%" })
764
+ ] }),
765
+ spo2Status && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: `vitals-card__status vitals-card__status--${spo2Status.color}`, children: spo2Status.label })
766
+ ] }),
767
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vitals-card__item", children: [
768
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vitals-card__label", children: "Stress" }),
769
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "vitals-card__value", children: [
770
+ stressIndex !== void 0 ? stressIndex : "--",
771
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("small", { className: "vitals-card__unit", children: "/100" })
772
+ ] }),
773
+ stressStatus && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: `vitals-card__status vitals-card__status--${stressStatus.color}`, children: stressStatus.label })
774
+ ] }),
775
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vitals-card__item", children: [
776
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vitals-card__label", children: "HRV" }),
777
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "vitals-card__value", children: [
778
+ hrvSdnn !== void 0 ? hrvSdnn : "--",
779
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("small", { className: "vitals-card__unit", children: "ms" })
780
+ ] }),
781
+ hrvStatus && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: `vitals-card__status vitals-card__status--${hrvStatus.color}`, children: hrvStatus.label })
782
+ ] })
783
+ ] }),
784
+ showAll && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vitals-card__extended", children: [
785
+ respiratoryRate !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vitals-card__item vitals-card__item--small", children: [
786
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vitals-card__label", children: "Respiration" }),
787
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "vitals-card__value", children: [
788
+ respiratoryRate,
789
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("small", { className: "vitals-card__unit", children: "/min" })
790
+ ] })
791
+ ] }),
792
+ bloodPressure && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vitals-card__item vitals-card__item--small", children: [
793
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vitals-card__label", children: "Blood Pressure" }),
794
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "vitals-card__value", children: [
795
+ bloodPressure.systolic,
796
+ "/",
797
+ bloodPressure.diastolic,
798
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("small", { className: "vitals-card__unit", children: "mmHg" })
799
+ ] })
800
+ ] }),
801
+ recoveryScore !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vitals-card__item vitals-card__item--small", children: [
802
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vitals-card__label", children: "Recovery" }),
803
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "vitals-card__value", children: [
804
+ recoveryScore,
805
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("small", { className: "vitals-card__unit", children: "%" })
806
+ ] })
807
+ ] }),
808
+ hrvRmssd !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vitals-card__item vitals-card__item--small", children: [
809
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vitals-card__label", children: "RMSSD" }),
810
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "vitals-card__value", children: [
811
+ hrvRmssd,
812
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("small", { className: "vitals-card__unit", children: "ms" })
813
+ ] })
814
+ ] })
815
+ ] })
816
+ ] });
817
+ };
818
+ var VitalsCardFromResult = ({ result, ...props }) => {
819
+ if (!result) {
820
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(VitalsCard, { ...props });
821
+ }
822
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
823
+ VitalsCard,
824
+ {
825
+ heartRate: result.heart_rate,
826
+ spo2: result.spo2,
827
+ stressIndex: result.stress_index,
828
+ hrvSdnn: result.hrv_sdnn,
829
+ hrvRmssd: result.hrv_rmssd,
830
+ respiratoryRate: result.respiratory_rate,
831
+ bloodPressure: result.blood_pressure,
832
+ recoveryScore: result.recovery_score,
833
+ ...props
834
+ }
835
+ );
836
+ };
837
+
838
+ // src/index.ts
839
+ var VERSION = "1.0.0";
840
+ // Annotate the CommonJS export names for ESM import in node:
841
+ 0 && (module.exports = {
842
+ PulseClient,
843
+ PulseScanner,
844
+ VERSION,
845
+ VitalsCard,
846
+ VitalsCardFromResult,
847
+ useCamera,
848
+ useFaceDetection,
849
+ useRecording
850
+ });