ava-engine 0.2.0 → 0.4.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.
- package/.env.example +11 -0
- package/README.md +23 -3
- package/lib/admin.mjs +284 -0
- package/lib/analytics.mjs +73 -0
- package/lib/calls.mjs +97 -0
- package/lib/cli/config.mjs +1 -0
- package/lib/cli/main.mjs +77 -4
- package/lib/cli/openrouter-oauth.mjs +87 -0
- package/lib/cli/setup-server.mjs +32 -4
- package/lib/cli/setup.mjs +27 -3
- package/lib/draft.mjs +53 -0
- package/lib/guard.mjs +73 -28
- package/lib/library.mjs +76 -0
- package/lib/links.mjs +148 -0
- package/lib/mock.mjs +88 -0
- package/lib/models.mjs +10 -4
- package/lib/openrouter.mjs +9 -1
- package/lib/ours.mjs +43 -29
- package/lib/platforms.mjs +77 -1
- package/lib/playbooks.mjs +120 -0
- package/lib/safe-fetch.mjs +79 -0
- package/lib/settings.mjs +187 -0
- package/lib/spend.mjs +62 -0
- package/lib/state.mjs +46 -7
- package/lib/stt.mjs +4 -1
- package/lib/team.mjs +191 -0
- package/lib/tts.mjs +18 -11
- package/lib/webhooks.mjs +112 -0
- package/package.json +3 -3
- package/public/app.js +86 -3
- package/public/dashboard.css +430 -0
- package/public/dashboard.html +76 -0
- package/public/dashboard.js +1096 -0
- package/public/embed.js +60 -0
- package/public/index.html +18 -9
- package/public/login.css +30 -0
- package/public/login.html +37 -0
- package/public/login.js +27 -0
- package/public/setup.css +139 -55
- package/public/setup.html +45 -18
- package/public/setup.js +43 -10
- package/public/style.css +21 -0
- package/server.mjs +164 -44
package/public/setup.html
CHANGED
|
@@ -12,34 +12,60 @@
|
|
|
12
12
|
<div class="shell">
|
|
13
13
|
<aside class="rail" aria-label="Setup progress">
|
|
14
14
|
<div class="brand">
|
|
15
|
-
<svg viewBox="0 0
|
|
16
|
-
<span>
|
|
15
|
+
<svg class="wm" viewBox="0 0 66 27.8" role="img" aria-label="Ava"><path transform="translate(1.9 1.9)" d="M0 24L9.5 0L19 24M21.6 0L31.1 24L40.6 0M43.2 24L52.7 0L62.2 24" fill="none" stroke="currentColor" stroke-width="3.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
16
|
+
<span class="brand-tag">Engine setup</span>
|
|
17
17
|
</div>
|
|
18
18
|
<ol class="steps" id="steps">
|
|
19
|
-
<li data-step="ai">AI replies</li>
|
|
20
|
-
<li data-step="voice">Voice</li>
|
|
21
|
-
<li data-step="speech">Speech recognition</li>
|
|
22
|
-
<li data-step="access">Access</li>
|
|
23
|
-
<li data-step="review">Launch</li>
|
|
19
|
+
<li data-step="ai"><span class="steps-name">AI replies</span><span class="steps-sub">OpenRouter or demo</span></li>
|
|
20
|
+
<li data-step="voice"><span class="steps-name">Voice</span><span class="steps-sub">How Ava sounds</span></li>
|
|
21
|
+
<li data-step="speech"><span class="steps-name">Speech recognition</span><span class="steps-sub">Offline, optional</span></li>
|
|
22
|
+
<li data-step="access"><span class="steps-name">Access</span><span class="steps-sub">Who can reach Ava</span></li>
|
|
23
|
+
<li data-step="review"><span class="steps-name">Launch</span><span class="steps-sub">Open the dashboard</span></li>
|
|
24
24
|
</ol>
|
|
25
|
-
<
|
|
25
|
+
<div class="rail-card" aria-hidden="true">
|
|
26
|
+
<span class="rail-orb"></span>
|
|
27
|
+
<span class="rail-card-text"><b>Ava</b><span class="mono">getting ready</span></span>
|
|
28
|
+
<span class="rail-wave"><i></i><i></i><i></i><i></i><i></i></span>
|
|
29
|
+
</div>
|
|
30
|
+
<p class="rail-note">Saved on this computer only, in <code id="configFile">~/.ava-engine/.env</code>.</p>
|
|
26
31
|
</aside>
|
|
27
32
|
|
|
28
33
|
<main class="panel">
|
|
34
|
+
<div class="progress" aria-hidden="true"><i id="progressBar"></i></div>
|
|
29
35
|
<div id="fatal" class="banner error" role="alert" hidden></div>
|
|
30
36
|
|
|
37
|
+
<!-- 0 · Welcome -->
|
|
38
|
+
<section class="step welcome" data-step="welcome" aria-labelledby="h-welcome">
|
|
39
|
+
<div class="hero-orb" aria-hidden="true"><i></i><i></i><span></span></div>
|
|
40
|
+
<p class="eyebrow">Welcome</p>
|
|
41
|
+
<h1 id="h-welcome">Let's get Ava ready</h1>
|
|
42
|
+
<p class="lead">A live, screen-aware guide for your customers. Five short steps, about two minutes. You can change any of it later.</p>
|
|
43
|
+
<ul class="welcome-list">
|
|
44
|
+
<li><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/><circle cx="12" cy="12" r="3.2"/></svg><span><b>Pick its brain</b><span>Connect OpenRouter for real answers, or start in demo mode</span></span></li>
|
|
45
|
+
<li><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M11.5 4.5 6.8 8.6H3.2v6.8h3.6l4.7 4.1V4.5Z"/><path d="M15.4 9.6a3.6 3.6 0 0 1 0 4.8M18 7a7 7 0 0 1 0 10"/></svg><span><b>Give it a voice</b><span>Free and offline, or premium cloud voices</span></span></li>
|
|
46
|
+
<li><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4.5" y="10.5" width="15" height="10" rx="2.5"/><path d="M8 10.5V8a4 4 0 0 1 8 0v2.5"/></svg><span><b>Decide who gets in</b><span>Just you, or your team and customers with a password</span></span></li>
|
|
47
|
+
</ul>
|
|
48
|
+
<div class="nav"><span></span><button type="button" class="btn primary" data-next>Get started<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></button></div>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
31
51
|
<!-- 1 · AI -->
|
|
32
|
-
<section class="step" data-step="ai" aria-labelledby="h-ai">
|
|
52
|
+
<section class="step" data-step="ai" aria-labelledby="h-ai" hidden>
|
|
33
53
|
<p class="eyebrow">Step 1 of 5</p>
|
|
34
54
|
<h1 id="h-ai">How should Ava think?</h1>
|
|
35
55
|
<p class="lead">Ava's AI runs through <a href="https://openrouter.ai" target="_blank" rel="noopener">OpenRouter</a>: one key for every model. You can also look around first with canned replies.</p>
|
|
36
56
|
<div class="cards" role="radiogroup" aria-label="AI mode">
|
|
37
|
-
<label class="card"><input type="radio" name="mode" value="full" checked><span class="card-body"><b>With real AI</b><span>Recommended. Needs an OpenRouter key.</span></span></label>
|
|
38
|
-
<label class="card"><input type="radio" name="mode" value="mock"><span class="card-body"><b>Just look around first</b><span>No key. Canned replies; voice, screen sharing and everything else work.</span></span></label>
|
|
57
|
+
<label class="card"><input type="radio" name="mode" value="full" checked><span class="card-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/><circle cx="12" cy="12" r="3.2"/></svg></span><span class="card-body"><b>With real AI</b><span>Recommended. Needs an OpenRouter key.</span></span></label>
|
|
58
|
+
<label class="card"><input type="radio" name="mode" value="mock"><span class="card-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z"/><circle cx="12" cy="12" r="2.8"/></svg></span><span class="card-body"><b>Just look around first</b><span>No key. Canned replies; voice, screen sharing and everything else work.</span></span></label>
|
|
39
59
|
</div>
|
|
40
|
-
<div class="field" id="keyField">
|
|
60
|
+
<div class="field keybox" id="keyField">
|
|
41
61
|
<div id="keepKeyRow" class="keep" hidden><label><input type="checkbox" id="keepKey" checked> Keep my current key <code id="currentKey"></code></label></div>
|
|
42
62
|
<div id="newKeyRow">
|
|
63
|
+
<div class="connect">
|
|
64
|
+
<button type="button" class="btn primary" id="connectOr"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1"/><path d="M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg>Connect OpenRouter</button>
|
|
65
|
+
<p class="hint">Sign in to OpenRouter and approve, and Ava gets its own key. Nothing to copy.</p>
|
|
66
|
+
<p class="status" id="connectStatus" role="status" aria-live="polite"></p>
|
|
67
|
+
</div>
|
|
68
|
+
<p class="or"><span>or paste a key</span></p>
|
|
43
69
|
<label for="orKey">OpenRouter API key</label>
|
|
44
70
|
<div class="row">
|
|
45
71
|
<input id="orKey" type="password" autocomplete="off" spellcheck="false" placeholder="sk-or-v1-…">
|
|
@@ -49,7 +75,7 @@
|
|
|
49
75
|
<p class="status" id="keyStatus" role="status" aria-live="polite"></p>
|
|
50
76
|
</div>
|
|
51
77
|
</div>
|
|
52
|
-
<div class="nav"><
|
|
78
|
+
<div class="nav"><button type="button" class="btn ghost" data-back>Back</button><button type="button" class="btn primary" data-next>Continue</button></div>
|
|
53
79
|
</section>
|
|
54
80
|
|
|
55
81
|
<!-- 2 · Voice -->
|
|
@@ -89,8 +115,8 @@
|
|
|
89
115
|
<p class="eyebrow">Step 4 of 5</p>
|
|
90
116
|
<h1 id="h-access">Who will use this Ava?</h1>
|
|
91
117
|
<div class="cards" role="radiogroup" aria-label="Who will use Ava">
|
|
92
|
-
<label class="card"><input type="radio" name="audience" value="me" checked><span class="card-body"><b>Just me, on this computer</b><span>Safest. Reachable only at localhost.</span></span></label>
|
|
93
|
-
<label class="card"><input type="radio" name="audience" value="others"><span class="card-body"><b>My team or customers</b><span>Over a network or the internet. Needs a password, because every call spends your API credit.</span></span></label>
|
|
118
|
+
<label class="card"><input type="radio" name="audience" value="me" checked><span class="card-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4.5" width="18" height="12" rx="2"/><path d="M8 20h8M12 16.5V20"/></svg></span><span class="card-body"><b>Just me, on this computer</b><span>Safest. Reachable only at localhost.</span></span></label>
|
|
119
|
+
<label class="card"><input type="radio" name="audience" value="others"><span class="card-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg></span><span class="card-body"><b>My team or customers</b><span>Over a network or the internet. Needs a password, because every call spends your API credit.</span></span></label>
|
|
94
120
|
</div>
|
|
95
121
|
<div class="field" id="passwordField" hidden>
|
|
96
122
|
<div id="keepPassRow" class="keep" hidden><label><input type="checkbox" id="keepPass" checked> Keep my current password</label></div>
|
|
@@ -112,16 +138,17 @@
|
|
|
112
138
|
<section class="step" data-step="review" aria-labelledby="h-review" hidden>
|
|
113
139
|
<p class="eyebrow">Step 5 of 5</p>
|
|
114
140
|
<h1 id="h-review">Ready to launch</h1>
|
|
141
|
+
<p class="lead">Here's your setup. Launch Ava and your dashboard opens: test a call, describe your product, then share a link with customers.</p>
|
|
115
142
|
<dl class="summary" id="summary"></dl>
|
|
116
143
|
<p class="status" id="launchStatus" role="status" aria-live="polite"></p>
|
|
117
|
-
<div class="nav"><button type="button" class="btn ghost" data-back>Back</button><button type="button" class="btn primary" id="launch">Launch Ava
|
|
144
|
+
<div class="nav"><button type="button" class="btn ghost" data-back>Back</button><button type="button" class="btn primary" id="launch">Launch Ava<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></button></div>
|
|
118
145
|
</section>
|
|
119
146
|
|
|
120
147
|
<!-- launching -->
|
|
121
148
|
<section class="step launching" data-step="launching" aria-live="polite" hidden>
|
|
122
|
-
<div class="orb" aria-hidden="true"></div>
|
|
149
|
+
<div class="hero-orb big" aria-hidden="true"><i></i><i></i><span></span></div>
|
|
123
150
|
<h1>Starting Ava…</h1>
|
|
124
|
-
<p class="lead" id="launchMsg">This takes a few seconds.
|
|
151
|
+
<p class="lead" id="launchMsg">This takes a few seconds. Your dashboard opens here when it's ready.</p>
|
|
125
152
|
</section>
|
|
126
153
|
</main>
|
|
127
154
|
</div>
|
package/public/setup.js
CHANGED
|
@@ -4,8 +4,15 @@
|
|
|
4
4
|
(() => {
|
|
5
5
|
const $ = (id) => document.getElementById(id);
|
|
6
6
|
const TOKEN = new URLSearchParams(location.hash.slice(1)).get("token") || "";
|
|
7
|
-
const ORDER = ["ai", "voice", "speech", "access", "review"];
|
|
8
|
-
const
|
|
7
|
+
const ORDER = ["welcome", "ai", "voice", "speech", "access", "review"];
|
|
8
|
+
const STEPS = ORDER.slice(1); // the numbered steps in the rail
|
|
9
|
+
const ICON = {
|
|
10
|
+
kokoro: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 19c0-8 5-13 14-14-1 9-6 14-14 14Z"/><path d="M5 19 13 11"/></svg>',
|
|
11
|
+
elevenlabs: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 10v4M8 7v10M12 4v16M16 8v8M20 11v2"/></svg>',
|
|
12
|
+
browser: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4.5" width="18" height="14" rx="2.5"/><path d="M3 9h18M6.5 6.8h.01M9 6.8h.01"/></svg>',
|
|
13
|
+
pack: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5.5 11.5a6.5 6.5 0 0 0 13 0M12 18v3"/></svg>',
|
|
14
|
+
};
|
|
15
|
+
const S = { step: "welcome", state: null, keyOk: false, elevenOk: false, selectedPacks: new Set(), pollTimer: null };
|
|
9
16
|
|
|
10
17
|
async function api(path, body) {
|
|
11
18
|
const r = await fetch(`/api/setup/${path}`, {
|
|
@@ -25,7 +32,8 @@
|
|
|
25
32
|
function show(step) {
|
|
26
33
|
S.step = step;
|
|
27
34
|
document.querySelectorAll(".step").forEach((s) => { s.hidden = s.dataset.step !== step; });
|
|
28
|
-
const idx =
|
|
35
|
+
const idx = STEPS.indexOf(step) === -1 ? (step === "launching" ? STEPS.length : -1) : STEPS.indexOf(step);
|
|
36
|
+
$("progressBar").style.width = `${step === "welcome" ? 4 : step === "launching" ? 100 : ((idx + 1) / (STEPS.length + 1)) * 100}%`;
|
|
29
37
|
document.querySelectorAll("#steps li").forEach((li, i) => {
|
|
30
38
|
li.classList.toggle("current", i === idx);
|
|
31
39
|
li.classList.toggle("done", idx > i || step === "launching");
|
|
@@ -39,7 +47,7 @@
|
|
|
39
47
|
}
|
|
40
48
|
function canLeave(step) {
|
|
41
49
|
if (step === "ai" && radio("mode") === "full" && !(S.state.current.openrouterKey && $("keepKey").checked) && !S.keyOk) {
|
|
42
|
-
setStatus($("keyStatus"), "
|
|
50
|
+
setStatus($("keyStatus"), "Connect OpenRouter or check a key first, or choose “Just look around first”.", "err"); return false;
|
|
43
51
|
}
|
|
44
52
|
if (step === "voice" && radio("voice") === "elevenlabs" && !(S.state.current.elevenKey && $("keepEleven").checked) && !S.elevenOk) {
|
|
45
53
|
setStatus($("elevenStatus"), "Check your ElevenLabs key first, or pick another voice.", "err"); return false;
|
|
@@ -56,6 +64,30 @@
|
|
|
56
64
|
});
|
|
57
65
|
|
|
58
66
|
// ── 1 · AI ──
|
|
67
|
+
// Connect OpenRouter: sign-in opens in a new tab; the setup server receives the
|
|
68
|
+
// key and keeps it. This page only learns that it worked, plus a masked key.
|
|
69
|
+
async function connectOpenRouter() {
|
|
70
|
+
const tab = window.open("about:blank", "_blank"); // open now, while the click still counts, so it isn't blocked
|
|
71
|
+
setStatus($("connectStatus"), "Opening OpenRouter…");
|
|
72
|
+
try {
|
|
73
|
+
const { url } = await api("openrouter/start", {});
|
|
74
|
+
if (tab) { tab.opener = null; tab.location = url; } else location.assign(url);
|
|
75
|
+
setStatus($("connectStatus"), "Waiting for you to approve in the OpenRouter tab…");
|
|
76
|
+
clearInterval(S.connectTimer);
|
|
77
|
+
S.connectTimer = setInterval(async () => {
|
|
78
|
+
let r; try { r = await api("openrouter/status"); } catch { return; }
|
|
79
|
+
if (r.status === "connected") {
|
|
80
|
+
clearInterval(S.connectTimer);
|
|
81
|
+
S.connected = true; S.keyOk = true; $("orKey").value = "";
|
|
82
|
+
setStatus($("connectStatus"), `✓ Connected (${r.key}). Set a credit limit on it at openrouter.ai/keys.`, "ok");
|
|
83
|
+
setStatus($("keyStatus"), "");
|
|
84
|
+
} else if (r.status === "error") {
|
|
85
|
+
clearInterval(S.connectTimer);
|
|
86
|
+
setStatus($("connectStatus"), `✗ ${r.error}`, "err");
|
|
87
|
+
}
|
|
88
|
+
}, 1500);
|
|
89
|
+
} catch (e) { tab?.close(); setStatus($("connectStatus"), `✗ ${e.message}`, "err"); }
|
|
90
|
+
}
|
|
59
91
|
function syncMode() {
|
|
60
92
|
const full = radio("mode") === "full";
|
|
61
93
|
$("keyField").hidden = !full;
|
|
@@ -80,7 +112,7 @@
|
|
|
80
112
|
$("voiceCards").innerHTML = "";
|
|
81
113
|
for (const [id, info] of Object.entries(v)) {
|
|
82
114
|
const label = document.createElement("label"); label.className = "card";
|
|
83
|
-
label.innerHTML = `<input type="radio" name="voice"><span class="card-body"><b></b><span></span></span>`;
|
|
115
|
+
label.innerHTML = `<input type="radio" name="voice"><span class="card-icon">${ICON[id] || ICON.browser}</span><span class="card-body"><b></b><span></span></span>`;
|
|
84
116
|
const input = label.querySelector("input"); input.value = id; input.checked = id === cur;
|
|
85
117
|
label.querySelector("b").textContent = info.label;
|
|
86
118
|
label.querySelector(".card-body span").textContent = id === "kokoro" && S.state.kokoro.installed ? "Free, runs on this computer. Installed." : info.hint[0].toUpperCase() + info.hint.slice(1) + ".";
|
|
@@ -103,7 +135,7 @@
|
|
|
103
135
|
for (const [id, p] of Object.entries(S.state.packs)) {
|
|
104
136
|
const installed = S.state.installedPacks.includes(id);
|
|
105
137
|
const label = document.createElement("label"); label.className = "card";
|
|
106
|
-
label.innerHTML = `<input type="checkbox"><span class="card-body"><b></b><span></span></span>`;
|
|
138
|
+
label.innerHTML = `<input type="checkbox"><span class="card-icon">${ICON.pack}</span><span class="card-body"><b></b><span></span></span>`;
|
|
107
139
|
const input = label.querySelector("input"); input.value = id;
|
|
108
140
|
input.checked = installed || S.selectedPacks.has(id); input.disabled = installed;
|
|
109
141
|
label.querySelector("b").textContent = `${p.label} · ${p.size}`;
|
|
@@ -163,7 +195,7 @@
|
|
|
163
195
|
const mode = radio("mode"), voice = radio("voice"), audience = radio("audience");
|
|
164
196
|
return {
|
|
165
197
|
mode,
|
|
166
|
-
openrouterKey: mode === "mock" ? null : S.state.current.openrouterKey && $("keepKey").checked ? "keep" : $("orKey").value.trim(),
|
|
198
|
+
openrouterKey: mode === "mock" ? null : S.state.current.openrouterKey && $("keepKey").checked ? "keep" : S.connected && !$("orKey").value.trim() ? "connected" : $("orKey").value.trim(),
|
|
167
199
|
voice,
|
|
168
200
|
elevenKey: voice === "elevenlabs" ? (S.state.current.elevenKey && $("keepEleven").checked ? "keep" : $("elKey").value.trim()) : undefined,
|
|
169
201
|
elevenVoice: voice === "elevenlabs" ? $("elVoice").value.trim() || null : undefined,
|
|
@@ -184,7 +216,7 @@
|
|
|
184
216
|
["Voice", S.state.voices[a.voice].label],
|
|
185
217
|
["Speech recognition", packs.length ? packs.join(", ") : "Chrome's built-in recognition"],
|
|
186
218
|
["Access", a.audience === "me" ? "This computer only" : "Password protected"],
|
|
187
|
-
["
|
|
219
|
+
["Dashboard", `http://localhost:${a.port}/dashboard`],
|
|
188
220
|
];
|
|
189
221
|
if (a.audience === "others" && a.password !== "keep") rows.splice(4, 0, ["Password", a.password]);
|
|
190
222
|
const dl = $("summary"); dl.innerHTML = "";
|
|
@@ -246,7 +278,8 @@
|
|
|
246
278
|
$("keepEleven").addEventListener("change", syncVoice);
|
|
247
279
|
document.querySelectorAll('input[name="audience"]').forEach((i) => i.addEventListener("change", syncAudience));
|
|
248
280
|
$("keepPass").addEventListener("change", syncAudience);
|
|
249
|
-
$("orKey").addEventListener("input", () => { S.keyOk =
|
|
281
|
+
$("orKey").addEventListener("input", () => { S.keyOk = !!S.connected && !$("orKey").value.trim(); setStatus($("keyStatus"), ""); });
|
|
282
|
+
$("connectOr").addEventListener("click", connectOpenRouter);
|
|
250
283
|
$("elKey").addEventListener("input", () => { S.elevenOk = false; setStatus($("elevenStatus"), ""); });
|
|
251
284
|
$("checkKey").addEventListener("click", () => checkKey("openrouter", $("orKey"), $("keyStatus"), "keyOk"));
|
|
252
285
|
$("checkEleven").addEventListener("click", () => checkKey("elevenlabs", $("elKey"), $("elevenStatus"), "elevenOk"));
|
|
@@ -254,7 +287,7 @@
|
|
|
254
287
|
$("regen").addEventListener("click", async () => { $("password").value = (await api("password")).password; });
|
|
255
288
|
$("copyPass").addEventListener("click", async () => { try { await navigator.clipboard.writeText($("password").value); $("copyPass").textContent = "Copied"; setTimeout(() => { $("copyPass").textContent = "Copy"; }, 1400); } catch {} });
|
|
256
289
|
$("launch").addEventListener("click", launch);
|
|
257
|
-
show("
|
|
290
|
+
show("welcome");
|
|
258
291
|
pollJobs();
|
|
259
292
|
}
|
|
260
293
|
// A fresh setup link opened in this same tab only changes the #token, which
|
package/public/style.css
CHANGED
|
@@ -96,6 +96,10 @@ b { font-weight: 600; }
|
|
|
96
96
|
.pill { display: flex; align-items: center; gap: 7px; padding: 5px 11px 5px 9px; border-radius: 999px; font-family: var(--mono); font-size: 10px; font-weight: 500; letter-spacing: 0.1em; }
|
|
97
97
|
.pill .dot { width: 6px; height: 6px; border-radius: 50%; }
|
|
98
98
|
.pill.standby { background: var(--control); border: 1px solid var(--edge); color: var(--ink2); }
|
|
99
|
+
.pill.demo { background: #FFF6E5; border: 1px solid #F2D49A; color: #7A4B00; }
|
|
100
|
+
.pill.demo[hidden], .ob-demo[hidden] { display: none; }
|
|
101
|
+
.ob-demo { margin-top: 12px; padding: 10px 14px; border-radius: 10px; background: #FFF6E5; border: 1px solid #F2D49A; color: #5C3A00; font-size: 13.5px; }
|
|
102
|
+
.ob-demo code { font-family: var(--mono); font-size: 12.5px; }
|
|
99
103
|
.pill.standby .dot { border: 1px solid var(--meta); }
|
|
100
104
|
.pill.live { background: var(--brandtint); border: 1px solid var(--brandline); color: var(--amber); }
|
|
101
105
|
.pill.live .dot { background: var(--amber); }
|
|
@@ -834,3 +838,20 @@ body { height: 100dvh; }
|
|
|
834
838
|
.rep-foot { justify-content: flex-end; }
|
|
835
839
|
.rep-foot .ghostbtn { white-space: nowrap; }
|
|
836
840
|
}
|
|
841
|
+
|
|
842
|
+
/* ── lobby extras: dashboard link (operators) and the journey preview (share links) ── */
|
|
843
|
+
.ob-section[hidden], .ob-dashlink[hidden] { display: none; }
|
|
844
|
+
.ob-dashlink { align-self: flex-start; display: inline-flex; align-items: center; gap: 5px; margin-bottom: 8px; padding: 5px 12px 5px 8px; border-radius: 99px; border: 1px solid var(--edge); background: var(--control); color: var(--ink2); font-size: 13px; font-weight: 500; text-decoration: none; transition: border-color .15s, color .15s; }
|
|
845
|
+
.ob-dashlink:hover { color: var(--ink); border-color: #C6C6D0; }
|
|
846
|
+
.ob-journey { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; counter-reset: j; }
|
|
847
|
+
.ob-journey li { counter-increment: j; display: flex; align-items: center; gap: 12px; font-size: 14.5px; }
|
|
848
|
+
.ob-journey li::before { content: counter(j); width: 26px; height: 26px; flex: none; display: grid; place-items: center; border-radius: 50%; font: 500 12px var(--mono); color: var(--amber); background: var(--brandtint); border: 1px solid var(--brandline); }
|
|
849
|
+
.ob-journey + .ob-divider { margin-top: 4px; }
|
|
850
|
+
|
|
851
|
+
/* ── share-link (customer) mode: the call without operator controls ── */
|
|
852
|
+
.customer #obAgent, .customer #obVoiceField, .customer #obSttField, .customer .ob-dashlink,
|
|
853
|
+
.customer #consolebtn, .customer #modelchip, .customer #agentlog, .customer .rep-right, .customer .rep-replay { display: none !important; }
|
|
854
|
+
.customer .ob-gear { grid-template-columns: 1fr; }
|
|
855
|
+
.customer .rep-left { flex: 1; }
|
|
856
|
+
.wf-nosteps .wf-head, .wf-nosteps .wf-steps { display: none; }
|
|
857
|
+
.ob-unavailable .ob-section, .ob-unavailable .ob-divider, .ob-unavailable .ob-actions, .ob-unavailable .ob-priv { display: none !important; }
|