
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
HIPAA-compliant telehealth video consultation room with live telemetry vitals, clinical SOAP notes, in-call encrypted chat, and prescription generator drawer.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/telehealth-video-room.json$npx shadcn@latest add https://uipkge.dev/r/react/telehealth-video-room.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/telehealth-video-room.json$bunx shadcn@latest add https://uipkge.dev/r/react/telehealth-video-room.jsonnpx shadcn@latest add @uipkge-react/telehealth-video-roomInstalls to:components/blocks/Type aliases exported from this item's source. Use these to shape the data you pass in.
PrescriptionIteminterface PrescriptionItem {
id: string
drugName: string
dosage: string
sig: string
quantity: string
refills: number
pharmacy: string
timestamp: string
status: 'transmitted' | 'pending'
rxNumber: string
}ChatMessageinterface ChatMessage {
id: string
sender: 'doctor' | 'patient'
senderName: string
text: string
timestamp: string
attachment?: {
name: string
size: string
type: 'pdf' | 'image'
}
}'use client'
import * as React from 'react'
import {
Activity,
AlertCircle,
Check,
CheckCircle2,
ChevronRight,
Clock,
Download,
ExternalLink,
Eye,
FileCheck,
FileText,
Heart,
HeartPulse,
Lock,
Maximize2,
Mic,
MicOff,
MoreVertical,
PanelRightClose,
PanelRightOpen,
Paperclip,
PhoneOff,
Pill,
Plus,
RefreshCw,
Send,
Share2,
ShieldCheck,
Sliders,
Thermometer,
UploadCloud,
User,
Video,
VideoOff,
Volume2,
Wifi,
X,
MessageSquare,
Settings,
} from 'lucide-react'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Textarea } from '@/components/ui/textarea'
interface PrescriptionItem {
id: string
drugName: string
dosage: string
sig: string
quantity: string
refills: number
pharmacy: string
timestamp: string
status: 'transmitted' | 'pending'
rxNumber: string
}
interface ChatMessage {
id: string
sender: 'doctor' | 'patient'
senderName: string
text: string
timestamp: string
attachment?: {
name: string
size: string
type: 'pdf' | 'image'
}
}
const drugOptions = [
{
id: 'metoprolol',
name: 'Metoprolol Succinate ER',
defaultDosage: '25mg PO Daily (Morning)',
defaultQty: '30 tablets',
refills: '2',
},
{
id: 'propranolol',
name: 'Propranolol HCl',
defaultDosage: '10mg PO BID (Twice Daily)',
defaultQty: '60 tablets',
refills: '1',
},
{
id: 'diltiazem',
name: 'Diltiazem HCl Extended Release',
defaultDosage: '120mg PO Daily',
defaultQty: '30 capsules',
refills: '3',
},
{ id: 'lisinopril', name: 'Lisinopril', defaultDosage: '10mg PO Daily', defaultQty: '30 tablets', refills: '3' },
]
export function TelehealthVideoRoom() {
// Interactive Call State
const [isMuted, setIsMuted] = React.useState(false)
const [isCameraOff, setIsCameraOff] = React.useState(false)
const [isScreenSharing, setIsScreenSharing] = React.useState(false)
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true)
const [activeTab, setActiveTab] = React.useState<'notes' | 'chat' | 'vitals'>('notes')
const [isCallEnded, setIsCallEnded] = React.useState(false)
const [showSettingsModal, setShowSettingsModal] = React.useState(false)
const [callDurationSeconds, setCallDurationSeconds] = React.useState(868) // 14:28
const [unreadChatCount, setUnreadChatCount] = React.useState(0)
const [lastVitalsUpdate, setLastVitalsUpdate] = React.useState('Just now')
const [isRefreshingVitals, setIsRefreshingVitals] = React.useState(false)
// Timer effect
React.useEffect(() => {
if (isCallEnded) return
const interval = setInterval(() => {
setCallDurationSeconds((prev) => prev + 1)
}, 1000)
return () => clearInterval(interval)
}, [isCallEnded])
const formattedDuration = React.useMemo(() => {
const mins = Math.floor(callDurationSeconds / 60)
const secs = callDurationSeconds % 60
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}, [callDurationSeconds])
// SOAP Notes State
const [chiefComplaint] = React.useState(
'Recurrent palpitations, sudden resting tachycardia (120-140 bpm), and lightheadedness over the past 2 weeks following morning coffee.',
)
const [soapSubjective] = React.useState(
'Patient reports 4 distinct episodes in past 14 days. Episodes last ~5-10 minutes with abrupt onset and offset. No syncope, mild lightheadedness. Denies chest pressure or radiating pain. Reports high work stress and 3-4 espresso shots daily.',
)
const [soapObjective] = React.useState(
'In-call vitals: HR 74 bpm regular sinus, BP 128/82 mmHg, SpO2 98%. Telemetry strip transmitted via Smart Hub shows NSR with infrequent PACs. No ST elevation or ischemic changes.',
)
const [soapAssessment] = React.useState(
'1. Paroxysmal Supraventricular Tachycardia (PSVT) vs. Benign Premature Atrial Contractions (PACs) secondary to high caffeine consumption and sympathetic stress.\n2. Stage 1 Essential Hypertension - well controlled on current regimen.',
)
const [soapPlan, setSoapPlan] = React.useState(
'1. Ordered 14-day continuous Holter monitor patch.\n2. Titrate Metoprolol Succinate ER to 25mg PO daily.\n3. Restrict caffeine to ≤1 cup/day; hydration goal 2.5L daily.\n4. Follow-up video consultation in 3 weeks or ASAP if syncope occurs.',
)
// Prescription Generator State
const [selectedDrugPreset, setSelectedDrugPreset] = React.useState('metoprolol')
const [customDosage, setCustomDosage] = React.useState('25mg PO Daily in Morning')
const [customQuantity, setCustomQuantity] = React.useState('30 tablets')
const [customRefills, setCustomRefills] = React.useState('2')
const [rxSuccessMessage, setRxSuccessMessage] = React.useState('')
const [prescriptions, setPrescriptions] = React.useState<PrescriptionItem[]>([
{
id: 'rx-1',
drugName: 'Metoprolol Succinate ER 25mg',
dosage: 'Take 1 tablet by mouth daily in the morning',
sig: '1 tab PO QAM',
quantity: '30 tablets',
refills: 2,
pharmacy: 'CVS Pharmacy #4021 (Austin, TX)',
timestamp: '10:24 AM',
status: 'transmitted',
rxNumber: 'RX-994182',
},
])
function handleSelectDrugPreset(drugId: string) {
setSelectedDrugPreset(drugId)
const found = drugOptions.find((d) => d.id === drugId)
if (found) {
setCustomDosage(found.defaultDosage)
setCustomQuantity(found.defaultQty)
setCustomRefills(found.refills)
}
}
function handleGeneratePrescription() {
const current = drugOptions.find((d) => d.id === selectedDrugPreset)
const drugName = current ? current.name : 'Custom Medication'
const newRx: PrescriptionItem = {
id: `rx-${Date.now()}`,
drugName: `${drugName} (${customDosage})`,
dosage: customDosage,
sig: `Take as directed: ${customDosage}`,
quantity: customQuantity,
refills: parseInt(customRefills) || 1,
pharmacy: 'CVS Pharmacy #4021 (Austin, TX)',
timestamp: 'Just now',
status: 'transmitted',
rxNumber: `RX-${Math.floor(100000 + Math.random() * 900000)}`,
}
setPrescriptions((prev) => [newRx, ...prev])
setRxSuccessMessage(`e-Rx signed & securely transmitted to CVS Pharmacy #4021 (${newRx.rxNumber})`)
setTimeout(() => {
setRxSuccessMessage('')
}, 4500)
}
// Chat State
const [chatInput, setChatInput] = React.useState('')
const [chatMessages, setChatMessages] = React.useState<ChatMessage[]>([
{
id: 'msg-1',
sender: 'doctor',
senderName: 'Dr. Sarah Jenkins, MD',
text: 'Hello David! I have your chart open and we are receiving your continuous telemetry stream. How are you feeling right now?',
timestamp: '10:15 AM',
},
{
id: 'msg-2',
sender: 'patient',
senderName: 'David Chen (You)',
text: 'Good morning Dr. Jenkins. Feeling better than yesterday, but had another fluttering sensation around 8 AM after my second cup of coffee.',
timestamp: '10:17 AM',
},
{
id: 'msg-3',
sender: 'patient',
senderName: 'David Chen (You)',
text: 'I captured the rhythm episode on my smart watch and exported the 30-second single lead PDF strip here.',
timestamp: '10:18 AM',
attachment: {
name: 'Apple_Watch_ECG_Lead_I_2026.pdf',
size: '1.4 MB · PDF ECG Report',
type: 'pdf',
},
},
{
id: 'msg-4',
sender: 'doctor',
senderName: 'Dr. Sarah Jenkins, MD',
text: 'Thank you, David! Reviewing the strip now. The baseline is normal sinus rhythm with occasional isolated PACs. This matches our telemetry.',
timestamp: '10:20 AM',
},
])
function handleSendMessage() {
const text = chatInput.trim()
if (!text) return
setChatMessages((prev) => [
...prev,
{
id: `msg-${Date.now()}`,
sender: 'patient',
senderName: 'David Chen (You)',
text,
timestamp: 'Just now',
},
])
setChatInput('')
// Simulated provider automated acknowledgment
setTimeout(() => {
if (!isCallEnded) {
setChatMessages((prev) => [
...prev,
{
id: `msg-doc-${Date.now()}`,
sender: 'doctor',
senderName: 'Dr. Sarah Jenkins, MD',
text: 'Noted! I have added this observation into your encounter notes and prescription schedule.',
timestamp: 'Just now',
},
])
}
}, 1200)
}
function handleOpenTab(tab: 'notes' | 'chat' | 'vitals') {
setActiveTab(tab)
setIsSidebarOpen(true)
}
function handleEndCall() {
setIsCallEnded(true)
}
function handleRestartCall() {
setIsCallEnded(false)
setCallDurationSeconds(0)
}
function handleRefreshVitals() {
setIsRefreshingVitals(true)
setTimeout(() => {
setIsRefreshingVitals(false)
setLastVitalsUpdate('Just now')
}, 800)
}
return (
<div data-slot="telehealth-video-room" className="bg-background text-foreground w-full space-y-4">
{/* Top Room Bar */}
<header className="bg-card rounded-xl border p-4 shadow-xs sm:px-6 sm:py-3.5">
<div className="flex flex-wrap items-center justify-between gap-3">
{/* Call Metadata & HIPAA Badge */}
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-2">
<span className="relative flex size-3">
{!isCallEnded && (
<span className="bg-success absolute inline-flex h-full w-full rounded-full opacity-75" />
)}
<span
className={`relative inline-flex size-3 rounded-full ${isCallEnded ? 'bg-zinc-400' : 'bg-success'}`}
/>
</span>
<div className="flex items-center gap-1.5 font-mono text-sm font-semibold tracking-tight">
<Clock className="text-muted-foreground size-3.5" />
<span>{isCallEnded ? 'Call Ended' : formattedDuration}</span>
</div>
</div>
<Separator orientation="vertical" className="hidden h-5 sm:block" />
{/* HIPAA Encrypted Connection Badge */}
<Badge
variant="outline"
className="border-success/30 bg-success/10 text-success gap-1.5 text-xs font-medium"
>
<Lock className="text-success size-3" />
<span>HIPAA Encrypted · AES-256</span>
</Badge>
<Separator orientation="vertical" className="hidden h-5 md:block" />
{/* Doctor & Patient Context Strip */}
<div className="hidden items-center gap-2 lg:flex">
<div className="flex items-center gap-1.5 text-xs">
<span className="text-muted-foreground">Provider:</span>
<span className="text-foreground font-semibold">Dr. Sarah Jenkins, MD</span>
<span className="text-muted-foreground">(Cardiology)</span>
</div>
<div className="flex items-center gap-1.5 text-xs">
<span className="text-muted-foreground">Patient:</span>
<span className="text-foreground font-semibold">David Chen</span>
<span className="text-muted-foreground font-mono text-xs">(MRN-88412)</span>
</div>
</div>
</div>
{/* Header Actions: End Call & Network Status */}
<div className="flex items-center gap-2.5">
<div className="text-muted-foreground hidden items-center gap-1.5 text-xs sm:flex">
<Wifi className="text-success size-3.5" />
<span>HD Connection (18ms)</span>
</div>
{!isCallEnded ? (
<Button
variant="destructive"
size="sm"
className="h-8 gap-1.5 px-3 text-xs font-semibold shadow-xs"
onClick={handleEndCall}
>
<PhoneOff className="size-3.5" />
<span>End Consultation</span>
</Button>
) : (
<Button
variant="default"
size="sm"
className="h-8 gap-1.5 px-3 text-xs font-semibold"
onClick={handleRestartCall}
>
<RefreshCw className="size-3.5" />
<span>Reconnect Call</span>
</Button>
)}
</div>
</div>
</header>
{/* Main Video Room Grid (Video Canvas + Collapsible Workspace) */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-12">
{/* Video Call Canvas (Center 8 cols when open, full 12 cols when collapsed) */}
<section
className={`relative flex flex-col justify-between overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950 p-4 text-white shadow-lg transition-colors duration-200 ${
isSidebarOpen ? 'min-h-[560px] lg:col-span-8 xl:col-span-8' : 'min-h-[600px] lg:col-span-12'
}`}
>
{/* Overlay Top: Provider Status & Watermark */}
<div className="z-10 flex flex-wrap items-start justify-between gap-3">
{/* Doctor Name & Active Speaking Indicator */}
<div className="flex items-center gap-2.5 rounded-lg border border-zinc-700/60 bg-zinc-900/85 p-2 shadow-md backdrop-blur-md">
<Avatar className="border-success/50 size-8 border">
<AvatarFallback className="bg-success/15 text-success text-xs font-semibold">SJ</AvatarFallback>
</Avatar>
<div>
<div className="flex items-center gap-1.5">
<span className="text-xs font-semibold text-zinc-100">Dr. Sarah Jenkins, MD</span>
<Badge
variant="outline"
className="border-success/40 bg-success/20 text-success px-1 py-0 text-xs font-normal"
>
Attending
</Badge>
</div>
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<div className="flex items-center gap-1">
<Mic className="text-success size-3" />
<div className="flex items-center gap-0.5">
<span className="bg-success size-1 animate-pulse rounded-full" />
<span className="bg-success h-2.5 w-0.5 animate-[pulse_0.8s_ease-in-out_infinite] rounded-full" />
<span className="bg-success h-3.5 w-0.5 animate-[pulse_1.2s_ease-in-out_infinite] rounded-full" />
<span className="bg-success h-1.5 w-0.5 animate-[pulse_0.6s_ease-in-out_infinite] rounded-full" />
</div>
<span className="text-success text-xs font-medium">Speaking</span>
</div>
<span>·</span>
<span>St. Jude Telehealth Clinic</span>
</div>
</div>
</div>
{/* Feed Encryption & Signal Badge */}
<div className="flex items-center gap-1.5 rounded-lg border border-zinc-800 bg-zinc-900/80 px-2.5 py-1 text-xs text-zinc-300 backdrop-blur-sm">
<ShieldCheck className="text-success size-3.5" />
<span className="font-mono text-xs">1080p 60fps · BAA Protected</span>
</div>
</div>
{/* Main Video Placeholder Canvas (Simulated high-res clinical video stream) */}
<div className="relative my-auto flex flex-col items-center justify-center pt-12 pb-28 text-center">
{/* Background radial glow */}
<div className="pointer-events-none absolute inset-0 bg-radial from-emerald-950/20 via-zinc-950/80 to-zinc-950" />
<div className="relative z-10 space-y-4">
{/* Simulated Video Avatar Portrait with active ring */}
<div className="relative mx-auto size-28 sm:size-36">
<div className="absolute -inset-1 rounded-full bg-gradient-to-tr from-emerald-500 to-teal-400 opacity-60 blur-xs" />
<div className="border-success/80 relative flex size-full items-center justify-center overflow-hidden rounded-full border-2 bg-zinc-900 shadow-xl">
<Avatar className="size-full">
<AvatarFallback className="text-success bg-zinc-900 text-xl font-bold sm:text-2xl">
SJ
</AvatarFallback>
</Avatar>
</div>
<span className="bg-success absolute right-2 bottom-2 size-4 rounded-full ring-2 ring-zinc-950" />
</div>
{/* Provider Live Status Title */}
<div className="space-y-1">
<h3 className="text-base font-semibold tracking-tight text-zinc-100 sm:text-lg">
Dr. Sarah Jenkins, MD
</h3>
<p className="text-muted-foreground text-xs">
Department of Cardiovascular Medicine · Clinical Telehealth Encounter
</p>
</div>
{/* Video Stream Diagnostic Watermark */}
<div className="text-muted-foreground inline-flex max-w-full items-center gap-2 rounded-full border border-zinc-800 bg-zinc-900/70 px-3 py-1 font-mono text-xs">
<span className="bg-success size-1.5 shrink-0 rounded-full" />
<span className="truncate">Session: #TH-9021-TX</span>
<span className="hidden sm:inline">·</span>
<span className="hidden truncate sm:inline">Latency: 24ms (0% loss)</span>
</div>
</div>
</div>
{/* Picture-in-Picture Self Video Tile (Patient View - Bottom Right) */}
<div className="absolute right-4 bottom-20 z-20 aspect-video w-36 overflow-hidden rounded-xl border-2 border-zinc-700/80 bg-zinc-900 shadow-sm transition-colors sm:right-6 sm:bottom-20 sm:w-48">
{!isCameraOff ? (
<div className="relative flex size-full flex-col justify-between bg-gradient-to-b from-zinc-800 to-zinc-950 p-2 text-white">
<div className="flex items-center justify-between">
<Badge variant="outline" className="border-zinc-700 bg-zinc-900/80 px-1 py-0 text-xs text-zinc-300">
You (720p)
</Badge>
<div
className={`flex size-4 items-center justify-center rounded-full ${
isMuted ? 'bg-destructive text-white' : 'bg-success/80 text-white'
}`}
>
{isMuted ? <MicOff className="size-2.5" /> : <Mic className="size-2.5" />}
</div>
</div>
<div className="flex items-center gap-1.5">
<Avatar className="size-5">
<AvatarFallback className="bg-zinc-700 text-xs font-bold text-zinc-100">DC</AvatarFallback>
</Avatar>
<span className="truncate text-xs font-medium text-zinc-200">David Chen</span>
</div>
</div>
) : (
<div className="text-muted-foreground flex size-full flex-col items-center justify-center gap-1 bg-zinc-950 p-2 text-center">
<VideoOff className="text-muted-foreground size-4" />
<span className="text-xs font-medium">Camera Off</span>
</div>
)}
</div>
{/* Floating Call Controls Bar (Bottom Center) */}
<div className="z-20 flex items-center justify-center pt-4">
<div className="flex flex-wrap items-center justify-center gap-1.5 rounded-full border border-zinc-700/70 bg-zinc-900/90 p-2 shadow-sm backdrop-blur-md sm:gap-2 sm:px-4">
{/* Mute / Unmute Button */}
<Button
variant={isMuted ? 'destructive' : 'secondary'}
size="icon"
className="size-9 rounded-full sm:size-10"
aria-label={isMuted ? 'Unmute microphone' : 'Mute microphone'}
onClick={() => setIsMuted(!isMuted)}
>
{isMuted ? <MicOff className="size-4" /> : <Mic className="text-success size-4" />}
</Button>
{/* Video On / Off Button */}
<Button
variant={isCameraOff ? 'destructive' : 'secondary'}
size="icon"
className="size-9 rounded-full sm:size-10"
aria-label={isCameraOff ? 'Turn on camera' : 'Turn off camera'}
onClick={() => setIsCameraOff(!isCameraOff)}
>
{isCameraOff ? <VideoOff className="size-4" /> : <Video className="text-success size-4" />}
</Button>
{/* Screen Share Button */}
<Button
variant={isScreenSharing ? 'default' : 'secondary'}
size="icon"
className="size-9 rounded-full sm:size-10"
aria-label={isScreenSharing ? 'Stop sharing screen' : 'Share screen'}
onClick={() => setIsScreenSharing(!isScreenSharing)}
>
<Share2 className="size-4" />
</Button>
<Separator orientation="vertical" className="mx-1 h-6 bg-zinc-700" />
{/* Tab Quick Toggle Buttons */}
<Button
variant={isSidebarOpen && activeTab === 'notes' ? 'default' : 'secondary'}
size="sm"
className="h-9 gap-1.5 rounded-full px-3 text-xs sm:h-10 sm:px-3.5"
onClick={() => handleOpenTab('notes')}
>
<FileText className="size-3.5" />
<span className="hidden sm:inline">SOAP Notes</span>
</Button>
<Button
variant={isSidebarOpen && activeTab === 'chat' ? 'default' : 'secondary'}
size="sm"
className="relative h-9 gap-1.5 rounded-full px-3 text-xs sm:h-10 sm:px-3.5"
onClick={() => handleOpenTab('chat')}
>
<MessageSquare className="size-3.5" />
<span className="hidden sm:inline">In-Call Chat</span>
{!isSidebarOpen && unreadChatCount > 0 && (
<span className="bg-destructive text-destructive-foreground absolute -top-1 -right-1 flex size-4 items-center justify-center rounded-full text-xs font-bold">
{unreadChatCount}
</span>
)}
</Button>
<Button
variant={isSidebarOpen && activeTab === 'vitals' ? 'default' : 'secondary'}
size="sm"
className="h-9 gap-1.5 rounded-full px-3 text-xs sm:h-10 sm:px-3.5"
onClick={() => handleOpenTab('vitals')}
>
<HeartPulse className="text-destructive size-3.5" />
<span className="hidden sm:inline">Vitals</span>
</Button>
<Separator orientation="vertical" className="mx-1 h-6 bg-zinc-700" />
{/* Device Settings Toggle */}
<Button
variant="secondary"
size="icon"
className="size-9 rounded-full sm:size-10"
aria-label="Audio and Video Settings"
onClick={() => setShowSettingsModal(!showSettingsModal)}
>
<Settings className="size-4 text-zinc-300" />
</Button>
{/* Expand / Collapse Sidebar Toggle */}
<Button
variant="secondary"
size="icon"
className="size-9 rounded-full sm:size-10"
aria-label={isSidebarOpen ? 'Collapse side workspace' : 'Open side workspace'}
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
>
{isSidebarOpen ? (
<PanelRightClose className="size-4 text-zinc-300" />
) : (
<PanelRightOpen className="size-4 text-zinc-300" />
)}
</Button>
</div>
</div>
{/* Quick Settings Flyout / Drawer Banner */}
{showSettingsModal && (
<div className="bg-card text-card-foreground absolute top-16 right-4 z-30 w-72 rounded-xl border p-4 shadow-xl">
<div className="flex items-center justify-between pb-2">
<h4 className="text-muted-foreground text-xs font-bold tracking-wider uppercase">AV Devices & HIPAA</h4>
<Button
variant="ghost"
size="icon"
className="size-6"
aria-label="Close settings modal"
onClick={() => setShowSettingsModal(false)}
>
<X className="size-3.5" />
</Button>
</div>
<div className="space-y-3 pt-1 text-xs">
<div>
<label className="text-muted-foreground block font-medium">Microphone</label>
<div className="text-foreground mt-0.5 rounded border p-1.5 font-mono text-xs">
Default - MacBook Pro Mic (CoreAudio)
</div>
</div>
<div>
<label className="text-muted-foreground block font-medium">Camera</label>
<div className="text-foreground mt-0.5 rounded border p-1.5 font-mono text-xs">
FaceTime HD Camera (1080p)
</div>
</div>
<div>
<label className="text-muted-foreground block font-medium">Speaker Output</label>
<div className="text-foreground mt-0.5 rounded border p-1.5 font-mono text-xs">
Studio Display Speakers
</div>
</div>
</div>
</div>
)}
</section>
{/* Collapsible Side Workspace (Right 1/3 when open) */}
{isSidebarOpen && (
<aside className="bg-card flex flex-col overflow-hidden rounded-xl border shadow-xs lg:col-span-4 xl:col-span-4">
<Tabs
value={activeTab}
onValueChange={(v) => setActiveTab(v as 'notes' | 'chat' | 'vitals')}
className="flex h-full flex-col"
>
{/* Workspace Tab Navigation Header */}
<div className="bg-muted/30 flex items-center justify-between border-b px-3 py-2.5">
<TabsList className="grid h-8 w-full max-w-[320px] grid-cols-3">
<TabsTrigger value="notes" className="gap-1 text-xs font-medium">
<FileText className="size-3" />
<span>Notes & Rx</span>
</TabsTrigger>
<TabsTrigger value="chat" className="gap-1 text-xs font-medium">
<MessageSquare className="size-3" />
<span>Chat</span>
</TabsTrigger>
<TabsTrigger value="vitals" className="gap-1 text-xs font-medium">
<HeartPulse className="text-destructive size-3" />
<span>Vitals</span>
</TabsTrigger>
</TabsList>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-foreground ml-1 size-8"
aria-label="Close sidebar"
onClick={() => setIsSidebarOpen(false)}
>
<PanelRightClose className="size-4" />
</Button>
</div>
{/* TAB 1: Clinical Notes & Rx */}
<TabsContent value="notes" className="flex-1 space-y-4 overflow-y-auto p-4 focus-visible:outline-none">
{/* Patient Demographics Pill Strip */}
<div className="bg-muted/40 space-y-2 rounded-lg border p-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Avatar className="size-7">
<AvatarFallback className="bg-primary/10 text-primary text-xs font-bold">DC</AvatarFallback>
</Avatar>
<div>
<span className="text-foreground text-xs font-bold">David Chen</span>
<span className="text-muted-foreground ml-1 text-xs">44y · Male</span>
</div>
</div>
<Badge variant="destructive" className="text-xs font-normal">
Allergy: Penicillin
</Badge>
</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-1 border-t pt-2 text-xs">
<span>DOB: 05/14/1982</span>
<span>Pharmacy: CVS #4021 (Austin)</span>
</div>
</div>
{/* Chief Complaint */}
<div className="space-y-1.5">
<label className="text-muted-foreground flex items-center gap-1.5 text-xs font-bold tracking-wider uppercase">
<AlertCircle className="text-warning size-3.5" />
Chief Complaint
</label>
<div className="bg-muted/20 text-foreground rounded-lg border p-2.5 text-xs leading-relaxed">
{chiefComplaint}
</div>
</div>
{/* SOAP Documentation Cards */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-bold tracking-wider uppercase">
SOAP Encounter Note
</span>
<span className="text-muted-foreground font-mono text-xs">ICD-10: I47.1</span>
</div>
{/* S: Subjective */}
<div className="bg-card space-y-1 rounded-lg border p-3">
<div className="text-foreground flex items-center gap-1.5 text-xs font-semibold">
<span className="bg-primary text-primary-foreground flex size-4 items-center justify-center rounded-full text-xs">
S
</span>
<span>Subjective</span>
</div>
<p className="text-muted-foreground text-xs leading-relaxed">{soapSubjective}</p>
</div>
{/* O: Objective */}
<div className="bg-card space-y-1 rounded-lg border p-3">
<div className="text-foreground flex items-center gap-1.5 text-xs font-semibold">
<span className="bg-primary text-primary-foreground flex size-4 items-center justify-center rounded-full text-xs">
O
</span>
<span>Objective (Telemetry & Exam)</span>
</div>
<p className="text-muted-foreground text-xs leading-relaxed">{soapObjective}</p>
</div>
{/* A: Assessment */}
<div className="bg-card space-y-1 rounded-lg border p-3">
<div className="text-foreground flex items-center gap-1.5 text-xs font-semibold">
<span className="bg-primary text-primary-foreground flex size-4 items-center justify-center rounded-full text-xs">
A
</span>
<span>Assessment & Differential</span>
</div>
<p className="text-muted-foreground text-xs leading-relaxed whitespace-pre-line">
{soapAssessment}
</p>
</div>
{/* P: Plan (Editable) */}
<div className="bg-card space-y-2 rounded-lg border p-3">
<div className="flex items-center justify-between">
<div className="text-foreground flex items-center gap-1.5 text-xs font-semibold">
<span className="bg-primary text-primary-foreground flex size-4 items-center justify-center rounded-full text-xs">
P
</span>
<span>Clinical Plan & Orders</span>
</div>
<span className="text-muted-foreground text-xs">Editable</span>
</div>
<Textarea
value={soapPlan}
onValueChange={(v) => setSoapPlan(v)}
rows={3}
className="resize-y text-xs leading-relaxed"
placeholder="Enter medical orders, follow-up recommendations..."
/>
</div>
</div>
{/* Quick e-Prescription Generator */}
<Card className="border-primary/30 shadow-xs">
<CardHeader className="px-3.5 pt-3 pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-foreground flex items-center gap-1.5 text-xs font-bold">
<Pill className="text-primary size-3.5" />
Electronic Prescription (e-Rx)
</CardTitle>
<Badge variant="outline" className="font-mono text-xs">
NPI #1894021
</Badge>
</div>
<CardDescription className="text-xs">
Generate and electronically sign DEA-compliant prescription
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 px-3.5 pb-3">
<div className="space-y-1.5">
<label className="text-muted-foreground text-xs font-medium">Select Medication</label>
<div className="grid grid-cols-2 gap-1.5">
{drugOptions.map((drug) => (
<Button
key={drug.id}
type="button"
variant={selectedDrugPreset === drug.id ? 'default' : 'outline'}
size="sm"
className="h-7 justify-start truncate px-2 text-xs"
onClick={() => handleSelectDrugPreset(drug.id)}
>
{drug.name}
</Button>
))}
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<label className="text-muted-foreground text-xs">Dosage & Frequency</label>
<Input
value={customDosage}
onChange={(e) => setCustomDosage(e.target.value)}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
<label className="text-muted-foreground text-xs">Quantity & Refills</label>
<Input
value={customQuantity}
onChange={(e) => setCustomQuantity(e.target.value)}
className="h-7 text-xs"
/>
</div>
</div>
{rxSuccessMessage && (
<div className="border-success/30 bg-success/10 text-success flex items-center gap-1.5 rounded-md border p-2 text-xs font-medium">
<CheckCircle2 className="size-3.5 shrink-0" />
<span>{rxSuccessMessage}</span>
</div>
)}
<Button
size="sm"
className="w-full gap-1.5 text-xs font-semibold"
onClick={handleGeneratePrescription}
>
<FileCheck className="size-3.5" />
<span>Sign & Transmit e-Rx to Pharmacy</span>
</Button>
{/* Encounter Prescriptions History */}
{prescriptions.length > 0 && (
<div className="space-y-1.5 border-t pt-1">
<span className="text-muted-foreground text-xs font-medium">
Active Prescriptions this Visit:
</span>
{prescriptions.map((rx) => (
<div key={rx.id} className="bg-muted/30 space-y-1 rounded border p-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-foreground font-semibold">{rx.drugName}</span>
<Badge variant="outline" className="border-success/40 bg-success/10 text-success text-xs">
{rx.status === 'transmitted' ? 'Transmitted' : 'Pending'}
</Badge>
</div>
<p className="text-muted-foreground text-xs">
{rx.dosage} · {rx.quantity} (Refills: {rx.refills})
</p>
<p className="text-muted-foreground font-mono text-xs">
{rx.pharmacy} · {rx.rxNumber}
</p>
</div>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* TAB 2: In-Call Chat */}
<TabsContent value="chat" className="flex h-[460px] flex-col focus-visible:outline-none">
{/* HIPAA Banner */}
<div className="bg-success/10 text-success flex items-center gap-2 border-b p-2.5 text-xs">
<ShieldCheck className="text-success size-4 shrink-0" />
<span>Encrypted Consultation Chat · Archived in EHR Audit Log</span>
</div>
{/* Messages List */}
<div className="flex-1 space-y-3.5 overflow-y-auto p-4">
{chatMessages.map((msg) => (
<div
key={msg.id}
className={`flex flex-col gap-1 text-xs ${
msg.sender === 'patient' ? 'items-end' : 'items-start'
}`}
>
<div className="text-muted-foreground flex items-center gap-1.5 px-1 text-xs">
<span>{msg.senderName}</span>
<span>·</span>
<span>{msg.timestamp}</span>
</div>
{/* Message bubble */}
<div
className={`max-w-[85%] rounded-2xl px-3.5 py-2 leading-relaxed shadow-xs ${
msg.sender === 'patient'
? 'bg-primary text-primary-foreground rounded-br-xs'
: 'bg-muted text-foreground rounded-bl-xs'
}`}
>
<p>{msg.text}</p>
{/* Attachment preview if present */}
{msg.attachment && (
<div className="mt-2 flex items-center justify-between gap-2 rounded-lg border border-white/20 bg-black/10 p-2 text-xs">
<div className="flex min-w-0 items-center gap-2">
<FileText className="size-4 shrink-0" />
<div className="min-w-0">
<p className="truncate font-medium">{msg.attachment.name}</p>
<p className="text-xs opacity-80">{msg.attachment.size}</p>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="size-6 shrink-0 text-current hover:bg-white/10"
aria-label="Download attachment"
>
<Download className="size-3.5" />
</Button>
</div>
)}
</div>
</div>
))}
</div>
{/* Chat Composer */}
<div className="bg-muted/20 space-y-2 border-t p-3">
<div className="flex items-center gap-1.5">
<Input
value={chatInput}
onChange={(e) => setChatInput(e.target.value)}
placeholder="Type encrypted message to Dr. Jenkins..."
className="h-8 text-xs"
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
}
}}
/>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-foreground size-8 shrink-0"
aria-label="Attach clinical file"
>
<Paperclip className="size-4" />
</Button>
<Button
size="sm"
className="h-8 shrink-0 gap-1 px-3 text-xs"
disabled={!chatInput.trim()}
onClick={handleSendMessage}
>
<Send className="size-3.5" />
<span>Send</span>
</Button>
</div>
<div className="text-muted-foreground flex items-center justify-between px-1 text-xs">
<span>Press Enter to send</span>
<span>BAA Encrypted</span>
</div>
</div>
</TabsContent>
{/* TAB 3: Patient Vitals & Telemetry */}
<TabsContent value="vitals" className="flex-1 space-y-4 overflow-y-auto p-4 focus-visible:outline-none">
{/* Telemetry Stream Banner */}
<div className="bg-muted/30 flex items-center justify-between rounded-lg border p-3">
<div className="flex items-center gap-2">
<span className="relative flex size-2.5">
<span className="bg-success absolute inline-flex h-full w-full rounded-full opacity-75" />
<span className="bg-success relative inline-flex size-2.5 rounded-full" />
</span>
<div>
<h4 className="text-foreground text-xs font-bold">Live Telemetry Feed</h4>
<p className="text-muted-foreground text-xs">BLE Medical Hub · Updated {lastVitalsUpdate}</p>
</div>
</div>
<Button
variant="outline"
size="sm"
className="h-7 gap-1.5 text-xs"
disabled={isRefreshingVitals}
onClick={handleRefreshVitals}
>
<RefreshCw className={`size-3 ${isRefreshingVitals ? 'animate-spin' : ''}`} />
<span>Refresh</span>
</Button>
</div>
{/* Vitals Telemetry Grid */}
<div className="grid grid-cols-2 gap-3">
{/* Blood Pressure */}
<Card className="space-y-2 p-3">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-semibold">Blood Pressure</span>
<Badge variant="outline" className="border-success/40 bg-success/10 text-success text-xs">
Normal
</Badge>
</div>
<div>
<div className="flex items-baseline gap-1">
<span className="text-foreground font-mono text-xl font-bold tracking-tight">128/82</span>
<span className="text-muted-foreground text-xs">mmHg</span>
</div>
<p className="text-muted-foreground mt-0.5 text-xs">Target: <130/85 mmHg</p>
</div>
<div className="text-muted-foreground flex items-center justify-between border-t pt-1.5 text-xs">
<span>Smart Cuff</span>
<span className="text-foreground font-medium">MAP: 97</span>
</div>
</Card>
{/* Heart Rate */}
<Card className="space-y-2 p-3">
<div className="flex items-center justify-between">
<span className="text-muted-foreground flex items-center gap-1 text-xs font-semibold">
<Heart className="text-destructive size-3" />
Heart Rate
</span>
<Badge variant="outline" className="border-success/40 bg-success/10 text-success text-xs">
NSR
</Badge>
</div>
<div>
<div className="flex items-baseline gap-1">
<span className="text-foreground font-mono text-xl font-bold tracking-tight">74</span>
<span className="text-muted-foreground text-xs">bpm</span>
</div>
<p className="text-muted-foreground mt-0.5 text-xs">Resting (Range: 68-88)</p>
</div>
<div className="text-muted-foreground flex items-center justify-between border-t pt-1.5 text-xs">
<span>Rhythm: Sinus</span>
<span className="text-success font-medium">Regular</span>
</div>
</Card>
{/* Oxygen Saturation (SpO2) */}
<Card className="space-y-2 p-3">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-semibold">Oxygen (SpO2)</span>
<Badge variant="outline" className="border-success/40 bg-success/10 text-success text-xs">
Optimal
</Badge>
</div>
<div>
<div className="flex items-baseline gap-1">
<span className="text-foreground font-mono text-xl font-bold tracking-tight">98%</span>
<span className="text-muted-foreground text-xs">Ambient</span>
</div>
<p className="text-muted-foreground mt-0.5 text-xs">Room Air (FiO2 21%)</p>
</div>
<div className="text-muted-foreground flex items-center justify-between border-t pt-1.5 text-xs">
<span>PulseOx Probe</span>
<span className="text-foreground font-medium">PI: 4.8%</span>
</div>
</Card>
{/* Body Temperature */}
<Card className="space-y-2 p-3">
<div className="flex items-center justify-between">
<span className="text-muted-foreground flex items-center gap-1 text-xs font-semibold">
<Thermometer className="text-warning size-3" />
Temperature
</span>
<Badge variant="secondary" className="text-xs">
Afebrile
</Badge>
</div>
<div>
<div className="flex items-baseline gap-1">
<span className="text-foreground font-mono text-xl font-bold tracking-tight">98.4°</span>
<span className="text-muted-foreground text-xs">F</span>
</div>
<p className="text-muted-foreground mt-0.5 text-xs">36.9°C (Temporal Scan)</p>
</div>
<div className="text-muted-foreground flex items-center justify-between border-t pt-1.5 text-xs">
<span>Scanner Hub</span>
<span className="text-foreground font-medium">Normal</span>
</div>
</Card>
{/* Respiration Rate */}
<Card className="space-y-2 p-3">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-semibold">Respiration</span>
<Badge variant="secondary" className="text-xs">
Eupneic
</Badge>
</div>
<div>
<div className="flex items-baseline gap-1">
<span className="text-foreground font-mono text-xl font-bold tracking-tight">16</span>
<span className="text-muted-foreground text-xs">br/min</span>
</div>
<p className="text-muted-foreground mt-0.5 text-xs">Range: 12-20 normal</p>
</div>
<div className="text-muted-foreground flex items-center justify-between border-t pt-1.5 text-xs">
<span>Chest Sensor</span>
<span className="text-foreground font-medium">Steady</span>
</div>
</Card>
{/* Fasting Blood Glucose */}
<Card className="space-y-2 p-3">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-semibold">Glucose</span>
<Badge variant="outline" className="border-success/40 bg-success/10 text-success text-xs">
Normal
</Badge>
</div>
<div>
<div className="flex items-baseline gap-1">
<span className="text-foreground font-mono text-xl font-bold tracking-tight">94</span>
<span className="text-muted-foreground text-xs">mg/dL</span>
</div>
<p className="text-muted-foreground mt-0.5 text-xs">Fasting 3.5h post-meal</p>
</div>
<div className="text-muted-foreground flex items-center justify-between border-t pt-1.5 text-xs">
<span>Continuous CGM</span>
<span className="text-success font-medium">In Range</span>
</div>
</Card>
</div>
{/* Physician Vitals Summary */}
<div className="bg-muted/20 space-y-1.5 rounded-lg border p-3 text-xs">
<div className="text-foreground flex items-center gap-1.5 font-semibold">
<ShieldCheck className="text-success size-3.5" />
<span>Physician Baseline Parameter Assessment</span>
</div>
<p className="text-muted-foreground leading-relaxed">
All live telemetry telemetry values remain within normal hemodynamic thresholds. Patient exhibits no
signs of acute decompensation or malignant arrhythmias during the video encounter.
</p>
</div>
</TabsContent>
</Tabs>
</aside>
)}
</div>
</div>
)
}
export default TelehealthVideoRoom
Raw manifest:https://uipkge.dev/r/react/telehealth-video-room.json