scribe-widget 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.
@@ -0,0 +1,27 @@
1
+ import { GetSessionStatusResponse } from 'med-scribe-alliance-ts-sdk';
2
+
3
+ interface ResultsStateProps {
4
+ result: GetSessionStatusResponse;
5
+ onNewRecording: () => void;
6
+ }
7
+
8
+ export function ResultsState({ result, onNewRecording }: ResultsStateProps) {
9
+ return (
10
+ <div className="results-state">
11
+ <div className="results-header">
12
+ <span className="results-title">Recording Complete</span>
13
+ <button className="new-recording-btn" onClick={onNewRecording}>
14
+ New Recording
15
+ </button>
16
+ </div>
17
+ <div className="results-content">
18
+ <div className="transcript-section">
19
+ <div className="section-title">Transcript</div>
20
+ <div className="transcript-text">
21
+ {result.transcript || 'No transcript available.'}
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,227 @@
1
+ import { useState, useRef, useCallback, useEffect } from 'react';
2
+ import { ScribeClient, GetSessionStatusResponse } from 'med-scribe-alliance-ts-sdk';
3
+ import { WidgetState, ScribeWidgetConfig } from '../types';
4
+
5
+ interface UseScribeSessionReturn {
6
+ state: WidgetState;
7
+ elapsedTime: number;
8
+ result: GetSessionStatusResponse | null;
9
+ errorMessage: string;
10
+ startRecording: () => Promise<void>;
11
+ pauseRecording: () => void;
12
+ resumeRecording: () => void;
13
+ stopRecording: () => Promise<void>;
14
+ reset: () => void;
15
+ }
16
+
17
+ export function useScribeSession(config: ScribeWidgetConfig): UseScribeSessionReturn {
18
+ const [state, setState] = useState<WidgetState>('idle');
19
+ const [elapsedTime, setElapsedTime] = useState(0);
20
+ const [result, setResult] = useState<GetSessionStatusResponse | null>(null);
21
+ const [errorMessage, setErrorMessage] = useState('');
22
+
23
+ const clientRef = useRef<ScribeClient | null>(null);
24
+ const timerRef = useRef<number | null>(null);
25
+ const startTimeRef = useRef<number>(0);
26
+ const pausedTimeRef = useRef<number>(0);
27
+
28
+ const log = useCallback((...args: unknown[]) => {
29
+ if (config.debug) {
30
+ console.log('[EkaScribe]', ...args);
31
+ }
32
+ }, [config.debug]);
33
+
34
+ // Initialize SDK client only when baseUrl is provided
35
+ useEffect(() => {
36
+ if (!config.baseUrl) {
37
+ log('Skipping SDK init - no baseUrl provided');
38
+ return;
39
+ }
40
+
41
+ const initClient = async () => {
42
+ try {
43
+ clientRef.current = new ScribeClient({
44
+ apiKey: config.apiKey,
45
+ baseUrl: config.baseUrl,
46
+ debug: config.debug,
47
+ });
48
+ await clientRef.current.init();
49
+ log('SDK initialized');
50
+ } catch (error) {
51
+ log('Failed to initialize SDK', error);
52
+ }
53
+ };
54
+
55
+ initClient();
56
+
57
+ return () => {
58
+ if (timerRef.current) {
59
+ clearInterval(timerRef.current);
60
+ }
61
+ };
62
+ }, [config.apiKey, config.baseUrl, config.debug, log]);
63
+
64
+ const startTimer = useCallback(() => {
65
+ timerRef.current = window.setInterval(() => {
66
+ const elapsed = Math.floor((Date.now() - startTimeRef.current) / 1000);
67
+ setElapsedTime(elapsed);
68
+ }, 1000);
69
+ }, []);
70
+
71
+ const stopTimer = useCallback(() => {
72
+ if (timerRef.current) {
73
+ clearInterval(timerRef.current);
74
+ timerRef.current = null;
75
+ }
76
+ }, []);
77
+
78
+ const checkMicrophonePermission = async (): Promise<PermissionState> => {
79
+ try {
80
+ const result = await navigator.permissions.query({ name: 'microphone' as PermissionName });
81
+ return result.state;
82
+ } catch {
83
+ return 'prompt';
84
+ }
85
+ };
86
+
87
+ const requestMicrophonePermission = async (): Promise<boolean> => {
88
+ try {
89
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
90
+ stream.getTracks().forEach(track => track.stop());
91
+ return true;
92
+ } catch (error) {
93
+ log('Microphone permission denied', error);
94
+ return false;
95
+ }
96
+ };
97
+
98
+ const showError = useCallback((message: string) => {
99
+ setErrorMessage(message);
100
+ setState('error');
101
+ }, []);
102
+
103
+ const startRecording = useCallback(async () => {
104
+ const permission = await checkMicrophonePermission();
105
+
106
+ if (permission === 'denied') {
107
+ showError('Microphone access is blocked. Please enable it in your browser settings.');
108
+ return;
109
+ }
110
+
111
+ if (permission === 'prompt') {
112
+ setState('permission');
113
+ const granted = await requestMicrophonePermission();
114
+ if (!granted) {
115
+ showError('Microphone access is required to record.');
116
+ return;
117
+ }
118
+ }
119
+
120
+ if (!clientRef.current) {
121
+ showError('SDK not initialized. Please try again.');
122
+ return;
123
+ }
124
+
125
+ try {
126
+ setState('recording');
127
+
128
+ await clientRef.current.startRecording({
129
+ templates: config.templates || ['soap'],
130
+ languageHint: config.languageHint,
131
+ });
132
+
133
+ startTimeRef.current = Date.now();
134
+ pausedTimeRef.current = 0;
135
+ setElapsedTime(0);
136
+ startTimer();
137
+
138
+ log('Recording started');
139
+ } catch (error) {
140
+ log('Failed to start recording', error);
141
+ showError('Failed to start recording. Please try again.');
142
+ }
143
+ }, [config.templates, config.languageHint, log, showError, startTimer]);
144
+
145
+ const pauseRecording = useCallback(() => {
146
+ if (!clientRef.current) return;
147
+
148
+ clientRef.current.pauseRecording();
149
+ setState('paused');
150
+ pausedTimeRef.current = Date.now();
151
+ stopTimer();
152
+
153
+ log('Recording paused');
154
+ }, [log, stopTimer]);
155
+
156
+ const resumeRecording = useCallback(() => {
157
+ if (!clientRef.current) return;
158
+
159
+ clientRef.current.resumeRecording();
160
+ setState('recording');
161
+
162
+ if (pausedTimeRef.current) {
163
+ startTimeRef.current += Date.now() - pausedTimeRef.current;
164
+ pausedTimeRef.current = 0;
165
+ }
166
+
167
+ startTimer();
168
+
169
+ log('Recording resumed');
170
+ }, [log, startTimer]);
171
+
172
+ const stopRecording = useCallback(async () => {
173
+ if (!clientRef.current) return;
174
+
175
+ try {
176
+ stopTimer();
177
+ setState('processing');
178
+
179
+ log('Stopping recording...');
180
+ const endResponse = await clientRef.current.endRecording();
181
+ log('Recording ended', endResponse);
182
+
183
+ log('Polling for completion...');
184
+ const finalResult = await clientRef.current.pollForCompletion(undefined, {
185
+ maxAttempts: 60,
186
+ intervalMs: 2000,
187
+ onProgress: (status) => {
188
+ log('Status update:', status.status);
189
+ },
190
+ });
191
+
192
+ setResult(finalResult);
193
+ log('Final result:', finalResult);
194
+
195
+ setState('results');
196
+
197
+ if (config.onResult) {
198
+ config.onResult(finalResult);
199
+ }
200
+ } catch (error) {
201
+ log('Failed to stop recording', error);
202
+ showError('Failed to process recording. Please try again.');
203
+ if (config.onError && error instanceof Error) {
204
+ config.onError(error);
205
+ }
206
+ }
207
+ }, [config, log, showError, stopTimer]);
208
+
209
+ const reset = useCallback(() => {
210
+ setResult(null);
211
+ setErrorMessage('');
212
+ setElapsedTime(0);
213
+ setState('idle');
214
+ }, []);
215
+
216
+ return {
217
+ state,
218
+ elapsedTime,
219
+ result,
220
+ errorMessage,
221
+ startRecording,
222
+ pauseRecording,
223
+ resumeRecording,
224
+ stopRecording,
225
+ reset,
226
+ };
227
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,123 @@
1
+ import { createRoot, Root } from 'react-dom/client';
2
+ import { App } from './App';
3
+ import { ScribeWidgetConfig } from './types';
4
+ import widgetStyles from './styles/widget.css?inline';
5
+
6
+ export type { ScribeWidgetConfig, WidgetState } from './types';
7
+
8
+ class ScribeWidget {
9
+ private container: HTMLDivElement;
10
+ private shadowRoot: ShadowRoot;
11
+ private root: Root | null = null;
12
+ private config: ScribeWidgetConfig;
13
+ private visible: boolean = true;
14
+
15
+ constructor(config: ScribeWidgetConfig) {
16
+ this.config = {
17
+ templates: ['soap'],
18
+ languageHint: ['en'],
19
+ position: { bottom: 20, right: 20 },
20
+ ...config,
21
+ };
22
+
23
+ // Create container element
24
+ this.container = document.createElement('div');
25
+ this.container.id = 'eka-scribe-widget';
26
+
27
+ // Attach shadow DOM
28
+ this.shadowRoot = this.container.attachShadow({ mode: 'closed' });
29
+
30
+ // Inject styles
31
+ const styleEl = document.createElement('style');
32
+ styleEl.textContent = widgetStyles;
33
+ this.shadowRoot.appendChild(styleEl);
34
+
35
+ // Create React mount point
36
+ const mountPoint = document.createElement('div');
37
+ mountPoint.id = 'eka-scribe-root';
38
+ this.shadowRoot.appendChild(mountPoint);
39
+
40
+ // Create React root
41
+ this.root = createRoot(mountPoint);
42
+ }
43
+
44
+ private render(): void {
45
+ if (!this.root || !this.visible) return;
46
+
47
+ this.root.render(
48
+ <App
49
+ config={this.config}
50
+ onClose={() => this.hide()}
51
+ />
52
+ );
53
+ }
54
+
55
+ public mount(target?: HTMLElement | string): void {
56
+ const targetEl = typeof target === 'string'
57
+ ? document.querySelector(target)
58
+ : target || document.body;
59
+
60
+ if (targetEl) {
61
+ targetEl.appendChild(this.container);
62
+ this.render();
63
+ }
64
+ }
65
+
66
+ public unmount(): void {
67
+ if (this.root) {
68
+ this.root.unmount();
69
+ this.root = null;
70
+ }
71
+ this.container.remove();
72
+ }
73
+
74
+ public show(): void {
75
+ this.visible = true;
76
+ this.container.style.display = 'block';
77
+ this.render();
78
+ }
79
+
80
+ public hide(): void {
81
+ this.visible = false;
82
+ this.container.style.display = 'none';
83
+ }
84
+
85
+ public isVisible(): boolean {
86
+ return this.visible;
87
+ }
88
+ }
89
+
90
+ // Global initialization function for script tag usage
91
+ let widgetInstance: ScribeWidget | null = null;
92
+
93
+ function initEkaScribe(config: ScribeWidgetConfig): ScribeWidget {
94
+ if (widgetInstance) {
95
+ widgetInstance.unmount();
96
+ }
97
+ widgetInstance = new ScribeWidget(config);
98
+ widgetInstance.mount();
99
+ return widgetInstance;
100
+ }
101
+
102
+ function getEkaScribe(): ScribeWidget | null {
103
+ return widgetInstance;
104
+ }
105
+
106
+ // Expose to window for script tag usage
107
+ if (typeof window !== 'undefined') {
108
+ (window as Window & { EkaScribe?: unknown }).EkaScribe = {
109
+ init: initEkaScribe,
110
+ getInstance: getEkaScribe,
111
+ Widget: ScribeWidget,
112
+ };
113
+ }
114
+
115
+ // Named exports for ES module usage
116
+ export { ScribeWidget, initEkaScribe as init, getEkaScribe as getInstance };
117
+
118
+ // Default export for module usage
119
+ export default {
120
+ init: initEkaScribe,
121
+ getInstance: getEkaScribe,
122
+ Widget: ScribeWidget,
123
+ };