@natoe/colab
v0.1.24
Published
Real-time, study-aware collaboration UI for radiology workflows. Drop-in chat components and React hooks built on Phoenix Channels — designed to plug into a host app that already manages auth, file uploads, and DICOM viewing.
Keywords
Readme
@natoe/colab
Real-time, study-aware collaboration UI for radiology workflows. Drop-in chat components and React hooks built on Phoenix Channels — designed to plug into a host app that already manages auth, file uploads, and DICOM viewing.
Install
npm install @natoe/colab
# or
pnpm add @natoe/colab
# or
yarn add @natoe/colabPeer dependencies: react >= 18, react-dom >= 18.
Quick start
Wrap your app in CollabProvider, then drop <CollabPopup> (or one of the
other surfaces) wherever you want chat to appear.
import { CollabProvider, CollabPopup } from '@natoe/colab';
function App() {
const config = {
socketUrl: 'wss://your-backend/socket',
getToken: () => localStorage.getItem('jwt') ?? '',
userId: currentUser.id,
userRole: 'radiologist', // 'lab' | 'radiologist' | 'physician' | 'admin'
userName: currentUser.name,
// Host-app callbacks — colab calls these, never implements them.
onOpenDicom: (studyId, storageId) => openViewer(studyId, storageId),
onUploadFile: async (file, fileName) => {
const url = await uploadToS3(file, fileName);
return url;
},
onDeepLink: (path) => router.push(path),
onError: (err) => reportToSentry(err),
};
return (
<CollabProvider config={config} apiBaseUrl="https://your-backend">
{/* ...your app... */}
<CollabPopup
orderId={order.id}
patientData={{
orderId: order.id,
patientName: 'Jane Doe',
patientAge: '54',
patientSex: 'F',
studyType: 'CT Chest',
studyId: order.dicomStudyId, // PACS UID — gates the "View DICOM" button
storageId: 'PACS', // required alongside studyId
}}
participantIds={[labUserId, radiologistId, adminId]}
isOpen={chatOpen}
onClose={() => setChatOpen(false)}
/>
</CollabProvider>
);
}What's in the box
Surfaces (pick one or compose)
| Component | Use when… |
| --------------- | ------------------------------------------------------------------------ |
| CollabPopup | Floating, draggable chat window — typical for table-row "Open chat". |
| CollabPanel | Full-bleed panel, e.g. side-docked next to a viewer. |
| CollabInline | Last-N-messages preview embedded directly in a table row. |
| CollabInbox | WhatsApp-style two-pane inbox: conversation list + active conversation. |
Provider
CollabProvider owns the socket connection, identity, and host callbacks.
Wrap your app once near the root.
Hooks (for custom UI)
useCollab— read config, socket, error streamuseConversation— single conversation: messages, typing, send/edit/deleteuseConversationList— inbox list with unread counts and last activityuseMessages— paginated message history with realtime appendsuseInlineCollab— batch preview fetcher for table rowsuseUnreadCount— total unread badge for nav barsuseChannelSettings,usePinnedMessages,useDeepLinks,useAudioRecorder
Composable parts
If the prebuilt surfaces don't fit, compose your own from
ConversationList, MessageList, MessageBubble, MessageInput,
PatientHeader, ChannelSettings, etc.
CollabConfig reference
| Field | Type | Notes |
| ------------- | ----------------------------------------------- | ------------------------------------------------ |
| socketUrl | string | Phoenix WebSocket endpoint. |
| getToken | () => string | Returns the current JWT. Re-read each connect. |
| userId | string | Must match your backend's user identity. |
| userRole | 'lab' \| 'radiologist' \| 'physician' \| 'admin' | |
| userName | string | Displayed in messages and presence. |
| userAvatar | string? | Optional avatar URL. |
| onOpenDicom | (studyId, storageId) => void | Opens the host's DICOM viewer. Gates the button. |
| onUploadFile| (file, fileName?) => Promise<string> | Returns a public URL after upload. |
| onDeepLink | (path: string) => void | Resolves natoe://... links inside messages. |
| onError | (err: CollabError) => void | Centralized error sink. |
PatientData reference
Conversations are study-aware. Pass a PatientData object per chat surface so
the header and "View DICOM" button render with the right context.
| Field | Required | Notes |
| -------------------- | -------- | -------------------------------------------------------- |
| orderId | yes | Stable per-case identifier. |
| patientName | yes | |
| studyId | no | PACS study UID. Pair with storageId to show "View DICOM". |
| storageId | no | e.g. 'PACS'. Required alongside studyId. |
| patientAge, patientSex, studyType, bodyParts, referringPhysician, labName, displayOrderId | no | Header chrome. |
How conversations are created
A conversation is materialized the first time someone sends a message — empty chats don't clutter the inbox. After that first message, anyone with access to the case can join.
Who can send the first message:
- Lab / Admin — any case
- Radiologist — only after being assigned
- Physician — only after the case is completed
When a new role joins (e.g. an assigned radiologist), they see the full backlog from before they joined.
Message types
Text, voice (record-and-send), images, files (up to 20 MB), system events,
and deep links (natoe://...) that the host app resolves via onDeepLink.
Key features
- Reply to a specific message (frozen quote snapshot)
- Pin up to 3 messages per channel
- Read receipts ("Seen by …")
- Typing indicators (debounced)
- Unread counts per channel and total
- Channel settings: rename, picture, add/remove participants
TypeScript
Fully typed. All public types are exported from the root:
import type {
CollabConfig,
PatientData,
Conversation,
Message,
Participant,
CollabError,
} from '@natoe/colab';