@loupekit/sdk 0.10.15 → 0.10.16-next.62
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/README.md +42 -0
- package/dist/index.global.js +616 -11
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +616 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -654,6 +654,99 @@ var STYLES = (
|
|
|
654
654
|
font-size: 10px; white-space: pre-wrap; overflow-wrap: anywhere;
|
|
655
655
|
}
|
|
656
656
|
|
|
657
|
+
/* ------------------------------------------------- consent-gated agent navigation */
|
|
658
|
+
.consent {
|
|
659
|
+
margin: 8px 10px 0; padding: 10px; border: 1px solid var(--pin); border-radius: 10px;
|
|
660
|
+
background: var(--bg-2);
|
|
661
|
+
}
|
|
662
|
+
.cs-head { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink); }
|
|
663
|
+
.cs-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--pin); flex: none; animation: loupe-pulse 1.4s ease-in-out infinite; }
|
|
664
|
+
@media (prefers-reduced-motion: reduce) { .cs-dot { animation: none; } }
|
|
665
|
+
.cs-url {
|
|
666
|
+
margin-top: 6px; padding: 6px 7px; border-radius: 7px; background: var(--bg);
|
|
667
|
+
font-family: ui-monospace, Menlo, monospace; font-size: 10.5px; color: var(--ink);
|
|
668
|
+
overflow-wrap: anywhere;
|
|
669
|
+
}
|
|
670
|
+
.cs-why { margin-top: 6px; font-size: 11.5px; line-height: 1.45; color: var(--muted); }
|
|
671
|
+
.cs-btns { display: flex; justify-content: flex-end; gap: 6px; margin-top: 9px; }
|
|
672
|
+
.cs-btns button { padding: 5px 10px; border-radius: 7px; font-size: 11.5px; font-weight: 600; cursor: pointer; }
|
|
673
|
+
.cs-deny { border: 1px solid var(--line); background: var(--bg); color: var(--ink); }
|
|
674
|
+
.cs-deny:hover { border-color: var(--accent); }
|
|
675
|
+
.cs-go { border: 1px solid var(--accent); background: var(--accent); color: #fff; }
|
|
676
|
+
|
|
677
|
+
/* --------------------------------------------------------------- generate + iterate */
|
|
678
|
+
.genwrap { margin-top: 8px; }
|
|
679
|
+
.gen-open {
|
|
680
|
+
width: 100%; padding: 7px; border: 1px dashed var(--line); border-radius: 8px;
|
|
681
|
+
background: transparent; color: var(--muted); font-size: 11.5px; font-weight: 600; cursor: pointer;
|
|
682
|
+
}
|
|
683
|
+
.gen-open:hover { border-color: var(--accent); color: var(--accent); }
|
|
684
|
+
.genhead { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
|
|
685
|
+
.gen-t { flex: 1; font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--muted); }
|
|
686
|
+
.gen-nav { display: inline-flex; align-items: center; gap: 2px; }
|
|
687
|
+
.gen-step {
|
|
688
|
+
width: 20px; height: 20px; padding: 0; border: 1px solid var(--line); border-radius: 6px;
|
|
689
|
+
background: var(--bg); color: var(--ink); font-size: 13px; line-height: 1; cursor: pointer;
|
|
690
|
+
}
|
|
691
|
+
.gen-step:disabled { opacity: .4; cursor: default; }
|
|
692
|
+
.gen-n { min-width: 32px; text-align: center; font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
693
|
+
.gen-undo, .gen-x {
|
|
694
|
+
padding: 3px 8px; border: 1px solid var(--line); border-radius: 7px;
|
|
695
|
+
background: var(--bg); color: var(--ink); font-size: 11px; cursor: pointer;
|
|
696
|
+
}
|
|
697
|
+
.gen-undo:disabled { opacity: .4; cursor: default; }
|
|
698
|
+
.gen-undo:hover, .gen-x:hover { border-color: var(--accent); }
|
|
699
|
+
.genbusy, .genempty { padding: 14px; text-align: center; font-size: 11.5px; color: var(--muted); }
|
|
700
|
+
/* the preview plane: generated markup over the original capture, sandboxed.
|
|
701
|
+
min-height matters \u2014 the iframe is absolutely positioned, so a thread with no
|
|
702
|
+
screenshot would otherwise give the plane nothing in-flow and collapse it to a
|
|
703
|
+
couple of pixels. */
|
|
704
|
+
.genplane { position: relative; min-height: 150px; border: 1px solid var(--line); border-radius: 9px; overflow: hidden; background: var(--bg); }
|
|
705
|
+
.genbase { display: block; width: 100%; }
|
|
706
|
+
.genframe {
|
|
707
|
+
position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: transparent;
|
|
708
|
+
transition: opacity .08s linear;
|
|
709
|
+
}
|
|
710
|
+
.genslider { display: flex; align-items: center; gap: 7px; margin-top: 6px; }
|
|
711
|
+
.gs-lab { font-size: 10.5px; color: var(--muted); }
|
|
712
|
+
.gs-range { flex: 1; min-width: 0; accent-color: var(--accent); }
|
|
713
|
+
.gs-n { min-width: 32px; text-align: right; font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
714
|
+
.gennotes {
|
|
715
|
+
margin-top: 6px; padding: 6px 8px; border-radius: 7px; background: var(--bg-3);
|
|
716
|
+
font-size: 11px; line-height: 1.45; color: var(--muted);
|
|
717
|
+
}
|
|
718
|
+
.geniter { display: flex; gap: 5px; margin-top: 7px; }
|
|
719
|
+
.iter-in {
|
|
720
|
+
flex: 1; min-width: 0; padding: 6px 8px; border: 1px solid var(--line); border-radius: 8px;
|
|
721
|
+
background: var(--bg); color: var(--ink); font-size: 11.5px;
|
|
722
|
+
}
|
|
723
|
+
.iter-in:focus { outline: none; border-color: var(--accent); }
|
|
724
|
+
.iter-send {
|
|
725
|
+
flex: none; padding: 6px 10px; border: 1px solid var(--accent); border-radius: 8px;
|
|
726
|
+
background: var(--accent); color: #fff; font-size: 11.5px; font-weight: 600; cursor: pointer;
|
|
727
|
+
}
|
|
728
|
+
.iter-send:disabled { opacity: .5; cursor: default; }
|
|
729
|
+
/* the access gate, when there is no generator */
|
|
730
|
+
.gengate { padding: 10px; border: 1px solid var(--line); border-radius: 9px; background: var(--bg-2); }
|
|
731
|
+
.gate-t { font-size: 12px; font-weight: 700; color: var(--ink); }
|
|
732
|
+
.gate-b { margin-top: 4px; font-size: 11.5px; line-height: 1.45; color: var(--muted); }
|
|
733
|
+
.gate-ask {
|
|
734
|
+
margin-top: 8px; padding: 6px 10px; border: 1px solid var(--accent); border-radius: 8px;
|
|
735
|
+
background: var(--accent); color: #fff; font-size: 11.5px; font-weight: 600; cursor: pointer;
|
|
736
|
+
}
|
|
737
|
+
.gate-ask:disabled { opacity: .6; cursor: default; }
|
|
738
|
+
.gate-hint { margin-top: 7px; font-size: 10.5px; color: var(--muted); }
|
|
739
|
+
.gate-hint code { font-family: ui-monospace, Menlo, monospace; color: var(--ink); }
|
|
740
|
+
|
|
741
|
+
/* dictation */
|
|
742
|
+
.voice {
|
|
743
|
+
width: 30px; flex: none; padding: 0; border: 1px solid var(--line); border-radius: 8px;
|
|
744
|
+
background: var(--bg-2); color: var(--muted); font-size: 13px; cursor: pointer;
|
|
745
|
+
}
|
|
746
|
+
.voice:hover:not(:disabled) { border-color: var(--accent); color: var(--ink); }
|
|
747
|
+
.voice:disabled { opacity: .45; cursor: not-allowed; }
|
|
748
|
+
.voice.on { border-color: var(--pin); color: var(--pin); background: var(--bg-3); }
|
|
749
|
+
|
|
657
750
|
/* ------------------------------------------------------- hint card (once per view) */
|
|
658
751
|
.hint {
|
|
659
752
|
position: relative; margin: 8px 12px 0; padding: 10px 28px 10px 11px;
|
|
@@ -2378,7 +2471,7 @@ async function domToForeignObjectSvg(node, options) {
|
|
|
2378
2471
|
log.time("embed node");
|
|
2379
2472
|
embedNode(clone, context);
|
|
2380
2473
|
const count = tasks.length;
|
|
2381
|
-
let
|
|
2474
|
+
let current2 = 0;
|
|
2382
2475
|
const runTask = async () => {
|
|
2383
2476
|
while (true) {
|
|
2384
2477
|
const task = tasks.pop();
|
|
@@ -2389,10 +2482,10 @@ async function domToForeignObjectSvg(node, options) {
|
|
|
2389
2482
|
} catch (error) {
|
|
2390
2483
|
context.log.warn("Failed to run task", error);
|
|
2391
2484
|
}
|
|
2392
|
-
progress?.(++
|
|
2485
|
+
progress?.(++current2, count);
|
|
2393
2486
|
}
|
|
2394
2487
|
};
|
|
2395
|
-
progress?.(
|
|
2488
|
+
progress?.(current2, count);
|
|
2396
2489
|
await Promise.all([...Array.from({ length: 4 })].map(runTask));
|
|
2397
2490
|
log.timeEnd("embed node");
|
|
2398
2491
|
await onEmbedNode?.(clone);
|
|
@@ -2956,6 +3049,86 @@ function awaitingReview(comments) {
|
|
|
2956
3049
|
return comments.filter((c) => c.status === "in_review");
|
|
2957
3050
|
}
|
|
2958
3051
|
|
|
3052
|
+
// ../shared/dist/iteration.js
|
|
3053
|
+
function emptyIterations() {
|
|
3054
|
+
return { items: [], index: -1 };
|
|
3055
|
+
}
|
|
3056
|
+
function current(state) {
|
|
3057
|
+
return state.index >= 0 ? state.items[state.index] ?? null : null;
|
|
3058
|
+
}
|
|
3059
|
+
function addIteration(state, iteration) {
|
|
3060
|
+
const head = state.items.slice(0, state.index + 1);
|
|
3061
|
+
const items = [...head, iteration].slice(-MAX_ITERATIONS);
|
|
3062
|
+
return { items, index: items.length - 1 };
|
|
3063
|
+
}
|
|
3064
|
+
var MAX_ITERATIONS = 20;
|
|
3065
|
+
function canUndo(state) {
|
|
3066
|
+
return state.items.length > 0;
|
|
3067
|
+
}
|
|
3068
|
+
function undo(state) {
|
|
3069
|
+
if (!state.items.length)
|
|
3070
|
+
return state;
|
|
3071
|
+
const items = state.items.slice(0, -1);
|
|
3072
|
+
return { items, index: items.length - 1 };
|
|
3073
|
+
}
|
|
3074
|
+
function canMove(state, delta) {
|
|
3075
|
+
const next = state.index + delta;
|
|
3076
|
+
return next >= 0 && next < state.items.length;
|
|
3077
|
+
}
|
|
3078
|
+
function move(state, delta) {
|
|
3079
|
+
return canMove(state, delta) ? { ...state, index: state.index + delta } : state;
|
|
3080
|
+
}
|
|
3081
|
+
function stackLabel(state) {
|
|
3082
|
+
return state.items.length ? `${state.index + 1} / ${state.items.length}` : "";
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
// ../shared/dist/consent.js
|
|
3086
|
+
function emptyConsent() {
|
|
3087
|
+
return { state: "idle", request: null, history: [] };
|
|
3088
|
+
}
|
|
3089
|
+
function isNavigableUrl(raw) {
|
|
3090
|
+
try {
|
|
3091
|
+
const u = new URL(raw);
|
|
3092
|
+
return u.protocol === "http:" || u.protocol === "https:";
|
|
3093
|
+
} catch {
|
|
3094
|
+
return false;
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
function requestNavigation(record, request) {
|
|
3098
|
+
if (!isNavigableUrl(request.url))
|
|
3099
|
+
return record;
|
|
3100
|
+
return {
|
|
3101
|
+
state: "requested",
|
|
3102
|
+
request: {
|
|
3103
|
+
id: request.id ?? `nav${Date.now().toString(36)}`,
|
|
3104
|
+
at: request.at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3105
|
+
url: request.url,
|
|
3106
|
+
reason: request.reason,
|
|
3107
|
+
requester: request.requester
|
|
3108
|
+
},
|
|
3109
|
+
history: record.history
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
3112
|
+
function isPending(record) {
|
|
3113
|
+
return record.state === "requested" && record.request !== null;
|
|
3114
|
+
}
|
|
3115
|
+
function decide(record, granted, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3116
|
+
if (!isPending(record))
|
|
3117
|
+
return { record, navigateTo: null };
|
|
3118
|
+
const url = record.request.url;
|
|
3119
|
+
return {
|
|
3120
|
+
record: {
|
|
3121
|
+
state: granted ? "granted" : "denied",
|
|
3122
|
+
request: null,
|
|
3123
|
+
history: [...record.history, { url, decision: granted ? "granted" : "denied", at: now }]
|
|
3124
|
+
},
|
|
3125
|
+
navigateTo: granted ? url : null
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
function withdraw(record) {
|
|
3129
|
+
return isPending(record) ? { ...record, state: "idle", request: null } : record;
|
|
3130
|
+
}
|
|
3131
|
+
|
|
2959
3132
|
// ../shared/dist/index.js
|
|
2960
3133
|
var COMMENT_STAGES = ["queue", "todo", "in_progress", "in_review", "resolved"];
|
|
2961
3134
|
var STAGE_LABELS = {
|
|
@@ -3003,7 +3176,7 @@ function normalizePriority(value) {
|
|
|
3003
3176
|
}
|
|
3004
3177
|
|
|
3005
3178
|
// src/app.ts
|
|
3006
|
-
var SDK_VERSION = true ? "0.10.
|
|
3179
|
+
var SDK_VERSION = true ? "0.10.16" : "dev";
|
|
3007
3180
|
var ACCENTS = [
|
|
3008
3181
|
{ id: "indigo", dark: "#6b73e6", light: "#4a55d6", soft: "rgba(107,115,230,0.12)" },
|
|
3009
3182
|
{ id: "violet", dark: "#a06be6", light: "#7c3fd4", soft: "rgba(160,107,230,0.14)" },
|
|
@@ -3159,6 +3332,18 @@ var LoupeApp = class {
|
|
|
3159
3332
|
this.summaryOpen = false;
|
|
3160
3333
|
/** Per-browser project settings: a repo override and the environment URLs. */
|
|
3161
3334
|
this.project = { environments: [] };
|
|
3335
|
+
/** Iteration history per thread — generating a change is a stack, not a one-shot. */
|
|
3336
|
+
this.iterations = /* @__PURE__ */ new Map();
|
|
3337
|
+
/** The thread whose generate pane is open, if any. */
|
|
3338
|
+
this.genOpen = null;
|
|
3339
|
+
/** Threads with a generation in flight. */
|
|
3340
|
+
this.genBusy = /* @__PURE__ */ new Set();
|
|
3341
|
+
/** Opacity of the generated plane over the original, 0..1 (the compare slider). */
|
|
3342
|
+
this.genOpacity = 0.6;
|
|
3343
|
+
/** The navigation consent state machine — a URL only ever comes out of `decide`. */
|
|
3344
|
+
this.consent = emptyConsent();
|
|
3345
|
+
/** The live SpeechRecognition instance while dictating, if any. */
|
|
3346
|
+
this.voice = null;
|
|
3162
3347
|
this.projSearch = "";
|
|
3163
3348
|
this.projOpen = false;
|
|
3164
3349
|
this.projResults = [];
|
|
@@ -3395,6 +3580,9 @@ var LoupeApp = class {
|
|
|
3395
3580
|
window.removeEventListener("pointerup", this.onResizeUp);
|
|
3396
3581
|
this.saveState();
|
|
3397
3582
|
};
|
|
3583
|
+
/** Draft follow-ups, kept out of the render path so the input never loses focus. */
|
|
3584
|
+
this.iterDraft = /* @__PURE__ */ new Map();
|
|
3585
|
+
this.voiceTarget = null;
|
|
3398
3586
|
this.cfg = cfg;
|
|
3399
3587
|
this.store = cfg.apiBase ? new HttpAdapter(cfg.apiBase, cfg.user, cfg.userHmac, cfg.headers, cfg.credentials) : new LocalStorageAdapter();
|
|
3400
3588
|
}
|
|
@@ -3416,7 +3604,13 @@ var LoupeApp = class {
|
|
|
3416
3604
|
this.observe();
|
|
3417
3605
|
this.watchNavigation();
|
|
3418
3606
|
if (!this.tourDone && this.open && !this.isMobile()) this.startTour();
|
|
3419
|
-
if (this.cfg.autoOpen) this.setMode("inspect");
|
|
3607
|
+
if (this.cfg.autoOpen) this.setMode(this.cfg.tool === "note" ? "free" : "inspect");
|
|
3608
|
+
}
|
|
3609
|
+
/** Arm a tool from outside — the extension's context menus, or a host's own button. */
|
|
3610
|
+
openTool(tool) {
|
|
3611
|
+
this.open = true;
|
|
3612
|
+
this.applyDockLayout();
|
|
3613
|
+
this.setMode(tool === "note" ? "free" : "inspect");
|
|
3420
3614
|
}
|
|
3421
3615
|
/**
|
|
3422
3616
|
* Reload comments when the page URL changes without a full reload (SPA
|
|
@@ -3649,7 +3843,9 @@ var LoupeApp = class {
|
|
|
3649
3843
|
});
|
|
3650
3844
|
const resize = el("div", "resize");
|
|
3651
3845
|
resize.addEventListener("pointerdown", this.onResizeDown);
|
|
3652
|
-
|
|
3846
|
+
this.consentEl = el("div", "consent");
|
|
3847
|
+
this.consentEl.style.display = "none";
|
|
3848
|
+
dock.append(head, this.minBar, this.consentEl, tabs, homeView, commentsView, activityView, ...customViews, resize);
|
|
3653
3849
|
for (const [id, view] of [["home", homeView], ["comments", commentsView], ["activity", activityView]]) {
|
|
3654
3850
|
this.viewEls.set(id, view);
|
|
3655
3851
|
}
|
|
@@ -3964,7 +4160,7 @@ var LoupeApp = class {
|
|
|
3964
4160
|
}
|
|
3965
4161
|
pop.classList.add("open");
|
|
3966
4162
|
const envs = this.project.environments;
|
|
3967
|
-
pop.innerHTML = `<div class="pp-head"><span>Repository</span><button class="pp-x" data-role="pp-close" aria-label="Close">\u2715</button></div><div class="pp-cur">${repo ? `New comments are filed against <b>${escapeHtml(repo)}</b>.` : "This page is not linked to a repository yet."}</div>` + (this.cfg.repos ? `<input class="pp-search" type="search" placeholder="Search repositories\u2026" value="${escapeAttr(this.projSearch)}" aria-label="Search repositories"><div class="pp-list" id="loupe-pp-repos"></div>` : `<div class="pp-empty">No repository list to search. Pass <code>repos</code> to <code>init()</code> \u2014 a string array, or a function the panel calls with the search text.</div>`) + (repo ? `<button class="pp-clear" data-role="pp-clear">Unlink this page</button>` : "") + `<div class="pp-sep"></div><div class="pp-head"><span>Environments</span></div><div class="pp-list">` + (envs.length ? envs.map((u, i) => `<div class="pp-env"><span class="pp-env-u" title="${escapeAttr(u)}">${escapeHtml(u)}</span><button class="pp-x" data-env-rm="${i}" aria-label="Remove environment">\u2715</button></div>`).join("") : `<div class="pp-empty">No environment URLs yet.</div>`) + `</div><div class="pp-add"><input class="pp-env-url" type="url" placeholder="https://staging.example.com" value="${escapeAttr(this.envDraft)}" aria-label="Environment URL"><button class="pp-add-b" data-role="env-add">Add</button></div><div class="pp-err" id="loupe-pp-err"></div>`;
|
|
4163
|
+
pop.innerHTML = `<div class="pp-head"><span>Repository</span><button class="pp-x" data-role="pp-close" aria-label="Close">\u2715</button></div><div class="pp-cur">${repo ? `New comments are filed against <b>${escapeHtml(repo)}</b>.` : "This page is not linked to a repository yet."}</div>` + (this.cfg.repos ? `<input class="pp-search" type="search" placeholder="Search repositories\u2026" value="${escapeAttr(this.projSearch)}" aria-label="Search repositories"><div class="pp-list" id="loupe-pp-repos"></div>` : `<div class="pp-empty">No repository list to search. Pass <code>repos</code> to <code>init()</code> \u2014 a string array, or a function the panel calls with the search text.</div>`) + (repo ? `<button class="pp-clear" data-role="pp-clear">Unlink this page</button>` : "") + `<div class="pp-sep"></div><div class="pp-head"><span>Environments</span></div><div class="pp-list">` + (envs.length ? envs.map((u, i) => `<div class="pp-env"><span class="pp-env-u" title="${escapeAttr(u)}">${escapeHtml(u)}</span><button class="pp-x" data-env-rm="${i}" aria-label="Remove environment">\u2715</button></div>`).join("") : `<div class="pp-empty">No environment URLs yet.</div>`) + `</div><div class="pp-add"><input class="pp-env-url" type="url" placeholder="https://staging.example.com" value="${escapeAttr(this.envDraft)}" aria-label="Environment URL"><button class="pp-add-b" data-role="env-add">Add</button></div><div class="pp-sep"></div><div class="pp-head"><span>Local AI</span></div><div class="pp-empty">Any OpenAI-compatible server \u2014 used by the Generate pane.</div><div class="pp-add"><input class="pp-ai-url" type="url" placeholder="http://localhost:11434" value="${escapeAttr(this.project.localAi?.url ?? "")}" aria-label="Local AI endpoint"></div><div class="pp-add"><input class="pp-ai-model" type="text" placeholder="llama3.2" value="${escapeAttr(this.project.localAi?.model ?? "")}" aria-label="Model name"><button class="pp-add-b" data-role="ai-save">Save</button></div><div class="pp-add"><button class="pp-clear" data-role="ai-test">Test connection</button></div><div class="pp-ai-out" id="loupe-pp-ai"></div><div class="pp-err" id="loupe-pp-err"></div>`;
|
|
3968
4164
|
this.renderRepoList();
|
|
3969
4165
|
this.renderProjectError();
|
|
3970
4166
|
pop.querySelector('[data-role="pp-close"]').onclick = () => this.setProjectOpen(false);
|
|
@@ -4020,6 +4216,25 @@ var LoupeApp = class {
|
|
|
4020
4216
|
this.renderProject();
|
|
4021
4217
|
pop.querySelector(".pp-env-url")?.focus();
|
|
4022
4218
|
};
|
|
4219
|
+
for (const sel of [".pp-ai-url", ".pp-ai-model"]) {
|
|
4220
|
+
pop.querySelector(sel)?.addEventListener("click", (e) => e.stopPropagation());
|
|
4221
|
+
}
|
|
4222
|
+
pop.querySelector('[data-role="ai-save"]').onclick = () => {
|
|
4223
|
+
const url2 = normalizeEnvUrl(pop.querySelector(".pp-ai-url").value);
|
|
4224
|
+
const model = pop.querySelector(".pp-ai-model").value.trim();
|
|
4225
|
+
if (!url2 || !model) {
|
|
4226
|
+
this.setLocalAiStatus(this.project.localAi ? `Saved: ${this.project.localAi.model} at ${this.project.localAi.url}` : "Nothing saved yet.");
|
|
4227
|
+
this.projError = !url2 ? "Enter a full http:// or https:// endpoint URL." : "Enter a model name.";
|
|
4228
|
+
this.renderProjectError();
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
this.projError = "";
|
|
4232
|
+
this.setLocalAi({ url: url2, model });
|
|
4233
|
+
this.setLocalAiStatus(`Saved: ${model} at ${url2}`);
|
|
4234
|
+
};
|
|
4235
|
+
pop.querySelector('[data-role="ai-test"]').onclick = () => {
|
|
4236
|
+
void this.testLocalAi(pop);
|
|
4237
|
+
};
|
|
4023
4238
|
}
|
|
4024
4239
|
/** Per-browser project settings, keyed separately from the dock's UI state. */
|
|
4025
4240
|
loadProject() {
|
|
@@ -4029,7 +4244,8 @@ var LoupeApp = class {
|
|
|
4029
4244
|
const p = raw ? JSON.parse(raw) : null;
|
|
4030
4245
|
const repo = typeof p?.repo === "string" && p.repo ? p.repo : void 0;
|
|
4031
4246
|
const stored = Array.isArray(p?.environments) ? p.environments.filter((u) => typeof u === "string") : null;
|
|
4032
|
-
|
|
4247
|
+
const localAi = p?.localAi && typeof p.localAi.url === "string" && typeof p.localAi.model === "string" ? { url: p.localAi.url, model: p.localAi.model } : void 0;
|
|
4248
|
+
this.project = { repo, environments: stored ?? fromConfig, localAi };
|
|
4033
4249
|
} catch {
|
|
4034
4250
|
this.project = { environments: fromConfig };
|
|
4035
4251
|
}
|
|
@@ -4463,7 +4679,7 @@ var LoupeApp = class {
|
|
|
4463
4679
|
prioSel.value,
|
|
4464
4680
|
typeSel.value
|
|
4465
4681
|
);
|
|
4466
|
-
btns.append(cancel, save);
|
|
4682
|
+
btns.append(this.voiceButton(ta), cancel, save);
|
|
4467
4683
|
row.append(btns);
|
|
4468
4684
|
c.append(label, title, ta, attach, meta, row);
|
|
4469
4685
|
const w = 320, h = 380;
|
|
@@ -4910,6 +5126,7 @@ var LoupeApp = class {
|
|
|
4910
5126
|
this.minBar.innerHTML = `<span class="logo">\u25CE</span><span class="mtext"><b>${openCount}</b> open ${this.scope === "all" ? "in this project" : "on this page"}</span><span class="mrestore" aria-hidden="true">\u25B8</span>`;
|
|
4911
5127
|
for (const t of this.tabList) d.classList.toggle(`tab-${t.id}`, this.tab === t.id);
|
|
4912
5128
|
for (const [id, view] of this.viewEls) view.classList.toggle("on", id === this.tab);
|
|
5129
|
+
this.renderConsent();
|
|
4913
5130
|
this.renderHome();
|
|
4914
5131
|
d.querySelectorAll(".tabs .tab").forEach((b) => b.classList.toggle("on", b.dataset.tab === this.tab));
|
|
4915
5132
|
this.posMenu.querySelectorAll("[data-pos]").forEach((b) => b.classList.toggle("on", b.dataset.pos === this.dockMode));
|
|
@@ -4997,9 +5214,9 @@ var LoupeApp = class {
|
|
|
4997
5214
|
if (!this.repoSel) return;
|
|
4998
5215
|
const repos = [...new Set(this.visibleComments.map((c) => c.repo).filter((r) => !!r))].sort();
|
|
4999
5216
|
this.repoSel.style.display = this.scope === "all" && repos.length > 1 ? "" : "none";
|
|
5000
|
-
const
|
|
5217
|
+
const current2 = this.repoFilter;
|
|
5001
5218
|
this.repoSel.innerHTML = `<option value="">All repos</option>` + repos.map((r) => `<option value="${escapeHtml(r)}">${escapeHtml(r)}</option>`).join("");
|
|
5002
|
-
this.repoSel.value =
|
|
5219
|
+
this.repoSel.value = current2;
|
|
5003
5220
|
}
|
|
5004
5221
|
itemView(c, i) {
|
|
5005
5222
|
const detached = this.pins.get(c.id)?.classList.contains("detached") && !isResolved(c);
|
|
@@ -5110,6 +5327,7 @@ var LoupeApp = class {
|
|
|
5110
5327
|
};
|
|
5111
5328
|
actions.append(doneBtn, del);
|
|
5112
5329
|
detail.appendChild(actions);
|
|
5330
|
+
detail.appendChild(this.generateView(c));
|
|
5113
5331
|
item.appendChild(detail);
|
|
5114
5332
|
item.onclick = () => {
|
|
5115
5333
|
if (open) this.expanded.delete(c.id);
|
|
@@ -5199,6 +5417,381 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
5199
5417
|
this.renderHome();
|
|
5200
5418
|
};
|
|
5201
5419
|
}
|
|
5420
|
+
// ---- generate + iterate ---------------------------------------------------
|
|
5421
|
+
/**
|
|
5422
|
+
* The generate pane for one thread: the preview plane, its opacity comparison
|
|
5423
|
+
* against the original capture, the iteration stack and the iterate input.
|
|
5424
|
+
*
|
|
5425
|
+
* The panel owns all of that; producing the markup is the host's `generate`
|
|
5426
|
+
* function. Without one there is nothing to preview, so the pane offers the
|
|
5427
|
+
* access gate instead of a dead button.
|
|
5428
|
+
*/
|
|
5429
|
+
generateView(c) {
|
|
5430
|
+
const wrap = el("div", "genwrap");
|
|
5431
|
+
const state = this.iterations.get(c.id) ?? emptyIterations();
|
|
5432
|
+
const cur = current(state);
|
|
5433
|
+
const busy = this.genBusy.has(c.id);
|
|
5434
|
+
if (this.genOpen !== c.id) {
|
|
5435
|
+
const open = el("button", "gen-open", cur ? "\u2726 Change preview" : "\u2726 Generate a change");
|
|
5436
|
+
open.onclick = (e) => {
|
|
5437
|
+
e.stopPropagation();
|
|
5438
|
+
this.genOpen = c.id;
|
|
5439
|
+
this.renderList();
|
|
5440
|
+
};
|
|
5441
|
+
wrap.appendChild(open);
|
|
5442
|
+
return wrap;
|
|
5443
|
+
}
|
|
5444
|
+
if (!this.cfg.generate) {
|
|
5445
|
+
const gate = el("div", "gengate");
|
|
5446
|
+
gate.innerHTML = `<div class="gate-t">Generating needs access</div><div class="gate-b">This project has no generator configured for you yet. Ask for access and an owner can switch it on.</div>`;
|
|
5447
|
+
const ask = el("button", "gate-ask", "Request access to generate");
|
|
5448
|
+
ask.onclick = async (e) => {
|
|
5449
|
+
e.stopPropagation();
|
|
5450
|
+
ask.disabled = true;
|
|
5451
|
+
ask.textContent = "Request sent";
|
|
5452
|
+
await this.cfg.onRequestAccess?.({
|
|
5453
|
+
capability: "generate",
|
|
5454
|
+
user: this.cfg.user,
|
|
5455
|
+
projectKey: this.cfg.projectKey
|
|
5456
|
+
});
|
|
5457
|
+
this.addActivity({
|
|
5458
|
+
kind: "access.request",
|
|
5459
|
+
label: "Requested access to generate",
|
|
5460
|
+
detail: this.cfg.projectKey,
|
|
5461
|
+
level: "warn"
|
|
5462
|
+
});
|
|
5463
|
+
};
|
|
5464
|
+
gate.appendChild(ask);
|
|
5465
|
+
if (!this.cfg.onRequestAccess) {
|
|
5466
|
+
gate.appendChild(el("div", "gate-hint", "Pass onRequestAccess to init() to route this somewhere."));
|
|
5467
|
+
}
|
|
5468
|
+
wrap.appendChild(gate);
|
|
5469
|
+
wrap.appendChild(this.genClose(c));
|
|
5470
|
+
return wrap;
|
|
5471
|
+
}
|
|
5472
|
+
const head = el("div", "genhead");
|
|
5473
|
+
head.innerHTML = `<span class="gen-t">Generate</span>`;
|
|
5474
|
+
const nav = el("span", "gen-nav");
|
|
5475
|
+
const prev = el("button", "gen-step", "\u2039");
|
|
5476
|
+
prev.disabled = !canMove(state, -1);
|
|
5477
|
+
prev.setAttribute("aria-label", "Previous iteration");
|
|
5478
|
+
prev.onclick = (e) => {
|
|
5479
|
+
e.stopPropagation();
|
|
5480
|
+
this.setIterations(c.id, move(state, -1));
|
|
5481
|
+
};
|
|
5482
|
+
const label = el("span", "gen-n", stackLabel(state));
|
|
5483
|
+
const next = el("button", "gen-step", "\u203A");
|
|
5484
|
+
next.disabled = !canMove(state, 1);
|
|
5485
|
+
next.setAttribute("aria-label", "Next iteration");
|
|
5486
|
+
next.onclick = (e) => {
|
|
5487
|
+
e.stopPropagation();
|
|
5488
|
+
this.setIterations(c.id, move(state, 1));
|
|
5489
|
+
};
|
|
5490
|
+
nav.append(prev, label, next);
|
|
5491
|
+
head.appendChild(nav);
|
|
5492
|
+
const undo2 = el("button", "gen-undo", "Undo");
|
|
5493
|
+
undo2.disabled = !canUndo(state) || busy;
|
|
5494
|
+
undo2.onclick = (e) => {
|
|
5495
|
+
e.stopPropagation();
|
|
5496
|
+
this.setIterations(c.id, undo(state));
|
|
5497
|
+
this.addActivity({ kind: "generate.undo", label: "Undid a generated change" });
|
|
5498
|
+
};
|
|
5499
|
+
head.append(undo2, this.genClose(c));
|
|
5500
|
+
wrap.appendChild(head);
|
|
5501
|
+
if (busy) {
|
|
5502
|
+
wrap.appendChild(el("div", "genbusy", "Generating\u2026"));
|
|
5503
|
+
} else if (cur) {
|
|
5504
|
+
const plane = el("div", "genplane");
|
|
5505
|
+
if (c.screenshot) {
|
|
5506
|
+
const base = el("img", "genbase");
|
|
5507
|
+
base.src = c.screenshot;
|
|
5508
|
+
base.alt = "Original capture";
|
|
5509
|
+
plane.appendChild(base);
|
|
5510
|
+
}
|
|
5511
|
+
const frame = el("iframe", "genframe");
|
|
5512
|
+
frame.setAttribute("sandbox", "");
|
|
5513
|
+
frame.setAttribute("title", "Generated change preview");
|
|
5514
|
+
frame.srcdoc = `<!doctype html><meta charset="utf-8"><style>html,body{margin:0;padding:10px;background:#fff;font:13px -apple-system,system-ui,sans-serif;color:#111}${cur.css ?? ""}</style>${cur.html}`;
|
|
5515
|
+
frame.style.opacity = String(this.genOpacity);
|
|
5516
|
+
plane.appendChild(frame);
|
|
5517
|
+
wrap.appendChild(plane);
|
|
5518
|
+
const slider = el("div", "genslider");
|
|
5519
|
+
slider.innerHTML = `<span class="gs-lab">Compare</span>`;
|
|
5520
|
+
const range = el("input", "gs-range");
|
|
5521
|
+
range.type = "range";
|
|
5522
|
+
range.min = "0";
|
|
5523
|
+
range.max = "100";
|
|
5524
|
+
range.value = String(Math.round(this.genOpacity * 100));
|
|
5525
|
+
range.setAttribute("aria-label", "Generated change opacity");
|
|
5526
|
+
range.oninput = () => {
|
|
5527
|
+
this.genOpacity = Number(range.value) / 100;
|
|
5528
|
+
frame.style.opacity = String(this.genOpacity);
|
|
5529
|
+
};
|
|
5530
|
+
slider.append(range, el("span", "gs-n", `${Math.round(this.genOpacity * 100)}%`));
|
|
5531
|
+
wrap.appendChild(slider);
|
|
5532
|
+
if (cur.notes) wrap.appendChild(el("div", "gennotes", cur.notes));
|
|
5533
|
+
} else {
|
|
5534
|
+
wrap.appendChild(el("div", "genempty", "Nothing generated yet."));
|
|
5535
|
+
}
|
|
5536
|
+
const iter = el("div", "geniter");
|
|
5537
|
+
const kind = document.createElement("select");
|
|
5538
|
+
kind.className = "mini";
|
|
5539
|
+
kind.title = "How to treat your follow-up";
|
|
5540
|
+
kind.setAttribute("aria-label", "Iteration kind");
|
|
5541
|
+
kind.append(
|
|
5542
|
+
optionEl("Refine", "refine", true),
|
|
5543
|
+
optionEl("Revise", "revise", false)
|
|
5544
|
+
);
|
|
5545
|
+
const input = el("input", "iter-in");
|
|
5546
|
+
input.type = "text";
|
|
5547
|
+
input.placeholder = cur ? "Refine it, e.g. \u201Clarger button\u201D" : "What should change?";
|
|
5548
|
+
input.value = this.iterDraft.get(c.id) ?? "";
|
|
5549
|
+
input.oninput = () => this.iterDraft.set(c.id, input.value);
|
|
5550
|
+
for (const n of [kind, input]) n.addEventListener("click", (e) => e.stopPropagation());
|
|
5551
|
+
const send = el("button", "iter-send", "Send");
|
|
5552
|
+
send.disabled = busy;
|
|
5553
|
+
send.onclick = (e) => {
|
|
5554
|
+
e.stopPropagation();
|
|
5555
|
+
const prompt = (this.iterDraft.get(c.id) ?? "").trim();
|
|
5556
|
+
if (!prompt) return;
|
|
5557
|
+
this.iterDraft.delete(c.id);
|
|
5558
|
+
void this.runGenerate(c, prompt, kind.value);
|
|
5559
|
+
};
|
|
5560
|
+
input.onkeydown = (e) => {
|
|
5561
|
+
if (e.key === "Enter") send.click();
|
|
5562
|
+
};
|
|
5563
|
+
iter.append(kind, input, send);
|
|
5564
|
+
wrap.appendChild(iter);
|
|
5565
|
+
return wrap;
|
|
5566
|
+
}
|
|
5567
|
+
genClose(c) {
|
|
5568
|
+
const x = el("button", "gen-x", "\u2715");
|
|
5569
|
+
x.setAttribute("aria-label", "Close");
|
|
5570
|
+
x.onclick = (e) => {
|
|
5571
|
+
e.stopPropagation();
|
|
5572
|
+
this.genOpen = null;
|
|
5573
|
+
this.renderList();
|
|
5574
|
+
};
|
|
5575
|
+
return x;
|
|
5576
|
+
}
|
|
5577
|
+
setIterations(id, state) {
|
|
5578
|
+
this.iterations.set(id, state);
|
|
5579
|
+
this.renderList();
|
|
5580
|
+
}
|
|
5581
|
+
/** Ask the host's generator for a change, and stack the result. */
|
|
5582
|
+
async runGenerate(c, prompt, kind) {
|
|
5583
|
+
const generate = this.cfg.generate;
|
|
5584
|
+
if (!generate) return;
|
|
5585
|
+
const state = this.iterations.get(c.id) ?? emptyIterations();
|
|
5586
|
+
this.genBusy.add(c.id);
|
|
5587
|
+
this.genOpen = c.id;
|
|
5588
|
+
this.renderList();
|
|
5589
|
+
const started = Date.now();
|
|
5590
|
+
try {
|
|
5591
|
+
const out = await generate({
|
|
5592
|
+
comment: c,
|
|
5593
|
+
prompt,
|
|
5594
|
+
kind,
|
|
5595
|
+
previous: current(state) ?? void 0,
|
|
5596
|
+
localAi: this.project.localAi
|
|
5597
|
+
});
|
|
5598
|
+
const next = addIteration(state, {
|
|
5599
|
+
id: `it${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
5600
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5601
|
+
html: out.html,
|
|
5602
|
+
css: out.css,
|
|
5603
|
+
notes: out.notes,
|
|
5604
|
+
prompt,
|
|
5605
|
+
kind
|
|
5606
|
+
});
|
|
5607
|
+
this.genBusy.delete(c.id);
|
|
5608
|
+
this.iterations.set(c.id, next);
|
|
5609
|
+
this.addActivity({
|
|
5610
|
+
kind: `generate.${kind}`,
|
|
5611
|
+
label: `${kind === "generate" ? "Generated" : "Iterated on"} \u201C${c.title || prompt}\u201D`,
|
|
5612
|
+
detail: `${Math.round((Date.now() - started) / 1e3)}s \xB7 ${stackLabel(next)}`
|
|
5613
|
+
});
|
|
5614
|
+
} catch (e) {
|
|
5615
|
+
this.genBusy.delete(c.id);
|
|
5616
|
+
this.addActivity({
|
|
5617
|
+
kind: "generate.error",
|
|
5618
|
+
label: `Generation failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
5619
|
+
level: "error"
|
|
5620
|
+
});
|
|
5621
|
+
}
|
|
5622
|
+
this.renderList();
|
|
5623
|
+
}
|
|
5624
|
+
// ---- agent navigation (consent-gated) -------------------------------------
|
|
5625
|
+
/**
|
|
5626
|
+
* An agent asks to move the browser. Nothing navigates here — the request is
|
|
5627
|
+
* queued and the user answers it. `decide()` is the only thing that ever yields a
|
|
5628
|
+
* URL, and it yields one only on an explicit grant.
|
|
5629
|
+
*/
|
|
5630
|
+
requestNavigation(url, opts = {}) {
|
|
5631
|
+
const next = requestNavigation(this.consent, { url, ...opts });
|
|
5632
|
+
if (next === this.consent) return;
|
|
5633
|
+
this.consent = next;
|
|
5634
|
+
this.open = true;
|
|
5635
|
+
this.saveState();
|
|
5636
|
+
this.applyDockLayout();
|
|
5637
|
+
this.renderConsent();
|
|
5638
|
+
this.addActivity({
|
|
5639
|
+
kind: "nav.request",
|
|
5640
|
+
label: `Asked to open ${url}`,
|
|
5641
|
+
detail: opts.requester,
|
|
5642
|
+
level: "warn"
|
|
5643
|
+
});
|
|
5644
|
+
}
|
|
5645
|
+
/** Local-AI settings, used as the default in GenerateRequest.localAi. */
|
|
5646
|
+
setLocalAi(config) {
|
|
5647
|
+
this.project = { ...this.project, localAi: config ?? void 0 };
|
|
5648
|
+
this.saveProject();
|
|
5649
|
+
this.renderProject();
|
|
5650
|
+
}
|
|
5651
|
+
setLocalAiStatus(text) {
|
|
5652
|
+
const box = this.homeEl?.querySelector("#loupe-pp-ai");
|
|
5653
|
+
if (box) box.textContent = text;
|
|
5654
|
+
}
|
|
5655
|
+
/**
|
|
5656
|
+
* Ask the configured endpoint what it serves. A real check against the real
|
|
5657
|
+
* server — an OpenAI-compatible `/v1/models` is what Ollama, llama.cpp and the
|
|
5658
|
+
* rest all expose — with a timeout, so a wrong port reports rather than hangs.
|
|
5659
|
+
*/
|
|
5660
|
+
async testLocalAi(pop) {
|
|
5661
|
+
const url = normalizeEnvUrl(pop.querySelector(".pp-ai-url").value);
|
|
5662
|
+
const model = pop.querySelector(".pp-ai-model").value.trim();
|
|
5663
|
+
if (!url) {
|
|
5664
|
+
this.projError = "Enter a full http:// or https:// endpoint URL.";
|
|
5665
|
+
this.renderProjectError();
|
|
5666
|
+
return;
|
|
5667
|
+
}
|
|
5668
|
+
this.projError = "";
|
|
5669
|
+
this.setLocalAiStatus("Checking\u2026");
|
|
5670
|
+
const ctl = new AbortController();
|
|
5671
|
+
const timer = setTimeout(() => ctl.abort(), 4e3);
|
|
5672
|
+
try {
|
|
5673
|
+
const res = await fetch(`${url}/v1/models`, { signal: ctl.signal });
|
|
5674
|
+
if (!res.ok) {
|
|
5675
|
+
this.setLocalAiStatus(`Reachable, but it answered ${res.status}.`);
|
|
5676
|
+
return;
|
|
5677
|
+
}
|
|
5678
|
+
const body = await res.json().catch(() => null);
|
|
5679
|
+
const ids = Array.isArray(body?.data) ? body.data.map((m) => String(m?.id ?? "")).filter(Boolean) : [];
|
|
5680
|
+
if (model && ids.length && !ids.some((i) => i === model || i.startsWith(`${model}:`))) {
|
|
5681
|
+
const shown = ids.slice(0, 4).join(", ");
|
|
5682
|
+
this.setLocalAiStatus(`Connected, but no \u201C${model}\u201D \u2014 it serves: ${shown}${ids.length > 4 ? ", \u2026" : ""}`);
|
|
5683
|
+
return;
|
|
5684
|
+
}
|
|
5685
|
+
this.setLocalAiStatus(
|
|
5686
|
+
ids.length ? `Connected \u2014 ${ids.length} model${ids.length === 1 ? "" : "s"} available.` : "Connected."
|
|
5687
|
+
);
|
|
5688
|
+
} catch (e) {
|
|
5689
|
+
this.setLocalAiStatus(e?.name === "AbortError" ? "Timed out. Is the server running, and does it allow this origin (CORS)?" : "Could not reach it. Check the URL, and that the server allows this origin (CORS).");
|
|
5690
|
+
} finally {
|
|
5691
|
+
clearTimeout(timer);
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
5694
|
+
renderConsent() {
|
|
5695
|
+
if (!this.consentEl) return;
|
|
5696
|
+
if (!isPending(this.consent)) {
|
|
5697
|
+
this.consentEl.innerHTML = "";
|
|
5698
|
+
this.consentEl.style.display = "none";
|
|
5699
|
+
return;
|
|
5700
|
+
}
|
|
5701
|
+
const r = this.consent.request;
|
|
5702
|
+
this.consentEl.style.display = "";
|
|
5703
|
+
this.consentEl.innerHTML = `<div class="cs-head"><span class="cs-dot"></span><b>${escapeHtml(r.requester ?? "An agent")}</b> wants to open a page</div><div class="cs-url">${escapeHtml(r.url)}</div>` + (r.reason ? `<div class="cs-why">${escapeHtml(r.reason)}</div>` : "") + `<div class="cs-btns"><button class="cs-deny">Stay here</button><button class="cs-go">Go there</button></div>`;
|
|
5704
|
+
this.consentEl.querySelector(".cs-deny").onclick = () => {
|
|
5705
|
+
const { record } = decide(this.consent, false);
|
|
5706
|
+
this.consent = record;
|
|
5707
|
+
this.renderConsent();
|
|
5708
|
+
this.addActivity({ kind: "nav.deny", label: `Declined opening ${r.url}` });
|
|
5709
|
+
};
|
|
5710
|
+
this.consentEl.querySelector(".cs-go").onclick = () => {
|
|
5711
|
+
const { record, navigateTo } = decide(this.consent, true);
|
|
5712
|
+
this.consent = record;
|
|
5713
|
+
this.renderConsent();
|
|
5714
|
+
this.addActivity({ kind: "nav.grant", label: `Opened ${navigateTo}` });
|
|
5715
|
+
if (navigateTo) window.location.assign(navigateTo);
|
|
5716
|
+
};
|
|
5717
|
+
}
|
|
5718
|
+
/** Drop a pending request without deciding it — e.g. the panel is closing. */
|
|
5719
|
+
abandonNavigation() {
|
|
5720
|
+
this.consent = withdraw(this.consent);
|
|
5721
|
+
this.renderConsent();
|
|
5722
|
+
}
|
|
5723
|
+
/** The dictation button, or nothing at all where the browser has no speech API. */
|
|
5724
|
+
voiceButton(target) {
|
|
5725
|
+
const Ctor = window.SpeechRecognition ?? window.webkitSpeechRecognition;
|
|
5726
|
+
const b = el("button", "voice", "");
|
|
5727
|
+
this.voiceEl = b;
|
|
5728
|
+
if (!Ctor) {
|
|
5729
|
+
b.textContent = "\u{1F3A4}";
|
|
5730
|
+
b.disabled = true;
|
|
5731
|
+
b.title = "Dictation is not available in this browser";
|
|
5732
|
+
b.setAttribute("aria-label", "Dictation unavailable");
|
|
5733
|
+
return b;
|
|
5734
|
+
}
|
|
5735
|
+
const idle = () => {
|
|
5736
|
+
b.classList.remove("on");
|
|
5737
|
+
b.textContent = "\u{1F3A4}";
|
|
5738
|
+
b.setAttribute("aria-label", "Dictate");
|
|
5739
|
+
b.title = "Dictate";
|
|
5740
|
+
};
|
|
5741
|
+
const listening = () => {
|
|
5742
|
+
b.classList.add("on");
|
|
5743
|
+
b.textContent = "\u23FA";
|
|
5744
|
+
b.setAttribute("aria-label", "Stop dictating");
|
|
5745
|
+
b.title = "Listening \u2014 click to stop";
|
|
5746
|
+
};
|
|
5747
|
+
idle();
|
|
5748
|
+
b.onclick = (e) => {
|
|
5749
|
+
e.stopPropagation();
|
|
5750
|
+
if (this.voice) {
|
|
5751
|
+
this.stopVoice();
|
|
5752
|
+
idle();
|
|
5753
|
+
return;
|
|
5754
|
+
}
|
|
5755
|
+
const rec = new Ctor();
|
|
5756
|
+
rec.continuous = true;
|
|
5757
|
+
rec.interimResults = true;
|
|
5758
|
+
rec.lang = document.documentElement.lang || "en-US";
|
|
5759
|
+
let base = target.value ? `${target.value} ` : "";
|
|
5760
|
+
rec.onresult = (ev) => {
|
|
5761
|
+
let text = "";
|
|
5762
|
+
for (let i = ev.resultIndex; i < ev.results.length; i++) text += ev.results[i][0].transcript;
|
|
5763
|
+
target.value = (base + text).replace(/\s+/g, " ").trimStart();
|
|
5764
|
+
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
5765
|
+
};
|
|
5766
|
+
rec.onerror = (ev) => {
|
|
5767
|
+
this.stopVoice();
|
|
5768
|
+
idle();
|
|
5769
|
+
this.addActivity({ kind: "voice.error", label: `Dictation failed: ${ev?.error ?? "unknown"}`, level: "error" });
|
|
5770
|
+
};
|
|
5771
|
+
rec.onend = () => {
|
|
5772
|
+
this.voice = null;
|
|
5773
|
+
idle();
|
|
5774
|
+
};
|
|
5775
|
+
this.voice = rec;
|
|
5776
|
+
this.voiceTarget = target;
|
|
5777
|
+
try {
|
|
5778
|
+
rec.start();
|
|
5779
|
+
listening();
|
|
5780
|
+
} catch {
|
|
5781
|
+
this.voice = null;
|
|
5782
|
+
idle();
|
|
5783
|
+
}
|
|
5784
|
+
};
|
|
5785
|
+
return b;
|
|
5786
|
+
}
|
|
5787
|
+
stopVoice() {
|
|
5788
|
+
try {
|
|
5789
|
+
this.voice?.stop();
|
|
5790
|
+
} catch {
|
|
5791
|
+
}
|
|
5792
|
+
this.voice = null;
|
|
5793
|
+
this.voiceTarget = null;
|
|
5794
|
+
}
|
|
5202
5795
|
flash(id) {
|
|
5203
5796
|
const c = this.comments.find((x) => x.id === id);
|
|
5204
5797
|
const pin = this.pins.get(id);
|
|
@@ -5474,12 +6067,24 @@ function setActivityStatus(status) {
|
|
|
5474
6067
|
function clearActivity() {
|
|
5475
6068
|
app?.clearActivity();
|
|
5476
6069
|
}
|
|
6070
|
+
function requestNavigation2(url, opts) {
|
|
6071
|
+
app?.requestNavigation(url, opts);
|
|
6072
|
+
}
|
|
6073
|
+
function setLocalAi(config) {
|
|
6074
|
+
app?.setLocalAi(config);
|
|
6075
|
+
}
|
|
6076
|
+
function openTool(tool) {
|
|
6077
|
+
app?.openTool(tool);
|
|
6078
|
+
}
|
|
5477
6079
|
export {
|
|
5478
6080
|
clearActivity,
|
|
5479
6081
|
connectTab,
|
|
5480
6082
|
destroy,
|
|
5481
6083
|
init,
|
|
6084
|
+
openTool,
|
|
6085
|
+
requestNavigation2 as requestNavigation,
|
|
5482
6086
|
setActivityStatus,
|
|
6087
|
+
setLocalAi,
|
|
5483
6088
|
trackActivity
|
|
5484
6089
|
};
|
|
5485
6090
|
//# sourceMappingURL=index.js.map
|