@nora-technology/recorder v1.0.31
NoraRecorder Integration Guide for EHR Platforms
IMPORTANT UPDATE: The package has been renamed from
@nora/recorderto@nora-technology/recorder. This documentation has been updated to reflect this change. If you were previously using@nora/recorder, please update your dependencies to use the new package name.
Table of Contents
- Overview
- Integration Requirements
- Installation Methods
- Basic Integration
- Display Modes
- Task Types
- Context Recommendations System
- Customization Options
- Multi-Tab Environment Handling
- Lifecycle Management
- Event Handling
- Framework-Specific Integration
- API Reference
- Recording Workflow
- Security Considerations
- Troubleshooting
- Advanced Usage
- Best Practices
- Example Implementations
Overview
NoraRecorder is an NPM package (@nora-technology/recorder) that provides a voice recording component for medical applications, specifically designed for integration with Electronic Health Record (EHR) platforms. The component allows doctors to record consultations, which are then processed to generate structured clinical notes that can be inserted back into the EHR.
Key capabilities:
- Audio recording with pause, resume, and stop functionality
- Support for multiple microphones
- Integration with note generation services
- Support for different languages and templates
- Built-in processing status tracking
- Compatible with multi-tab EHR environments
Integration Requirements
Before integrating NoraRecorder, ensure you have the following:
- API Key: Provided by Nora for your specific EHR platform
- API Base URL: The endpoint for Nora's backend services
- Doctor ID: A unique identifier for each doctor using the system
- Consultation ID: A unique identifier for each consultation/patient encounter
These parameters are required for proper operation and to ensure notes are correctly associated with doctors and consultations.
Installation Methods
NPM Installation
For projects using a modern JavaScript build system:
npm install @nora-technology/recorder
# or
yarn add @nora-technology/recorderCDN Integration
For direct integration into HTML:
<!-- Latest version (not recommended for production) -->
<script src="https://unpkg.com/@nora-technology/recorder@latest/dist/nora-recorder-easy.js"></script>
<!-- Specific version (recommended for production) -->
<script src="https://unpkg.com/@nora-technology/recorder@1.0.29/dist/nora-recorder-easy.js"></script>Basic Integration
Required Parameters
Every NoraRecorder initialization requires these parameters:
| Parameter | Description | Type | Example |
|---|---|---|---|
apiKey | Authentication key provided by Nora | string | "your-api-key-123" |
doctorID | Unique identifier for the doctor | string | "doctor-uuid-456" |
consultationID | Unique identifier for the consultation | string | "consult-uuid-789" |
apiBaseUrl | Base URL for Nora's API services | string | "https://api.nora.ai/v1" |
taskType | Type of task being recorded (optional) | string | "clinical_notes" (default), "referral_letter", "sick_note", "patient_notes", "vitals" |
Integration Pattern
NoraRecorder uses a promise-based factory function pattern for reliable initialization and error handling:
// Wait for NoraRecorder to be ready
NoraRecorder.ready(function() {
// Initialize with factory function (NOT constructor)
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
taskType: 'clinical_notes' // Optional: defaults to 'clinical_notes'
}).then(recorder => {
console.log('Recorder initialized successfully');
// Store reference if needed for later interaction
window.currentRecorder = recorder;
}).catch(error => {
console.error('Failed to initialize recorder:', error);
});
});
// Handle loading errors
NoraRecorder.onError(function(error) {
console.error('Failed to load NoraRecorder:', error);
});
// Clean up when done
// NoraRecorder.cleanup();Important: NoraRecorder uses a factory function pattern
NoraRecorder(options)that returns a Promise. This provides proper script loading, initialization, and error handling.
This pattern handles script loading, initialization, and readiness detection automatically. It uses a promise-based API for better error handling and asynchronous flow.
How the Loader Works
The NoraRecorder integration pattern consists of several key components working together:
1. Bundled Flag Detection
At the very beginning of execution, the loader sets a global flag:
window.__NORA_RECORDER_BUNDLED = true;This flag indicates that the bundled version is being used, preventing attempts to load external scripts that would conflict.
2. Module Initialization Order
The loader follows this sequence: 1. Sets the bundled flag immediately (outside any function scope) 2. Initializes the loader module with state management 3. Initializes the main NoraRecorder module 4. Creates a factory function wrapper around the original constructor 5. Sets up global objects and ready state
3. State Management
The loader manages multiple states:
LOADING: Initial state while script is loadingREADY: NoraRecorder is ready to useERROR: An error occurred during loading
These states determine how callbacks are handled and when initialization is possible.
4. Ready and Error Callbacks
The loader maintains queues of callbacks for both ready and error states:
NoraRecorder.ready(callback); // Called when ready
NoraRecorder.onError(callback); // Called if loading failsWhen the state changes, all queued callbacks are executed once.
5. Promise-based Initialization
The factory function returns a promise that resolves with the recorder instance:
NoraRecorder(options).then(recorder => {
// Use recorder
}).catch(error => {
// Handle error
});6. Automatic Script Loading (CDN Use Case)
When the loader is used in standalone mode (not bundled), it: 1. Checks if NoraRecorder is already available 2. If not, loads the script from a CDN 3. Monitors load status and sets ready state when available
Display Modes
NoraRecorder supports two display modes that determine how the component appears in your EHR interface.
Floating Mode
In floating mode, the recorder appears as a draggable button that floats above the content:
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
// Optional position configuration
position: { x: 20, y: 20 }
}).then(recorder => {
console.log('Floating recorder initialized');
});Characteristics:
- Positioned absolutely in the viewport
- Draggable by users to any position
- Always visible (high z-index)
- Can be initially positioned with the
positionparameter
Embedded Mode
In embedded mode, the recorder is embedded within a container in your EHR interface:
// Create a container for the recorder
const container = document.getElementById('recorder-container');
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
// Container for embedding
container: container,
// Must set position to null for embedded mode
position: null,
// Recommended for embedded mode
snackbarPosition: "below-component"
}).then(recorder => {
console.log('Embedded recorder initialized');
});HTML Setup for Embedded Mode:
<!-- Container for embedded recorder - minimum dimensions are important -->
<div id="recorder-container" style="min-height: 48px; min-width: 290px; display: inline-block;"></div>Characteristics:
- Positioned within the specified container
- Not draggable
- Visibility depends on container visibility
- Follows container scroll position
- Requires minimum dimensions (290px width Ć 48px height)
Task Types
NoraRecorder supports different task types that determine which endpoint is used for processing recordings and how the user interface behaves. Each task type corresponds to a specific type of medical documentation and provides a tailored experience for different clinical workflows.
Available Task Types
| Task Type | Internal Name | Description | Endpoint | Terminal Status |
|---|---|---|---|---|
| Clinical Notes | clinical_notes | Standard consultation notes (default) | /nora/clinical_notes | sent |
| Referral Letter | referral_letter | Letters referring patients to specialists | /nora/referral_letter | generated |
| Sick Note | sick_note | Medical certificates for sick leave | /nora/sick_note | generated |
| Patient Notes | patient_notes | General patient documentation | /nora/patient_notes | generated |
| Vitals | vitals | Recording of vital signs and measurements | /nora/vitals | generated |
Task Type Behavior Differences
The task type system provides different user experiences based on the type of documentation being created:
Clinical Notes (clinical_notes)
- UI Elements: Shows template selection, language selection, microphone selection, and mode toggle (consultation/dictation)
- Headers Sent: Includes
template_usedandCon-Modeheaders in API requests - Terminal Status: Reaches completion when status is
sent - Use Case: Standard consultation documentation with full template and mode options
Non-Clinical Notes Task Types
All other task types (referral_letter, sick_note, patient_notes, vitals) share the same simplified interface:
- UI Elements: Shows only language selection and microphone selection
- Task Type Display: Shows "Recording for Task Type" text instead of template/mode controls
- Headers Sent: Does NOT send
template_usedorCon-Modeheaders (to avoid API rejection) - Terminal Status: Reaches completion when status is
generated - Use Case: Specialized documentation types that don't require template or mode selection
- Generate Button: For non-clinical task types, if clinical notes already exist for the consultation, a Generate button appears in the recorder interface. This allows generating the specific task type content without recording, using existing clinical notes as context.
Generate Button Functionality
For non-clinical task types (referral_letter, sick_note, patient_notes, vitals), when clinical notes already exist for the consultation, a Generate button appears in the recorder interface. This button allows you to generate content for the specific task type using the existing clinical notes as context, without needing to record new audio.
Generate Button Features:
- Context-Aware: Uses existing clinical notes from the same consultation as context
- No Recording Required: Generates content directly without audio recording
- Same Event System: Emits the same status events as recording (
generation-started,generation-stopped,generation-failed) - Task-Type Specific: Generates content appropriate for the selected task type
- Pre-Prompt Integration: Optionally uses EHR content via
prePromptCallbackfor enhanced context
Status Events Emitted:
// When Generate button is clicked
document.addEventListener('nora-recorder-status', (event) => {
const { type, consultationID, taskType } = event.detail;
switch (type) {
case 'generation-started':
console.log(`Generate started for ${taskType}`);
break;
case 'generation-stopped':
console.log(`Generate processing for ${taskType}`);
break;
case 'generation-failed':
console.error(`Generate failed for ${taskType}`);
break;
}
});Content Events:
The Generate button produces the same content-ready event as recording when processing completes:
document.addEventListener('nora-recorder-content', (event) => {
const { type, content, taskType } = event.detail;
if (type === 'content-ready') {
// Generated content is ready, route to appropriate field
console.log(`Generated ${taskType} content: ${content.length} characters`);
}
});Task Type Configuration
// Clinical Notes (default) - Full interface with template and mode selection
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT'
// taskType defaults to 'clinical_notes'
}).then(recorder => {
console.log('Recording for Clinical Notes');
// UI will show: template selection, mode toggle, language, microphone
});
// Referral Letter - Simplified interface
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
taskType: 'referral_letter'
}).then(recorder => {
console.log('Recording for Referral Letter');
// UI will show: "Recording for Referral Letter" text, language, microphone
});
// Sick Note - Simplified interface
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
taskType: 'sick_note'
}).then(recorder => {
console.log('Recording for Sick Note');
// UI will show: "Recording for Sick Note" text, language, microphone
});
// Patient Notes - Simplified interface
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
taskType: 'patient_notes'
}).then(recorder => {
console.log('Recording for Patient Notes');
// UI will show: "Recording for Patient Notes" text, language, microphone
});
// Vitals - Simplified interface
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
taskType: 'vitals'
}).then(recorder => {
console.log('Recording for Vitals');
// UI will show: "Recording for Vitals" text, language, microphone
});Task Type Notifications
When recording starts, the user will see a notification indicating the task type:
- "Recording for Clinical Notes"
- "Recording for Referral Letter"
- "Recording for Sick Note"
- "Recording for Patient Notes"
- "Recording for Vitals"
This helps users understand what type of documentation they are creating.
EHR Integration Patterns
Single Consultation, Multiple Task Types
In real EHR systems, you typically want to use the same consultation ID for all task types within a single patient visit, but create separate recorders for each type of documentation:
// Example: Patient consultation with multiple documentation types
const CONSULTATION_ID = "PATIENT-VISIT-12345"; // Same for all task types
const DOCTOR_ID = "DR-SMITH-001";
// Clinical notes recorder
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: DOCTOR_ID,
consultationID: CONSULTATION_ID, // Same consultation ID
apiBaseUrl: 'YOUR_API_ENDPOINT',
taskType: 'clinical_notes',
container: document.getElementById('clinical-notes-recorder')
}).then(recorder => {
// Store reference for clinical notes
window.clinicalNotesRecorder = recorder;
});
// Referral letter recorder (separate instance, same consultation)
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: DOCTOR_ID,
consultationID: CONSULTATION_ID, // Same consultation ID
apiBaseUrl: 'YOUR_API_ENDPOINT',
taskType: 'referral_letter',
container: document.getElementById('referral-letter-recorder')
}).then(recorder => {
// Store reference for referral letter
window.referralLetterRecorder = recorder;
});
// Sick note recorder (separate instance, same consultation)
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: DOCTOR_ID,
consultationID: CONSULTATION_ID, // Same consultation ID
apiBaseUrl: 'YOUR_API_ENDPOINT',
taskType: 'sick_note',
container: document.getElementById('sick-note-recorder')
}).then(recorder => {
// Store reference for sick note
window.sickNoteRecorder = recorder;
});Dynamic Task Type Switching
For applications that need to switch between task types dynamically, create a new recorder instance for each task type:
let currentRecorder = null;
const CONSULTATION_ID = "PATIENT-VISIT-12345";
function switchToTaskType(taskType) {
// Clean up existing recorder
if (currentRecorder) {
currentRecorder.cleanup();
currentRecorder = null;
}
// Clear container
const container = document.getElementById('recorder-container');
container.innerHTML = '';
// Initialize new recorder for the selected task type
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: CONSULTATION_ID, // Same consultation ID
apiBaseUrl: 'YOUR_API_ENDPOINT',
taskType: taskType,
container: container,
position: null,
snackbarPosition: "below-component"
}).then(recorder => {
currentRecorder = recorder;
console.log(`Switched to ${taskType} recorder`);
});
}
// Usage
switchToTaskType('clinical_notes'); // Shows full interface
switchToTaskType('referral_letter'); // Shows simplified interface
switchToTaskType('sick_note'); // Shows simplified interfaceContent Integration by Task Type
Each task type generates content that should be integrated into the appropriate section of your EHR:
// Listen for content events and route to appropriate EHR sections
document.addEventListener('nora-recorder-content', (event) => {
const { type, consultationID, content, taskType } = event.detail;
if (type === 'content-ready') {
// Route content to appropriate EHR field based on task type
switch (taskType) {
case 'clinical_notes':
// Insert into main clinical notes field
document.getElementById('clinical-notes-textarea').value = content;
break;
case 'referral_letter':
// Insert into referral letter field
document.getElementById('referral-letter-textarea').value = content;
break;
case 'sick_note':
// Insert into sick note field
document.getElementById('sick-note-textarea').value = content;
break;
case 'patient_notes':
// Insert into patient notes field
document.getElementById('patient-notes-textarea').value = content;
break;
case 'vitals':
// Insert into vitals field or parse structured vitals data
document.getElementById('vitals-textarea').value = content;
break;
}
// Show success notification
console.log(`${taskType} content generated and inserted`);
}
});Completion Interface
All task types show a completion interface (green checkmark + delete button) when they reach their terminal status:
- Clinical Notes: Shows completion when
processingStatus === 'sent' - Other Task Types: Show completion when
processingStatus === 'generated'
The delete button is task-type aware and will:
- Show appropriate tooltip (e.g., "Delete referral letter recording and restart")
- Route delete requests to the correct endpoint for the task type
- Reset the recorder to allow new recordings of the same task type
API Behavior by Task Type
Clinical Notes
// API request includes template and mode headers
Headers: {
'DoctorID': 'DOCTOR_ID',
'ConsultationID': 'CONSULTATION_ID',
'template_used': 'selected_template_id',
'Con-Mode': 'consultation', // or 'dictation'
'lang': 'en',
'Audio-Duration': '45',
'Microphone-Selected': 'Default Microphone'
}Non-Clinical Notes Task Types
// API request excludes template and mode headers
Headers: {
'DoctorID': 'DOCTOR_ID',
'ConsultationID': 'CONSULTATION_ID',
// NO template_used header
// NO Con-Mode header
'lang': 'en',
'Audio-Duration': '45',
'Microphone-Selected': 'Default Microphone'
}This ensures that non-clinical notes task types don't send headers that would cause API rejection.
Context Recommendations System
The Context Recommendations System is an intelligent feature that analyzes completed clinical notes and provides contextual recommendations for additional documentation that may be needed for the consultation. This system helps ensure comprehensive patient care by suggesting relevant follow-up actions.
Context Overview
The context system works by: 1. Analyzing completed clinical notes using AI to understand the consultation content 2. Identifying potential needs such as referrals, sick notes, blood tests, etc. 3. Displaying recommendations in a user-friendly dropdown component 4. Integrating seamlessly with the existing NoraRecorder workflow
Key Features:
- Automatic context analysis after clinical notes completion
- Visual recommendations dropdown with clear icons and labels
- Configurable positioning (below, right, or left of recorder)
- Optional system that can be completely disabled
- Non-intrusive design that doesn't interfere with normal workflow
How Context Checking Works
The context system operates in two main scenarios:
Scenario 1: Clinical Notes Task Type
When using the clinical_notes task type:
1. User records and completes clinical notes
2. System automatically calls the context API with the consultation details
3. Context recommendations appear below the recorder
4. User can expand/collapse the recommendations dropdown
Scenario 2: Non-Clinical Notes Task Types
When using other task types (referral_letter, sick_note, etc.):
1. System checks if clinical notes already exist for this consultation
2. If clinical notes exist, context recommendations are shown immediately
3. If no clinical notes exist, no context recommendations are displayed
Context Configuration Options
The context system can be configured during NoraRecorder initialization:
NoraRecorder({
// Required parameters
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
// Context system configuration
enableContextChecker: true, // Enable/disable context system (default: true)
contextPosition: "below" // Position: "below", "right", "left" (default: "below")
}).then(recorder => {
console.log('Recorder with context recommendations initialized');
});Configuration Parameters
| Parameter | Type | Description | Default | Options |
|---|---|---|---|---|
enableContextChecker | boolean | Enable/disable entire context system | true | true, false |
contextPosition | string | Position of context dropdown relative to recorder | "below" | "below", "right", "left" |
Position Options
Below (Default)
contextPosition: "below" // Appears directly below the recorderRight Side
contextPosition: "right" // Appears to the right of the recorderLeft Side
contextPosition: "left" // Appears to the left of the recorderContext API Integration
The context system integrates with your backend API to analyze clinical notes and provide recommendations.
API Endpoint
- URL:
{apiBaseUrl}/nora/context_checker - Method: GET
- Headers:
x-api-key: Your API keyDoctorID: Doctor identifierConsultationID: Consultation identifier
Expected Response Format
{
"context_checker_output": {
"referral_needed": true,
"sick_note_needed": false,
"patient_notes_needed": false,
"blood_test_needed": true,
"follow_up_consultation_needed": false
}
}Recommendation Types
| API Field | Display Label | Icon | Description |
|---|---|---|---|
referral_needed | Referral Letter | š | Patient may need referral to specialist |
sick_note_needed | Sick Note | š„ | Patient may need medical certificate |
patient_notes_needed | Patient Notes | š | Additional patient documentation needed |
blood_test_needed | Blood Test | 𩸠| Blood work may be required |
follow_up_consultation_needed | Follow-up Consultation | š | Follow-up appointment recommended |
Context UI Components
The context system provides a clean, collapsible dropdown interface:
Visual Design
- Width: 290px (matches recorder width)
- Header: "Nora recommendations" with expand/collapse icon
- Content: List of recommended actions with icons and status indicators
- Styling: Consistent with recorder design language
- Animation: Smooth expand/collapse transitions
User Interaction
- Click header to expand/collapse recommendations
- Hover effects on recommendation items
- Visual indicators show "Recommended" status for each item
- Empty state shows "No additional recommendations" when no actions needed
Example UI States
Collapsed State
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Nora recommendations ā¼ ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāExpanded State with Recommendations
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Nora recommendations ā² ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā š Referral Letter [Recommended] ā
ā š©ø Blood Test [Recommended] ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāExpanded State with No Recommendations
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Nora recommendations ā² ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
No additional recommendations ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāContext Event Handling
The context system doesn't emit separate events but integrates with the existing NoraRecorder event system. Context recommendations appear automatically based on the clinical workflow.
Automatic Triggers
Clinical Notes Completion
// Context check is triggered automatically when clinical notes reach terminal status
// No additional code needed - recommendations appear automaticallyNon-Clinical Notes Initialization
// Context check happens automatically during initialization if clinical notes exist
// No additional code needed - recommendations appear if applicableProgrammatic Access
You can check if context recommendations are visible:
// Check if context component is currently displayed
const contextVisible = document.querySelector('.context-checker-container') !== null;
if (contextVisible) {
console.log('Context recommendations are currently displayed');
}Disabling Context System
The context system can be completely disabled if not needed for your EHR integration:
Complete Disable
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
enableContextChecker: false // Completely disable context system
}).then(recorder => {
console.log('Recorder without context recommendations initialized');
});Benefits of Disabling
- Reduced API calls: No requests to context endpoint
- Simplified UI: No context dropdown component
- Lower bandwidth: Less network traffic
- Faster initialization: Fewer background processes
When to Disable
- EHR systems that don't need context recommendations
- Testing environments where context isn't relevant
- Minimal implementations focused only on recording
- Systems with their own recommendation engines
Troubleshooting Context Issues
Common Issues and Solutions
Context Recommendations Not Appearing
Check if context system is enabled
console.log('Context enabled:', recorder.enableContextChecker);Verify API endpoint configuration
- Ensure
/nora/context_checkerendpoint exists - Check API key has permissions for context endpoint
- Verify CORS settings allow context API calls
- Ensure
Check clinical notes status
- Context only appears after clinical notes completion
- For non-clinical task types, clinical notes must exist first
Context API Errors
- Check network tab for failed API calls to context endpoint
- Verify headers include
x-api-key,DoctorID,ConsultationID - Check API response format matches expected structure
Context Component Positioning Issues
- Ensure container has relative positioning (for embedded mode)
- Check for CSS conflicts that might affect positioning
- Verify minimum container dimensions (290px width minimum)
Debug Mode
Enable debug logging to troubleshoot context issues:
// Check console for context-related log messages
// Look for messages starting with "Context check" or "Context checker"Testing Context System
Use the provided test sites to verify context functionality:
single-consultation-demo.html- Context system enabledno-context.html- Context system disabled
Compare behavior between these sites to ensure context system is working correctly.
Customization Options
Visual Customization
NoraRecorder allows customization of its appearance:
NoraRecorder({
// Required parameters
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
// Visual customization
size: 70, // Button size in pixels
primaryColor: "#00796b", // Primary color
secondaryColor: "#e0f2f1" // Secondary color
}).then(recorder => {
console.log('Customized recorder initialized');
});Positioning
For floating mode, you can control the initial position:
NoraRecorder({
// Required parameters
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
// Positioning (in pixels from top-left)
position: { x: 50, y: 100 }
}).then(recorder => {
console.log('Positioned recorder initialized');
});Notifications
You can control how notifications appear:
NoraRecorder({
// Required parameters
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
// Notification positioning
snackbarPosition: "below-component" // or "bottom-center"
}).then(recorder => {
console.log('Recorder with custom notifications initialized');
});Pre-Prompt Integration (Optional)
The NoraRecorder supports an optional pre-prompt feature that allows EHRs to provide additional context from existing notes or patient data. This context is sent along with the audio recording to improve the AI's understanding and generate more accurate notes.
Key Features:
- Completely optional - existing integrations continue to work unchanged
- Dynamic content retrieval - gets fresh content at upload time
- Flexible callback system - EHRs can implement any logic to gather context
- Graceful error handling - continues without pre-prompt if callback fails
- Works with all task types
Basic Pre-Prompt Usage
NoraRecorder({
// Required parameters
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
// Optional: Provide current notes as context
prePromptCallback: function() {
// Return current content from EHR notes field
const notesTextarea = document.getElementById('clinical-notes-textarea');
return notesTextarea ? notesTextarea.value.trim() : '';
}
}).then(recorder => {
console.log('Recorder with pre-prompt capability initialized');
});Advanced Pre-Prompt Usage
The callback can return any relevant context from the EHR:
NoraRecorder({
// Required parameters
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
// Advanced pre-prompt callback
prePromptCallback: function() {
const sections = [];
// Patient demographics
const patientName = document.getElementById('patient-name')?.textContent;
if (patientName) sections.push(`Patient: ${patientName}`);
// Existing notes
const existingNotes = document.getElementById('notes-textarea')?.value;
if (existingNotes?.trim()) sections.push(`Existing Notes:\n${existingNotes}`);
// Vital signs
const vitals = document.getElementById('vitals-section')?.textContent;
if (vitals?.trim()) sections.push(`Vitals: ${vitals}`);
// Current medications
const medications = Array.from(document.querySelectorAll('.medication-item'))
.map(item => item.textContent.trim())
.filter(text => text);
if (medications.length) sections.push(`Current Medications:\n${medications.join('\n')}`);
// Chief complaint
const chiefComplaint = document.getElementById('chief-complaint')?.value;
if (chiefComplaint?.trim()) sections.push(`Chief Complaint: ${chiefComplaint}`);
return sections.join('\n\n');
}
}).then(recorder => {
console.log('Recorder with comprehensive pre-prompt initialized');
});Pre-Prompt Flow
- EHR initializes NoraRecorder with optional
prePromptCallback - User records audio normally
- User clicks stop/upload button
- IF
prePromptCallbackis provided:- Recorder calls the callback to get current EHR content
- Content is included in the presigned URL request as
task_type_pre_prompt
- Backend processes both audio and pre-prompt for enhanced context
- AI generates more accurate notes using both audio and existing context
Error Handling
The pre-prompt system includes robust error handling:
NoraRecorder({
// Required parameters
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT',
prePromptCallback: function() {
try {
// Your EHR-specific logic here
return getNotesFromComplexEHRSystem();
} catch (error) {
// If callback fails, recorder continues without pre-prompt
console.warn('Pre-prompt callback failed:', error);
return ''; // Return empty string to continue without context
}
}
});Error Handling Behavior:
- If
prePromptCallbackthrows an error, it logs a warning and continues without pre-prompt - If callback returns non-string value, it's treated as empty
- If callback returns empty/null, no pre-prompt is sent
- Backend handles missing pre-prompt gracefully
Integration Examples by EHR Type
Simple Text Area EHR:
prePromptCallback: function() {
return document.getElementById('notes-textarea').value || '';
}Multi-Section EHR:
prePromptCallback: function() {
const sections = {
'Chief Complaint': document.getElementById('chief-complaint')?.value,
'History': document.getElementById('history')?.value,
'Assessment': document.getElementById('assessment')?.value
};
return Object.entries(sections)
.filter(([key, value]) => value?.trim())
.map(([key, value]) => `${key}: ${value}`)
.join('\n\n');
}Structured Data EHR:
prePromptCallback: function() {
// Get structured data from your EHR's data model
const patientData = getCurrentPatientData();
const visitData = getCurrentVisitData();
const context = [];
if (patientData.allergies?.length) {
context.push(`Allergies: ${patientData.allergies.join(', ')}`);
}
if (visitData.chiefComplaint) {
context.push(`Chief Complaint: ${visitData.chiefComplaint}`);
}
if (visitData.vitals) {
context.push(`Vitals: BP ${visitData.vitals.bp}, HR ${visitData.vitals.hr}`);
}
return context.join('\n');
}Multi-Tab Environment Handling
Single Recording Limitation
NoraRecorder is designed to prevent multiple simultaneous recordings across different consultations or tabs. This is an intentional behavior to ensure that users don't accidentally start multiple recordings.
Important: Only one recording can be active at any time across all NoraRecorder instances.
Correct Multi-Tab Implementation Pattern
IMPORTANT: The correct pattern for multi-tab environments is to use a single recorder instance that gets reinitialized when switching between tabs/consultations, not multiple simultaneous instances.
// ā
CORRECT: Single instance with reinitialization
let currentRecorder = null;
function switchToConsultation(consultationID) {
// Clean up existing recorder
if (currentRecorder) {
currentRecorder.cleanup();
currentRecorder = null;
}
// Initialize new recorder
NoraRecorder({
consultationID: consultationID,
// ... other config
}).then(recorder => {
currentRecorder = recorder;
});
}Avoid:
// ā INCORRECT: Multiple simultaneous instances
const recorders = {};
recorders.tab1 = NoraRecorder({consultationID: 'CONSULT-001'});
recorders.tab2 = NoraRecorder({consultationID: 'CONSULT-002'});Tab Navigation Behavior
When a user navigates between tabs in an EHR system:
- Clean up the current recorder completely before switching
- Initialize a new recorder for the new consultation
- Update UI indicators to show which tab has an active recording
- Preserve recording state across the switch (if a recording was in progress, it continues in the background)
Status Management Across Tabs
Use the global recording status to update UI indicators across tabs:
// Track which consultation is currently recording
let recordingConsultationID = null;
// Listen for recording status changes
document.addEventListener('nora-recorder-status', (event) => {
const { type, consultationID } = event.detail;
switch (type) {
case 'recording-started':
recordingConsultationID = consultationID;
updateTabIndicators(consultationID, 'recording');
break;
case 'recording-stopped':
case 'recording-discarded':
recordingConsultationID = null;
updateTabIndicators(null, 'stopped');
break;
}
});
function updateTabIndicators(recordingConsultationID, status) {
document.querySelectorAll('.consultation-tab').forEach(tab => {
const tabConsultationID = tab.getAttribute('data-consultation-id');
const indicator = tab.querySelector('.recording-indicator');
if (recordingConsultationID === tabConsultationID && status === 'recording') {
indicator.style.display = 'block';
indicator.className = 'recording-indicator active';
} else {
indicator.style.display = 'none';
indicator.className = 'recording-indicator';
}
});
}
// Check global recording status
function updateUIForCurrentTab() {
if (NoraRecorder.isRecordingInProgress()) {
const activeConsultationID = NoraRecorder.getActiveConsultationID();
updateTabIndicators(activeConsultationID, 'recording');
} else {
updateTabIndicators(null, 'stopped');
}
}Complete Multi-Tab Example
// Configuration
const API_CONFIG = {
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT'
};
// Track current state
let currentRecorder = null;
let currentConsultationID = null;
// Consultation tabs data
const consultations = {
'tab1': { id: 'CONSULT-001', patientName: 'John Doe' },
'tab2': { id: 'CONSULT-002', patientName: 'Jane Smith' },
'tab3': { id: 'CONSULT-003', patientName: 'Bob Wilson' }
};
// Switch between consultations
function switchToConsultation(consultationID, tabElement) {
// Update tab UI
document.querySelectorAll('.consultation-tab').forEach(tab => {
tab.classList.remove('active');
});
tabElement.classList.add('active');
// Skip if already on this consultation
if (currentConsultationID === consultationID && currentRecorder) {
console.log(`Already on consultation ${consultationID}`);
return;
}
console.log(`Switching to consultation ${consultationID}`);
// Clean up existing recorder
if (currentRecorder) {
currentRecorder.cleanup();
currentRecorder = null;
}
// Clear and prepare container
const container = document.getElementById('recorder-container');
container.innerHTML = '';
// Initialize recorder for new consultation
NoraRecorder.ready(function() {
NoraRecorder({
...API_CONFIG,
consultationID: consultationID,
container: container,
position: null,
snackbarPosition: "below-component"
}).then(recorder => {
currentRecorder = recorder;
currentConsultationID = consultationID;
// Update UI
updatePatientInfo(consultationID);
updateRecordingIndicators();
console.log(`ā
Recorder ready for ${consultationID}`);
}).catch(error => {
console.error(`ā Error initializing recorder for ${consultationID}:`, error);
});
});
}
// Set up tab click handlers
document.querySelectorAll('.consultation-tab').forEach(tab => {
tab.addEventListener('click', () => {
const consultationID = tab.getAttribute('data-consultation-id');
switchToConsultation(consultationID, tab);
});
});
// Update recording indicators across tabs
function updateRecordingIndicators() {
const activeRecordingConsultationID = NoraRecorder.isRecordingInProgress()
? NoraRecorder.getActiveConsultationID()
: null;
document.querySelectorAll('.consultation-tab').forEach(tab => {
const tabConsultationID = tab.getAttribute('data-consultation-id');
const indicator = tab.querySelector('.recording-indicator');
if (activeRecordingConsultationID === tabConsultationID) {
indicator.style.display = 'block';
indicator.classList.add('recording');
} else {
indicator.style.display = 'none';
indicator.classList.remove('recording');
}
});
}
// Listen for recording events to update indicators
document.addEventListener('nora-recorder-status', (event) => {
updateRecordingIndicators();
});
// Initialize with first consultation
document.addEventListener('DOMContentLoaded', () => {
const firstTab = document.querySelector('.consultation-tab');
if (firstTab) {
const firstConsultationID = firstTab.getAttribute('data-consultation-id');
switchToConsultation(firstConsultationID, firstTab);
}
});
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
if (currentRecorder) {
currentRecorder.cleanup();
}
});Lifecycle Management
Initialization
Initialize a NoraRecorder instance when a consultation view is loaded:
function onConsultationPageLoad(consultationData) {
// Wait for NoraRecorder to be ready
NoraRecorder.ready(function() {
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: currentUser.id,
consultationID: consultationData.id,
apiBaseUrl: 'YOUR_API_ENDPOINT'
}).then(recorder => {
// Store reference for later cleanup
window.currentRecorder = recorder;
});
});
}Cleanup
Clean up the recorder when a consultation view is unloaded:
function onConsultationPageUnload() {
// Clean up NoraRecorder resources
if (window.currentRecorder) {
NoraRecorder.cleanup();
window.currentRecorder = null;
}
}Page Navigation Handling
For single-page applications (SPAs), handle recorder lifecycle during route changes:
// Example for a SPA router
router.beforeEach((to, from, next) => {
// Clean up previous recorder if navigating away from consultation
if (from.name === 'consultation' && to.name !== 'consultation') {
if (window.currentRecorder) {
NoraRecorder.cleanup();
window.currentRecorder = null;
}
}
next();
});
router.afterEach((to) => {
// Initialize new recorder if navigating to consultation
if (to.name === 'consultation') {
initializeRecorder(to.params.consultationId);
}
});Event Handling
Recording Status Events
NoraRecorder emits events when recording status changes:
// Listen for recorder status events
document.addEventListener('nora-recorder-status', (event) => {
const { type, consultationID, doctorID, timestamp } = event.detail;
console.log(`Recording event: ${type} for consultation ${consultationID}`);
// Handle different event types
switch (type) {
case 'recording-started':
showRecordingIndicator(consultationID);
break;
case 'recording-stopped':
case 'recording-discarded':
hideRecordingIndicator(consultationID);
break;
case 'recording-paused':
updateRecordingIndicator(consultationID, 'paused');
break;
case 'recording-resumed':
updateRecordingIndicator(consultationID, 'recording');
break;
// Handle generate button events
case 'generation-started':
showGenerationIndicator(consultationID);
break;
case 'generation-stopped':
hideGenerationIndicator(consultationID);
break;
case 'generation-failed':
showGenerationError(consultationID);
break;
}
});Note: The same event system is used for both recording and generate button functionality. When users click the Generate button (available for non-clinical task types when clinical notes exist), it emits generation-started, generation-stopped, and generation-failed events using the same nora-recorder-status event type.
The component dispatches a nora-recorder-status event to both the container element and the document object. The event includes:
{
type: 'recording-started' | 'recording-stopped' | 'recording-paused' | 'recording-resumed' | 'recording-discarded' | 'generation-started' | 'generation-stopped' | 'generation-failed',
consultationID: 'UNIQUE_CONSULTATION_ID',
doctorID: 'DOCTOR_ID',
timestamp: 'ISO_TIMESTAMP'
}You can listen at the document level (for all recorders) or on a specific recorder container:
// Listen on a specific container
const recorderContainer = document.getElementById('recorder-container');
recorderContainer.addEventListener('nora-recorder-status', (event) => {
// Handle event...
});Integration with Tab Systems
Use recording events to update tab indicators:
// Update tab UI based on recording status
function updateTabUI() {
const tabs = document.querySelectorAll('.patient-tab');
// Check if recording is active
if (NoraRecorder.isRecordingInProgress()) {
const activeConsultationID = NoraRecorder.getActiveConsultationID();
// Update tab indicators
tabs.forEach(tab => {
const tabConsultationID = tab.getAttribute('data-consultation-id');
if (tabConsultationID === activeConsultationID) {
tab.classList.add('recording-active');
tab.querySelector('.recording-indicator').style.display = 'inline-block';
} else {
tab.classList.remove('recording-active');
tab.querySelector('.recording-indicator').style.display = 'none';
}
});
} else {
// No active recording - remove all indicators
tabs.forEach(tab => {
tab.classList.remove('recording-active');
tab.querySelector('.recording-indicator').style.display = 'none';
});
}
}
// Call this function whenever tabs are rendered or when recording status changes
document.addEventListener('nora-recorder-status', updateTabUI);Example Implementation with Recording Indicators
Here's how you might implement recording indicators on tabs in a tabbed EHR system:
// Map of active recording consultations
let activeRecordingConsultationID = null;
// Listen for recorder status events
document.addEventListener('nora-recorder-status', (event) => {
const { type, consultationID } = event.detail;
// Handle recording started
if (type === 'recording-started') {
activeRecordingConsultationID = consultationID;
// Update all tabs to show recording indicator on the correct tab
document.querySelectorAll('.patient-tab').forEach(tab => {
const tabConsultationID = tab.getAttribute('data-consultation-id');
if (tabConsultationID === consultationID) {
tab.classList.add('recording');
tab.querySelector('.recording-indicator').style.display = 'block';
} else {
tab.classList.remove('recording');
tab.querySelector('.recording-indicator').style.display = 'none';
}
});
}
// Handle recording stopped or discarded
else if (type === 'recording-stopped' || type === 'recording-discarded') {
activeRecordingConsultationID = null;
// Remove recording indicators from all tabs
document.querySelectorAll('.patient-tab').forEach(tab => {
tab.classList.remove('recording');
tab.querySelector('.recording-indicator').style.display = 'none';
});
}
});This implementation ensures users always know which consultation has an active recording, even when navigating between different patients or views in the EHR.
Content Events
NoraRecorder provides a comprehensive event system for accessing generated content when recordings are processed and completed. This allows your EHR application to automatically receive and integrate the processed clinical notes.
Content Event Types
The recorder emits nora-recorder-content events when content becomes available:
| Event Type | Description | When Triggered |
|---|---|---|
content-ready | Generated content is available | When processing completes successfully |
Listening for Content Events
You can listen for content events at both the document level and container level:
// Listen at document level (recommended for global handling)
document.addEventListener('nora-recorder-content', (event) => {
const {
type,
consultationID,
doctorID,
content,
timestamp,
status,
processingStatus
} = event.detail;
console.log(`Content event: ${type}`);
console.log(`Consultation: ${consultationID}`);
console.log(`Generated content length: ${content.length}`);
// Handle the generated content
if (type === 'content-ready') {
insertContentIntoEHR(consultationID, content);
}
});
// Or listen on a specific recorder container
const recorderContainer = document.getElementById('recorder-container');
recorderContainer.addEventListener('nora-recorder-content', (event) => {
// Handle content for this specific recorder
handleGeneratedContent(event.detail);
});Event Detail Structure
Content events include comprehensive information about the generated content:
{
type: 'content-ready',
consultationID: 'CONSULTATION_ID',
doctorID: 'DOCTOR_ID',
timestamp: '2024-01-15T10:30:00.000Z',
status: 'COMPLETED',
processingStatus: 'sent', // or 'generated'
content: 'Chief Complaint: Patient presents with...' // The actual generated content
}Integration Examples
Basic Content Integration
function setupContentListener() {
document.addEventListener('nora-recorder-content', (event) => {
const { type, consultationID, content } = event.detail;
if (type === 'content-ready') {
// Find the appropriate text area or editor for this consultation
const notesTextarea = document.querySelector(`[data-consultation-id="${consultationID}"] textarea`);
if (notesTextarea) {
// Insert the generated content
notesTextarea.value = content;
// Trigger any change events your EHR needs
notesTextarea.dispatchEvent(new Event('input', { bubbles: true }));
// Show success message
showNotification('Clinical notes generated successfully!', 'success');
}
}
});
}
// Initialize the listener when your application starts
setupContentListener();Advanced Content Processing
function setupAdvancedContentHandler() {
document.addEventListener('nora-recorder-content', async (event) => {
const { type, consultationID, content, doctorID } = event.detail;
if (type === 'content-ready') {
try {
// Parse structured content if using templates
const structuredNotes = parseStructuredContent(content);
// Update different sections of the EHR
updateEHRSections(consultationID, structuredNotes);
// Save to your backend
await saveNotesToEHR({
consultationId: consultationID,
doctorId: doctorID,
notes: content,
timestamp: new Date().toISOString()
});
// Update UI to show content is saved
markConsultationAsCompleted(consultationID);
} catch (error) {
console.error('Error processing generated content:', error);
showNotification('Error saving generated notes', 'error');
}
}
});
}
function parseStructuredContent(content) {
// Example: Parse content based on your template structure
const sections = {
chiefComplaint: '',
historyOfPresentIllness: '',
assessment: '',
plan: ''
};
// Add your parsing logic here based on your templates
// This would depend on the structure of your generated content
return sections;
}
function updateEHRSections(consultationID, sections) {
// Update different form fields based on parsed content
Object.keys(sections).forEach(sectionKey => {
const field = document.querySelector(`[data-consultation-id="${consultationID}"] [data-field="${sectionKey}"]`);
if (field) {
field.value = sections[sectionKey];
}
});
}React Integration with Content Events
import React, { useEffect, useState } from 'react';
const ConsultationNotesComponent = ({ consultationID }) => {
const [notes, setNotes] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => {
const handleContentEvent = (event) => {
const { type, consultationID: eventConsultationID, content } = event.detail;
// Only handle events for this specific consultation
if (eventConsultationID === consultationID && type === 'content-ready') {
setNotes(content);
setIsProcessing(false);
// Optional: Auto-save to your backend
saveNotesToBackend(consultationID, content);
}
};
const handleStatusEvent = (event) => {
const { type, consultationID: eventConsultationID } = event.detail;
if (eventConsultationID === consultationID) {
if (type === 'recording-stopped') {
setIsProcessing(true);
}
}
};
// Listen for both content and status events
document.addEventListener('nora-recorder-content', handleContentEvent);
document.addEventListener('nora-recorder-status', handleStatusEvent);
return () => {
document.removeEventListener('nora-recorder-content', handleContentEvent);
document.removeEventListener('nora-recorder-status', handleStatusEvent);
};
}, [consultationID]);
const saveNotesToBackend = async (consultationId, content) => {
try {
await fetch('/api/consultations/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ consultationId, notes: content })
});
} catch (error) {
console.error('Error saving notes:', error);
}
};
return (
<div>
<h3>Clinical Notes</h3>
{isProcessing && <p>Processing recording...</p>}
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Clinical notes will appear here automatically..."
rows={15}
cols={80}
/>
{notes && (
<p className="success-message">
ā Notes generated automatically from recording
</p>
)}
</div>
);
};Programmatic Content Access
In addition to events, you can also programmatically check for and retrieve content:
// Initialize recorder and store reference
NoraRecorder({
apiKey: 'YOUR_API_KEY',
doctorID: 'DOCTOR_ID',
consultationID: 'CONSULTATION_ID',
apiBaseUrl: 'YOUR_API_ENDPOINT'
}).then(recorder => {
// Store reference for later use
window.currentRecorder = recorder;
// Check if content is already available (for existing recordings)
if (recorder.isContentReady()) {
const content = recorder.getGeneratedContent();
console.log('Existing content found:', content);
insertContentIntoEHR(content);
}
// Access subscription information
const subscriptionLevel = recorder.getSubscriptionLevel();
const monthlyUsageLeft = recorder.getMonthlyUsageLeft();
console.log('User subscription:', subscriptionLevel);
console.log('Monthly usage remaining:', monthlyUsageLeft);
// Apply subscription-based logic
if (subscriptionLevel === 'FreeTrial' && monthlyUsageLeft <= 5) {
showUpgradePrompt();
} else if (subscriptionLevel === 'Premium') {
enablePremiumFeatures();
}
});
// Later, you can check for content availability
function checkForGeneratedContent() {
if (window.currentRecorder && window.currentRecorder.isContentReady()) {
const content = window.currentRecorder.getGeneratedContent();
return content;
}
return null;
}
// Example subscription-based functionality
function checkSubscriptionLimits() {
if (window.currentRecorder) {
const usageLeft = window.currentRecorder.getMonthlyUsageLeft();
const subscriptionLevel = window.currentRecorder.getSubscriptionLevel();
if (usageLeft !== null && usageLeft <= 0) {
showUsageLimitReachedMessage(subscriptionLevel);
return false; // Prevent recording
}
if (usageLeft !== null && usageLeft <= 10) {
showLowUsageWarning(usageLeft, subscriptionLevel);
}
return true; // Allow recording
}
return true;
}Best Practices for Content Integration
- Event-Driven Integration: Use events for real-time content integration rather than polling
- Consultation-Specific Handling: Always check the
consultationIDto ensure content goes to the right place - Error Handling: Implement proper error handling for content processing and saving
- Auto-Save: Consider automatically saving generated content to your backend
- User Feedback: Provide clear feedback when content is being processed and when it's ready
- Content Validation: Validate generated content before inserting into critical EHR fields
Framework-Specific Integration
React Integration
For React applications, create a wrapper component:
import React, { useEffect, useRef, useState } from 'react';
import '@nora-technology/recorder/easy'; // Import the enhanced version
const NoraRecorderComponent = ({
apiKey,
doctorID,
consultationID,
apiBaseUrl,
embedded = true,
onRecorderReady,
customStyles = {} // Optional styling customizations
}) => {
const containerRef = useRef(null);
const recorderInstanceRef = useRef(null);
const [error, setError] = useState(null);
const [isInitialized, setIsInitialized] = useState(false);
// Default styles for the container
const defaultStyles = {
minHeight: '48px',
minWidth: '290px',
display: 'inline-block',
position: 'relative'
};
useEffect(() => {
let isMounted = true;
// Initialize when NoraRecorder is ready
window.NoraRecorder.ready(function() {
if (!isMounted) return;
window.NoraRecorder({
apiKey,
doctorID,
consultationID,
apiBaseUrl,
...(embedded ? {
container: containerRef.current,
position: null,
snackbarPosition: "below-component"
} : {
position: { x: 20, y: 20 }
})
}).then(recorder => {
if (!isMounted) {
// If component unmounted during initialization, clean up
recorder.cleanup();
return;
}
recorderInstanceRef.current = recorder;
setIsInitialized(true);
if (onRecorderReady) onRecorderReady(recorder);
}).catch(err => {
if (isMounted) {
setError(`Error initializing recorder: ${err.message || err}`);
}
});
});
// Handle loading errors
window.NoraRecorder.onError(function(error) {
if (isMounted) {
setError(`Failed to load NoraRecorder: ${error.message || error}`);
}
});
// Cleanup on unmount
return () => {
isMounted = false;
if (recorderInstanceRef.current) {
try {
window.NoraRecorder.cleanup();
recorderInstanceRef.current = null;
} catch (err) {
console.error('Error cleaning up recorder:', err);
}
}
};
}, [apiKey, doctorID, consultationID, apiBaseUrl, embedded, onRecorderReady]);
return (
<div>
{error && <div className="recorder-error">{error}</div>}
<div
ref={containerRef}
className="nora-recorder-container"
style={{...defaultStyles, ...customStyles}}
/>
</div>
);
};
export default NoraRecorderComponent;Class Component Implementation
For projects still using class components:
import React, { Component } from 'react';
import '@nora-technology/recorder';
class NoraRecorderComponent extends Component {
constructor(props) {
super(props);
this.state = {
isInitialized: false,
error: null
};
// Create refs
this.containerRef = React.createRef();
this.recorder = null;
}
componentDidMount() {
this.initializeRecorder();
}
componentDidUpdate(prevProps) {
// Re-initialize if key props change
if (
prevProps.consultationID !== this.props.consultationID ||
prevProps.doctorID !== this.props.doctorID ||
prevProps.apiKey !== this.props.apiKey ||
prevProps.apiBaseUrl !== this.props.apiBaseUrl
) {
this.cl7 months ago
7 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago