@loupekit/sdk 0.10.15-next.60 → 0.10.16-next.61
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.global.js
CHANGED
|
@@ -25,7 +25,10 @@ var Loupe = (() => {
|
|
|
25
25
|
connectTab: () => connectTab,
|
|
26
26
|
destroy: () => destroy,
|
|
27
27
|
init: () => init,
|
|
28
|
+
openTool: () => openTool,
|
|
29
|
+
requestNavigation: () => requestNavigation2,
|
|
28
30
|
setActivityStatus: () => setActivityStatus,
|
|
31
|
+
setLocalAi: () => setLocalAi,
|
|
29
32
|
trackActivity: () => trackActivity
|
|
30
33
|
});
|
|
31
34
|
|
|
@@ -685,6 +688,99 @@ var Loupe = (() => {
|
|
|
685
688
|
font-size: 10px; white-space: pre-wrap; overflow-wrap: anywhere;
|
|
686
689
|
}
|
|
687
690
|
|
|
691
|
+
/* ------------------------------------------------- consent-gated agent navigation */
|
|
692
|
+
.consent {
|
|
693
|
+
margin: 8px 10px 0; padding: 10px; border: 1px solid var(--pin); border-radius: 10px;
|
|
694
|
+
background: var(--bg-2);
|
|
695
|
+
}
|
|
696
|
+
.cs-head { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink); }
|
|
697
|
+
.cs-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--pin); flex: none; animation: loupe-pulse 1.4s ease-in-out infinite; }
|
|
698
|
+
@media (prefers-reduced-motion: reduce) { .cs-dot { animation: none; } }
|
|
699
|
+
.cs-url {
|
|
700
|
+
margin-top: 6px; padding: 6px 7px; border-radius: 7px; background: var(--bg);
|
|
701
|
+
font-family: ui-monospace, Menlo, monospace; font-size: 10.5px; color: var(--ink);
|
|
702
|
+
overflow-wrap: anywhere;
|
|
703
|
+
}
|
|
704
|
+
.cs-why { margin-top: 6px; font-size: 11.5px; line-height: 1.45; color: var(--muted); }
|
|
705
|
+
.cs-btns { display: flex; justify-content: flex-end; gap: 6px; margin-top: 9px; }
|
|
706
|
+
.cs-btns button { padding: 5px 10px; border-radius: 7px; font-size: 11.5px; font-weight: 600; cursor: pointer; }
|
|
707
|
+
.cs-deny { border: 1px solid var(--line); background: var(--bg); color: var(--ink); }
|
|
708
|
+
.cs-deny:hover { border-color: var(--accent); }
|
|
709
|
+
.cs-go { border: 1px solid var(--accent); background: var(--accent); color: #fff; }
|
|
710
|
+
|
|
711
|
+
/* --------------------------------------------------------------- generate + iterate */
|
|
712
|
+
.genwrap { margin-top: 8px; }
|
|
713
|
+
.gen-open {
|
|
714
|
+
width: 100%; padding: 7px; border: 1px dashed var(--line); border-radius: 8px;
|
|
715
|
+
background: transparent; color: var(--muted); font-size: 11.5px; font-weight: 600; cursor: pointer;
|
|
716
|
+
}
|
|
717
|
+
.gen-open:hover { border-color: var(--accent); color: var(--accent); }
|
|
718
|
+
.genhead { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
|
|
719
|
+
.gen-t { flex: 1; font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--muted); }
|
|
720
|
+
.gen-nav { display: inline-flex; align-items: center; gap: 2px; }
|
|
721
|
+
.gen-step {
|
|
722
|
+
width: 20px; height: 20px; padding: 0; border: 1px solid var(--line); border-radius: 6px;
|
|
723
|
+
background: var(--bg); color: var(--ink); font-size: 13px; line-height: 1; cursor: pointer;
|
|
724
|
+
}
|
|
725
|
+
.gen-step:disabled { opacity: .4; cursor: default; }
|
|
726
|
+
.gen-n { min-width: 32px; text-align: center; font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
727
|
+
.gen-undo, .gen-x {
|
|
728
|
+
padding: 3px 8px; border: 1px solid var(--line); border-radius: 7px;
|
|
729
|
+
background: var(--bg); color: var(--ink); font-size: 11px; cursor: pointer;
|
|
730
|
+
}
|
|
731
|
+
.gen-undo:disabled { opacity: .4; cursor: default; }
|
|
732
|
+
.gen-undo:hover, .gen-x:hover { border-color: var(--accent); }
|
|
733
|
+
.genbusy, .genempty { padding: 14px; text-align: center; font-size: 11.5px; color: var(--muted); }
|
|
734
|
+
/* the preview plane: generated markup over the original capture, sandboxed.
|
|
735
|
+
min-height matters \u2014 the iframe is absolutely positioned, so a thread with no
|
|
736
|
+
screenshot would otherwise give the plane nothing in-flow and collapse it to a
|
|
737
|
+
couple of pixels. */
|
|
738
|
+
.genplane { position: relative; min-height: 150px; border: 1px solid var(--line); border-radius: 9px; overflow: hidden; background: var(--bg); }
|
|
739
|
+
.genbase { display: block; width: 100%; }
|
|
740
|
+
.genframe {
|
|
741
|
+
position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: transparent;
|
|
742
|
+
transition: opacity .08s linear;
|
|
743
|
+
}
|
|
744
|
+
.genslider { display: flex; align-items: center; gap: 7px; margin-top: 6px; }
|
|
745
|
+
.gs-lab { font-size: 10.5px; color: var(--muted); }
|
|
746
|
+
.gs-range { flex: 1; min-width: 0; accent-color: var(--accent); }
|
|
747
|
+
.gs-n { min-width: 32px; text-align: right; font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
748
|
+
.gennotes {
|
|
749
|
+
margin-top: 6px; padding: 6px 8px; border-radius: 7px; background: var(--bg-3);
|
|
750
|
+
font-size: 11px; line-height: 1.45; color: var(--muted);
|
|
751
|
+
}
|
|
752
|
+
.geniter { display: flex; gap: 5px; margin-top: 7px; }
|
|
753
|
+
.iter-in {
|
|
754
|
+
flex: 1; min-width: 0; padding: 6px 8px; border: 1px solid var(--line); border-radius: 8px;
|
|
755
|
+
background: var(--bg); color: var(--ink); font-size: 11.5px;
|
|
756
|
+
}
|
|
757
|
+
.iter-in:focus { outline: none; border-color: var(--accent); }
|
|
758
|
+
.iter-send {
|
|
759
|
+
flex: none; padding: 6px 10px; border: 1px solid var(--accent); border-radius: 8px;
|
|
760
|
+
background: var(--accent); color: #fff; font-size: 11.5px; font-weight: 600; cursor: pointer;
|
|
761
|
+
}
|
|
762
|
+
.iter-send:disabled { opacity: .5; cursor: default; }
|
|
763
|
+
/* the access gate, when there is no generator */
|
|
764
|
+
.gengate { padding: 10px; border: 1px solid var(--line); border-radius: 9px; background: var(--bg-2); }
|
|
765
|
+
.gate-t { font-size: 12px; font-weight: 700; color: var(--ink); }
|
|
766
|
+
.gate-b { margin-top: 4px; font-size: 11.5px; line-height: 1.45; color: var(--muted); }
|
|
767
|
+
.gate-ask {
|
|
768
|
+
margin-top: 8px; padding: 6px 10px; border: 1px solid var(--accent); border-radius: 8px;
|
|
769
|
+
background: var(--accent); color: #fff; font-size: 11.5px; font-weight: 600; cursor: pointer;
|
|
770
|
+
}
|
|
771
|
+
.gate-ask:disabled { opacity: .6; cursor: default; }
|
|
772
|
+
.gate-hint { margin-top: 7px; font-size: 10.5px; color: var(--muted); }
|
|
773
|
+
.gate-hint code { font-family: ui-monospace, Menlo, monospace; color: var(--ink); }
|
|
774
|
+
|
|
775
|
+
/* dictation */
|
|
776
|
+
.voice {
|
|
777
|
+
width: 30px; flex: none; padding: 0; border: 1px solid var(--line); border-radius: 8px;
|
|
778
|
+
background: var(--bg-2); color: var(--muted); font-size: 13px; cursor: pointer;
|
|
779
|
+
}
|
|
780
|
+
.voice:hover:not(:disabled) { border-color: var(--accent); color: var(--ink); }
|
|
781
|
+
.voice:disabled { opacity: .45; cursor: not-allowed; }
|
|
782
|
+
.voice.on { border-color: var(--pin); color: var(--pin); background: var(--bg-3); }
|
|
783
|
+
|
|
688
784
|
/* ------------------------------------------------------- hint card (once per view) */
|
|
689
785
|
.hint {
|
|
690
786
|
position: relative; margin: 8px 12px 0; padding: 10px 28px 10px 11px;
|
|
@@ -2409,7 +2505,7 @@ var Loupe = (() => {
|
|
|
2409
2505
|
log.time("embed node");
|
|
2410
2506
|
embedNode(clone, context);
|
|
2411
2507
|
const count = tasks.length;
|
|
2412
|
-
let
|
|
2508
|
+
let current2 = 0;
|
|
2413
2509
|
const runTask = async () => {
|
|
2414
2510
|
while (true) {
|
|
2415
2511
|
const task = tasks.pop();
|
|
@@ -2420,10 +2516,10 @@ var Loupe = (() => {
|
|
|
2420
2516
|
} catch (error) {
|
|
2421
2517
|
context.log.warn("Failed to run task", error);
|
|
2422
2518
|
}
|
|
2423
|
-
progress?.(++
|
|
2519
|
+
progress?.(++current2, count);
|
|
2424
2520
|
}
|
|
2425
2521
|
};
|
|
2426
|
-
progress?.(
|
|
2522
|
+
progress?.(current2, count);
|
|
2427
2523
|
await Promise.all([...Array.from({ length: 4 })].map(runTask));
|
|
2428
2524
|
log.timeEnd("embed node");
|
|
2429
2525
|
await onEmbedNode?.(clone);
|
|
@@ -2987,6 +3083,86 @@ var Loupe = (() => {
|
|
|
2987
3083
|
return comments.filter((c) => c.status === "in_review");
|
|
2988
3084
|
}
|
|
2989
3085
|
|
|
3086
|
+
// ../shared/dist/iteration.js
|
|
3087
|
+
function emptyIterations() {
|
|
3088
|
+
return { items: [], index: -1 };
|
|
3089
|
+
}
|
|
3090
|
+
function current(state) {
|
|
3091
|
+
return state.index >= 0 ? state.items[state.index] ?? null : null;
|
|
3092
|
+
}
|
|
3093
|
+
function addIteration(state, iteration) {
|
|
3094
|
+
const head = state.items.slice(0, state.index + 1);
|
|
3095
|
+
const items = [...head, iteration].slice(-MAX_ITERATIONS);
|
|
3096
|
+
return { items, index: items.length - 1 };
|
|
3097
|
+
}
|
|
3098
|
+
var MAX_ITERATIONS = 20;
|
|
3099
|
+
function canUndo(state) {
|
|
3100
|
+
return state.items.length > 0;
|
|
3101
|
+
}
|
|
3102
|
+
function undo(state) {
|
|
3103
|
+
if (!state.items.length)
|
|
3104
|
+
return state;
|
|
3105
|
+
const items = state.items.slice(0, -1);
|
|
3106
|
+
return { items, index: items.length - 1 };
|
|
3107
|
+
}
|
|
3108
|
+
function canMove(state, delta) {
|
|
3109
|
+
const next = state.index + delta;
|
|
3110
|
+
return next >= 0 && next < state.items.length;
|
|
3111
|
+
}
|
|
3112
|
+
function move(state, delta) {
|
|
3113
|
+
return canMove(state, delta) ? { ...state, index: state.index + delta } : state;
|
|
3114
|
+
}
|
|
3115
|
+
function stackLabel(state) {
|
|
3116
|
+
return state.items.length ? `${state.index + 1} / ${state.items.length}` : "";
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
// ../shared/dist/consent.js
|
|
3120
|
+
function emptyConsent() {
|
|
3121
|
+
return { state: "idle", request: null, history: [] };
|
|
3122
|
+
}
|
|
3123
|
+
function isNavigableUrl(raw) {
|
|
3124
|
+
try {
|
|
3125
|
+
const u = new URL(raw);
|
|
3126
|
+
return u.protocol === "http:" || u.protocol === "https:";
|
|
3127
|
+
} catch {
|
|
3128
|
+
return false;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
function requestNavigation(record, request) {
|
|
3132
|
+
if (!isNavigableUrl(request.url))
|
|
3133
|
+
return record;
|
|
3134
|
+
return {
|
|
3135
|
+
state: "requested",
|
|
3136
|
+
request: {
|
|
3137
|
+
id: request.id ?? `nav${Date.now().toString(36)}`,
|
|
3138
|
+
at: request.at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3139
|
+
url: request.url,
|
|
3140
|
+
reason: request.reason,
|
|
3141
|
+
requester: request.requester
|
|
3142
|
+
},
|
|
3143
|
+
history: record.history
|
|
3144
|
+
};
|
|
3145
|
+
}
|
|
3146
|
+
function isPending(record) {
|
|
3147
|
+
return record.state === "requested" && record.request !== null;
|
|
3148
|
+
}
|
|
3149
|
+
function decide(record, granted, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3150
|
+
if (!isPending(record))
|
|
3151
|
+
return { record, navigateTo: null };
|
|
3152
|
+
const url = record.request.url;
|
|
3153
|
+
return {
|
|
3154
|
+
record: {
|
|
3155
|
+
state: granted ? "granted" : "denied",
|
|
3156
|
+
request: null,
|
|
3157
|
+
history: [...record.history, { url, decision: granted ? "granted" : "denied", at: now }]
|
|
3158
|
+
},
|
|
3159
|
+
navigateTo: granted ? url : null
|
|
3160
|
+
};
|
|
3161
|
+
}
|
|
3162
|
+
function withdraw(record) {
|
|
3163
|
+
return isPending(record) ? { ...record, state: "idle", request: null } : record;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
2990
3166
|
// ../shared/dist/index.js
|
|
2991
3167
|
var COMMENT_STAGES = ["queue", "todo", "in_progress", "in_review", "resolved"];
|
|
2992
3168
|
var STAGE_LABELS = {
|
|
@@ -3034,7 +3210,7 @@ var Loupe = (() => {
|
|
|
3034
3210
|
}
|
|
3035
3211
|
|
|
3036
3212
|
// src/app.ts
|
|
3037
|
-
var SDK_VERSION = true ? "0.10.
|
|
3213
|
+
var SDK_VERSION = true ? "0.10.16" : "dev";
|
|
3038
3214
|
var ACCENTS = [
|
|
3039
3215
|
{ id: "indigo", dark: "#6b73e6", light: "#4a55d6", soft: "rgba(107,115,230,0.12)" },
|
|
3040
3216
|
{ id: "violet", dark: "#a06be6", light: "#7c3fd4", soft: "rgba(160,107,230,0.14)" },
|
|
@@ -3190,6 +3366,18 @@ var Loupe = (() => {
|
|
|
3190
3366
|
this.summaryOpen = false;
|
|
3191
3367
|
/** Per-browser project settings: a repo override and the environment URLs. */
|
|
3192
3368
|
this.project = { environments: [] };
|
|
3369
|
+
/** Iteration history per thread — generating a change is a stack, not a one-shot. */
|
|
3370
|
+
this.iterations = /* @__PURE__ */ new Map();
|
|
3371
|
+
/** The thread whose generate pane is open, if any. */
|
|
3372
|
+
this.genOpen = null;
|
|
3373
|
+
/** Threads with a generation in flight. */
|
|
3374
|
+
this.genBusy = /* @__PURE__ */ new Set();
|
|
3375
|
+
/** Opacity of the generated plane over the original, 0..1 (the compare slider). */
|
|
3376
|
+
this.genOpacity = 0.6;
|
|
3377
|
+
/** The navigation consent state machine — a URL only ever comes out of `decide`. */
|
|
3378
|
+
this.consent = emptyConsent();
|
|
3379
|
+
/** The live SpeechRecognition instance while dictating, if any. */
|
|
3380
|
+
this.voice = null;
|
|
3193
3381
|
this.projSearch = "";
|
|
3194
3382
|
this.projOpen = false;
|
|
3195
3383
|
this.projResults = [];
|
|
@@ -3426,6 +3614,9 @@ var Loupe = (() => {
|
|
|
3426
3614
|
window.removeEventListener("pointerup", this.onResizeUp);
|
|
3427
3615
|
this.saveState();
|
|
3428
3616
|
};
|
|
3617
|
+
/** Draft follow-ups, kept out of the render path so the input never loses focus. */
|
|
3618
|
+
this.iterDraft = /* @__PURE__ */ new Map();
|
|
3619
|
+
this.voiceTarget = null;
|
|
3429
3620
|
this.cfg = cfg;
|
|
3430
3621
|
this.store = cfg.apiBase ? new HttpAdapter(cfg.apiBase, cfg.user, cfg.userHmac, cfg.headers, cfg.credentials) : new LocalStorageAdapter();
|
|
3431
3622
|
}
|
|
@@ -3447,7 +3638,13 @@ var Loupe = (() => {
|
|
|
3447
3638
|
this.observe();
|
|
3448
3639
|
this.watchNavigation();
|
|
3449
3640
|
if (!this.tourDone && this.open && !this.isMobile()) this.startTour();
|
|
3450
|
-
if (this.cfg.autoOpen) this.setMode("inspect");
|
|
3641
|
+
if (this.cfg.autoOpen) this.setMode(this.cfg.tool === "note" ? "free" : "inspect");
|
|
3642
|
+
}
|
|
3643
|
+
/** Arm a tool from outside — the extension's context menus, or a host's own button. */
|
|
3644
|
+
openTool(tool) {
|
|
3645
|
+
this.open = true;
|
|
3646
|
+
this.applyDockLayout();
|
|
3647
|
+
this.setMode(tool === "note" ? "free" : "inspect");
|
|
3451
3648
|
}
|
|
3452
3649
|
/**
|
|
3453
3650
|
* Reload comments when the page URL changes without a full reload (SPA
|
|
@@ -3680,7 +3877,9 @@ var Loupe = (() => {
|
|
|
3680
3877
|
});
|
|
3681
3878
|
const resize = el("div", "resize");
|
|
3682
3879
|
resize.addEventListener("pointerdown", this.onResizeDown);
|
|
3683
|
-
|
|
3880
|
+
this.consentEl = el("div", "consent");
|
|
3881
|
+
this.consentEl.style.display = "none";
|
|
3882
|
+
dock.append(head, this.minBar, this.consentEl, tabs, homeView, commentsView, activityView, ...customViews, resize);
|
|
3684
3883
|
for (const [id, view] of [["home", homeView], ["comments", commentsView], ["activity", activityView]]) {
|
|
3685
3884
|
this.viewEls.set(id, view);
|
|
3686
3885
|
}
|
|
@@ -3995,7 +4194,7 @@ var Loupe = (() => {
|
|
|
3995
4194
|
}
|
|
3996
4195
|
pop.classList.add("open");
|
|
3997
4196
|
const envs = this.project.environments;
|
|
3998
|
-
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>`;
|
|
4197
|
+
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>`;
|
|
3999
4198
|
this.renderRepoList();
|
|
4000
4199
|
this.renderProjectError();
|
|
4001
4200
|
pop.querySelector('[data-role="pp-close"]').onclick = () => this.setProjectOpen(false);
|
|
@@ -4051,6 +4250,25 @@ var Loupe = (() => {
|
|
|
4051
4250
|
this.renderProject();
|
|
4052
4251
|
pop.querySelector(".pp-env-url")?.focus();
|
|
4053
4252
|
};
|
|
4253
|
+
for (const sel of [".pp-ai-url", ".pp-ai-model"]) {
|
|
4254
|
+
pop.querySelector(sel)?.addEventListener("click", (e) => e.stopPropagation());
|
|
4255
|
+
}
|
|
4256
|
+
pop.querySelector('[data-role="ai-save"]').onclick = () => {
|
|
4257
|
+
const url2 = normalizeEnvUrl(pop.querySelector(".pp-ai-url").value);
|
|
4258
|
+
const model = pop.querySelector(".pp-ai-model").value.trim();
|
|
4259
|
+
if (!url2 || !model) {
|
|
4260
|
+
this.setLocalAiStatus(this.project.localAi ? `Saved: ${this.project.localAi.model} at ${this.project.localAi.url}` : "Nothing saved yet.");
|
|
4261
|
+
this.projError = !url2 ? "Enter a full http:// or https:// endpoint URL." : "Enter a model name.";
|
|
4262
|
+
this.renderProjectError();
|
|
4263
|
+
return;
|
|
4264
|
+
}
|
|
4265
|
+
this.projError = "";
|
|
4266
|
+
this.setLocalAi({ url: url2, model });
|
|
4267
|
+
this.setLocalAiStatus(`Saved: ${model} at ${url2}`);
|
|
4268
|
+
};
|
|
4269
|
+
pop.querySelector('[data-role="ai-test"]').onclick = () => {
|
|
4270
|
+
void this.testLocalAi(pop);
|
|
4271
|
+
};
|
|
4054
4272
|
}
|
|
4055
4273
|
/** Per-browser project settings, keyed separately from the dock's UI state. */
|
|
4056
4274
|
loadProject() {
|
|
@@ -4060,7 +4278,8 @@ var Loupe = (() => {
|
|
|
4060
4278
|
const p = raw ? JSON.parse(raw) : null;
|
|
4061
4279
|
const repo = typeof p?.repo === "string" && p.repo ? p.repo : void 0;
|
|
4062
4280
|
const stored = Array.isArray(p?.environments) ? p.environments.filter((u) => typeof u === "string") : null;
|
|
4063
|
-
|
|
4281
|
+
const localAi = p?.localAi && typeof p.localAi.url === "string" && typeof p.localAi.model === "string" ? { url: p.localAi.url, model: p.localAi.model } : void 0;
|
|
4282
|
+
this.project = { repo, environments: stored ?? fromConfig, localAi };
|
|
4064
4283
|
} catch {
|
|
4065
4284
|
this.project = { environments: fromConfig };
|
|
4066
4285
|
}
|
|
@@ -4494,7 +4713,7 @@ var Loupe = (() => {
|
|
|
4494
4713
|
prioSel.value,
|
|
4495
4714
|
typeSel.value
|
|
4496
4715
|
);
|
|
4497
|
-
btns.append(cancel, save);
|
|
4716
|
+
btns.append(this.voiceButton(ta), cancel, save);
|
|
4498
4717
|
row.append(btns);
|
|
4499
4718
|
c.append(label, title, ta, attach, meta, row);
|
|
4500
4719
|
const w = 320, h = 380;
|
|
@@ -4941,6 +5160,7 @@ var Loupe = (() => {
|
|
|
4941
5160
|
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>`;
|
|
4942
5161
|
for (const t of this.tabList) d.classList.toggle(`tab-${t.id}`, this.tab === t.id);
|
|
4943
5162
|
for (const [id, view] of this.viewEls) view.classList.toggle("on", id === this.tab);
|
|
5163
|
+
this.renderConsent();
|
|
4944
5164
|
this.renderHome();
|
|
4945
5165
|
d.querySelectorAll(".tabs .tab").forEach((b) => b.classList.toggle("on", b.dataset.tab === this.tab));
|
|
4946
5166
|
this.posMenu.querySelectorAll("[data-pos]").forEach((b) => b.classList.toggle("on", b.dataset.pos === this.dockMode));
|
|
@@ -5028,9 +5248,9 @@ var Loupe = (() => {
|
|
|
5028
5248
|
if (!this.repoSel) return;
|
|
5029
5249
|
const repos = [...new Set(this.visibleComments.map((c) => c.repo).filter((r) => !!r))].sort();
|
|
5030
5250
|
this.repoSel.style.display = this.scope === "all" && repos.length > 1 ? "" : "none";
|
|
5031
|
-
const
|
|
5251
|
+
const current2 = this.repoFilter;
|
|
5032
5252
|
this.repoSel.innerHTML = `<option value="">All repos</option>` + repos.map((r) => `<option value="${escapeHtml(r)}">${escapeHtml(r)}</option>`).join("");
|
|
5033
|
-
this.repoSel.value =
|
|
5253
|
+
this.repoSel.value = current2;
|
|
5034
5254
|
}
|
|
5035
5255
|
itemView(c, i) {
|
|
5036
5256
|
const detached = this.pins.get(c.id)?.classList.contains("detached") && !isResolved(c);
|
|
@@ -5141,6 +5361,7 @@ var Loupe = (() => {
|
|
|
5141
5361
|
};
|
|
5142
5362
|
actions.append(doneBtn, del);
|
|
5143
5363
|
detail.appendChild(actions);
|
|
5364
|
+
detail.appendChild(this.generateView(c));
|
|
5144
5365
|
item.appendChild(detail);
|
|
5145
5366
|
item.onclick = () => {
|
|
5146
5367
|
if (open) this.expanded.delete(c.id);
|
|
@@ -5230,6 +5451,381 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
5230
5451
|
this.renderHome();
|
|
5231
5452
|
};
|
|
5232
5453
|
}
|
|
5454
|
+
// ---- generate + iterate ---------------------------------------------------
|
|
5455
|
+
/**
|
|
5456
|
+
* The generate pane for one thread: the preview plane, its opacity comparison
|
|
5457
|
+
* against the original capture, the iteration stack and the iterate input.
|
|
5458
|
+
*
|
|
5459
|
+
* The panel owns all of that; producing the markup is the host's `generate`
|
|
5460
|
+
* function. Without one there is nothing to preview, so the pane offers the
|
|
5461
|
+
* access gate instead of a dead button.
|
|
5462
|
+
*/
|
|
5463
|
+
generateView(c) {
|
|
5464
|
+
const wrap = el("div", "genwrap");
|
|
5465
|
+
const state = this.iterations.get(c.id) ?? emptyIterations();
|
|
5466
|
+
const cur = current(state);
|
|
5467
|
+
const busy = this.genBusy.has(c.id);
|
|
5468
|
+
if (this.genOpen !== c.id) {
|
|
5469
|
+
const open = el("button", "gen-open", cur ? "\u2726 Change preview" : "\u2726 Generate a change");
|
|
5470
|
+
open.onclick = (e) => {
|
|
5471
|
+
e.stopPropagation();
|
|
5472
|
+
this.genOpen = c.id;
|
|
5473
|
+
this.renderList();
|
|
5474
|
+
};
|
|
5475
|
+
wrap.appendChild(open);
|
|
5476
|
+
return wrap;
|
|
5477
|
+
}
|
|
5478
|
+
if (!this.cfg.generate) {
|
|
5479
|
+
const gate = el("div", "gengate");
|
|
5480
|
+
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>`;
|
|
5481
|
+
const ask = el("button", "gate-ask", "Request access to generate");
|
|
5482
|
+
ask.onclick = async (e) => {
|
|
5483
|
+
e.stopPropagation();
|
|
5484
|
+
ask.disabled = true;
|
|
5485
|
+
ask.textContent = "Request sent";
|
|
5486
|
+
await this.cfg.onRequestAccess?.({
|
|
5487
|
+
capability: "generate",
|
|
5488
|
+
user: this.cfg.user,
|
|
5489
|
+
projectKey: this.cfg.projectKey
|
|
5490
|
+
});
|
|
5491
|
+
this.addActivity({
|
|
5492
|
+
kind: "access.request",
|
|
5493
|
+
label: "Requested access to generate",
|
|
5494
|
+
detail: this.cfg.projectKey,
|
|
5495
|
+
level: "warn"
|
|
5496
|
+
});
|
|
5497
|
+
};
|
|
5498
|
+
gate.appendChild(ask);
|
|
5499
|
+
if (!this.cfg.onRequestAccess) {
|
|
5500
|
+
gate.appendChild(el("div", "gate-hint", "Pass onRequestAccess to init() to route this somewhere."));
|
|
5501
|
+
}
|
|
5502
|
+
wrap.appendChild(gate);
|
|
5503
|
+
wrap.appendChild(this.genClose(c));
|
|
5504
|
+
return wrap;
|
|
5505
|
+
}
|
|
5506
|
+
const head = el("div", "genhead");
|
|
5507
|
+
head.innerHTML = `<span class="gen-t">Generate</span>`;
|
|
5508
|
+
const nav = el("span", "gen-nav");
|
|
5509
|
+
const prev = el("button", "gen-step", "\u2039");
|
|
5510
|
+
prev.disabled = !canMove(state, -1);
|
|
5511
|
+
prev.setAttribute("aria-label", "Previous iteration");
|
|
5512
|
+
prev.onclick = (e) => {
|
|
5513
|
+
e.stopPropagation();
|
|
5514
|
+
this.setIterations(c.id, move(state, -1));
|
|
5515
|
+
};
|
|
5516
|
+
const label = el("span", "gen-n", stackLabel(state));
|
|
5517
|
+
const next = el("button", "gen-step", "\u203A");
|
|
5518
|
+
next.disabled = !canMove(state, 1);
|
|
5519
|
+
next.setAttribute("aria-label", "Next iteration");
|
|
5520
|
+
next.onclick = (e) => {
|
|
5521
|
+
e.stopPropagation();
|
|
5522
|
+
this.setIterations(c.id, move(state, 1));
|
|
5523
|
+
};
|
|
5524
|
+
nav.append(prev, label, next);
|
|
5525
|
+
head.appendChild(nav);
|
|
5526
|
+
const undo2 = el("button", "gen-undo", "Undo");
|
|
5527
|
+
undo2.disabled = !canUndo(state) || busy;
|
|
5528
|
+
undo2.onclick = (e) => {
|
|
5529
|
+
e.stopPropagation();
|
|
5530
|
+
this.setIterations(c.id, undo(state));
|
|
5531
|
+
this.addActivity({ kind: "generate.undo", label: "Undid a generated change" });
|
|
5532
|
+
};
|
|
5533
|
+
head.append(undo2, this.genClose(c));
|
|
5534
|
+
wrap.appendChild(head);
|
|
5535
|
+
if (busy) {
|
|
5536
|
+
wrap.appendChild(el("div", "genbusy", "Generating\u2026"));
|
|
5537
|
+
} else if (cur) {
|
|
5538
|
+
const plane = el("div", "genplane");
|
|
5539
|
+
if (c.screenshot) {
|
|
5540
|
+
const base = el("img", "genbase");
|
|
5541
|
+
base.src = c.screenshot;
|
|
5542
|
+
base.alt = "Original capture";
|
|
5543
|
+
plane.appendChild(base);
|
|
5544
|
+
}
|
|
5545
|
+
const frame = el("iframe", "genframe");
|
|
5546
|
+
frame.setAttribute("sandbox", "");
|
|
5547
|
+
frame.setAttribute("title", "Generated change preview");
|
|
5548
|
+
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}`;
|
|
5549
|
+
frame.style.opacity = String(this.genOpacity);
|
|
5550
|
+
plane.appendChild(frame);
|
|
5551
|
+
wrap.appendChild(plane);
|
|
5552
|
+
const slider = el("div", "genslider");
|
|
5553
|
+
slider.innerHTML = `<span class="gs-lab">Compare</span>`;
|
|
5554
|
+
const range = el("input", "gs-range");
|
|
5555
|
+
range.type = "range";
|
|
5556
|
+
range.min = "0";
|
|
5557
|
+
range.max = "100";
|
|
5558
|
+
range.value = String(Math.round(this.genOpacity * 100));
|
|
5559
|
+
range.setAttribute("aria-label", "Generated change opacity");
|
|
5560
|
+
range.oninput = () => {
|
|
5561
|
+
this.genOpacity = Number(range.value) / 100;
|
|
5562
|
+
frame.style.opacity = String(this.genOpacity);
|
|
5563
|
+
};
|
|
5564
|
+
slider.append(range, el("span", "gs-n", `${Math.round(this.genOpacity * 100)}%`));
|
|
5565
|
+
wrap.appendChild(slider);
|
|
5566
|
+
if (cur.notes) wrap.appendChild(el("div", "gennotes", cur.notes));
|
|
5567
|
+
} else {
|
|
5568
|
+
wrap.appendChild(el("div", "genempty", "Nothing generated yet."));
|
|
5569
|
+
}
|
|
5570
|
+
const iter = el("div", "geniter");
|
|
5571
|
+
const kind = document.createElement("select");
|
|
5572
|
+
kind.className = "mini";
|
|
5573
|
+
kind.title = "How to treat your follow-up";
|
|
5574
|
+
kind.setAttribute("aria-label", "Iteration kind");
|
|
5575
|
+
kind.append(
|
|
5576
|
+
optionEl("Refine", "refine", true),
|
|
5577
|
+
optionEl("Revise", "revise", false)
|
|
5578
|
+
);
|
|
5579
|
+
const input = el("input", "iter-in");
|
|
5580
|
+
input.type = "text";
|
|
5581
|
+
input.placeholder = cur ? "Refine it, e.g. \u201Clarger button\u201D" : "What should change?";
|
|
5582
|
+
input.value = this.iterDraft.get(c.id) ?? "";
|
|
5583
|
+
input.oninput = () => this.iterDraft.set(c.id, input.value);
|
|
5584
|
+
for (const n of [kind, input]) n.addEventListener("click", (e) => e.stopPropagation());
|
|
5585
|
+
const send = el("button", "iter-send", "Send");
|
|
5586
|
+
send.disabled = busy;
|
|
5587
|
+
send.onclick = (e) => {
|
|
5588
|
+
e.stopPropagation();
|
|
5589
|
+
const prompt = (this.iterDraft.get(c.id) ?? "").trim();
|
|
5590
|
+
if (!prompt) return;
|
|
5591
|
+
this.iterDraft.delete(c.id);
|
|
5592
|
+
void this.runGenerate(c, prompt, kind.value);
|
|
5593
|
+
};
|
|
5594
|
+
input.onkeydown = (e) => {
|
|
5595
|
+
if (e.key === "Enter") send.click();
|
|
5596
|
+
};
|
|
5597
|
+
iter.append(kind, input, send);
|
|
5598
|
+
wrap.appendChild(iter);
|
|
5599
|
+
return wrap;
|
|
5600
|
+
}
|
|
5601
|
+
genClose(c) {
|
|
5602
|
+
const x = el("button", "gen-x", "\u2715");
|
|
5603
|
+
x.setAttribute("aria-label", "Close");
|
|
5604
|
+
x.onclick = (e) => {
|
|
5605
|
+
e.stopPropagation();
|
|
5606
|
+
this.genOpen = null;
|
|
5607
|
+
this.renderList();
|
|
5608
|
+
};
|
|
5609
|
+
return x;
|
|
5610
|
+
}
|
|
5611
|
+
setIterations(id, state) {
|
|
5612
|
+
this.iterations.set(id, state);
|
|
5613
|
+
this.renderList();
|
|
5614
|
+
}
|
|
5615
|
+
/** Ask the host's generator for a change, and stack the result. */
|
|
5616
|
+
async runGenerate(c, prompt, kind) {
|
|
5617
|
+
const generate = this.cfg.generate;
|
|
5618
|
+
if (!generate) return;
|
|
5619
|
+
const state = this.iterations.get(c.id) ?? emptyIterations();
|
|
5620
|
+
this.genBusy.add(c.id);
|
|
5621
|
+
this.genOpen = c.id;
|
|
5622
|
+
this.renderList();
|
|
5623
|
+
const started = Date.now();
|
|
5624
|
+
try {
|
|
5625
|
+
const out = await generate({
|
|
5626
|
+
comment: c,
|
|
5627
|
+
prompt,
|
|
5628
|
+
kind,
|
|
5629
|
+
previous: current(state) ?? void 0,
|
|
5630
|
+
localAi: this.project.localAi
|
|
5631
|
+
});
|
|
5632
|
+
const next = addIteration(state, {
|
|
5633
|
+
id: `it${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
5634
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5635
|
+
html: out.html,
|
|
5636
|
+
css: out.css,
|
|
5637
|
+
notes: out.notes,
|
|
5638
|
+
prompt,
|
|
5639
|
+
kind
|
|
5640
|
+
});
|
|
5641
|
+
this.genBusy.delete(c.id);
|
|
5642
|
+
this.iterations.set(c.id, next);
|
|
5643
|
+
this.addActivity({
|
|
5644
|
+
kind: `generate.${kind}`,
|
|
5645
|
+
label: `${kind === "generate" ? "Generated" : "Iterated on"} \u201C${c.title || prompt}\u201D`,
|
|
5646
|
+
detail: `${Math.round((Date.now() - started) / 1e3)}s \xB7 ${stackLabel(next)}`
|
|
5647
|
+
});
|
|
5648
|
+
} catch (e) {
|
|
5649
|
+
this.genBusy.delete(c.id);
|
|
5650
|
+
this.addActivity({
|
|
5651
|
+
kind: "generate.error",
|
|
5652
|
+
label: `Generation failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
5653
|
+
level: "error"
|
|
5654
|
+
});
|
|
5655
|
+
}
|
|
5656
|
+
this.renderList();
|
|
5657
|
+
}
|
|
5658
|
+
// ---- agent navigation (consent-gated) -------------------------------------
|
|
5659
|
+
/**
|
|
5660
|
+
* An agent asks to move the browser. Nothing navigates here — the request is
|
|
5661
|
+
* queued and the user answers it. `decide()` is the only thing that ever yields a
|
|
5662
|
+
* URL, and it yields one only on an explicit grant.
|
|
5663
|
+
*/
|
|
5664
|
+
requestNavigation(url, opts = {}) {
|
|
5665
|
+
const next = requestNavigation(this.consent, { url, ...opts });
|
|
5666
|
+
if (next === this.consent) return;
|
|
5667
|
+
this.consent = next;
|
|
5668
|
+
this.open = true;
|
|
5669
|
+
this.saveState();
|
|
5670
|
+
this.applyDockLayout();
|
|
5671
|
+
this.renderConsent();
|
|
5672
|
+
this.addActivity({
|
|
5673
|
+
kind: "nav.request",
|
|
5674
|
+
label: `Asked to open ${url}`,
|
|
5675
|
+
detail: opts.requester,
|
|
5676
|
+
level: "warn"
|
|
5677
|
+
});
|
|
5678
|
+
}
|
|
5679
|
+
/** Local-AI settings, used as the default in GenerateRequest.localAi. */
|
|
5680
|
+
setLocalAi(config) {
|
|
5681
|
+
this.project = { ...this.project, localAi: config ?? void 0 };
|
|
5682
|
+
this.saveProject();
|
|
5683
|
+
this.renderProject();
|
|
5684
|
+
}
|
|
5685
|
+
setLocalAiStatus(text) {
|
|
5686
|
+
const box = this.homeEl?.querySelector("#loupe-pp-ai");
|
|
5687
|
+
if (box) box.textContent = text;
|
|
5688
|
+
}
|
|
5689
|
+
/**
|
|
5690
|
+
* Ask the configured endpoint what it serves. A real check against the real
|
|
5691
|
+
* server — an OpenAI-compatible `/v1/models` is what Ollama, llama.cpp and the
|
|
5692
|
+
* rest all expose — with a timeout, so a wrong port reports rather than hangs.
|
|
5693
|
+
*/
|
|
5694
|
+
async testLocalAi(pop) {
|
|
5695
|
+
const url = normalizeEnvUrl(pop.querySelector(".pp-ai-url").value);
|
|
5696
|
+
const model = pop.querySelector(".pp-ai-model").value.trim();
|
|
5697
|
+
if (!url) {
|
|
5698
|
+
this.projError = "Enter a full http:// or https:// endpoint URL.";
|
|
5699
|
+
this.renderProjectError();
|
|
5700
|
+
return;
|
|
5701
|
+
}
|
|
5702
|
+
this.projError = "";
|
|
5703
|
+
this.setLocalAiStatus("Checking\u2026");
|
|
5704
|
+
const ctl = new AbortController();
|
|
5705
|
+
const timer = setTimeout(() => ctl.abort(), 4e3);
|
|
5706
|
+
try {
|
|
5707
|
+
const res = await fetch(`${url}/v1/models`, { signal: ctl.signal });
|
|
5708
|
+
if (!res.ok) {
|
|
5709
|
+
this.setLocalAiStatus(`Reachable, but it answered ${res.status}.`);
|
|
5710
|
+
return;
|
|
5711
|
+
}
|
|
5712
|
+
const body = await res.json().catch(() => null);
|
|
5713
|
+
const ids = Array.isArray(body?.data) ? body.data.map((m) => String(m?.id ?? "")).filter(Boolean) : [];
|
|
5714
|
+
if (model && ids.length && !ids.some((i) => i === model || i.startsWith(`${model}:`))) {
|
|
5715
|
+
const shown = ids.slice(0, 4).join(", ");
|
|
5716
|
+
this.setLocalAiStatus(`Connected, but no \u201C${model}\u201D \u2014 it serves: ${shown}${ids.length > 4 ? ", \u2026" : ""}`);
|
|
5717
|
+
return;
|
|
5718
|
+
}
|
|
5719
|
+
this.setLocalAiStatus(
|
|
5720
|
+
ids.length ? `Connected \u2014 ${ids.length} model${ids.length === 1 ? "" : "s"} available.` : "Connected."
|
|
5721
|
+
);
|
|
5722
|
+
} catch (e) {
|
|
5723
|
+
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).");
|
|
5724
|
+
} finally {
|
|
5725
|
+
clearTimeout(timer);
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5728
|
+
renderConsent() {
|
|
5729
|
+
if (!this.consentEl) return;
|
|
5730
|
+
if (!isPending(this.consent)) {
|
|
5731
|
+
this.consentEl.innerHTML = "";
|
|
5732
|
+
this.consentEl.style.display = "none";
|
|
5733
|
+
return;
|
|
5734
|
+
}
|
|
5735
|
+
const r = this.consent.request;
|
|
5736
|
+
this.consentEl.style.display = "";
|
|
5737
|
+
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>`;
|
|
5738
|
+
this.consentEl.querySelector(".cs-deny").onclick = () => {
|
|
5739
|
+
const { record } = decide(this.consent, false);
|
|
5740
|
+
this.consent = record;
|
|
5741
|
+
this.renderConsent();
|
|
5742
|
+
this.addActivity({ kind: "nav.deny", label: `Declined opening ${r.url}` });
|
|
5743
|
+
};
|
|
5744
|
+
this.consentEl.querySelector(".cs-go").onclick = () => {
|
|
5745
|
+
const { record, navigateTo } = decide(this.consent, true);
|
|
5746
|
+
this.consent = record;
|
|
5747
|
+
this.renderConsent();
|
|
5748
|
+
this.addActivity({ kind: "nav.grant", label: `Opened ${navigateTo}` });
|
|
5749
|
+
if (navigateTo) window.location.assign(navigateTo);
|
|
5750
|
+
};
|
|
5751
|
+
}
|
|
5752
|
+
/** Drop a pending request without deciding it — e.g. the panel is closing. */
|
|
5753
|
+
abandonNavigation() {
|
|
5754
|
+
this.consent = withdraw(this.consent);
|
|
5755
|
+
this.renderConsent();
|
|
5756
|
+
}
|
|
5757
|
+
/** The dictation button, or nothing at all where the browser has no speech API. */
|
|
5758
|
+
voiceButton(target) {
|
|
5759
|
+
const Ctor = window.SpeechRecognition ?? window.webkitSpeechRecognition;
|
|
5760
|
+
const b = el("button", "voice", "");
|
|
5761
|
+
this.voiceEl = b;
|
|
5762
|
+
if (!Ctor) {
|
|
5763
|
+
b.textContent = "\u{1F3A4}";
|
|
5764
|
+
b.disabled = true;
|
|
5765
|
+
b.title = "Dictation is not available in this browser";
|
|
5766
|
+
b.setAttribute("aria-label", "Dictation unavailable");
|
|
5767
|
+
return b;
|
|
5768
|
+
}
|
|
5769
|
+
const idle = () => {
|
|
5770
|
+
b.classList.remove("on");
|
|
5771
|
+
b.textContent = "\u{1F3A4}";
|
|
5772
|
+
b.setAttribute("aria-label", "Dictate");
|
|
5773
|
+
b.title = "Dictate";
|
|
5774
|
+
};
|
|
5775
|
+
const listening = () => {
|
|
5776
|
+
b.classList.add("on");
|
|
5777
|
+
b.textContent = "\u23FA";
|
|
5778
|
+
b.setAttribute("aria-label", "Stop dictating");
|
|
5779
|
+
b.title = "Listening \u2014 click to stop";
|
|
5780
|
+
};
|
|
5781
|
+
idle();
|
|
5782
|
+
b.onclick = (e) => {
|
|
5783
|
+
e.stopPropagation();
|
|
5784
|
+
if (this.voice) {
|
|
5785
|
+
this.stopVoice();
|
|
5786
|
+
idle();
|
|
5787
|
+
return;
|
|
5788
|
+
}
|
|
5789
|
+
const rec = new Ctor();
|
|
5790
|
+
rec.continuous = true;
|
|
5791
|
+
rec.interimResults = true;
|
|
5792
|
+
rec.lang = document.documentElement.lang || "en-US";
|
|
5793
|
+
let base = target.value ? `${target.value} ` : "";
|
|
5794
|
+
rec.onresult = (ev) => {
|
|
5795
|
+
let text = "";
|
|
5796
|
+
for (let i = ev.resultIndex; i < ev.results.length; i++) text += ev.results[i][0].transcript;
|
|
5797
|
+
target.value = (base + text).replace(/\s+/g, " ").trimStart();
|
|
5798
|
+
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
5799
|
+
};
|
|
5800
|
+
rec.onerror = (ev) => {
|
|
5801
|
+
this.stopVoice();
|
|
5802
|
+
idle();
|
|
5803
|
+
this.addActivity({ kind: "voice.error", label: `Dictation failed: ${ev?.error ?? "unknown"}`, level: "error" });
|
|
5804
|
+
};
|
|
5805
|
+
rec.onend = () => {
|
|
5806
|
+
this.voice = null;
|
|
5807
|
+
idle();
|
|
5808
|
+
};
|
|
5809
|
+
this.voice = rec;
|
|
5810
|
+
this.voiceTarget = target;
|
|
5811
|
+
try {
|
|
5812
|
+
rec.start();
|
|
5813
|
+
listening();
|
|
5814
|
+
} catch {
|
|
5815
|
+
this.voice = null;
|
|
5816
|
+
idle();
|
|
5817
|
+
}
|
|
5818
|
+
};
|
|
5819
|
+
return b;
|
|
5820
|
+
}
|
|
5821
|
+
stopVoice() {
|
|
5822
|
+
try {
|
|
5823
|
+
this.voice?.stop();
|
|
5824
|
+
} catch {
|
|
5825
|
+
}
|
|
5826
|
+
this.voice = null;
|
|
5827
|
+
this.voiceTarget = null;
|
|
5828
|
+
}
|
|
5233
5829
|
flash(id) {
|
|
5234
5830
|
const c = this.comments.find((x) => x.id === id);
|
|
5235
5831
|
const pin = this.pins.get(id);
|
|
@@ -5505,6 +6101,15 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
5505
6101
|
function clearActivity() {
|
|
5506
6102
|
app?.clearActivity();
|
|
5507
6103
|
}
|
|
6104
|
+
function requestNavigation2(url, opts) {
|
|
6105
|
+
app?.requestNavigation(url, opts);
|
|
6106
|
+
}
|
|
6107
|
+
function setLocalAi(config) {
|
|
6108
|
+
app?.setLocalAi(config);
|
|
6109
|
+
}
|
|
6110
|
+
function openTool(tool) {
|
|
6111
|
+
app?.openTool(tool);
|
|
6112
|
+
}
|
|
5508
6113
|
return __toCommonJS(src_exports);
|
|
5509
6114
|
})();
|
|
5510
6115
|
//# sourceMappingURL=index.global.js.map
|