Skip to content

Commit aedf62f

Browse files
committed
feat: add interactive approval playground to homepage
1 parent 46de6b9 commit aedf62f

2 files changed

Lines changed: 648 additions & 9 deletions

File tree

src/App.jsx

Lines changed: 186 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,51 @@ const enterpriseIntegrations = [
148148
{ name: "EDR · network controls", label: "Respond with context", text: "Use endpoint risk and network policy without tying your security program to one AI agent.", icon: PlugZap },
149149
];
150150

151+
const approvalScenarios = [
152+
{
153+
id: "customer-report",
154+
label: "Share customer data",
155+
agent: "Claude Code",
156+
title: "Export customer report",
157+
action: "Send customer-report.csv to an external collaboration workspace.",
158+
walletBody: "Claude Code wants to send a sensitive file outside the company workspace.",
159+
requestedBy: "Research team workspace",
160+
policy: "A manager must approve",
161+
policyDetail: "Customer data cannot leave the company without a manager's decision.",
162+
destination: "external.collab",
163+
digest: "8F2A…9C11",
164+
icon: Database,
165+
},
166+
{
167+
id: "production-change",
168+
label: "Change production",
169+
agent: "Codex",
170+
title: "Update production workflow",
171+
action: "Modify the deployment workflow used by the production environment.",
172+
walletBody: "Codex wants to change a workflow that can deploy code to production.",
173+
requestedBy: "Platform team workspace",
174+
policy: "Engineering lead approval",
175+
policyDetail: "Production changes require an engineering lead to review the exact update.",
176+
destination: "github.com/company/app",
177+
digest: "2C41…A77B",
178+
icon: GitFork,
179+
},
180+
{
181+
id: "private-repository",
182+
label: "Connect private code",
183+
agent: "OpenClaw",
184+
title: "Connect private repository",
185+
action: "Give a new agent access to a private source-code repository.",
186+
walletBody: "OpenClaw wants access to private company code for the first time.",
187+
requestedBy: "Operations workspace",
188+
policy: "Repository owner approval",
189+
policyDetail: "A repository owner must approve new agent access to private code.",
190+
destination: "private repository",
191+
digest: "71DD…3B04",
192+
icon: PlugZap,
193+
},
194+
];
195+
151196
const reveal = {
152197
hidden: { opacity: 0, y: 28 },
153198
visible: (delay = 0) => ({
@@ -418,11 +463,11 @@ function Hero() {
418463
>
419464
<motion.a
420465
className="button button-brand"
421-
href="#product"
466+
href="#approval-demo"
422467
whileHover={{ y: -2 }}
423468
whileTap={{ scale: 0.98 }}
424469
>
425-
Explore the product <ArrowDown size={15} />
470+
Try a signed approval <ArrowDown size={15} />
426471
</motion.a>
427472
<motion.a
428473
className="button button-quiet"
@@ -1060,7 +1105,7 @@ function AgentSecurityPrimer() {
10601105
);
10611106
}
10621107

1063-
function VtiWalletMock({ available, decision, onApprove, onDeny, onReset }) {
1108+
function VtiWalletMock({ available, decision, onApprove, onDeny, onReset, request = approvalScenarios[0] }) {
10641109
const reduceMotion = useReducedMotion();
10651110

10661111
return (
@@ -1082,12 +1127,12 @@ function VtiWalletMock({ available, decision, onApprove, onDeny, onReset }) {
10821127
<motion.div className="wallet-request" key="pending" initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }}>
10831128
<div className="wallet-request-icon"><BellRing size={20} /></div>
10841129
<p className="eyebrow">ONEComputer needs a decision</p>
1085-
<h3>Export customer report</h3>
1086-
<p>Claude Code wants to send a sensitive file outside the company workspace.</p>
1130+
<h3>{request.title}</h3>
1131+
<p>{request.walletBody}</p>
10871132
<div className="wallet-request-facts">
1088-
<span><small>Requested by</small><b>Research team workspace</b></span>
1089-
<span><small>Policy</small><b>A manager must approve</b></span>
1090-
<span><small>Task digest</small><code>8F2A…9C11</code></span>
1133+
<span><small>Requested by</small><b>{request.requestedBy}</b></span>
1134+
<span><small>Policy</small><b>{request.policy}</b></span>
1135+
<span><small>Task digest</small><code>{request.digest}</code></span>
10911136
<span><small>Expires</small><b>4 minutes</b></span>
10921137
</div>
10931138
<div className="wallet-actions">
@@ -1114,6 +1159,138 @@ function VtiWalletMock({ available, decision, onApprove, onDeny, onReset }) {
11141159
);
11151160
}
11161161

1162+
function HomeApprovalDemo() {
1163+
const reduceMotion = useReducedMotion();
1164+
const [scenarioIndex, setScenarioIndex] = useState(0);
1165+
const [decision, setDecision] = useState("pending");
1166+
const request = approvalScenarios[scenarioIndex];
1167+
const RequestIcon = request.icon;
1168+
1169+
const chooseScenario = (index) => {
1170+
setScenarioIndex(index);
1171+
setDecision("pending");
1172+
};
1173+
1174+
const outcome = decision === "approved"
1175+
? "Approval verified · this action may continue once"
1176+
: decision === "denied"
1177+
? "Denial verified · this action remains blocked"
1178+
: "Action paused · waiting for the manager";
1179+
1180+
return (
1181+
<section className="section home-approval" id="approval-demo">
1182+
<div className="home-approval-glow" aria-hidden="true" />
1183+
<div className="shell">
1184+
<Reveal className="section-intro home-approval-intro">
1185+
<p className="eyebrow">Try the approval path</p>
1186+
<h2>A sensitive action should never approve itself.</h2>
1187+
<p>Choose something an agent wants to do, then make the decision in the separate VTI wallet. ONEComputer can pause the request and verify the answer—but it cannot create the answer.</p>
1188+
</Reveal>
1189+
1190+
<div className="approval-scenarios" role="group" aria-label="Choose an approval example">
1191+
{approvalScenarios.map((scenario, index) => {
1192+
const Icon = scenario.icon;
1193+
const selected = index === scenarioIndex;
1194+
return (
1195+
<motion.button
1196+
type="button"
1197+
className={selected ? "is-active" : ""}
1198+
key={scenario.id}
1199+
aria-pressed={selected}
1200+
onClick={() => chooseScenario(index)}
1201+
whileHover={{ y: -2 }}
1202+
whileTap={{ scale: 0.98 }}
1203+
>
1204+
<span><Icon size={15} /></span>
1205+
<small>Example {String(index + 1).padStart(2, "0")}</small>
1206+
<b>{scenario.label}</b>
1207+
</motion.button>
1208+
);
1209+
})}
1210+
</div>
1211+
1212+
<div className={`approval-playground is-${decision}`}>
1213+
<div className="approval-request-panel">
1214+
<div className="approval-panel-top">
1215+
<span><i /> ONEComputer workspace</span>
1216+
<small>{request.agent} · isolated</small>
1217+
</div>
1218+
1219+
<AnimatePresence mode="wait">
1220+
<motion.div
1221+
className="approval-request-copy"
1222+
key={request.id}
1223+
initial={{ opacity: 0, y: 12 }}
1224+
animate={{ opacity: 1, y: 0 }}
1225+
exit={{ opacity: 0, y: -10 }}
1226+
>
1227+
<span className="approval-request-icon"><RequestIcon size={20} /></span>
1228+
<p className="eyebrow">Agent request</p>
1229+
<h3>{request.title}</h3>
1230+
<p>{request.action}</p>
1231+
<div className="approval-request-details">
1232+
<span><small>Destination</small><b>{request.destination}</b></span>
1233+
<span><small>Request digest</small><code>{request.digest}</code></span>
1234+
</div>
1235+
</motion.div>
1236+
</AnimatePresence>
1237+
1238+
<div className="approval-policy-trigger">
1239+
<ShieldCheck size={17} />
1240+
<span><small>Why ONEComputer paused it</small><b>{request.policyDetail}</b></span>
1241+
</div>
1242+
1243+
<div className="approval-event-list" aria-label="Approval verification progress">
1244+
<span className="is-complete"><i><Check size={11} /></i><b>Request detected</b><small>{request.agent}</small></span>
1245+
<span className="is-complete"><i><LockKeyhole size={11} /></i><b>Action paused</b><small>Policy applied</small></span>
1246+
<span className="is-complete"><i><Send size={11} /></i><b>Sent to wallet</b><small>Encrypted</small></span>
1247+
<span className={decision !== "pending" ? "is-complete" : ""}><i>{decision !== "pending" ? <Check size={11} /> : <span />}</i><b>Decision verified</b><small>{decision === "pending" ? "Waiting" : "Signed"}</small></span>
1248+
</div>
1249+
1250+
<motion.div
1251+
className="approval-outcome"
1252+
aria-live="polite"
1253+
key={`${request.id}-${decision}`}
1254+
initial={{ opacity: 0, y: 8 }}
1255+
animate={{ opacity: 1, y: 0 }}
1256+
>
1257+
<span>{decision === "approved" ? <BadgeCheck size={18} /> : decision === "denied" ? <X size={18} /> : <BellRing size={18} />}</span>
1258+
<div><small>ONEComputer status</small><b>{outcome}</b></div>
1259+
</motion.div>
1260+
</div>
1261+
1262+
<div className="approval-secure-link" aria-hidden="true">
1263+
<motion.span
1264+
animate={reduceMotion ? undefined : { x: ["-42%", "42%", "-42%"] }}
1265+
transition={{ duration: 4.2, repeat: Infinity, ease: "easeInOut" }}
1266+
/>
1267+
<div><Fingerprint size={15} /><b>OpenVTC</b><small>Secure request · signed response</small></div>
1268+
</div>
1269+
1270+
<div className="approval-wallet-column">
1271+
<div className="approval-instruction"><Smartphone size={15} /><span><b>Your turn</b><small>Approve or deny in the wallet</small></span><ArrowRight size={14} /></div>
1272+
<VtiWalletMock
1273+
available
1274+
decision={decision}
1275+
request={request}
1276+
onApprove={() => setDecision("approved")}
1277+
onDeny={() => setDecision("denied")}
1278+
onReset={() => setDecision("pending")}
1279+
/>
1280+
</div>
1281+
</div>
1282+
1283+
<div className="approval-principles">
1284+
<span><b>01</b><small>ONEComputer pauses the exact action</small></span>
1285+
<span><b>02</b><small>The manager decides on a separate device</small></span>
1286+
<span><b>03</b><small>The signed answer works for this request only</small></span>
1287+
<a href="/openvtc/">Understand OpenVTC <ArrowRight size={14} /></a>
1288+
</div>
1289+
</div>
1290+
</section>
1291+
);
1292+
}
1293+
11171294
function EnterpriseJourney({ id = "journey" }) {
11181295
const reduceMotion = useReducedMotion();
11191296
const [active, setActive] = useState(0);
@@ -1482,9 +1659,9 @@ export default function App({ page = "home" }) {
14821659
<main>
14831660
<Hero />
14841661
<ProductTour />
1662+
<HomeApprovalDemo />
14851663
<AgentCoverage />
14861664
<EnterpriseStory />
1487-
<EnterpriseJourney />
14881665
<SecurityFlow />
14891666
<EnterpriseIntegrations />
14901667
<OpenSource />

0 commit comments

Comments
 (0)