ava-engine 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.env.example +63 -0
  2. package/CLA.md +43 -0
  3. package/COMMERCIAL-LICENSE.md +43 -0
  4. package/LICENSE +73 -0
  5. package/README.md +240 -2
  6. package/THIRD_PARTY_NOTICES.md +38 -0
  7. package/TRADEMARKS.md +31 -0
  8. package/bin/ava-engine.mjs +12 -0
  9. package/lib/asr.mjs +147 -0
  10. package/lib/cli/apply.mjs +58 -0
  11. package/lib/cli/config.mjs +95 -0
  12. package/lib/cli/main.mjs +187 -0
  13. package/lib/cli/setup-server.mjs +195 -0
  14. package/lib/cli/setup.mjs +233 -0
  15. package/lib/fetch-models.mjs +104 -0
  16. package/lib/guard.mjs +92 -0
  17. package/lib/kokoro-model.mjs +51 -0
  18. package/lib/models.mjs +47 -0
  19. package/lib/openrouter.mjs +153 -0
  20. package/lib/ours.mjs +766 -0
  21. package/lib/platforms.mjs +201 -0
  22. package/lib/state.mjs +150 -0
  23. package/lib/stt.mjs +22 -0
  24. package/lib/tts.mjs +178 -0
  25. package/lib/workflow.mjs +18 -0
  26. package/package.json +80 -4
  27. package/public/app.js +1708 -0
  28. package/public/demo.js +420 -0
  29. package/public/favicon.svg +1 -0
  30. package/public/fonts/OFL.txt +96 -0
  31. package/public/fonts/inter-tight-var.woff2 +0 -0
  32. package/public/fonts/inter-var.woff2 +0 -0
  33. package/public/fonts/plex-mono-400.woff2 +0 -0
  34. package/public/fonts/plex-mono-500.woff2 +0 -0
  35. package/public/img/cta-1600.avif +0 -0
  36. package/public/img/cta-1600.webp +0 -0
  37. package/public/img/cta-2400.avif +0 -0
  38. package/public/img/cta-2400.webp +0 -0
  39. package/public/img/cta-960.avif +0 -0
  40. package/public/img/cta-960.webp +0 -0
  41. package/public/img/flows-1600.avif +0 -0
  42. package/public/img/flows-1600.webp +0 -0
  43. package/public/img/flows-2400.avif +0 -0
  44. package/public/img/flows-2400.webp +0 -0
  45. package/public/img/flows-960.avif +0 -0
  46. package/public/img/flows-960.webp +0 -0
  47. package/public/img/hero-1600.avif +0 -0
  48. package/public/img/hero-1600.webp +0 -0
  49. package/public/img/hero-2400.avif +0 -0
  50. package/public/img/hero-2400.webp +0 -0
  51. package/public/img/hero-960.avif +0 -0
  52. package/public/img/hero-960.webp +0 -0
  53. package/public/img/p-csm-480.avif +0 -0
  54. package/public/img/p-csm-480.webp +0 -0
  55. package/public/img/p-csm-800.avif +0 -0
  56. package/public/img/p-csm-800.webp +0 -0
  57. package/public/img/p-docs-480.avif +0 -0
  58. package/public/img/p-docs-480.webp +0 -0
  59. package/public/img/p-docs-800.avif +0 -0
  60. package/public/img/p-docs-800.webp +0 -0
  61. package/public/img/p-quiet-480.avif +0 -0
  62. package/public/img/p-quiet-480.webp +0 -0
  63. package/public/img/p-quiet-800.avif +0 -0
  64. package/public/img/p-quiet-800.webp +0 -0
  65. package/public/img/p-tickets-480.avif +0 -0
  66. package/public/img/p-tickets-480.webp +0 -0
  67. package/public/img/p-tickets-800.avif +0 -0
  68. package/public/img/p-tickets-800.webp +0 -0
  69. package/public/img/p-trial-480.avif +0 -0
  70. package/public/img/p-trial-480.webp +0 -0
  71. package/public/img/p-trial-800.avif +0 -0
  72. package/public/img/p-trial-800.webp +0 -0
  73. package/public/img/sees-1600.avif +0 -0
  74. package/public/img/sees-1600.webp +0 -0
  75. package/public/img/sees-2400.avif +0 -0
  76. package/public/img/sees-2400.webp +0 -0
  77. package/public/img/sees-960.avif +0 -0
  78. package/public/img/sees-960.webp +0 -0
  79. package/public/img/speaks-1600.avif +0 -0
  80. package/public/img/speaks-1600.webp +0 -0
  81. package/public/img/speaks-2400.avif +0 -0
  82. package/public/img/speaks-2400.webp +0 -0
  83. package/public/img/speaks-960.avif +0 -0
  84. package/public/img/speaks-960.webp +0 -0
  85. package/public/img/voice-1600.avif +0 -0
  86. package/public/img/voice-1600.webp +0 -0
  87. package/public/img/voice-2400.avif +0 -0
  88. package/public/img/voice-2400.webp +0 -0
  89. package/public/img/voice-960.avif +0 -0
  90. package/public/img/voice-960.webp +0 -0
  91. package/public/index.html +440 -0
  92. package/public/landing.html +975 -0
  93. package/public/pcm-worklet.js +11 -0
  94. package/public/perceptionWorker.js +103 -0
  95. package/public/rtc.js +60 -0
  96. package/public/setup.css +94 -0
  97. package/public/setup.html +129 -0
  98. package/public/setup.js +264 -0
  99. package/public/specialist.html +92 -0
  100. package/public/specialist.js +132 -0
  101. package/public/style.css +836 -0
  102. package/public/vendor/lenis.min.js +3 -0
  103. package/scripts/fetch-models.sh +15 -0
  104. package/server.mjs +502 -0
package/public/demo.js ADDED
@@ -0,0 +1,420 @@
1
+ "use strict";
2
+ /* global S, updateStageLayout, observeNow, sendTurn, joinCall -- defined by app.js, loaded first */
3
+ /* ═══════════════════════════════════════════════════════════════════════════
4
+ DEMO MODE — scripted, self-recording showcase runs.
5
+ Renders realistic platform screens to a canvas (which Ava's vision reads),
6
+ drives the REAL agents with scripted user messages + deliberate human errors,
7
+ plays Ava's real voice, and records the whole tab to a downloadable .webm.
8
+ Activated with ?demo=simple|moderate|complex.
9
+ ═══════════════════════════════════════════════════════════════════════════ */
10
+ (function () {
11
+ const params = new URLSearchParams(location.search);
12
+ const which = params.get("demo");
13
+ if (!which) return;
14
+
15
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
16
+
17
+ // ── canvas "screen" the agent sees + a demo banner ──
18
+ // main canvas = what's shared/recorded (sized to the stage). Screens draw into a
19
+ // fixed "site viewport" (siteCv), then get composited into a browser window.
20
+ let cv = document.createElement("canvas");
21
+ cv.width = 1600; cv.height = 900;
22
+ let g = cv.getContext("2d");
23
+ const SW = 1440, SH = 900;
24
+ const siteCv = document.createElement("canvas"); siteCv.width = SW; siteCv.height = SH;
25
+ const sg = siteCv.getContext("2d");
26
+ let stream = null;
27
+
28
+ // "Basewell" — a fictional backend platform drawn for the demo (light theme, orange accent)
29
+ const P = { bg: "#FBFAF8", side: "#F3F1ED", panel: "#FFFFFF", bd: "#E7E3DC", bd2: "#D9D3C9", card: "#FFFFFF", input: "#FFFFFF", tx: "#1A1714", mut: "#6B645B", fnt: "#9C948A", acc: "#F2541B", accInk: "#FFFFFF", amber: "#C2620A", red: "#D92D20", blue: "#2563EB",
30
+ btn2: "#FFFFFF", railOn: "rgba(26,23,20,0.07)", accTint: "rgba(242,84,27,0.10)", accLine: "rgba(242,84,27,0.32)", ok: "#15803D", okTint: "rgba(21,128,61,0.10)", okLine: "rgba(21,128,61,0.30)" };
31
+ const FONT = "'Inter','SF Pro Text',-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif";
32
+ function rr(x, y, w, h, r) { r = Math.min(r, w / 2, h / 2); g.beginPath(); g.moveTo(x + r, y); g.arcTo(x + w, y, x + w, y + h, r); g.arcTo(x + w, y + h, x, y + h, r); g.arcTo(x, y + h, x, y, r); g.arcTo(x, y, x + w, y, r); g.closePath(); }
33
+ function txt(t, x, y, o = {}) { g.fillStyle = o.c || P.tx; g.font = `${o.w || 400} ${o.s || 14}px ${FONT}`; g.textAlign = o.a || "left"; g.textBaseline = "alphabetic"; g.fillText(t, x, y); g.textAlign = "left"; }
34
+ function box(x, y, w, h, r, o = {}) { rr(x, y, w, h, r); if (o.fill) { g.fillStyle = o.fill; g.fill(); } if (o.stroke) { g.strokeStyle = o.stroke; g.lineWidth = o.sw || 1; g.stroke(); } }
35
+ function input(x, y, w, h, val, o = {}) { box(x, y, w, h, 7, { fill: o.fill || P.input, stroke: o.stroke || P.bd2 }); txt(val || o.ph || "", x + 13, y + h / 2 + 5, { s: 14.5, c: val ? (o.tc || P.tx) : (o.phc || P.fnt) }); }
36
+ function btn(x, y, w, h, label, o = {}) { box(x, y, w, h, 7, { fill: o.fill || P.acc, stroke: o.stroke }); txt(label, x + w / 2, y + h / 2 + 5, { s: o.s || 14, w: 600, c: o.tx || P.accInk, a: "center" }); }
37
+ function mark(cx, cy, r) { // Basewell mark: an orange tile with three stacked "disk" bars
38
+ g.fillStyle = P.acc; rr(cx - r, cy - r, r * 2, r * 2, r * 0.45); g.fill();
39
+ g.fillStyle = "#fff";
40
+ [[-0.42, 1.1], [0, 0.82], [0.42, 0.54]].forEach(([dy, w]) => { rr(cx - r * w / 2 - r * 0.2, cy + dy * r - r * 0.12, r * w + r * 0.4, r * 0.24, r * 0.12); g.fill(); });
41
+ }
42
+ function icon(name, x, y, c) { // 20px line icons, top-left at x,y
43
+ g.strokeStyle = c; g.fillStyle = c; g.lineWidth = 1.6; g.lineJoin = "round"; g.lineCap = "round";
44
+ const s = (fn) => { g.beginPath(); fn(); g.stroke(); };
45
+ if (name === "home") s(() => { g.moveTo(x, y + 8); g.lineTo(x + 10, y); g.lineTo(x + 20, y + 8); g.moveTo(x + 2, y + 7); g.lineTo(x + 2, y + 19); g.lineTo(x + 18, y + 19); g.lineTo(x + 18, y + 7); });
46
+ else if (name === "table") { s(() => { g.rect(x, y + 1, 20, 18); g.moveTo(x, y + 7); g.lineTo(x + 20, y + 7); g.moveTo(x + 7, y + 1); g.lineTo(x + 7, y + 19); }); }
47
+ else if (name === "sql") s(() => { g.moveTo(x + 4, y + 4); g.lineTo(x, y + 10); g.lineTo(x + 4, y + 16); g.moveTo(x + 12, y + 4); g.lineTo(x + 20, y + 4); });
48
+ else if (name === "db") { s(() => { g.ellipse(x + 10, y + 3, 8, 3, 0, 0, 7); }); s(() => { g.moveTo(x + 2, y + 3); g.lineTo(x + 2, y + 16); g.ellipse(x + 10, y + 16, 8, 3, 0, 0, Math.PI); g.moveTo(x + 18, y + 16); g.lineTo(x + 18, y + 3); }); }
49
+ else if (name === "auth") { s(() => { g.arc(x + 7, y + 6, 4, 0, 7); }); s(() => { g.moveTo(x, y + 19); g.arc(x + 7, y + 19, 7, Math.PI, 0); }); s(() => { g.moveTo(x + 14, y + 8); g.lineTo(x + 20, y + 8); g.moveTo(x + 17, y + 8); g.lineTo(x + 17, y + 13); }); }
50
+ else if (name === "storage") s(() => { g.moveTo(x, y + 4); g.lineTo(x + 7, y + 4); g.lineTo(x + 9, y + 7); g.lineTo(x + 20, y + 7); g.lineTo(x + 20, y + 18); g.lineTo(x, y + 18); g.closePath(); });
51
+ else if (name === "fn") { g.font = "700 18px " + FONT; g.fillText("ƒ", x + 5, y + 17); }
52
+ else if (name === "realtime") s(() => { g.moveTo(x, y + 10); g.lineTo(x + 5, y + 10); g.lineTo(x + 8, y + 3); g.lineTo(x + 12, y + 17); g.lineTo(x + 15, y + 10); g.lineTo(x + 20, y + 10); });
53
+ else if (name === "gear") { s(() => { g.arc(x + 10, y + 10, 4, 0, 7); }); s(() => { g.arc(x + 10, y + 10, 9, 0, 7); }); }
54
+ g.lineWidth = 1;
55
+ }
56
+
57
+ // icon rail (64px) + secondary nav panel (240px)
58
+ function sbChrome(project, active) {
59
+ g.fillStyle = P.bg; g.fillRect(0, 0, cv.width, cv.height);
60
+ g.fillStyle = P.side; g.fillRect(0, 0, 64, cv.height);
61
+ mark(32, 40, 12);
62
+ const rail = ["home", "table", "sql", "db", "auth", "storage", "fn", "realtime"];
63
+ rail.forEach((n, i) => { const y = 96 + i * 52; if (n === active) { g.fillStyle = P.railOn; rr(12, y - 6, 40, 40, 9); g.fill(); } icon(n, 22, y, n === active ? P.tx : P.mut); });
64
+ icon("gear", 22, cv.height - 60, P.mut);
65
+ // secondary panel
66
+ g.fillStyle = P.panel; g.fillRect(64, 0, 240, cv.height); g.strokeStyle = P.bd; g.beginPath(); g.moveTo(304, 0); g.lineTo(304, cv.height); g.stroke();
67
+ txt(project, 84, 44, { s: 15, w: 600 });
68
+ txt("▾", 280, 44, { s: 12, c: P.mut, a: "right" });
69
+ const nav = ["Project Overview", "Tables", "Query", "Database", "Users & Auth", "Files", "Functions", "Live sync", "Usage", "Logs", "API", "Settings"];
70
+ nav.forEach((n, i) => { const y = 92 + i * 40, on = n === active; if (on) { g.fillStyle = P.accTint; rr(76, y - 18, 216, 34, 7); g.fill(); } txt(n, 92, y + 4, { s: 13.5, c: on ? P.acc : P.mut, w: on ? 600 : 400 }); });
71
+ }
72
+ function topbar(title, right) {
73
+ g.fillStyle = P.bg; g.fillRect(304, 0, cv.width - 304, 60); g.strokeStyle = P.bd; g.beginPath(); g.moveTo(304, 60); g.lineTo(cv.width, 60); g.stroke();
74
+ txt(title, 336, 38, { s: 15, w: 500, c: P.tx });
75
+ if (right) { btn(cv.width - 150, 15, 118, 32, right, { fill: P.btn2, tx: P.tx, stroke: P.bd2, s: 13 }); }
76
+ }
77
+
78
+ // ── screens ──
79
+ function sbSignIn(o = {}) {
80
+ g.fillStyle = P.bg; g.fillRect(0, 0, cv.width, cv.height);
81
+ const cx = cv.width / 2, cw = 400, x = cx - cw / 2;
82
+ mark(cx, 150, 15);
83
+ txt("Welcome back", cx, 220, { s: 26, w: 700, a: "center" });
84
+ txt("Sign in to your account", cx, 250, { s: 15, c: P.mut, a: "center" });
85
+ let y = 290;
86
+ btn(x, y, cw, 44, "Continue with GitHub", { fill: P.btn2, tx: P.tx, stroke: P.bd2, s: 14 });
87
+ y += 58; btn(x, y, cw, 44, "Continue with SSO", { fill: P.btn2, tx: P.tx, stroke: P.bd2, s: 14 });
88
+ y += 66; g.strokeStyle = P.bd; g.beginPath(); g.moveTo(x, y); g.lineTo(x + cw, y); g.stroke(); g.fillStyle = P.bg; g.fillRect(cx - 18, y - 10, 36, 20); txt("or", cx, y + 5, { s: 13, c: P.mut, a: "center" });
89
+ y += 34; txt("Email", x, y, { s: 13, c: P.mut, w: 500 });
90
+ input(x, y + 10, cw, 44, o.email || "", { ph: "you@example.com", stroke: o.invalid ? P.red : P.bd2 });
91
+ y += 64; if (o.invalid) { txt("Please include an '@' in the email address.", x, y + 4, { s: 12.5, c: P.red, w: 500 }); y += 26; }
92
+ txt("Password", x, y, { s: 13, c: P.mut, w: 500 }); txt("Forgot Password?", x + cw, y, { s: 12.5, c: P.acc, a: "right" });
93
+ input(x, y + 10, cw, 44, o.pw || "••••••••••", {});
94
+ y += 74; btn(x, y, cw, 46, "Sign in", { s: 15 });
95
+ y += 78; txt("Don't have an account? Sign Up", cx, y, { s: 13.5, c: P.mut, a: "center" });
96
+ }
97
+ function sbOrgs() {
98
+ sbChrome("Select workspace", "Project Overview");
99
+ topbar("Workspaces");
100
+ txt("Your workspaces", 336, 118, { s: 22, w: 700 });
101
+ box(336, 150, cv.width - 400, 108, 12, { fill: P.panel, stroke: P.bd });
102
+ txt("You don't have any workspaces yet", 364, 200, { s: 16, w: 600 });
103
+ txt("Create a workspace to start building your project.", 364, 228, { s: 13.5, c: P.mut });
104
+ btn(cv.width - 260, 178, 190, 44, "+ New workspace", { s: 14 });
105
+ }
106
+ function sbNewOrg(o = {}) {
107
+ sbChrome("New workspace", "Project Overview");
108
+ topbar("Create a new workspace");
109
+ txt("Create a new workspace", 336, 122, { s: 22, w: 700 });
110
+ txt("This is your workspace on Basewell.", 336, 150, { s: 13.5, c: P.mut });
111
+ let y = 200; txt("Name", 336, y, { s: 13.5, c: P.mut, w: 500 });
112
+ input(336, y + 12, 460, 44, o.name || "", { ph: "Workspace name" });
113
+ y += 96; txt("Plan", 336, y, { s: 13.5, c: P.mut, w: 500 });
114
+ box(336, y + 12, 240, 44, 7, { fill: P.input, stroke: P.acc }); txt("Free — $0/month", 349, y + 40, { s: 14, w: 500 }); txt("▾", 556, y + 40, { s: 12, c: P.mut });
115
+ btn(336, y + 100, 180, 44, "Create workspace", { s: 14 });
116
+ }
117
+ function sbNewProject(o = {}) {
118
+ sbChrome("acme", "Project Overview");
119
+ topbar("New project");
120
+ txt("Create a new project", 336, 118, { s: 22, w: 700 });
121
+ txt("Your project will have its own dedicated instance and full Postgres database.", 336, 146, { s: 13, c: P.mut });
122
+ let y = 188;
123
+ txt("Workspace", 336, y, { s: 13, c: P.mut, w: 500 }); box(336, y + 10, 300, 42, 7, { fill: P.input, stroke: P.bd2 }); txt("acme", 349, y + 36, { s: 14 }); txt("▾", 616, y + 36, { s: 12, c: P.mut });
124
+ y += 84; txt("Project name", 336, y, { s: 13, c: P.mut, w: 500 }); input(336, y + 10, 520, 42, o.name || "acme-prod", {});
125
+ y += 84; txt("Database Password", 336, y, { s: 13, c: P.mut, w: 500 });
126
+ const weak = o.pw === "weak", strong = o.pw === "strong";
127
+ input(336, y + 10, 520, 42, o.pw ? "••••••••••••••••" : "", { ph: "Type a strong password", stroke: weak ? P.amber : strong ? P.ok : P.bd2 });
128
+ txt("Generate a password", 700, y, { s: 12.5, c: P.acc, a: "right" });
129
+ // strength meter
130
+ const segs = strong ? 4 : weak ? 1 : 0, cols = [P.red, P.amber, P.amber, P.ok];
131
+ for (let i = 0; i < 4; i++) { box(336 + i * 132, y + 62, 122, 5, 3, { fill: i < segs ? cols[segs - 1] : P.bd2 }); }
132
+ if (weak) txt("This password is too weak.", 336, y + 90, { s: 12.5, c: P.amber, w: 500 });
133
+ if (strong) txt("Strong — nice and secure.", 336, y + 90, { s: 12.5, c: P.ok, w: 500 });
134
+ y += 120; txt("Region", 336, y, { s: 13, c: P.mut, w: 500 }); box(336, y + 10, 300, 42, 7, { fill: P.input, stroke: P.bd2 }); txt("Southeast Asia (Singapore)", 349, y + 36, { s: 13.5 }); txt("▾", 616, y + 36, { s: 12, c: P.mut });
135
+ // footer
136
+ g.strokeStyle = P.bd; g.beginPath(); g.moveTo(304, cv.height - 74); g.lineTo(cv.width, cv.height - 74); g.stroke();
137
+ btn(cv.width - 340, cv.height - 60, 90, 40, "Cancel", { fill: P.btn2, tx: P.tx, stroke: P.bd2, s: 13.5 });
138
+ btn(cv.width - 240, cv.height - 60, 200, 40, "Create new project", { s: 14 });
139
+ }
140
+ function sbProvisioning() {
141
+ sbChrome("acme-prod", "Project Overview");
142
+ topbar("acme-prod", "Connect");
143
+ box(336, 120, cv.width - 400, 150, 12, { fill: P.panel, stroke: P.bd });
144
+ g.strokeStyle = P.bd2; g.lineWidth = 4; g.beginPath(); g.arc(400, 195, 22, 0, 7); g.stroke();
145
+ g.strokeStyle = P.acc; g.beginPath(); g.arc(400, 195, 22, -1.4, 1.2); g.stroke(); g.lineWidth = 1;
146
+ txt("Setting up your project", 448, 188, { s: 18, w: 600 });
147
+ txt("Provisioning your Postgres database — this usually takes a minute.", 448, 216, { s: 13.5, c: P.mut });
148
+ }
149
+ function sbDashboard() {
150
+ sbChrome("acme-prod", "Project Overview");
151
+ topbar("acme-prod", "Connect");
152
+ txt("Welcome to your project", 336, 118, { s: 22, w: 700 });
153
+ box(336, 138, 168, 34, 17, { fill: P.okTint, stroke: P.okLine });
154
+ g.fillStyle = P.ok; g.beginPath(); g.arc(358, 155, 5, 0, 7); g.fill(); txt("Project is healthy", 372, 160, { s: 13, c: P.ok, w: 600 });
155
+ // stat cards
156
+ const stats = [["Database", "Postgres 15.1", "healthy"], ["Auth", "0 users", "ready"], ["Storage", "0 files", "ready"], ["Live sync", "Connected", "live"]];
157
+ stats.forEach((s2, i) => { const x = 336 + i * 232, y = 196; box(x, y, 216, 104, 12, { fill: P.panel, stroke: P.bd }); txt(s2[0], x + 18, y + 34, { s: 15, w: 600 }); txt(s2[1], x + 18, y + 62, { s: 13.5, c: P.mut }); g.fillStyle = P.ok; g.beginPath(); g.arc(x + 22, y + 82, 4, 0, 7); g.fill(); txt(s2[2], x + 34, y + 86, { s: 12, c: P.ok }); });
158
+ // API keys card
159
+ const ay = 328; box(336, ay, cv.width - 400, 160, 12, { fill: P.panel, stroke: P.bd });
160
+ txt("Get your API keys", 360, ay + 34, { s: 16, w: 600 });
161
+ txt("Use these to connect your app to Basewell.", 360, ay + 60, { s: 13, c: P.mut });
162
+ txt("Project URL", 360, ay + 96, { s: 12, c: P.mut, w: 500 });
163
+ box(360, ay + 106, 460, 38, 7, { fill: P.input, stroke: P.bd2 }); txt("https://acme-prod.basewell.example", 373, ay + 130, { s: 13, c: P.tx });
164
+ txt("Public API key", 848, ay + 96, { s: 12, c: P.mut, w: 500 });
165
+ box(848, ay + 106, 460, 38, 7, { fill: P.input, stroke: P.bd2 }); txt("eyJhbGciOiJIUzI1NiIsInR5cCI6•••••••", 861, ay + 130, { s: 13, c: P.tx });
166
+ }
167
+
168
+ // ══════════ Stripe (light theme) ══════════
169
+ const SP = { bg: "#f6f9fc", side: "#ffffff", bd: "#e6ebf1", tx: "#1a1f36", mut: "#697386", pur: "#635bff", grn: "#3fb950", red: "#df1b41", amber: "#9a5b00", amberBg: "#fdecc8" };
170
+ function stChrome(active) {
171
+ g.fillStyle = SP.bg; g.fillRect(0, 0, cv.width, cv.height);
172
+ g.fillStyle = SP.side; g.fillRect(0, 0, 232, cv.height); g.strokeStyle = SP.bd; g.beginPath(); g.moveTo(232, 0); g.lineTo(232, cv.height); g.stroke();
173
+ txt("stripe", 28, 46, { s: 22, w: 700, c: SP.pur });
174
+ const nav = ["Home", "Balances", "Transactions", "Customers", "Product catalog", "Payments", "Connect", "Developers"];
175
+ nav.forEach((n, i) => { const y = 100 + i * 46, on = n === active; if (on) { g.fillStyle = "rgba(99,91,255,0.09)"; rr(12, y - 20, 208, 36, 8); g.fill(); } txt(n, 28, y + 4, { s: 14, c: on ? SP.pur : SP.mut, w: on ? 600 : 400 }); });
176
+ g.fillStyle = SP.side; g.fillRect(232, 0, cv.width - 232, 64); g.strokeStyle = SP.bd; g.beginPath(); g.moveTo(232, 64); g.lineTo(cv.width, 64); g.stroke();
177
+ box(cv.width - 210, 16, 170, 32, 16, { fill: SP.amberBg }); g.fillStyle = SP.amber; g.beginPath(); g.arc(cv.width - 186, 32, 5, 0, 7); g.fill(); txt("Test mode", cv.width - 170, 37, { s: 13, c: SP.amber, w: 700 });
178
+ }
179
+ function stDashboard() {
180
+ stChrome("Home"); txt("Today", 264, 116, { s: 24, w: 700, c: SP.tx });
181
+ box(264, 140, 360, 120, 10, { fill: "#fff", stroke: SP.bd }); txt("Gross volume", 288, 176, { s: 13, c: SP.mut, w: 500 }); txt("$0.00", 288, 214, { s: 30, w: 700, c: SP.tx }); txt("Test data", 288, 240, { s: 12, c: SP.amber, w: 600 });
182
+ box(648, 140, 360, 120, 10, { fill: "#fff", stroke: SP.bd }); txt("Payments", 672, 176, { s: 13, c: SP.mut, w: 500 }); txt("1 failed", 672, 214, { s: 30, w: 700, c: SP.red });
183
+ box(264, 288, cv.width - 304, 120, 10, { fill: "#fff", stroke: SP.bd }); txt("You're viewing test data", 288, 328, { s: 16, w: 600, c: SP.tx }); txt("Payments here use test cards and no real money moves. Toggle Test mode (top-right) off to go live.", 288, 356, { s: 13.5, c: SP.mut });
184
+ }
185
+ function stPayments() {
186
+ stChrome("Payments"); txt("Payments", 264, 116, { s: 24, w: 700, c: SP.tx });
187
+ box(264, 140, cv.width - 304, 320, 10, { fill: "#fff", stroke: SP.bd });
188
+ const cols = ["Amount", "Status", "Customer", "Date"]; cols.forEach((c, i) => txt(c, 288 + i * 250, 178, { s: 12, c: SP.mut, w: 600 }));
189
+ g.strokeStyle = SP.bd; g.beginPath(); g.moveTo(264, 196); g.lineTo(cv.width - 40, 196); g.stroke();
190
+ const rows = [["$49.00", "Failed", "test@acme.co", "Just now", SP.red], ["$20.00", "Succeeded", "jane@acme.co", "2:04 PM", SP.acc], ["$12.00", "Succeeded", "sam@acme.co", "1:40 PM", SP.acc]];
191
+ rows.forEach((r, i) => { const y = 234 + i * 56; txt(r[0], 288, y, { s: 14, w: 600, c: SP.tx }); box(538, y - 16, 90, 24, 12, { fill: r[4] === SP.red ? "#fce8ec" : "#e6f6ec" }); txt(r[1], 552, y, { s: 12, c: r[4], w: 600 }); txt(r[2], 788, y, { s: 13.5, c: SP.mut }); txt(r[3], 1038, y, { s: 13.5, c: SP.mut }); g.strokeStyle = SP.bd; g.beginPath(); g.moveTo(264, y + 20); g.lineTo(cv.width - 40, y + 20); g.stroke(); });
192
+ }
193
+ function stApiKeys() {
194
+ stChrome("Developers"); txt("API keys", 264, 116, { s: 24, w: 700, c: SP.tx }); txt("Test mode keys — safe to use in development.", 264, 144, { s: 13.5, c: SP.mut });
195
+ box(264, 172, cv.width - 304, 200, 10, { fill: "#fff", stroke: SP.bd });
196
+ txt("Publishable key", 288, 214, { s: 13, c: SP.mut, w: 600 }); box(288, 228, 560, 40, 7, { fill: SP.bg, stroke: SP.bd }); txt("pk_test_51Qw8kLZ2ab•••••••••••••••••", 302, 253, { s: 13.5, c: SP.tx });
197
+ txt("Secret key", 288, 308, { s: 13, c: SP.mut, w: 600 }); box(288, 322, 560, 40, 7, { fill: SP.bg, stroke: SP.bd }); txt("sk_test_51Qw8kLZ2•••••••••• Reveal", 302, 347, { s: 13.5, c: SP.tx });
198
+ btn(888, 228, 140, 40, "Reveal test key", { fill: SP.pur, tx: "#fff", s: 13 });
199
+ }
200
+
201
+ // ══════════ Google Workspace (light) ══════════
202
+ const GM = { bg: "#ffffff", panel: "#f6f8fc", red: "#ea4335", blue: "#1a73e8", tx: "#202124", mut: "#5f6368", bd: "#e0e3e7", chip: "#e8f0fe" };
203
+ function gmChrome(app) {
204
+ g.fillStyle = GM.bg; g.fillRect(0, 0, cv.width, cv.height);
205
+ g.fillStyle = "#fff"; g.fillRect(0, 0, cv.width, 68); g.strokeStyle = GM.bd; g.beginPath(); g.moveTo(0, 68); g.lineTo(cv.width, 68); g.stroke();
206
+ if (app === "gmail") { g.fillStyle = GM.red; box(28, 22, 30, 24, 5, { fill: GM.red }); txt("M", 34, 42, { s: 18, w: 700, c: "#fff" }); txt("Gmail", 68, 42, { s: 20, c: GM.mut }); }
207
+ else { box(28, 20, 30, 30, 6, { fill: "#fff", stroke: GM.bd }); txt("31", 34, 43, { s: 15, w: 700, c: GM.blue }); txt("Calendar", 68, 42, { s: 20, c: GM.mut }); }
208
+ box(320, 18, 640, 34, 17, { fill: GM.panel }); txt("Search " + (app === "gmail" ? "mail" : "calendar"), 344, 40, { s: 14, c: GM.mut });
209
+ }
210
+ function gmSidebar(active) {
211
+ box(24, 92, 150, 46, 23, { fill: "#c2e7ff" }); txt("✎ Compose", 46, 121, { s: 15, w: 600, c: GM.tx });
212
+ ["Inbox", "Starred", "Sent", "Drafts", "Spam"].forEach((n, i) => { const y = 176 + i * 44, on = n === (active || "Inbox"); if (on) { g.fillStyle = "#d3e3fd"; rr(16, y - 20, 190, 36, 18); g.fill(); } txt(n, 46, y + 4, { s: 14, c: on ? GM.tx : GM.mut, w: on ? 700 : 400 }); });
213
+ }
214
+ function gmInbox(rows) {
215
+ gmChrome("gmail"); gmSidebar("Inbox");
216
+ let y = 96; (rows || []).forEach((r, i) => { g.fillStyle = i === 0 ? "#fff" : "#fff"; g.fillRect(232, y, cv.width - 232, 52); g.strokeStyle = GM.bd; g.beginPath(); g.moveTo(232, y + 52); g.lineTo(cv.width, y + 52); g.stroke(); txt(r[0], 300, y + 32, { s: 14, w: 700, c: GM.tx }); txt(r[1], 520, y + 32, { s: 14, c: GM.tx }); txt(" — " + r[2], 520 + g.measureText(r[1]).width, y + 32, { s: 14, c: GM.mut }); txt(r[3], cv.width - 60, y + 32, { s: 12.5, c: GM.mut, a: "right" }); y += 52; });
217
+ }
218
+ function gmEmail(o) {
219
+ gmChrome("gmail"); gmSidebar("Inbox");
220
+ g.fillStyle = "#fff"; g.fillRect(232, 84, cv.width - 232, cv.height - 84);
221
+ txt(o.subj, 300, 140, { s: 22, w: 500, c: GM.tx });
222
+ g.fillStyle = o.color || "#7b1fa2"; g.beginPath(); g.arc(320, 190, 20, 0, 7); g.fill(); txt(o.from[0], 314, 196, { s: 18, w: 600, c: "#fff" });
223
+ txt(o.from, 356, 184, { s: 14.5, w: 700, c: GM.tx }); txt("<" + o.addr + ">", 356 + g.measureText(o.from).width + 10, 184, { s: 13.5, c: GM.mut });
224
+ txt("to me " + o.time, 356, 208, { s: 12.5, c: GM.mut });
225
+ (o.body || []).forEach((line, i) => txt(line, 300, 264 + i * 30, { s: 15, c: "#3c4043" }));
226
+ btn(300, cv.height - 120, 130, 44, "↰ Reply", { fill: "#fff", tx: GM.blue, stroke: GM.bd, s: 14.5 });
227
+ btn(444, cv.height - 120, 130, 44, "↪ Forward", { fill: "#fff", tx: GM.mut, stroke: GM.bd, s: 14.5 });
228
+ }
229
+ function calWeek() {
230
+ gmChrome("cal");
231
+ btn(24, 92, 130, 48, "✎ Create", { fill: "#fff", tx: GM.tx, stroke: GM.bd, s: 15 });
232
+ txt("March 2026", 232, 116, { s: 22, w: 500, c: GM.tx });
233
+ const days = ["MON 9", "TUE 10", "WED 11", "THU 12", "FRI 13"];
234
+ days.forEach((d, i) => { const x = 240 + i * 230; txt(d, x, 168, { s: 13, c: GM.mut, w: 600 }); g.strokeStyle = GM.bd; g.beginPath(); g.moveTo(x - 10, 180); g.lineTo(x - 10, cv.height - 20); g.stroke(); });
235
+ // an existing event
236
+ box(240 + 2 * 230 - 6, 320, 210, 70, 8, { fill: "#d2e3fc" }); txt("Standup", 240 + 2 * 230 + 8, 348, { s: 13, w: 600, c: "#174ea6" }); txt("10:00 – 10:30", 240 + 2 * 230 + 8, 368, { s: 12, c: "#174ea6" });
237
+ }
238
+ function calEvent(o = {}) {
239
+ calWeek();
240
+ g.fillStyle = "rgba(0,0,0,0.28)"; g.fillRect(0, 0, cv.width, cv.height);
241
+ box(cv.width / 2 - 280, 180, 560, 460, 14, { fill: "#fff" });
242
+ const x = cv.width / 2 - 240; const li = { fill: "#fff", stroke: GM.bd, tc: GM.tx, phc: GM.mut };
243
+ input(x, 220, 480, 50, o.title || "", { ph: "Add title", ...li });
244
+ txt("Thursday, March 12 · 2:00 – 3:00pm", x, 320, { s: 14, c: GM.tx });
245
+ txt("Add guests", x, 372, { s: 13, c: GM.mut, w: 500 }); input(x, 384, 480, 44, o.guests || "", { ph: "Guest emails", ...li });
246
+ txt("Add Google Meet video conferencing", x, 470, { s: 13.5, c: GM.blue, w: 500 });
247
+ btn(cv.width / 2 + 120, 580, 130, 44, "Save", { fill: GM.blue, tx: "#fff", s: 14.5 });
248
+ }
249
+
250
+ // draw the screen into the site viewport, then composite it as a browser window
251
+ // on a soft desktop backdrop that fills the whole canvas (no letterbox bars).
252
+ function render(fn, url) {
253
+ const mc = cv, mg = g; cv = siteCv; g = sg; sg.setTransform(1, 0, 0, 1, 0, 0); sg.clearRect(0, 0, SW, SH); fn(); cv = mc; g = mg;
254
+ const W = cv.width, H = cv.height, sAsp = SW / SH;
255
+ const bg = g.createLinearGradient(0, 0, 0, H); bg.addColorStop(0, "#eef1f6"); bg.addColorStop(1, "#dfe4ee"); g.fillStyle = bg; g.fillRect(0, 0, W, H);
256
+ const m = Math.round(Math.min(W, H) * 0.045), availW = W - 2 * m, availH = H - 2 * m;
257
+ let winW = availW, CH = winW * 0.05, winH = winW / sAsp + CH;
258
+ if (winH > availH) { winW = availH / (1 / sAsp + 0.05); CH = winW * 0.05; winH = availH; }
259
+ const wx = Math.round((W - winW) / 2), wy = Math.round((H - winH) / 2);
260
+ g.fillStyle = "rgba(18,22,38,0.20)"; rr(wx + 3, wy + 14, winW, winH, 16); g.fill();
261
+ g.fillStyle = "#fff"; rr(wx, wy, winW, winH, 16); g.fill();
262
+ g.fillStyle = "#e7eaf0"; rr(wx, wy, winW, CH, 16); g.fill(); g.fillRect(wx, wy + CH - 16, winW, 16);
263
+ ["#ff5f57", "#febc2e", "#28c840"].forEach((c, i) => { g.fillStyle = c; g.beginPath(); g.arc(wx + 24 + i * 20, wy + CH / 2, 6, 0, 7); g.fill(); });
264
+ g.fillStyle = "#fff"; rr(wx + winW * 0.26, wy + CH * 0.24, winW * 0.48, CH * 0.52, CH * 0.26); g.fill();
265
+ g.fillStyle = "#6b7280"; g.font = `${Math.round(CH * 0.32)}px ${FONT}`; g.textAlign = "center"; g.fillText(url || "", wx + winW * 0.5, wy + CH * 0.62); g.textAlign = "left";
266
+ g.drawImage(siteCv, 0, 0, SW, SH, wx, wy + CH, winW, winH - CH);
267
+ }
268
+ let lastDraw = null;
269
+ function paint(fn) { lastDraw = fn; render(fn, ""); } // captureStream samples the canvas continuously
270
+
271
+ // ── demo banner overlay ──
272
+ const banner = document.createElement("div");
273
+ banner.id = "demoBanner";
274
+ banner.style.cssText = "position:fixed;top:10px;left:50%;transform:translateX(-50%);z-index:200;background:#111827;color:#fff;font:600 13px/1.3 'Space Grotesk',sans-serif;padding:9px 16px;border-radius:999px;box-shadow:0 8px 30px rgba(0,0,0,.35);display:none;align-items:center;gap:10px;max-width:80vw";
275
+ document.body.appendChild(banner);
276
+ const setBanner = (t) => { banner.style.display = "flex"; banner.innerHTML = `<span style="width:8px;height:8px;border-radius:50%;background:#e5484d;display:inline-block;animation:none"></span> `; banner.append(String(t)); };
277
+
278
+ // ── recorder (getDisplayMedia + MediaRecorder → .webm with audio) ──
279
+ let mediaRec = null, chunks = [];
280
+ async function startRec(name) {
281
+ let disp;
282
+ try { disp = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: 30, width: { ideal: 2560 }, height: { ideal: 1440 } }, audio: true, preferCurrentTab: true, selfBrowserSurface: "include" }); }
283
+ catch { return false; }
284
+ // ask the shared track for the highest resolution the surface can give
285
+ try { const vt = disp.getVideoTracks()[0]; await vt.applyConstraints({ width: { ideal: 2560 }, height: { ideal: 1440 }, frameRate: { ideal: 30 } }); } catch {}
286
+ // prefer MP4 (H.264/AAC) — supported in recent Chrome — else fall back to webm
287
+ const mime = ["video/mp4;codecs=avc1.640028,mp4a.40.2", "video/mp4;codecs=h264,aac", "video/mp4", "video/webm;codecs=vp9,opus", "video/webm;codecs=vp8,opus", "video/webm"].find((m) => MediaRecorder.isTypeSupported(m)) || "video/webm";
288
+ const ext = mime.indexOf("mp4") >= 0 ? "mp4" : "webm";
289
+ mediaRec = new MediaRecorder(disp, { mimeType: mime, videoBitsPerSecond: 12_000_000, audioBitsPerSecond: 128_000 });
290
+ chunks = [];
291
+ mediaRec.ondataavailable = (e) => { if (e.data && e.data.size) chunks.push(e.data); };
292
+ mediaRec.onstop = () => {
293
+ const blob = new Blob(chunks, { type: mime.split(";")[0] });
294
+ const url = URL.createObjectURL(blob);
295
+ const a = document.createElement("a"); a.href = url; a.download = `${name}.${ext}`; document.body.appendChild(a); a.click(); a.remove();
296
+ setTimeout(() => URL.revokeObjectURL(url), 4000);
297
+ disp.getTracks().forEach((t) => t.stop());
298
+ };
299
+ disp.getVideoTracks()[0].addEventListener("ended", () => { try { mediaRec.state !== "inactive" && mediaRec.stop(); } catch {} });
300
+ mediaRec.start();
301
+ return true;
302
+ }
303
+ function stopRec() { try { mediaRec && mediaRec.state !== "inactive" && mediaRec.stop(); } catch {} }
304
+
305
+ // ── share the canvas as Ava's screen ──
306
+ function shareCanvas() {
307
+ // size the canvas to the stage's exact aspect so the shared screen FILLS it
308
+ // (no black letterbox bars) — the site's own background fills any slack.
309
+ try { const sr = document.getElementById("stage").getBoundingClientRect(); if (sr.width > 200 && sr.height > 150) { cv.width = Math.round(sr.width * 2); cv.height = Math.round(sr.height * 2); } } catch {}
310
+ if (lastDraw) render(lastDraw, ""); // resizing clears the canvas — repaint the current screen
311
+ stream = cv.captureStream(4);
312
+ const v = document.getElementById("video");
313
+ v.srcObject = stream; v.classList.add("on");
314
+ S.stream = stream; S.sharing = true;
315
+ document.getElementById("privacy").hidden = false; document.getElementById("privwin").textContent = "the demo screen";
316
+ document.getElementById("stagelabel").textContent = "stage · demo screen";
317
+ document.getElementById("share").classList.add("armed"); document.getElementById("sharelabel").textContent = "Sharing demo screen";
318
+ if (typeof updateStageLayout === "function") updateStageLayout();
319
+ }
320
+
321
+ // wait for Ava to finish the current turn + speaking (bounded)
322
+ async function settle(maxMs = 22000) {
323
+ const t0 = Date.now();
324
+ await sleep(500);
325
+ while (Date.now() - t0 < maxMs) { if (!S.turnInFlight && !S.speaking) break; await sleep(300); }
326
+ await sleep(400);
327
+ }
328
+
329
+ async function runStep(step) {
330
+ if (step.note) setBanner(step.note);
331
+ if (step.draw) { paint(step.draw); await sleep(step.settle || 1100); if (typeof observeNow === "function") { try { await observeNow(false); } catch {} } await sleep(700); }
332
+ if (step.say) { sendTurn(step.say); await settle(); }
333
+ await sleep(step.pause || 1200);
334
+ }
335
+
336
+ async function run(scn) {
337
+ // pick agent mode + platform, voice on but NO mic
338
+ S.agentMode = scn.mode;
339
+ if (scn.platformId) S.platform = (S.platforms || []).find((p) => p.id === scn.platformId) || null;
340
+ const realMic = window.startLiveMic; window.startLiveMic = function () {}; // silence the mic for the demo
341
+ paint(scn.steps[0].draw || sbSignIn);
342
+ await joinCall("voice");
343
+ window.startLiveMic = realMic;
344
+ // captions on for readability; voice already on in voice mode
345
+ try { S.cc = true; document.getElementById("capwrap").hidden = false; document.getElementById("ccbtn").classList.add("on"); } catch {}
346
+ shareCanvas();
347
+ await settle(); // let the greeting play
348
+ for (const step of scn.steps) await runStep(step);
349
+ setBanner("Demo complete — saving the recording…");
350
+ await sleep(2500);
351
+ stopRec();
352
+ setBanner("Saved. You can stop sharing now.");
353
+ }
354
+
355
+ // ── scenarios ──
356
+ const SCENARIOS = {
357
+ simple: {
358
+ name: "ava-demo-1-onboarding-simple", mode: "onboarding", platformId: "basewell",
359
+ steps: [
360
+ { draw: () => sbSignIn({ email: "" }), note: "Onboarding · Basewell sign-in", settle: 1400, say: "hey, I need to get set up on basewell" },
361
+ { draw: () => sbSignIn({ email: "alex.chenacme.co", invalid: true }), note: "Human error · typo in the email (missing @)", settle: 1600, say: "hmm it says something's wrong?" },
362
+ { draw: () => sbOrgs(), note: "Signed in → create a workspace", settle: 1400, say: "ok fixed it, I'm signed in now" },
363
+ { draw: () => sbNewOrg({ name: "acme" }), note: "Creating the workspace", settle: 1300, say: "made the workspace, what's next?" },
364
+ { draw: () => sbNewProject({ pw: "weak" }), note: "Human confusion · weak password → RAG answers", settle: 1600, say: "what should I put for the password?" },
365
+ { draw: () => sbNewProject({ pw: "strong" }), note: "Strong password set", settle: 1300, say: "done, I used generate" },
366
+ { draw: () => sbProvisioning(), note: "Vision watches provisioning (no turn)", settle: 1800 },
367
+ { draw: () => sbDashboard(), note: "Reached the dashboard — thinking marks it resolved", settle: 1500, say: "looks like it's ready now" },
368
+ ],
369
+ },
370
+ moderate: {
371
+ name: "ava-demo-2-technical-issue-stripe", mode: "flexible", platformId: "stripe",
372
+ steps: [
373
+ { draw: () => stDashboard(), note: "Technical issue · Stripe dashboard", settle: 1600, say: "hey, my payments aren't showing up in my real bank account" },
374
+ { draw: () => stPayments(), note: "Human confusion · a $49 payment shows Failed", settle: 1700, say: "and look, this payment just failed — why?" },
375
+ { draw: () => stApiKeys(), note: "RAG · where the API keys live", settle: 1500, say: "ok, where do I grab my API keys?" },
376
+ { draw: () => stDashboard(), note: "RAG · how to actually go live", settle: 1500, say: "so how do I switch this to real payments?" },
377
+ ],
378
+ },
379
+ complex: {
380
+ name: "ava-demo-3-flexibility-workspace", mode: "flexible", platformId: "google-workspace",
381
+ steps: [
382
+ { draw: () => gmInbox([["The Daily Byte", "Your daily AI roundup", "three labs shipped agents", "9:12 AM"], ["Korean Air", "SkyPass mileage plan", "integrate your miles", "8:40 AM"], ["Anthropic", "Astra gains ground", "the model race tightens", "Yesterday"], ["GitHub", "CI passed on main", "build #482 green", "Mar 9"]]), note: "Flexibility · Gmail inbox", settle: 1700, say: "read me my latest email" },
383
+ { draw: () => gmEmail({ from: "The Daily Byte", addr: "news@dailybyte.example", subj: "Your daily AI roundup", time: "9:12 AM", color: "#7b1fa2", body: ["Today in AI: three labs shipped agents,", "and a new model tops the leaderboard.", "Full stories inside."] }), note: "Vision reads the opened email", settle: 1600, say: "who's it from, and what's their email address?" },
384
+ { draw: () => gmEmail({ from: "The Daily Byte", addr: "news@dailybyte.example", subj: "Your daily AI roundup", time: "9:12 AM", color: "#7b1fa2", body: ["Today in AI: three labs shipped agents,", "and a new model tops the leaderboard.", "Full stories inside."] }), note: "Human pivot · changes task mid-flow", settle: 1300, say: "actually never mind — help me add a meeting to my calendar instead" },
385
+ { draw: () => calWeek(), note: "Pivoted to Google Calendar", settle: 1500, say: "ok I'm in calendar now, how do I add one?" },
386
+ { draw: () => calEvent({}), note: "Human confusion · which field is which", settle: 1600, say: "wait, which box is the title?" },
387
+ { draw: () => calEvent({ title: "Sync with Sarah", guests: "sarah@acme.co" }), note: "Filled it in — ready to save", settle: 1300, say: "done, added Sarah" },
388
+ { draw: () => calWeek(), note: "Event created — thinking marks it resolved", settle: 1500, say: "is it on my calendar now?" },
389
+ ],
390
+ },
391
+ };
392
+
393
+ // ── start overlay (one click = user gesture for getDisplayMedia + share picker) ──
394
+ function overlay(scn) {
395
+ const o = document.createElement("div");
396
+ o.style.cssText = "position:fixed;inset:0;z-index:300;background:rgba(11,10,25,.72);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center";
397
+ o.innerHTML = `<div style="background:#fff;border-radius:16px;padding:30px 34px;max-width:440px;font-family:'IBM Plex Sans',sans-serif;box-shadow:0 30px 80px rgba(0,0,0,.4)">
398
+ <div style="font-family:'Space Grotesk';font-weight:600;font-size:19px;margin-bottom:8px">Demo · ${scn.name.replace(/^ava-demo-\d+-/, "").replace(/-/g, " ")}</div>
399
+ <p style="font-size:13.5px;line-height:1.55;color:#545B6B;margin:0 0 18px">Click start, then in the share dialog pick <b>This Tab</b> and turn on <b>“Also share tab audio”</b> so Ava's voice is recorded. It then runs hands-free and downloads a <b>.webm</b> when done.</p>
400
+ <button id="demoGo" style="height:46px;width:100%;border:0;border-radius:10px;background:#4F46E5;color:#fff;font-family:'Space Grotesk';font-weight:600;font-size:14px;cursor:pointer">Start &amp; record</button>
401
+ </div>`;
402
+ document.body.appendChild(o);
403
+ o.querySelector("#demoGo").onclick = async () => {
404
+ o.querySelector("#demoGo").textContent = "Requesting screen + audio…";
405
+ const ok = await startRec(scn.name);
406
+ if (!ok) { o.querySelector("#demoGo").textContent = "Screen share was blocked — click to retry"; return; }
407
+ o.remove();
408
+ run(scn).catch((e) => setBanner("Demo error: " + (e && e.message)));
409
+ };
410
+ }
411
+
412
+ // test hook: run the engine without the recorder (used to verify in environments
413
+ // where screen-share is blocked). Not used by the real ?demo flow.
414
+ window.__demo = { run, startRec, SCENARIOS, shareCanvas, paint, cv, screens: { sbSignIn, sbOrgs, sbNewOrg, sbNewProject, sbProvisioning, sbDashboard, stDashboard, stPayments, stApiKeys, gmInbox: () => gmInbox([["The Daily Byte", "Your daily AI roundup", "three labs shipped agents", "9:12 AM"], ["Korean Air", "SkyPass mileage plan", "integrate your miles", "8:40 AM"], ["Anthropic", "Astra gains ground", "the model race tightens", "Yesterday"]]), gmEmail: () => gmEmail({ from: "The Daily Byte", addr: "news@dailybyte.example", subj: "Your daily AI roundup", time: "9:12 AM", color: "#7b1fa2", body: ["Today in AI: three labs shipped agents,", "and a new model tops the leaderboard."] }), calWeek, calEvent: () => calEvent({ title: "Sync with Sarah", guests: "sarah@acme.co" }) } };
415
+
416
+ // wait for the app to boot (platforms loaded), then show the start overlay
417
+ const scn = SCENARIOS[which];
418
+ if (!scn) return;
419
+ (async () => { for (let i = 0; i < 60; i++) { if (Array.isArray(S.platforms) && S.platforms.length) break; await sleep(200); } overlay(scn); })();
420
+ })();
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="6" fill="#0A0A0C"/><path transform="translate(2.67 8.4) scale(.3)" d="M0 24L9.5 0L19 24M21.6 0L31.1 24L40.6 0M43.2 24L52.7 0L62.2 24" stroke="#fff" fill="none" stroke-width="3.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
@@ -0,0 +1,96 @@
1
+ Inter and Inter Tight
2
+ Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
3
+
4
+ IBM Plex Mono
5
+ Copyright (c) 2017 IBM Corp. with Reserved Font Name "Plex"
6
+
7
+ These Font Software files are licensed under the SIL Open Font License, Version 1.1.
8
+ This license is copied below, and is also available with a FAQ at:
9
+ https://openfontlicense.org
10
+
11
+ -----------------------------------------------------------
12
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
13
+ -----------------------------------------------------------
14
+
15
+ PREAMBLE
16
+ The goals of the Open Font License (OFL) are to stimulate worldwide
17
+ development of collaborative font projects, to support the font creation
18
+ efforts of academic and linguistic communities, and to provide a free and
19
+ open framework in which fonts may be shared and improved in partnership
20
+ with others.
21
+
22
+ The OFL allows the licensed fonts to be used, studied, modified and
23
+ redistributed freely as long as they are not sold by themselves. The
24
+ fonts, including any derivative works, can be bundled, embedded,
25
+ redistributed and/or sold with any software provided that any reserved
26
+ names are not used by derivative works. The fonts and derivatives,
27
+ however, cannot be released under any other type of license. The
28
+ requirement for fonts to remain under this license does not apply
29
+ to any document created using the fonts or their derivatives.
30
+
31
+ DEFINITIONS
32
+ "Font Software" refers to the set of files released by the Copyright
33
+ Holder(s) under this license and clearly marked as such. This may
34
+ include source files, build scripts and documentation.
35
+
36
+ "Reserved Font Name" refers to any names specified as such after the
37
+ copyright statement(s).
38
+
39
+ "Original Version" refers to the collection of Font Software components as
40
+ distributed by the Copyright Holder(s).
41
+
42
+ "Modified Version" refers to any derivative made by adding to, deleting,
43
+ or substituting -- in part or in whole -- any of the components of the
44
+ Original Version, by changing formats or by porting the Font Software to a
45
+ new environment.
46
+
47
+ "Author" refers to any designer, engineer, programmer, technical
48
+ writer or other person who contributed to the Font Software.
49
+
50
+ PERMISSION & CONDITIONS
51
+ Permission is hereby granted, free of charge, to any person obtaining
52
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
53
+ redistribute, and sell modified and unmodified copies of the Font
54
+ Software, subject to the following conditions:
55
+
56
+ 1) Neither the Font Software nor any of its individual components,
57
+ in Original or Modified Versions, may be sold by itself.
58
+
59
+ 2) Original or Modified Versions of the Font Software may be bundled,
60
+ redistributed and/or sold with any software, provided that each copy
61
+ contains the above copyright notice and this license. These can be
62
+ included either as stand-alone text files, human-readable headers or
63
+ in the appropriate machine-readable metadata fields within text or
64
+ binary files as long as those fields can be easily viewed by the user.
65
+
66
+ 3) No Modified Version of the Font Software may use the Reserved Font
67
+ Name(s) unless explicit written permission is granted by the corresponding
68
+ Copyright Holder. This restriction only applies to the primary font name as
69
+ presented to the users.
70
+
71
+ 4) The name(s) of the Copyright Holder(s) and the Author(s) of the Font
72
+ Software shall not be used to promote, endorse or advertise any
73
+ Modified Version, except to acknowledge the contribution(s) of the
74
+ Copyright Holder(s) and the Author(s) or with their explicit written
75
+ permission.
76
+
77
+ 5) The Font Software, modified or unmodified, in part or in whole,
78
+ must be distributed entirely under this license, and must not be
79
+ distributed under any other license. The requirement for fonts to
80
+ remain under this license does not apply to any document created
81
+ using the Font Software.
82
+
83
+ TERMINATION
84
+ This license becomes null and void if any of the above conditions are
85
+ not met.
86
+
87
+ DISCLAIMER
88
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
89
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
90
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
91
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
92
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
93
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
94
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
95
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
96
+ OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file