symbiot 0.57.8 → 0.58.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/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/desktop.mjs +53 -7
- package/icon.ico +0 -0
- package/index.mjs +1 -1
- package/marketing.mjs +2 -1
- package/package.json +3 -2
- package/postinstall.mjs +12 -9
- package/ui.mjs +46 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
What each Symbiot release brought, newest first. When Approve bumps the version, it writes that release here from the tasks it approved, worded as release notes by your connected AI (or in the tasks' own words without one). The app shows the releases newer than yours under "What's new" when it offers an update, and once more after you update.
|
|
4
4
|
|
|
5
|
+
## 0.58.0
|
|
6
|
+
|
|
7
|
+
- **Talk to Symbiot instead of typing.** A mic on the talk bar: click it and speak. Your words appear as you say them, and a pause sends them, as Enter would. When you spoke, Symbiot reads its answer out too, in the most natural voice your computer has (typed questions stay quiet), and talking again cuts it off. It uses the speech recognition built into Chrome and Edge: no key, nothing to pay. If the microphone is blocked or missing, it says what to do.
|
|
8
|
+
|
|
9
|
+
## 0.57.9
|
|
10
|
+
|
|
11
|
+
- **On Windows, Symbiot is in the Start menu and on your desktop.** Installing puts a Symbiot shortcut with the orb in both (your real desktop, even when it's in OneDrive), and opens Symbiot right away, into setup. The shortcuts start it through a small hidden script, so no black console window flashes up; `symbiot uninstall` takes them away again. Installing on a Windows desktop now opens Symbiot too, and says so in the terminal.
|
|
12
|
+
|
|
5
13
|
## 0.57.8
|
|
6
14
|
|
|
7
15
|
- **An approved deploy with your server key goes through.** The membrane read `scp -i ~/.ssh/oracle_key …` as copying your key, when the key is only what scp signs in with (`ssh -i` already passed). So argena's deploy to the game server stayed blocked even after you approved it twice. Now scp's sign-in options don't count as files it copies. Copying a key or credentials themselves (`scp ~/.ssh/id_rsa host:`) is still stopped.
|
package/README.md
CHANGED
|
@@ -678,7 +678,7 @@ time; **Close** or Esc closes a part. In a text box, or over text you've selecte
|
|
|
678
678
|
right-click still gives the usual menu. Whatever needs you glows **amber**, here and
|
|
679
679
|
on the Dashboard. Talk to it in the bar at the bottom: "open tasks" opens Tasks,
|
|
680
680
|
"go" in the work view starts what's waiting, anything else goes to the same Symbiot
|
|
681
|
-
as every chat. Paste a screenshot into the bar (Ctrl+V) or drop one on it, and
|
|
681
|
+
as every chat. **Or just say it:** click the mic on the bar and speak; your words appear as you talk, and a pause sends them. When you spoke, Symbiot reads its answer out too (typed, it stays quiet), and talking again cuts it off. It uses the speech recognition built into Chrome and Edge, so there is no key and nothing to pay. Paste a screenshot into the bar (Ctrl+V) or drop one on it, and
|
|
682
682
|
Symbiot sees it with what you say (up to 4, with any AI you've connected that reads
|
|
683
683
|
images); if it hands the work to an agent, the agent gets the screenshot too.
|
|
684
684
|
Once you've said something, the chat has its own bar over the input: **Minimize**
|
package/desktop.mjs
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// log in, so the write-up still happens on days you never open Symbiot.
|
|
6
6
|
// `symbiot app` then finds that running copy and opens its window.
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { spawn } from "node:child_process";
|
|
8
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join, dirname } from "node:path";
|
|
11
|
-
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync, readdirSync, statSync, chmodSync } from "node:fs";
|
|
11
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync, readdirSync, statSync, chmodSync, rmdirSync } from "node:fs";
|
|
12
12
|
import { CONFIG_DIR, loadConfig, saveConfig, hasCmd } from "./core.mjs";
|
|
13
13
|
|
|
14
14
|
// The Android app (android/) runs this same code with SYMBIOT_ANDROID_APP=1. Its
|
|
@@ -191,9 +191,54 @@ function launcherPath(path, node) {
|
|
|
191
191
|
for (const d of [...String(path || "").split(":"), dirname(node)]) if (d && !/node_modules[\\/]\.bin|node-gyp-bin/.test(d) && !seen.has(d)) { seen.add(d); out.push(d); }
|
|
192
192
|
return out.join(":");
|
|
193
193
|
}
|
|
194
|
+
// ---- Windows: the Start menu and the desktop -------------------------------------
|
|
195
|
+
// A shortcut in the Start menu and one on the desktop, with the orb (icon.ico). Each
|
|
196
|
+
// runs a small script through wscript (open.vbs), which starts `symbiot open` with
|
|
197
|
+
// its window hidden: node is a console program, and run straight from a shortcut it
|
|
198
|
+
// would flash a black window.
|
|
199
|
+
// Windows' own APPDATA (when it's in this home); the desktop as Windows reports it
|
|
200
|
+
// (often in OneDrive), kept in desktop.txt so uninstalling finds that shortcut again.
|
|
201
|
+
const winDirs = (home = homedir()) => {
|
|
202
|
+
const env = process.env.APPDATA || "", appdata = env && env.toLowerCase().startsWith(home.toLowerCase()) ? env : join(home, "AppData", "Roaming"), own = join(appdata, "Symbiot");
|
|
203
|
+
let desk = ""; try { desk = readFileSync(join(own, "desktop.txt"), "utf8").trim(); } catch {}
|
|
204
|
+
return { own, menu: join(appdata, "Microsoft", "Windows", "Start Menu", "Programs", "Symbiot.lnk"), desk: desk || join(home, "Desktop", "Symbiot.lnk") };
|
|
205
|
+
};
|
|
206
|
+
const ICO_SRC = fileURLToPath(new URL("./icon.ico", import.meta.url));
|
|
207
|
+
// VBScript: "" is a quote inside a string
|
|
208
|
+
const vbsContent = (node, script) => `' Symbiot: start it (or show it) with no console window\r\nCreateObject("WScript.Shell").Run """${node.replace(/"/g, '""')}"" ""${script.replace(/"/g, '""')}"" open", 0, False\r\n`;
|
|
209
|
+
// PowerShell that makes one shortcut: single quotes, '' is a quote inside one
|
|
210
|
+
function shortcutPs(lnk, vbs, ico) {
|
|
211
|
+
const q = (s) => "'" + String(s).replace(/'/g, "''") + "'";
|
|
212
|
+
return `$s = (New-Object -ComObject WScript.Shell).CreateShortcut(${q(lnk)}); $s.TargetPath = (Join-Path $env:WINDIR 'System32\\wscript.exe'); $s.Arguments = ${q('"' + vbs + '"')}; $s.IconLocation = ${q(ico + ",0")}; $s.Description = 'Symbiot'; $s.WorkingDirectory = $env:USERPROFILE; $s.Save()`;
|
|
213
|
+
}
|
|
214
|
+
function installWindows({ node, script, home, run }) {
|
|
215
|
+
const d = winDirs(home), vbs = join(d.own, "open.vbs"), ico = join(d.own, "symbiot.ico"), want = vbsContent(node, script);
|
|
216
|
+
let wrote = false;
|
|
217
|
+
try {
|
|
218
|
+
mkdirSync(d.own, { recursive: true });
|
|
219
|
+
if (!existsSync(join(d.own, "desktop.txt")) && home === homedir()) {
|
|
220
|
+
const r = run("powershell", ["-NoProfile", "-NonInteractive", "-Command", "[Environment]::GetFolderPath('Desktop')"]);
|
|
221
|
+
const dir = r && r.status === 0 ? String(r.stdout || "").trim() : "";
|
|
222
|
+
if (dir) { d.desk = join(dir, "Symbiot.lnk"); writeFileSync(join(d.own, "desktop.txt"), d.desk); }
|
|
223
|
+
}
|
|
224
|
+
let had = ""; try { had = readFileSync(vbs, "utf8"); } catch {}
|
|
225
|
+
if (had !== want) { writeFileSync(vbs, want); wrote = true; }
|
|
226
|
+
if (!existsSync(ico)) { writeFileSync(ico, readFileSync(ICO_SRC)); wrote = true; }
|
|
227
|
+
for (const lnk of [d.menu, d.desk]) {
|
|
228
|
+
if (!wrote && existsSync(lnk)) continue;
|
|
229
|
+
try { mkdirSync(join(lnk, ".."), { recursive: true }); } catch {}
|
|
230
|
+
const r = run("powershell", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", shortcutPs(lnk, vbs, ico)]);
|
|
231
|
+
if (r && r.status !== 0) return { error: String((r.stderr || r.stdout || "") || "PowerShell couldn't make the shortcut").trim().slice(0, 300) };
|
|
232
|
+
wrote = true;
|
|
233
|
+
}
|
|
234
|
+
} catch (e) { return { error: (e && e.message) || String(e) }; }
|
|
235
|
+
return { written: wrote, file: d.menu, desktop: d.desk };
|
|
236
|
+
}
|
|
194
237
|
// Writes the entry and icon when they're missing or differ. { written, file } or { skipped }.
|
|
195
|
-
function installLauncher({ node = process.execPath, script, home = homedir(), platform = OS, path = process.env.PATH || "" } = {}) {
|
|
196
|
-
if (
|
|
238
|
+
function installLauncher({ node = process.execPath, script, home = homedir(), platform = OS, path = process.env.PATH || "", run = (c, a) => spawnSync(c, a, { encoding: "utf8", windowsHide: true, timeout: 30000 }) } = {}) {
|
|
239
|
+
if (!script) return { skipped: true };
|
|
240
|
+
if (platform === "win32") return installWindows({ node, script, home, run });
|
|
241
|
+
if (platform !== "linux") return { skipped: true };
|
|
197
242
|
const file = launcherFile(home), icon = launcherIcon(home), want = launcherContent(node, script, launcherPath(path, node));
|
|
198
243
|
let wrote = false;
|
|
199
244
|
try {
|
|
@@ -207,8 +252,9 @@ function installLauncher({ node = process.execPath, script, home = homedir(), pl
|
|
|
207
252
|
return { written: wrote, file };
|
|
208
253
|
}
|
|
209
254
|
function removeLauncher(home = homedir()) {
|
|
210
|
-
const gone = [];
|
|
211
|
-
for (const f of [launcherFile(home), launcherIcon(home)]) { try { if (existsSync(f)) { unlinkSync(f); gone.push(f); } } catch {} }
|
|
255
|
+
const gone = [], d = winDirs(home);
|
|
256
|
+
for (const f of [launcherFile(home), launcherIcon(home), d.menu, d.desk, join(d.own, "open.vbs"), join(d.own, "symbiot.ico"), join(d.own, "desktop.txt")]) { try { if (existsSync(f)) { unlinkSync(f); gone.push(f); } } catch {} }
|
|
257
|
+
try { rmdirSync(d.own); } catch {}
|
|
212
258
|
return gone;
|
|
213
259
|
}
|
|
214
260
|
function autostartState() { const file = autostartFile(); return { on: existsSync(file), file, ...(OS === "android-app" ? { phone: "app" } : OS === "android" ? { phone: "termux" } : {}) }; }
|
|
@@ -226,4 +272,4 @@ function setAutostart(on, script) {
|
|
|
226
272
|
} catch (e) { return { ...autostartState(), error: String((e && e.message) || e) }; }
|
|
227
273
|
}
|
|
228
274
|
|
|
229
|
-
export { iconSvg, setLauncherLook, launcherPath, launcherFile, launcherIcon, launcherContent, installLauncher, removeLauncher, weeklyCfg, lastSlot, weeklyDue, setWeekly, weeklyState, latestWeek, runWeekly, startWeekly, notifyCmd, desktopNotify, autostartFile, autostartContent, autostartState, setAutostart };
|
|
275
|
+
export { winDirs, vbsContent, shortcutPs, iconSvg, setLauncherLook, launcherPath, launcherFile, launcherIcon, launcherContent, installLauncher, removeLauncher, weeklyCfg, lastSlot, weeklyDue, setWeekly, weeklyState, latestWeek, runWeekly, startWeekly, notifyCmd, desktopNotify, autostartFile, autostartContent, autostartState, setAutostart };
|
package/icon.ico
ADDED
|
Binary file
|
package/index.mjs
CHANGED
|
@@ -360,7 +360,7 @@ async function cmdOpen() {
|
|
|
360
360
|
if (p && p.version) { openApp(`http://127.0.0.1:${port}/?t=${cfg.appToken}`); return; }
|
|
361
361
|
let fd = "ignore"; try { mkdirSync(CONFIG_DIR, { recursive: true }); fd = openSync(join(CONFIG_DIR, "app.log"), "a"); } catch {}
|
|
362
362
|
const env = { ...process.env }; delete env.SYMBIOT_NO_OPEN;
|
|
363
|
-
spawn(process.execPath, [realpathSync(fileURLToPath(import.meta.url)), "app"], { detached: true, stdio: ["ignore", fd, fd], env }).unref();
|
|
363
|
+
spawn(process.execPath, [realpathSync(fileURLToPath(import.meta.url)), "app"], { detached: true, stdio: ["ignore", fd, fd], env, windowsHide: true }).unref();
|
|
364
364
|
}
|
|
365
365
|
// symbiot uninstall: everything Symbiot put on this computer, then the program.
|
|
366
366
|
// --keep-data keeps ~/.config/symbiot (settings, tasks, memory); --yes doesn't ask.
|
package/marketing.mjs
CHANGED
|
@@ -113,7 +113,8 @@ function draftFiles(dir = MARKETING_DIR, names = [], { max = 30 } = {}) {
|
|
|
113
113
|
}
|
|
114
114
|
};
|
|
115
115
|
walk(root, 0);
|
|
116
|
-
|
|
116
|
+
// newest first; two written in the same moment, by name, so the order never depends on timing
|
|
117
|
+
return out.sort((a, b) => b.at - a.at || String(a.rel || a.file || "").localeCompare(String(b.rel || b.file || ""))).slice(0, max);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
// ---- a draft as the post it will be ---------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice \u2014 Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"postinstall.mjs",
|
|
49
49
|
"icon.svg",
|
|
50
50
|
"scanworker.mjs",
|
|
51
|
-
"estimate.mjs"
|
|
51
|
+
"estimate.mjs",
|
|
52
|
+
"icon.ico"
|
|
52
53
|
],
|
|
53
54
|
"engines": {
|
|
54
55
|
"node": ">=18"
|
package/postinstall.mjs
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
// npm install -g symbiot runs this: Symbiot
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// to your
|
|
1
|
+
// npm install -g symbiot runs this: Symbiot where you start apps (the app menu on
|
|
2
|
+
// Linux, the Start menu and the desktop on Windows), with its icon, so you never need
|
|
3
|
+
// a terminal to start it (desktop.mjs installLauncher), and then it opens, so setup
|
|
4
|
+
// starts right away. Only for a global install on a desktop, and it never fails the
|
|
5
|
+
// install. npm hides what install scripts print, so the line goes straight to your
|
|
6
|
+
// terminal (its console on Windows).
|
|
6
7
|
import { realpathSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { spawn } from "node:child_process";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
const
|
|
10
|
+
const WIN = process.platform === "win32";
|
|
11
|
+
const say = (s) => { try { writeFileSync(WIN ? "CONOUT$" : "/dev/tty", s + "\n"); } catch { console.log(s); } };
|
|
10
12
|
try {
|
|
11
13
|
if (process.env.npm_config_global === "true" && !process.env.SYMBIOT_NO_LAUNCHER) {
|
|
12
14
|
const script = realpathSync(fileURLToPath(new URL("./index.mjs", import.meta.url)));
|
|
13
15
|
const { installLauncher } = await import("./desktop.mjs");
|
|
14
16
|
const r = installLauncher({ script });
|
|
15
|
-
const desktop = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY) && !process.env.CI && !process.env.SYMBIOT_NO_OPEN;
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
const desktop = (WIN || process.platform === "darwin" || !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)) && !process.env.CI && !process.env.SYMBIOT_NO_OPEN;
|
|
18
|
+
const where = WIN ? "the Start menu or your desktop" : "your app menu";
|
|
19
|
+
if (desktop) { spawn(process.execPath, [script, "open"], { detached: true, stdio: "ignore", windowsHide: true }).unref(); say(`\nSymbiot is installed and opening now. Next time, open it from ${where}: no terminal needed.\n`); }
|
|
20
|
+
else if (r.file) say(`\nSymbiot is in ${where}: open it from there, no terminal needed.\n`);
|
|
18
21
|
}
|
|
19
22
|
} catch {}
|
package/ui.mjs
CHANGED
|
@@ -966,6 +966,15 @@ body.lq-light .onbin{background:rgba(255,255,255,.92);border-color:rgba(21,26,33
|
|
|
966
966
|
body.lq-light .onbcard,body.lq-light .onbbox,body.lq-light .onbapp{background:rgba(21,26,33,.035);border-color:rgba(21,26,33,.07)}
|
|
967
967
|
body.lq-light .onbdots i{background:rgba(21,26,33,.1)}body.lq-light .onbdots i.on{background:var(--amber)}body.lq-light .onbdots i.past{background:rgba(242,165,65,.5)}body.lq-light .onbrepos span{background:rgba(21,26,33,.06)}body.lq-light .onbbox code{background:rgba(21,26,33,.07)}
|
|
968
968
|
@media(max-width:640px){.onbcards{grid-template-columns:1fr}.onbin{padding:22px 18px}}
|
|
969
|
+
|
|
970
|
+
/* Talking to Symbiot: the mic on the talk bar, amber while it listens */
|
|
971
|
+
#lqmic{flex:none;width:40px;height:40px;padding:0;display:grid;place-items:center;border-radius:50%;border:0;background:transparent;color:inherit;cursor:pointer;opacity:.72;transition:opacity .15s,background .15s}
|
|
972
|
+
#lqmic:hover,#lqmic:focus-visible{opacity:1;background:rgba(255,255,255,.07)}
|
|
973
|
+
body.lq-light #lqmic:hover,body.lq-light #lqmic:focus-visible{background:rgba(21,26,33,.06)}
|
|
974
|
+
#lqmic.on{opacity:1;background:#F2A541;color:#0A0B0D;animation:lqmicpulse 1.4s ease-in-out infinite}
|
|
975
|
+
@keyframes lqmicpulse{0%,100%{box-shadow:0 0 0 4px rgba(242,165,65,.28)}50%{box-shadow:0 0 0 11px rgba(242,165,65,.05)}}
|
|
976
|
+
@media (prefers-reduced-motion: reduce){#lqmic.on{animation:none;box-shadow:0 0 0 4px rgba(242,165,65,.28)}}
|
|
977
|
+
#lqmic.hidden{display:none}
|
|
969
978
|
/* the Dashboard as a stream: time runs left to right, a current per feed, now on the right */
|
|
970
979
|
.bstream{position:relative;margin-top:10px;border-radius:18px;overflow:hidden;isolation:isolate;background:#08090B;box-shadow:0 0 0 1px rgba(255,255,255,.08)}
|
|
971
980
|
body.lq-light .bstream{background:#EBEEF0;box-shadow:0 0 0 1px rgba(21,26,33,.08)}
|
|
@@ -1038,7 +1047,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
1038
1047
|
<div id="lqtalk" aria-live="polite"></div>
|
|
1039
1048
|
<div id="lqtalkbar"></div>
|
|
1040
1049
|
<div id="lqatts" aria-live="polite"></div>
|
|
1041
|
-
<form id="lqform" class="lqglass"><label for="lqask" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)">Talk to Symbiot</label><textarea id="lqask" rows="1" autocomplete="off" placeholder="Talk to Symbiot, or tell it what to do (paste a screenshot to show it)"></textarea><button type="submit" id="lqsend" aria-label="Send"><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 13V3M3.5 7.5 8 3l4.5 4.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button></form>
|
|
1050
|
+
<form id="lqform" class="lqglass"><label for="lqask" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)">Talk to Symbiot</label><textarea id="lqask" rows="1" autocomplete="off" placeholder="Talk to Symbiot, or tell it what to do (paste a screenshot to show it)"></textarea><button type="button" id="lqmic" class="hidden" aria-label="Talk to Symbiot" aria-pressed="false" title="Talk: click, then speak"><svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/></svg></button><button type="submit" id="lqsend" aria-label="Send"><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 13V3M3.5 7.5 8 3l4.5 4.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button></form>
|
|
1042
1051
|
</div>
|
|
1043
1052
|
|
|
1044
1053
|
<div id="updatebar" class="updatebar"></div>
|
|
@@ -3004,14 +3013,47 @@ el.querySelectorAll('button').forEach(function(b){b.addEventListener('click',fun
|
|
|
3004
3013
|
function lqAttach(file){if(!file||['image/png','image/jpeg','image/webp','image/gif'].indexOf(file.type)<0||file.size>8*1024*1024||LQ.atts.length>=4)return;var r=new FileReader();r.onload=function(){LQ.atts.push({data:String(r.result)});lqAttsShow();};r.readAsDataURL(file);}
|
|
3005
3014
|
// The talk box wraps and grows with what's typed, to five lines, then scrolls.
|
|
3006
3015
|
function lqGrowAsk(){var i=$('lqask');if(!i||!i.style)return;i.style.height='auto';var h=Math.min(126,Math.max(44,i.scrollHeight||44));i.style.height=h+'px';if(i.classList)i.classList.toggle('full',(i.scrollHeight||0)>126);var f=$('lqform'),b=document.body;if(f&&f.offsetHeight&&b&&b.style&&b.style.setProperty)b.style.setProperty('--lqfh',f.offsetHeight+'px');}
|
|
3007
|
-
|
|
3016
|
+
|
|
3017
|
+
// ---- Talking to Symbiot ------------------------------------------------------------
|
|
3018
|
+
// The mic on the talk bar: click, speak, and what you say shows in the bar as you say
|
|
3019
|
+
// it; a pause sends it, as Enter would. Speech becomes text in the browser itself
|
|
3020
|
+
// (Chrome and Edge, no key); where it can't, there's no mic. When you spoke, the
|
|
3021
|
+
// answer is read out too (typed, it stays quiet), and talking again cuts it off.
|
|
3022
|
+
var VOICE={rec:null,on:false,spoke:false,final:'',base:'',err:'',ph:''};
|
|
3023
|
+
function voiceOk(){return !!(window.SpeechRecognition||window.webkitSpeechRecognition);}
|
|
3024
|
+
function voiceMic(on){var m=$('lqmic');if(!m)return;m.classList.toggle('on',on);m.setAttribute('aria-pressed',on?'true':'false');m.setAttribute('aria-label',on?'Stop listening':'Talk to Symbiot');var a=$('lqask');if(a){if(on){VOICE.ph=a.getAttribute('placeholder')||'';a.setAttribute('placeholder','Listening… speak, then pause to send');}else if(VOICE.ph)a.setAttribute('placeholder',VOICE.ph);}}
|
|
3025
|
+
function voiceSay(t){LQ.talkMin=false;lqTalkMode(true);LQ.talk.push({me:false,text:t});LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);}
|
|
3026
|
+
var VOICE_WHY={'not-allowed':'Symbiot couldn’t use your microphone. Allow it for this window (the icon at the top of the window, or your system’s privacy settings), then click the mic again.','service-not-allowed':'Symbiot couldn’t use your microphone. Allow it for this window, then click the mic again.','audio-capture':'No microphone was found. Plug one in, or check it’s switched on, then try again.','network':'Talking needs an internet connection: your browser turns speech into text online.'};
|
|
3027
|
+
function voiceToggle(){
|
|
3028
|
+
if(VOICE.on){try{VOICE.rec.stop();}catch(e){}return;}
|
|
3029
|
+
try{if(window.speechSynthesis)window.speechSynthesis.cancel();}catch(e){}
|
|
3030
|
+
var R=window.SpeechRecognition||window.webkitSpeechRecognition;if(!R)return;
|
|
3031
|
+
var r;try{r=new R();}catch(e){return;}
|
|
3032
|
+
var ask=$('lqask');VOICE.rec=r;VOICE.final='';VOICE.err='';VOICE.base=((ask&&ask.value)||'').trim();
|
|
3033
|
+
r.lang=navigator.language||'en-US';r.interimResults=true;r.continuous=false;r.maxAlternatives=1;
|
|
3034
|
+
r.onstart=function(){VOICE.on=true;voiceMic(true);lqAct();};
|
|
3035
|
+
r.onresult=function(ev){var interim='';for(var i=ev.resultIndex;i<ev.results.length;i++){var t=ev.results[i][0].transcript;if(ev.results[i].isFinal)VOICE.final+=t;else interim+=t;}
|
|
3036
|
+
if(ask){ask.value=((VOICE.base?VOICE.base+' ':'')+(VOICE.final+interim)).replace(/ +/g,' ').trim();lqGrowAsk();}};
|
|
3037
|
+
r.onerror=function(ev){VOICE.err=(ev&&ev.error)||'';};
|
|
3038
|
+
r.onend=function(){VOICE.on=false;voiceMic(false);VOICE.rec=null;var said=((ask&&ask.value)||'').trim(),why=VOICE_WHY[VOICE.err];
|
|
3039
|
+
if(why){voiceSay(why);return;}
|
|
3040
|
+
if(VOICE.final.trim()&&said){VOICE.spoke=true;lqSay();}};
|
|
3041
|
+
try{r.start();}catch(e){VOICE.on=false;voiceMic(false);}}
|
|
3042
|
+
// read out: plain words (no markdown marks), up to a few sentences; a voice in your language, a natural one if there is
|
|
3043
|
+
function voiceSpeak(t){if(!window.speechSynthesis||!t)return;
|
|
3044
|
+
var clean=String(t).replace(/[*_#>|]+/g,' ').replace(new RegExp(String.fromCharCode(96),'g'),'').replace(/https?:[^ ]+/g,'a link').replace(/ +/g,' ').trim();
|
|
3045
|
+
if(clean.length>700){var cut=clean.slice(0,700),at=Math.max(cut.lastIndexOf('. '),cut.lastIndexOf('? '),cut.lastIndexOf('! '));clean=(at>200?cut.slice(0,at+1):cut)+' The rest is on screen.';}
|
|
3046
|
+
try{var u=new SpeechSynthesisUtterance(clean),lang=(navigator.language||'en').slice(0,2).toLowerCase(),vs=window.speechSynthesis.getVoices().filter(function(v){return (v.lang||'').toLowerCase().indexOf(lang)===0;});
|
|
3047
|
+
var best=vs.filter(function(v){return /natural|neural|google|online/i.test(v.name);})[0]||vs[0];if(best)u.voice=best;u.lang=navigator.language||'en-US';u.rate=1.02;
|
|
3048
|
+
window.speechSynthesis.cancel();window.speechSynthesis.speak(u);}catch(e){}}
|
|
3049
|
+
function lqSay(){var spoke=VOICE.spoke;VOICE.spoke=false;var i=$('lqask');var q=((i&&i.value)||'').trim(),shots=LQ.atts.slice();if(!q&&!shots.length)return;if(!q)q='Take a look at this screenshot.';i.value='';lqGrowAsk();LQ.atts=[];lqAttsShow();lqAct();
|
|
3008
3050
|
var low=q.toLowerCase().replace(/^(please |can you |could you )/,''),hit='';
|
|
3009
3051
|
Object.keys(LQNAMES).forEach(function(k){var nm=LQNAMES[k].toLowerCase();if(hit)return;['open ','show ','go to ','take me to '].forEach(function(v){if(low.indexOf(v+nm)===0||low.indexOf(v+'my '+nm)===0)hit=k;});if(low===nm)hit=k;});
|
|
3010
3052
|
if(hit){lqTalkMode(true);lqUse(hit,null,'talk');lqGo(hit);return;}
|
|
3011
3053
|
if(/^(go|start|go for it|start them|start it)[.! ]*$/.test(low)){lqTalkMode(true);lqGoWork();return;}
|
|
3012
3054
|
if(/^(home|back)[.! ]*$/.test(low)&&LQ.scene==='work'){lqHome();return;}
|
|
3013
3055
|
LQ.talkMin=false;lqTalkMode(true);LQ.talk.push({me:true,text:q,imgs:shots.map(function(a){return a.data;})});LQ.talk=LQ.talk.slice(-8);lqTalkShow(true);
|
|
3014
|
-
api('/api/home/ask',{question:q,images:shots}).then(function(r){LQ.talk.push({me:false,steps:r.steps||[],text:r.error==='not-connected'?'Connect an AI in Settings to talk to me. Say "open settings".':(r.answer||r.error||'(no answer)')});LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);LQ.ripple=[0.5,0.47,LQ.t];lqLoad(false);}).catch(function(e){LQ.talk.push({me:false,text:String((e&&e.message)||e)});lqTalkShow(false);});}
|
|
3056
|
+
api('/api/home/ask',{question:q,images:shots}).then(function(r){LQ.talk.push({me:false,steps:r.steps||[],text:r.error==='not-connected'?'Connect an AI in Settings to talk to me. Say "open settings".':(r.answer||r.error||'(no answer)')});if(spoke)voiceSpeak(LQ.talk[LQ.talk.length-1].text);LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);LQ.ripple=[0.5,0.47,LQ.t];lqLoad(false);}).catch(function(e){LQ.talk.push({me:false,text:String((e&&e.message)||e)});lqTalkShow(false);});}
|
|
3015
3057
|
// The physics: each droplet a critically damped spring to its place (no wobble,
|
|
3016
3058
|
// no overshoot), pushed off its neighbours and the core where they'd overlap.
|
|
3017
3059
|
function lqStep(){var S0=lqSize();if(S0.w!==LQ.bw||S0.h!==LQ.bh){if(LQ.scene==='work'&&LQ.workData)lqBuildWork();else if(LQ.adapt)lqBuild();}var S=S0,cx=S.w/2,cy=(LQ.scene!=='work'&&LQ.cy)||S.h*0.47,k=0.022,c=2*Math.sqrt(k),th=LQ.theme,still=th.still,rest=LQ.mode==='rest',pool=LQ.mode==='pool',talk=LQ.talking&&!pool;
|
|
@@ -3244,6 +3286,7 @@ function lqInit(){var bd=document.body;if(!bd||!bd.classList)return;lqIcon(lqLoo
|
|
|
3244
3286
|
if(document.addEventListener){document.addEventListener('contextmenu',function(ev){var b=document.body,t=ev&&ev.target;if(!b||!b.classList||!b.classList.contains('lq-liquid'))return;if(t&&t.closest&&t.closest('input,textarea,select,[contenteditable]'))return;var sel=window.getSelection?String(window.getSelection()):'';if(sel)return;ev.preventDefault();lqBack();});
|
|
3245
3287
|
document.addEventListener('mouseup',function(ev){var b=document.body;if(ev&&ev.button===3&&b&&b.classList&&b.classList.contains('lq-liquid')){ev.preventDefault();lqBack();}});}
|
|
3246
3288
|
var L=$('liquid');if(L&&L.addEventListener){L.addEventListener('click',function(ev){var t=ev&&ev.target;if(t&&t.closest&&t.closest('button,input,form,.lqd'))return;var x=ev.clientX,y=ev.clientY,hit=null;LQ.drops.forEach(function(d){if(!hit&&Math.hypot(x-d.x,y-d.y)<=d.cr+8)hit=d;});if(hit)lqOpen(hit,ev);else if(LQ.openTag)lqTag(LQ.openTag);});L.addEventListener('pointermove',function(ev){LQ.pointer=[ev.clientX,ev.clientY];if(ev.pointerType==='touch')LQ.touch=true;lqAct();});L.addEventListener('pointerdown',function(ev){if(ev.pointerType==='touch')LQ.touch=true;lqAct();});}
|
|
3289
|
+
var mic=$('lqmic');if(mic&&voiceOk()){mic.classList.remove('hidden');mic.addEventListener('click',function(){voiceToggle();});try{if(window.speechSynthesis)window.speechSynthesis.getVoices();}catch(e){}}
|
|
3247
3290
|
var f=$('lqform');if(f&&f.addEventListener)f.addEventListener('submit',function(ev){if(ev&&ev.preventDefault)ev.preventDefault();lqSay();});
|
|
3248
3291
|
var ia=$('lqask');if(ia&&ia.addEventListener){ia.addEventListener('paste',function(ev){var it=(ev.clipboardData&&ev.clipboardData.items)||[],got=false;for(var k=0;k<it.length;k++){if(it[k].kind==='file'&&String(it[k].type).indexOf('image/')===0){lqAttach(it[k].getAsFile());got=true;}}if(got)ev.preventDefault();});
|
|
3249
3292
|
var fm=$('lqform');if(fm&&fm.addEventListener){fm.addEventListener('dragover',function(ev){ev.preventDefault();if(fm.classList)fm.classList.add('drop');});fm.addEventListener('dragleave',function(){if(fm.classList)fm.classList.remove('drop');});fm.addEventListener('drop',function(ev){ev.preventDefault();if(fm.classList)fm.classList.remove('drop');var fs=(ev.dataTransfer&&ev.dataTransfer.files)||[];for(var k=0;k<fs.length;k++)lqAttach(fs[k]);});}
|