From f45f6705639b690bdda84e2803bf989ae1bac7cb Mon Sep 17 00:00:00 2001 From: abrichr Date: Thu, 30 Jul 2026 18:23:34 -0400 Subject: [PATCH 1/6] feat: add Execute visual explanation components --- components/ExecuteApiExchange.js | 34 +++++ components/ExecuteApiExchange.module.css | 77 ++++++++++ components/ExecuteBoundaryFlow.js | 86 +++++++++++ components/ExecuteBoundaryFlow.module.css | 171 ++++++++++++++++++++++ components/ExecuteOutcomeCards.js | 49 +++++++ components/ExecuteOutcomeCards.module.css | 52 +++++++ 6 files changed, 469 insertions(+) create mode 100644 components/ExecuteApiExchange.js create mode 100644 components/ExecuteApiExchange.module.css create mode 100644 components/ExecuteBoundaryFlow.js create mode 100644 components/ExecuteBoundaryFlow.module.css create mode 100644 components/ExecuteOutcomeCards.js create mode 100644 components/ExecuteOutcomeCards.module.css diff --git a/components/ExecuteApiExchange.js b/components/ExecuteApiExchange.js new file mode 100644 index 0000000..391ef9c --- /dev/null +++ b/components/ExecuteApiExchange.js @@ -0,0 +1,34 @@ +import styles from './ExecuteApiExchange.module.css' + +function CodeLine({ children }) { + return {children} +} + +export default function ExecuteApiExchange() { + return ( +
+
+

Asynchronous by design

+

One transaction. A durable terminal receipt.

+

Your request gets an immediate acceptance. A signed webhook then delivers the terminal result when the runner has proof or needs reconciliation.

+
+
    +
  1. +

    1 · Submit an authorized transaction

    +
    POST /v1/executions{'{ "idempotency_key": "txn_…", "input": { … } }'}
    +

    Your product supplies structured input. The qualified workflow defines the permitted effect.

    +
  2. +
  3. +

    2 · Keep the transaction identity

    +
    202 Accepted{'{ "execution_id": "exec_…", "state": "queued" }'}
    +

    Acceptance is not a success claim. Poll this ID if your webhook endpoint is unavailable.

    +
  4. +
  5. +

    3 · Verify the terminal delivery

    +
    POST https://your-product.example/hooks/openadaptOpenAdapt-Signature: t=…, v1=…{'{ "event_type": "execution.terminal",'}{'  "receipt": { "outcome": "VERIFIED", … } }'}
    +

    Verify the signature and store the receipt. The outcome can also require reconciliation instead of a retry.

    +
  6. +
+
+ ) +} diff --git a/components/ExecuteApiExchange.module.css b/components/ExecuteApiExchange.module.css new file mode 100644 index 0000000..bf2bae1 --- /dev/null +++ b/components/ExecuteApiExchange.module.css @@ -0,0 +1,77 @@ +.exchange { + padding: clamp(1.25rem, 3vw, 2rem); + border: 1px solid var(--hairline); + border-radius: 1rem; + background: var(--panel); +} + +.heading { max-width: 42rem; } + +.heading h2 { + margin: 0.4rem 0 0; + color: var(--ink); + font-family: var(--font-display); + font-size: clamp(1.5rem, 3vw, 2rem); + line-height: 1.15; +} + +.heading > p:last-child { + margin: 0.75rem 0 0; + color: var(--ink-2); + font-size: 0.95rem; + line-height: 1.55; +} + +.messages { + display: grid; + gap: 1rem; + padding: 0; + margin: 1.5rem 0 0; + list-style: none; +} + +.messages li { + position: relative; + padding: 1rem; + border: 1px solid var(--hairline); + border-radius: 0.75rem; + background: var(--ground); +} + +.label { + margin: 0 0 0.65rem; + color: var(--accent); + font-family: var(--font-mono); + font-size: 0.68rem; + font-weight: 700; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +.messages pre { + padding: 0.8rem; + margin: 0; + overflow-x: auto; + border-radius: 0.5rem; + background: var(--ink); + color: var(--ground); + font-family: var(--font-mono); + font-size: clamp(0.68rem, 1.8vw, 0.76rem); + line-height: 1.65; +} + +.line { display: block; white-space: pre; } +.method { color: #a9d7b5; } +.status { color: #d9d68b; } +.webhook { color: #9bc8d7; } + +.note { + margin: 0.7rem 0 0; + color: var(--ink-2); + font-size: 0.78rem; + line-height: 1.45; +} + +@media (min-width: 760px) { + .messages { grid-template-columns: repeat(3, minmax(0, 1fr)); } +} diff --git a/components/ExecuteBoundaryFlow.js b/components/ExecuteBoundaryFlow.js new file mode 100644 index 0000000..2fad01c --- /dev/null +++ b/components/ExecuteBoundaryFlow.js @@ -0,0 +1,86 @@ +import styles from './ExecuteBoundaryFlow.module.css' + +const stages = [ + { + title: 'Your product', + detail: 'Authorized structured input', + kind: 'input', + }, + { + title: 'Customer-controlled runner', + detail: 'Local observation and actuation', + kind: 'runner', + }, + { + title: 'OpenAdapt', + detail: 'Proof or typed exception', + kind: 'proof', + }, +] + +function StageIcon({ kind }) { + if (kind === 'input') { + return ( + + ) + } + + if (kind === 'runner') { + return ( + + ) + } + + return ( + + ) +} + +function Connector({ returnPath = false }) { + return ( + + ) +} + +// A static, semantic flow remains available when motion is reduced. The moving +// dash is presentation-only; the text defines the actual execution boundary. +export default function ExecuteBoundaryFlow() { + return ( +
+
+ Authorized structured input moves from your product to a customer-controlled runner. The runner acts locally. OpenAdapt returns proof or a typed exception. +
+
+ {stages.map((stage, index) => ( +
+
+ + + {stage.title} + {stage.detail} + +
+ {index < stages.length - 1 && } +
+ ))} +
+
+ +

Signed receipt returns to your product after verification or a governed stop.

+
+
+ ) +} diff --git a/components/ExecuteBoundaryFlow.module.css b/components/ExecuteBoundaryFlow.module.css new file mode 100644 index 0000000..0221989 --- /dev/null +++ b/components/ExecuteBoundaryFlow.module.css @@ -0,0 +1,171 @@ +.figure { + margin: 0; + padding: 1.25rem; + border: 1px solid var(--hairline); + border-radius: 1rem; + background: var(--ground); +} + +.flow { + display: flex; + align-items: stretch; + justify-content: center; +} + +.part { + display: flex; + flex: 1 1 0; + align-items: center; + min-width: 0; +} + +.stage { + display: flex; + flex: 1 1 auto; + align-items: center; + gap: 0.75rem; + min-width: 0; + padding: 0.9rem; + border: 1px solid var(--hairline); + border-radius: 0.75rem; + background: var(--panel); +} + +.icon { + display: grid; + flex: 0 0 auto; + width: 2.25rem; + height: 2.25rem; + place-items: center; + border-radius: 999px; + background: color-mix(in srgb, var(--accent) 12%, transparent); + color: var(--accent); +} + +.icon svg { + width: 1.25rem; + height: 1.25rem; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.8; +} + +.stageCopy { + display: grid; + gap: 0.2rem; + min-width: 0; +} + +.stageCopy strong { + color: var(--ink); + font-size: 0.84rem; + line-height: 1.2; +} + +.stageCopy span, +.returnLane p { + color: var(--ink-2); + font-size: 0.72rem; + line-height: 1.35; +} + +.connector { + display: grid; + flex: 0 1 6.25rem; + justify-items: center; + gap: 0.15rem; + min-width: 3.5rem; + color: var(--accent); +} + +.connector svg { + width: 100%; + max-width: 5rem; + height: 1.5rem; + overflow: visible; +} + +.connector path { + fill: none; + stroke: currentColor; + stroke-dasharray: 4 4; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.6; + animation: travel 1.8s linear infinite; +} + +.connector span { + max-width: 5.5rem; + color: var(--ink-3); + font-size: 0.62rem; + line-height: 1.15; + text-align: center; +} + +.returnLane { + display: grid; + grid-template-columns: minmax(4rem, 14rem) minmax(0, 1fr); + align-items: center; + gap: 0.75rem; + max-width: 26rem; + margin: 1rem auto 0; +} + +.returnLane p { + margin: 0; +} + +.returnPath svg { + transform: scaleX(-1); +} + +.returnPath path { + animation-direction: reverse; +} + +.srOnly { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +@keyframes travel { + to { stroke-dashoffset: -16; } +} + +@media (prefers-reduced-motion: reduce) { + .connector path { animation: none; } +} + +@media (max-width: 760px) { + .flow, + .part { + flex-direction: column; + } + + .part { width: 100%; } + + .stage { width: 100%; } + + .connector { + flex-basis: auto; + min-height: 3.25rem; + } + + .connector svg { transform: rotate(90deg); } + + .returnLane { + grid-template-columns: 3.5rem minmax(0, 1fr); + margin-top: 0.5rem; + } + + .returnPath svg { transform: rotate(90deg) scaleX(-1); } +} diff --git a/components/ExecuteOutcomeCards.js b/components/ExecuteOutcomeCards.js new file mode 100644 index 0000000..108a677 --- /dev/null +++ b/components/ExecuteOutcomeCards.js @@ -0,0 +1,49 @@ +import styles from './ExecuteOutcomeCards.module.css' + +const outcomes = [ + { + name: 'VERIFIED', + detail: 'The configured identity, postcondition, and business-effect proof passed.', + type: 'verified', + }, + { + name: 'HALTED', + detail: 'The runner stopped when its authorization, target, identity, or policy evidence did not match.', + type: 'halted', + }, + { + name: 'RECONCILIATION_REQUIRED', + detail: 'A possible delivery needs an independent check. The runner does not repeat a consequential write blindly.', + type: 'reconcile', + }, +] + +function OutcomeIcon({ type }) { + const paths = type === 'verified' + ? + : type === 'halted' + ? <> + : <> + + return ( + + ) +} + +export default function ExecuteOutcomeCards() { + return ( +
    + {outcomes.map((outcome) => ( +
  • + +
    +

    {outcome.name}

    +

    {outcome.detail}

    +
    +
  • + ))} +
+ ) +} diff --git a/components/ExecuteOutcomeCards.module.css b/components/ExecuteOutcomeCards.module.css new file mode 100644 index 0000000..c216939 --- /dev/null +++ b/components/ExecuteOutcomeCards.module.css @@ -0,0 +1,52 @@ +.cards { + display: grid; + gap: 0.8rem; + padding: 0; + margin: 0; + list-style: none; +} + +.card { + display: flex; + gap: 0.8rem; + padding: 1rem; + border: 1px solid var(--hairline); + border-radius: 0.75rem; + background: var(--ground); +} + +.card > svg { + flex: 0 0 auto; + width: 1.5rem; + height: 1.5rem; + margin-top: 0.05rem; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} + +.card h3 { + margin: 0; + color: var(--ink); + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.06em; + line-height: 1.35; +} + +.card p { + margin: 0.4rem 0 0; + color: var(--ink-2); + font-size: 0.78rem; + line-height: 1.45; +} + +.verified { color: var(--accent); } +.halted { color: #955839; } +.reconcile { color: #77621d; } + +@media (min-width: 700px) { + .cards { grid-template-columns: repeat(3, minmax(0, 1fr)); } +} From 0db2027789f6e793aa1b56237c5a719b864a61c5 Mon Sep 17 00:00:00 2001 From: abrichr Date: Thu, 30 Jul 2026 18:36:10 -0400 Subject: [PATCH 2/6] feat: add provenance-bound RDP hybrid presentation --- components/RdpHybridPresentation.js | 323 ++++++++++++++++++++ components/RdpHybridPresentation.module.css | 95 ++++++ 2 files changed, 418 insertions(+) create mode 100644 components/RdpHybridPresentation.js create mode 100644 components/RdpHybridPresentation.module.css diff --git a/components/RdpHybridPresentation.js b/components/RdpHybridPresentation.js new file mode 100644 index 0000000..2a6a758 --- /dev/null +++ b/components/RdpHybridPresentation.js @@ -0,0 +1,323 @@ +import { useEffect, useMemo, useRef, useState } from 'react' + +import styles from './RdpHybridPresentation.module.css' + +const MISSING_TIMELINE = + 'The exported presentation has no exact media-time binding yet. OpenAdapt still shows the authenticated media and artifacts, but it does not infer a phase from playback time.' + +const clamp = (value, minimum, maximum) => + Math.min(Math.max(value, minimum), maximum) + +const timeLabel = (milliseconds) => { + const seconds = Math.max(0, Math.floor(milliseconds / 1000)) + return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}` +} + +const readJson = async (source) => { + const response = await fetch(source, { cache: 'force-cache' }) + if (!response.ok) throw new Error(`Could not load ${source}`) + return response.json() +} + +function Marker({ type }) { + if (type === 'identity') { + return ( + + ) + } + if (type === 'proof') { + return ( + + ) + } + if (type === 'halt') { + return ( + + ) + } + return ( + + ) +} + +function validChapter(chapter, durationMs) { + if (!chapter || typeof chapter !== 'object') return false + if (typeof chapter.id !== 'string' || !chapter.id) return false + if (typeof chapter.start_ms !== 'number' || typeof chapter.end_ms !== 'number') { + return false + } + if (chapter.start_ms < 0 || chapter.end_ms <= chapter.start_ms) return false + if (chapter.end_ms > durationMs) return false + if (!chapter.presentation || typeof chapter.presentation !== 'object') return false + if (typeof chapter.presentation.phase !== 'string') return false + if (!Array.isArray(chapter.presentation.graph_node_ids)) return false + return true +} + +function validTimeline(timeline, manifest) { + if (!timeline || typeof timeline !== 'object') return null + if (timeline.schema_version !== 'openadapt.rdp-media-timeline.v1') return null + if (timeline.video_sha256 !== manifest?.video_sha256) return null + if (!Number.isFinite(timeline.duration_ms) || timeline.duration_ms <= 0) return null + if (!Array.isArray(timeline.chapters) || !timeline.chapters.length) return null + if (!timeline.chapters.every((chapter) => validChapter(chapter, timeline.duration_ms))) { + return null + } + return timeline +} + +function activeChapter(timeline, currentMs) { + if (!timeline) return null + return ( + timeline.chapters.find( + (chapter) => currentMs >= chapter.start_ms && currentMs < chapter.end_ms + ) ?? timeline.chapters.at(-1) + ) +} + +export default function RdpHybridPresentation({ + videoSrc = '/demos/rdp/openadapt-rdp-demo.mp4', + poster = '/demos/rdp/poster.jpg', + manifestSrc = '/demos/rdp/presentation.manifest.json', + graphSrc = '/demos/rdp/program-graph.json', + timelineSrc = '/demos/rdp/presentation.timeline.json', +}) { + const videoRef = useRef(null) + const [manifest, setManifest] = useState(null) + const [graph, setGraph] = useState(null) + const [timelinePayload, setTimelinePayload] = useState(null) + const [loadError, setLoadError] = useState(null) + const [currentMs, setCurrentMs] = useState(0) + const [durationMs, setDurationMs] = useState(0) + const [playing, setPlaying] = useState(false) + const [reducedMotion, setReducedMotion] = useState(false) + + useEffect(() => { + let active = true + Promise.all([ + readJson(manifestSrc), + readJson(graphSrc), + readJson(timelineSrc).catch(() => null), + ]) + .then(([nextManifest, nextGraph, nextTimeline]) => { + if (!active) return + setManifest(nextManifest) + setGraph(nextGraph) + setTimelinePayload(nextTimeline) + }) + .catch((error) => { + if (active) setLoadError(error.message) + }) + return () => { + active = false + } + }, [graphSrc, manifestSrc, timelineSrc]) + + useEffect(() => { + const media = window.matchMedia('(prefers-reduced-motion: reduce)') + const update = () => setReducedMotion(media.matches) + update() + media.addEventListener?.('change', update) + return () => media.removeEventListener?.('change', update) + }, []) + + useEffect(() => { + const video = videoRef.current + if (!video?.requestVideoFrameCallback) return undefined + let callbackId + let mounted = true + const update = (_now, metadata) => { + if (!mounted) return + setCurrentMs(Math.round(metadata.mediaTime * 1000)) + callbackId = video.requestVideoFrameCallback(update) + } + callbackId = video.requestVideoFrameCallback(update) + return () => { + mounted = false + video.cancelVideoFrameCallback?.(callbackId) + } + }, []) + + const timeline = useMemo( + () => validTimeline(timelinePayload, manifest), + [manifest, timelinePayload] + ) + const chapter = activeChapter(timeline, currentMs) + const activeNodeIds = new Set(chapter?.presentation?.graph_node_ids ?? []) + const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [] + const progress = durationMs ? clamp((currentMs / durationMs) * 100, 0, 100) : 0 + + const toggle = () => { + const video = videoRef.current + if (!video) return + if (video.paused) void video.play() + else video.pause() + } + + const seek = (milliseconds) => { + const video = videoRef.current + if (!video || !timeline) return + const exactMs = clamp(milliseconds, 0, timeline.duration_ms) + video.currentTime = exactMs / 1000 + setCurrentMs(exactMs) + } + + return ( +
+
+
+ +
+
+ + + + +
+ +
+ {(timeline?.chapters ?? []).map((item, index) => { + const selected = item.id === chapter?.id + return ( + + ) + })} + {!timeline &&

{MISSING_TIMELINE}

} +
+ +
+
+
+

Compiled workflow

+ Only exact exported graph nodes appear here. +
+ {reducedMotion ? 'Reduced motion' : 'Media-synced'} +
+
    + {nodes.map((node) => ( +
  1. + {String(node.index + 1).padStart(2, '0')} + {node.title} + + {node.param + ? `input: $${node.param}` + : node.badges?.join(' · ') || node.kind} + +
  2. + ))} +
+
+ + {loadError &&

Evidence load error: {loadError}

} +
+ ) +} diff --git a/components/RdpHybridPresentation.module.css b/components/RdpHybridPresentation.module.css new file mode 100644 index 0000000..a0849ef --- /dev/null +++ b/components/RdpHybridPresentation.module.css @@ -0,0 +1,95 @@ +.shell { + overflow: hidden; + border: 1px solid rgba(104, 224, 198, 0.28); + border-radius: 24px; + background: + radial-gradient(circle at 78% 0%, rgba(38, 140, 126, 0.18), transparent 32%), + linear-gradient(145deg, #071214 0%, #0a181b 48%, #071012 100%); + box-shadow: 0 30px 80px -48px rgba(8, 66, 59, 0.9); + color: #edf8f5; +} + +.topline, +.consoleHeader, +.graphHeader, +.clock, +.provenance, +.chapter, +.signal, +.playButton { + display: flex; + align-items: center; +} + +.topline { + gap: 9px; + min-height: 42px; + padding: 0 18px; + border-bottom: 1px solid rgba(140, 211, 198, 0.14); + color: #a8c4be; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.liveDot { + width: 7px; + height: 7px; + border-radius: 999px; + background: #65e6bd; + box-shadow: 0 0 0 4px rgba(101, 230, 189, 0.12), 0 0 18px #65e6bd; +} + +.digest { margin-left: auto; color: #6f9088; } +.grid { display: grid; gap: 1px; background: rgba(140, 211, 198, 0.14); } +.stage { position: relative; min-height: 300px; overflow: hidden; background: #071011; } +.video { display: block; width: 100%; height: 100%; min-height: 300px; object-fit: contain; background: #050a0b; } +.frame { position: absolute; inset: 0; pointer-events: none; background: linear-gradient(90deg, rgba(102,255,220,.07) 1px, transparent 1px), linear-gradient(rgba(102,255,220,.05) 1px, transparent 1px); background-size: 42px 42px; mask-image: linear-gradient(transparent 72%, rgba(0,0,0,.8)); } +.phaseBadge { position: absolute; top: 16px; left: 16px; display: grid; gap: 3px; padding: 8px 10px; border: 1px solid rgba(127, 247, 220, 0.32); border-radius: 10px; background: rgba(4, 19, 19, 0.74); backdrop-filter: blur(12px); font-size: 11px; } +.phaseBadge span { color: #95cac0; } +.phaseBadge strong { color: #e7fffa; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; letter-spacing: .07em; text-transform: uppercase; } +.phaseBadge[data-known='false'] strong { color: #b8cfca; } +.playButton { position: absolute; right: 16px; bottom: 48px; gap: 8px; padding: 9px 12px; border: 1px solid rgba(159, 245, 223, .25); border-radius: 999px; background: rgba(4, 18, 18, .8); color: #e5faf5; font-size: 12px; font-weight: 700; backdrop-filter: blur(12px); } +.playButton:hover { border-color: #79e4c5; background: #102728; } +.scrubWrap { position: absolute; right: 16px; bottom: 12px; left: 16px; } +.scrubWrap input { width: 100%; accent-color: #68e3bd; } +.scrubWrap input:disabled { opacity: .45; cursor: not-allowed; } +.clock { justify-content: space-between; color: #b2d0c9; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; } +.console { min-height: 300px; padding: 20px; background: linear-gradient(180deg, #0b1e20, #081516); } +.consoleHeader { justify-content: space-between; padding-bottom: 14px; border-bottom: 1px solid rgba(149, 222, 205, .15); color: #80b6aa; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; letter-spacing: .08em; text-transform: uppercase; } +.consoleHeader p { margin: 0; } +.consoleHeader span { color: #68e3bd; } +.console h3 { margin: 22px 0 8px; color: #f0fffb; font-family: var(--font-display, ui-sans-serif); font-size: 26px; line-height: 1.05; } +.console > p { margin: 0; color: #abc4bf; font-size: 14px; line-height: 1.55; } +.signalList { display: grid; gap: 8px; margin-top: 18px; } +.signal { gap: 9px; padding: 8px; border: 1px solid rgba(147, 224, 207, .12); border-radius: 10px; background: rgba(111, 217, 193, .055); } +.icon { flex: 0 0 auto; width: 23px; height: 23px; fill: none; stroke: #75e7c3; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; } +.signal div { min-width: 0; display: grid; gap: 1px; } +.signal strong { color: #dff6f1; font-size: 11px; } +.signal span { overflow: hidden; color: #97b8b0; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } +.provenance { display: grid; grid-template-columns: auto 1fr; gap: 5px 10px; margin-top: 20px; padding-top: 14px; border-top: 1px solid rgba(149, 222, 205, .15); color: #78958e; font-size: 10px; } +.provenance code { overflow: hidden; color: #a9c9c1; text-overflow: ellipsis; white-space: nowrap; } +.rail { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; border-top: 1px solid rgba(140, 211, 198, .14); background: rgba(140, 211, 198, .14); } +.chapter { min-height: 84px; flex-direction: column; align-items: flex-start; justify-content: center; gap: 4px; padding: 12px 14px; border: 0; background: #0a1719; color: #d7f0ea; text-align: left; } +.chapter:hover, .chapter[data-active='true'] { background: #103034; } +.chapter[data-active='true'] { box-shadow: inset 0 2px 0 #69e2bd; } +.chapter > span { color: #67dfbc; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; } +.chapter strong { font-size: 12px; } +.chapter small { color: #87aaa2; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 9px; letter-spacing: .06em; text-transform: uppercase; } +.contractNote { grid-column: 1 / -1; margin: 0; padding: 14px 18px; background: #0a1719; color: #8eaaa4; font-size: 12px; line-height: 1.45; } +.graph { padding: 18px; border-top: 1px solid rgba(140, 211, 198, .14); background: #071213; } +.graphHeader { justify-content: space-between; gap: 18px; margin-bottom: 14px; } +.graphHeader p { margin: 0; color: #daf8f1; font-size: 13px; font-weight: 700; } +.graphHeader span { display: block; margin-top: 3px; color: #829e98; font-size: 11px; } +.motion { padding: 5px 7px; border: 1px solid rgba(128, 220, 196, .2); border-radius: 999px; color: #89bbb0 !important; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 9px !important; letter-spacing: .06em; text-transform: uppercase; white-space: nowrap; } +.graph ol { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; padding: 0; margin: 0; list-style: none; } +.graph li { position: relative; min-height: 88px; padding: 12px; border: 1px solid rgba(142, 207, 193, .14); border-radius: 12px; background: #0b1c1d; opacity: .58; transition: border-color .25s ease, background .25s ease, opacity .25s ease, transform .25s ease; } +.graph li[data-active='true'] { border-color: rgba(99, 230, 191, .72); background: linear-gradient(145deg, #10443e, #0d2728); box-shadow: 0 0 28px -13px #60e6bf; opacity: 1; transform: translateY(-2px); } +.graph li > span { display: block; margin-bottom: 8px; color: #5de0b9; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; } +.graph li strong { display: block; color: #ebfff9; font-size: 12px; line-height: 1.3; } +.graph li small { display: block; margin-top: 7px; color: #8caea6; font-size: 10px; line-height: 1.3; } +.error { margin: 0; padding: 12px 18px; border-top: 1px solid rgba(245, 148, 131, .24); background: rgba(107, 31, 24, .28); color: #ffb3a4; font-size: 12px; } +@media (min-width: 900px) { .grid { grid-template-columns: minmax(0, 1.45fr) minmax(300px, .55fr); } .stage, .console { min-height: 470px; } .video { min-height: 470px; } } +@media (max-width: 699px) { .topline { padding: 0 12px; } .digest { display: none; } .rail { grid-template-columns: repeat(2, minmax(0, 1fr)); } .graph ol { grid-template-columns: repeat(2, minmax(0, 1fr)); } .playButton { bottom: 52px; } } +@media (prefers-reduced-motion: reduce) { .graph li { transition: none; } .liveDot { box-shadow: none; } } From cb97e5424bc5659a8fcc59baf25c6462ecd7e4f7 Mon Sep 17 00:00:00 2001 From: abrichr Date: Thu, 30 Jul 2026 18:40:38 -0400 Subject: [PATCH 3/6] fix: bind hybrid RDP viewer to exported timeline --- components/RdpHybridPresentation.js | 237 +++++++++++++++++++++++----- 1 file changed, 194 insertions(+), 43 deletions(-) diff --git a/components/RdpHybridPresentation.js b/components/RdpHybridPresentation.js index 2a6a758..b487c77 100644 --- a/components/RdpHybridPresentation.js +++ b/components/RdpHybridPresentation.js @@ -3,7 +3,40 @@ import { useEffect, useMemo, useRef, useState } from 'react' import styles from './RdpHybridPresentation.module.css' const MISSING_TIMELINE = - 'The exported presentation has no exact media-time binding yet. OpenAdapt still shows the authenticated media and artifacts, but it does not infer a phase from playback time.' + 'The media timeline is unavailable. OpenAdapt shows the authenticated video and artifacts, but it does not infer a phase from playback time.' + +// These labels only describe renderer phases. The runtime facts stay in the +// exporter payload and are rendered below without a frontend interpretation. +const PHASE_PRESENTATION = { + execute_request: { + label: 'Authorized request', + detail: 'A qualified request enters the execution boundary.', + }, + demonstration: { + label: 'Demonstrate through RDP', + detail: 'The presentation replays retained operator input.', + }, + compiled_workflow: { + label: 'Compiled workflow', + detail: 'OpenAdapt exposes the exact exported workflow node.', + }, + governed_replay: { + label: 'Governed replay', + detail: 'The runner replays the qualified workflow with fresh observations.', + }, + independent_effect_check: { + label: 'Independent effect proof', + detail: 'The configured verifier determines the result.', + }, + wrong_record_refusal: { + label: 'Wrong-record refusal', + detail: 'The qualified workflow stops before the consequential action.', + }, + terminal_summary: { + label: 'Execution result', + detail: 'The presentation shows the retained terminal result.', + }, +} const clamp = (value, minimum, maximum) => Math.min(Math.max(value, minimum), maximum) @@ -54,41 +87,113 @@ function Marker({ type }) { ) } -function validChapter(chapter, durationMs) { - if (!chapter || typeof chapter !== 'object') return false - if (typeof chapter.id !== 'string' || !chapter.id) return false - if (typeof chapter.start_ms !== 'number' || typeof chapter.end_ms !== 'number') { - return false - } - if (chapter.start_ms < 0 || chapter.end_ms <= chapter.start_ms) return false - if (chapter.end_ms > durationMs) return false - if (!chapter.presentation || typeof chapter.presentation !== 'object') return false - if (typeof chapter.presentation.phase !== 'string') return false - if (!Array.isArray(chapter.presentation.graph_node_ids)) return false - return true -} - function validTimeline(timeline, manifest) { if (!timeline || typeof timeline !== 'object') return null - if (timeline.schema_version !== 'openadapt.rdp-media-timeline.v1') return null - if (timeline.video_sha256 !== manifest?.video_sha256) return null - if (!Number.isFinite(timeline.duration_ms) || timeline.duration_ms <= 0) return null - if (!Array.isArray(timeline.chapters) || !timeline.chapters.length) return null - if (!timeline.chapters.every((chapter) => validChapter(chapter, timeline.duration_ms))) { + if (timeline.schema_version !== 'openadapt.rdp-hybrid-presentation.v1') { return null } + const derivative = timeline.derivative + if (!derivative || typeof derivative !== 'object') return null + if (derivative.video_sha256 !== manifest?.video_sha256) return null + if (derivative.video !== manifest?.video) return null + if (!Number.isInteger(derivative.fps) || derivative.fps <= 0) return null + if (!Number.isInteger(derivative.frame_count) || derivative.frame_count <= 0) { + return null + } + if (timeline.program_graph_sha256 !== manifest?.program_graph_sha256) return null + if (!Array.isArray(timeline.timeline) || !timeline.timeline.length) return null + const duration = derivative.frame_count / derivative.fps + let expectedStart = 0 + const valid = timeline.timeline.every((entry) => { + if (!entry || typeof entry !== 'object') return false + if (typeof entry.phase !== 'string' || !PHASE_PRESENTATION[entry.phase]) { + return false + } + if (!Number.isInteger(entry.start_frame)) return false + if (!Number.isInteger(entry.end_frame_exclusive)) return false + if ( + entry.start_frame !== expectedStart || + entry.start_frame < 0 || + entry.end_frame_exclusive <= entry.start_frame + ) { + return false + } + if (entry.end_frame_exclusive > derivative.frame_count) return false + if (!Number.isFinite(entry.start_pts_s) || !Number.isFinite(entry.end_pts_s)) { + return false + } + if (entry.start_pts_s !== entry.start_frame / derivative.fps) return false + if (entry.end_pts_s !== entry.end_frame_exclusive / derivative.fps) return false + expectedStart = entry.end_frame_exclusive + return entry.end_pts_s <= duration + }) + if (!valid || expectedStart !== derivative.frame_count) return null return timeline } -function activeChapter(timeline, currentMs) { +function activeEntry(timeline, currentMs) { if (!timeline) return null + const frame = Math.min( + timeline.derivative.frame_count - 1, + Math.max(0, Math.floor((currentMs / 1000) * timeline.derivative.fps)) + ) return ( - timeline.chapters.find( - (chapter) => currentMs >= chapter.start_ms && currentMs < chapter.end_ms - ) ?? timeline.chapters.at(-1) + timeline.timeline.find( + (entry) => + frame >= entry.start_frame && frame < entry.end_frame_exclusive + ) ?? timeline.timeline.at(-1) ) } +function phaseChapters(timeline) { + if (!timeline) return [] + const chapters = [] + for (const entry of timeline.timeline) { + const previous = chapters.at(-1) + if (previous?.phase === entry.phase) { + previous.end_pts_s = entry.end_pts_s + previous.end_frame_exclusive = entry.end_frame_exclusive + } else { + chapters.push({ + phase: entry.phase, + start_pts_s: entry.start_pts_s, + end_pts_s: entry.end_pts_s, + start_frame: entry.start_frame, + end_frame_exclusive: entry.end_frame_exclusive, + }) + } + } + return chapters +} + +function factsAsSignals(facts) { + if (!facts || typeof facts !== 'object') return [] + const signals = [] + if (Object.hasOwn(facts, 'model_calls')) { + signals.push({ type: 'flow', label: 'Model calls', value: String(facts.model_calls) }) + } + if (typeof facts.identity === 'string') { + signals.push({ type: 'identity', label: 'Identity', value: facts.identity }) + } + if (typeof facts.effect === 'string') { + signals.push({ + type: facts.effect === 'not_written' ? 'halt' : 'proof', + label: 'Effect', + value: facts.effect, + }) + } + if (typeof facts.effect_verifier_kind === 'string') { + signals.push({ type: 'proof', label: 'Verifier', value: facts.effect_verifier_kind }) + } + if (typeof facts.authorization === 'string') { + signals.push({ type: 'identity', label: 'Authorization', value: facts.authorization }) + } + if (typeof facts.outcome === 'string') { + signals.push({ type: 'flow', label: 'Outcome', value: facts.outcome }) + } + return signals +} + export default function RdpHybridPresentation({ videoSrc = '/demos/rdp/openadapt-rdp-demo.mp4', poster = '/demos/rdp/poster.jpg', @@ -156,10 +261,25 @@ export default function RdpHybridPresentation({ () => validTimeline(timelinePayload, manifest), [manifest, timelinePayload] ) - const chapter = activeChapter(timeline, currentMs) - const activeNodeIds = new Set(chapter?.presentation?.graph_node_ids ?? []) + const entry = activeEntry(timeline, currentMs) + const chapters = useMemo(() => phaseChapters(timeline), [timeline]) + const chapter = chapters.find( + (item) => + entry && + entry.start_frame >= item.start_frame && + entry.start_frame < item.end_frame_exclusive + ) + const activeNodeIds = new Set( + entry?.compiled_graph?.node_id ? [entry.compiled_graph.node_id] : [] + ) const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [] - const progress = durationMs ? clamp((currentMs / durationMs) * 100, 0, 100) : 0 + const activeNode = nodes.find((node) => node.id === entry?.compiled_graph?.node_id) + const parameterNames = Array.isArray(graph?.bundle?.params) + ? graph.bundle.params + .map((parameter) => parameter?.name) + .filter((name) => typeof name === 'string') + : [] + const phaseContent = entry ? PHASE_PRESENTATION[entry.phase] : null const toggle = () => { const video = videoRef.current @@ -171,7 +291,13 @@ export default function RdpHybridPresentation({ const seek = (milliseconds) => { const video = videoRef.current if (!video || !timeline) return - const exactMs = clamp(milliseconds, 0, timeline.duration_ms) + const exactMs = clamp( + milliseconds, + 0, + Math.round( + (timeline.derivative.frame_count / timeline.derivative.fps) * 1000 + ) + ) video.currentTime = exactMs / 1000 setCurrentMs(exactMs) } @@ -213,9 +339,9 @@ export default function RdpHybridPresentation({ Your browser does not support this presentation.