diff --git a/components/Clip.js b/components/Clip.js index 4a9c439a..b3e61596 100644 --- a/components/Clip.js +++ b/components/Clip.js @@ -1,4 +1,3 @@ -import ExecutionEnvironmentOverlay from './ExecutionEnvironmentOverlay' import styles from './Clip.module.css' const BASE = '/how-it-works/' @@ -20,16 +19,13 @@ const BASE = '/how-it-works/' * * @param {object} clip - a step (or variant) entry from MANIFEST.json */ -export default function Clip({ clip, environment, reference }) { +export default function Clip({ clip }) { const aspectRatio = `${clip.width} / ${clip.height}` const src = clip.gif.startsWith('/') ? clip.gif : BASE + clip.gif return (
- {environment && reference && ( - - )}
{clip.caption} - {environment && ( - {environment.mediaCaption} - )}
) diff --git a/components/DentalHaltMoment.js b/components/DentalHaltMoment.js index a6366a22..da1ffac7 100644 --- a/components/DentalHaltMoment.js +++ b/components/DentalHaltMoment.js @@ -1,95 +1,51 @@ import Link from 'next/link' -import Clip from './Clip' - -import manifest from '../public/how-it-works/MANIFEST.json' - -// --------------------------------------------------------------------------- -// ASSET SLOTS — dental payer-portal footage -// -// A parallel product build is capturing real dental eligibility-verification -// footage (record + replay-with-halt). When those assets land under -// public/dental-demo/, replace DENTAL_CLIPS with entries shaped like the -// MANIFEST steps below (gif/alt/caption/width/height) and update the footage -// note. Until then we show our existing REAL replay footage — OpenAdapt -// driving a live OpenEMR instance — and label it as exactly that. Never point -// these slots at mocked or fabricated footage. -// --------------------------------------------------------------------------- -const DENTAL_CLIPS = null - -const FALLBACK_CLIPS = { - record: { - ...manifest.steps.record_openemr, - caption: - 'Record — real footage · OpenAdapt driving a live OpenEMR instance', - }, - replay: { - ...manifest.steps.run_openemr, - caption: - 'Run — real footage · the same engine that replays your verification workflow', - }, -} +import ReferenceDemoShowcase from './ReferenceDemoShowcase' export default function DentalHaltMoment() { - const clips = DENTAL_CLIPS || FALLBACK_CLIPS - const usingFallbackFootage = !DENTAL_CLIPS - return ( -
-

The halt moment

-

- It halts and asks. It never guesses. -

-

- OpenAdapt checks configured case-identity and page evidence - before consequential steps. If the evidence does not match — - a look-alike name, a changed portal layout, an unexpected popup - — the run stops and sends the case to your front - desk's ready-to-finish queue. -

-

- After a run, OpenAdapt confirms that the declared local result - artifacts — such as the case PDF and results-log entry — were - created for the scoped case. That verifies delivery, not the - payer's underlying accuracy, and this founding service - does not write benefits back into your practice-management - system. Anything ambiguous stays a human question with the - available run evidence attached. -

- -
- Staff first, founder-backed.{' '} - Your team handles MFA and CAPTCHA prompts and finishes routine - exceptions from the local queue. A phone-only result becomes - an evidence-rich ready-to-call task for staff; this founding - service does not place the call. If a portal exception still - needs help, OpenAdapt provides same-business-day assistance - only for practices that consented to assisted access and - portals that cleared the access review. -
- -
- - -
- - {usingFallbackFootage && ( -

- Footage note: these clips are real, unstaged screen - recordings of OpenAdapt recording and replaying a workflow - in a live OpenEMR instance — the same engine that runs - dental eligibility verification. Footage of a dental - payer-portal verification, including a visible halt, is - being captured with the founding cohort and will replace - these clips. + <> +

+

The governed run

+

+ It verifies or asks for help. It never guesses. +

+

+ OpenAdapt checks configured case identity and page evidence + before consequential steps. If the evidence does not match, + the run stops and sends the case to your front desk's + ready-to-finish queue with the available evidence attached.

- )} - -

+

+ After a run, OpenAdapt confirms that the declared local result + artifacts, such as the case PDF and results-log entry, were + created for the scoped case. That verifies delivery, not the + payer's underlying accuracy, and this founding service + does not write benefits back into your practice-management + system. +

+
+ Staff first, founder-backed.{' '} + Your team handles MFA and CAPTCHA prompts and finishes routine + exceptions locally. A phone-only result becomes an evidence-rich + ready-to-call task for staff; this service does not place the + call. OpenAdapt provides same-business-day assistance only for + practices that consented to assisted access and portals that + cleared the access review. +
+
+ + + +

See the wrong-record defense in detail →

-
+ ) } diff --git a/components/EvidenceMediaPlayer.js b/components/EvidenceMediaPlayer.js new file mode 100644 index 00000000..47554875 --- /dev/null +++ b/components/EvidenceMediaPlayer.js @@ -0,0 +1,393 @@ +import { useEffect, useMemo, useRef, useState } from 'react' + +import { + chooseOverlayPlacement, + decodedMediaFrameIndex, + exactTargetForDecodedFrame, + executionOverlayPresentation, + executionOverlayFrameAt, + mapTargetToContainedVideo, +} from '../lib/executionOverlayTimeline' +import styles from './EvidenceMediaPlayer.module.css' + +const clock = (seconds) => { + if (!Number.isFinite(seconds) || seconds < 0) return '0:00' + const whole = Math.floor(seconds) + return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, '0')}` +} + +export default function EvidenceMediaPlayer({ + media, + application, + phase, + exactPresentation = null, +}) { + const playerRef = useRef(null) + const stageRef = useRef(null) + const capsuleRef = useRef(null) + const videoRef = useRef(null) + const manuallyPaused = useRef(false) + const placementRef = useRef({ eventSequence: null, placement: null }) + const [playing, setPlaying] = useState(false) + const [visible, setVisible] = useState(false) + const [currentTime, setCurrentTime] = useState(0) + const [duration, setDuration] = useState(0) + const [motionAllowed, setMotionAllowed] = useState(false) + const [decodedFrameIndex, setDecodedFrameIndex] = useState(null) + const [geometryVersion, setGeometryVersion] = useState(0) + const [placement, setPlacement] = useState('hidden') + + useEffect(() => { + const reduced = window.matchMedia( + '(prefers-reduced-motion: reduce)' + ).matches + const saveData = navigator.connection?.saveData === true + setMotionAllowed(!reduced && !saveData) + }, []) + + useEffect(() => { + const player = playerRef.current + if (!player || typeof IntersectionObserver === 'undefined') { + setVisible(true) + return undefined + } + const observer = new IntersectionObserver( + ([entry]) => setVisible(entry.isIntersecting), + { threshold: 0.4 } + ) + observer.observe(player) + return () => observer.disconnect() + }, []) + + useEffect(() => { + const shouldPlay = motionAllowed && visible && !manuallyPaused.current + if (media.kind === 'gif') { + setPlaying(shouldPlay) + return undefined + } + + const video = videoRef.current + if (!video) return undefined + if (shouldPlay) void video.play().catch(() => setPlaying(false)) + else video.pause() + return undefined + }, [media.kind, media.src, motionAllowed, visible]) + + useEffect(() => { + const video = videoRef.current + if (!video || media.kind !== 'video') return undefined + setDecodedFrameIndex(null) + setCurrentTime(0) + video.load() + return undefined + }, [media.kind, media.src]) + + useEffect(() => { + const stage = stageRef.current + const capsule = capsuleRef.current + if (!stage || !capsule || typeof ResizeObserver === 'undefined') { + return undefined + } + const observer = new ResizeObserver(() => + setGeometryVersion((version) => version + 1) + ) + observer.observe(stage) + observer.observe(capsule) + return () => observer.disconnect() + }, []) + + useEffect(() => { + setDecodedFrameIndex(null) + if ( + !exactPresentation || + media.kind !== 'video' || + !videoRef.current?.requestVideoFrameCallback + ) { + return undefined + } + + const video = videoRef.current + let callbackId + const onVideoFrame = (_now, metadata) => { + setCurrentTime(metadata.mediaTime) + setDecodedFrameIndex( + decodedMediaFrameIndex(metadata.mediaTime, exactPresentation.binding) + ) + callbackId = video.requestVideoFrameCallback(onVideoFrame) + } + callbackId = video.requestVideoFrameCallback(onVideoFrame) + return () => video.cancelVideoFrameCallback?.(callbackId) + }, [exactPresentation, media.kind, media.src]) + + const toggle = () => { + if (media.kind === 'gif') { + const next = !playing + manuallyPaused.current = !next + setMotionAllowed(next) + setPlaying(next && visible) + return + } + const video = videoRef.current + if (!video) return + if (video.paused) { + manuallyPaused.current = false + setMotionAllowed(true) + void video.play().catch(() => setPlaying(false)) + } else { + manuallyPaused.current = true + video.pause() + } + } + + const syncVideo = () => { + const video = videoRef.current + if (!video) return + setPlaying(!video.paused) + setCurrentTime(video.currentTime) + setDuration(Number.isFinite(video.duration) ? video.duration : 0) + } + + const phaseLabel = phase === 'recording' ? 'Demonstration' : 'Compiled replay' + const frame = exactPresentation + ? executionOverlayFrameAt( + exactPresentation.timeline, + Math.round(currentTime * 1000) + ) + : null + const target = exactPresentation && frame?.visible + ? exactTargetForDecodedFrame( + exactPresentation.timeline, + exactPresentation.binding, + decodedFrameIndex + ) + : null + const mappedTarget = useMemo(() => { + const video = videoRef.current + if (!target || !video) return null + return mapTargetToContainedVideo(target, { + elementWidth: video.clientWidth, + elementHeight: video.clientHeight, + videoWidth: video.videoWidth, + videoHeight: video.videoHeight, + }) + // ResizeObserver increments geometryVersion when either box changes. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [target, geometryVersion]) + const protectedRegions = useMemo( + () => + Array.isArray(media.protectedRegions) + ? media.protectedRegions + : [], + [media.protectedRegions] + ) + const boundContext = frame + ? exactPresentation?.contextsBySequence?.[frame.event_sequence] ?? null + : null + const presentation = frame + ? executionOverlayPresentation(frame, boundContext, currentTime * 1000) + : null + + useEffect(() => { + const stage = stageRef.current + const capsule = capsuleRef.current + if (!stage || !capsule) return + const eventSequence = frame?.event_sequence ?? null + const retained = + placementRef.current.eventSequence === eventSequence + ? placementRef.current.placement + : null + const next = chooseOverlayPlacement({ + stageWidth: stage.clientWidth, + stageHeight: stage.clientHeight, + capsuleWidth: capsule.offsetWidth, + capsuleHeight: capsule.offsetHeight, + avoidRegions: mappedTarget + ? [...protectedRegions, mappedTarget] + : protectedRegions, + currentPlacement: retained, + }) + placementRef.current = { eventSequence, placement: next } + setPlacement(next) + }, [frame?.event_sequence, geometryVersion, mappedTarget, protectedRegions]) + + return ( +
+
+ {media.kind === 'video' ? ( + + ) : ( + {media.alt} + )} + + {mappedTarget && ( +