The 0.3.106 switch from a native menu to an HTML overlay for Pick AI gave the overlay a client-side window.blur handler that called api.close(). In the engine picker (same pattern) the close-on-click-away is driven from main with a `win.isFocused()` guard, so a focus flicker between the strip view and the pop does not count as a click-away; the LLM overlay missed that guard, so Electron's own view-to-view focus flicker on open was enough to close the picker the moment it opened. From 0.3.106 through 0.3.119 the Pick-AI button looked like a no-op: it rendered the pop internally (the DOM was there, 7 rows) but setVisible flipped back off before paint. Users saw a blank click. Fix is the same contract the engine, language, downloads and site-info pickers use: closeOnClickAway on main, no blur handler on the client. `llmPickerVisible` is tracked alongside setVisible so the gate can read it. The focus() call on open now runs only when the window is actually focused (same guard the other overlays use). The raise() call also passed `primaryW?.win` where a window record was expected, so the view never re-parented when a secondary window became primary — fixed to pass the record. Also renames DuckDuckGo AI → Duck AI in the engine catalog. duck.ai is Duck's own branded entry for the chat UI and matches what the host lands on; the longer name was ambiguous next to the DuckDuckGo search entry.
110 lines
5.5 KiB
HTML
110 lines
5.5 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Pick AI</title>
|
||
<style>
|
||
:root {
|
||
--bg: #141a24; --card: #1a2130; --card-hi: #212a3b; --line: #2a3344;
|
||
--ink: #e7eaf1; --mut: #8b98a9; --acid: #d6ff3d; --acid-tint: #d6ff3d1a;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
html, body { margin: 0; padding: 0; background: transparent; }
|
||
body {
|
||
display: inline-block;
|
||
font: 13px/1.3 system-ui, -apple-system, "Segoe UI", sans-serif;
|
||
color: var(--ink);
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
}
|
||
.pop {
|
||
display: flex; flex-direction: column;
|
||
background: var(--bg); border: 1px solid var(--line); border-radius: 12px;
|
||
box-shadow: 0 12px 40px rgba(0,0,0,.5), 0 2px 8px rgba(0,0,0,.4);
|
||
overflow: hidden; width: 240px;
|
||
}
|
||
.hdr { padding: 10px 14px 6px; font-size: 11px; font-weight: 600;
|
||
color: var(--mut); text-transform: uppercase; letter-spacing: .5px;
|
||
border-bottom: 1px solid var(--line); }
|
||
.list { display: flex; flex-direction: column; padding: 4px; }
|
||
.row { display: flex; align-items: center; gap: 10px;
|
||
padding: 8px 10px; border-radius: 8px; cursor: pointer; position: relative;
|
||
transition: background .08s; }
|
||
.row:hover { background: var(--card-hi); }
|
||
.row.cur { background: var(--acid-tint); }
|
||
.row.cur::before { content: ""; position: absolute; left: 2px; top: 20%; bottom: 20%;
|
||
width: 3px; border-radius: 2px; background: var(--acid); }
|
||
.ico { flex: none; width: 28px; height: 28px; border-radius: 8px; overflow: hidden;
|
||
display: grid; place-items: center; background: #0b0e14; }
|
||
.ico img, .ico svg { width: 100%; height: 100%; display: block; }
|
||
.ico .letter { font-weight: 800; font-size: 15px; color: var(--acid); }
|
||
.name { flex: 1; font-weight: 600; font-size: 13px; color: var(--ink); }
|
||
.check { flex: none; width: 14px; height: 14px; color: var(--acid); opacity: 0; }
|
||
.row.cur .check { opacity: 1; }
|
||
.footer { border-top: 1px solid var(--line); padding: 4px; }
|
||
.footer .row { color: var(--mut); font-weight: 500; }
|
||
.footer .ico { background: transparent; }
|
||
.footer .ico svg { width: 18px; height: 18px; color: var(--mut); }
|
||
.empty { padding: 16px; text-align: center; color: var(--mut); font-size: 12px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="pop" id="pop">
|
||
<div class="hdr">AI engine</div>
|
||
<div class="list" id="list"></div>
|
||
<div class="footer">
|
||
<div class="row" id="settingsRow">
|
||
<div class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></div>
|
||
<div class="name">Pick AI engines…</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
const api = window.llmPicker;
|
||
const list = document.getElementById("list");
|
||
const esc = (s) => String(s || "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
||
function render(state) {
|
||
const engines = state.engines || [];
|
||
const cur = state.current;
|
||
list.innerHTML = "";
|
||
if (!engines.length) {
|
||
list.innerHTML = '<div class="empty">No AI engine enabled.</div>';
|
||
return;
|
||
}
|
||
for (const e of engines) {
|
||
const row = document.createElement("div");
|
||
row.className = "row" + (e.id === cur ? " cur" : "");
|
||
const ico = e.icon
|
||
? '<img src="' + esc(e.icon) + '" alt="">'
|
||
: '<div class="letter">' + esc((e.name || "?").slice(0, 1).toUpperCase()) + '</div>';
|
||
row.innerHTML =
|
||
'<div class="ico">' + ico + '</div>' +
|
||
'<div class="name">' + esc(e.name) + '</div>' +
|
||
'<svg class="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
|
||
row.addEventListener("click", () => api.pick(e.id));
|
||
list.appendChild(row);
|
||
}
|
||
}
|
||
document.getElementById("settingsRow").addEventListener("click", () => api.settings());
|
||
document.addEventListener("keydown", (e) => { if (e.key === "Escape") api.close(); });
|
||
// Close-on-click-away is driven from main (see closeOnClickAway registration);
|
||
// relying on a client-side window.blur here closes the picker the moment it
|
||
// opens if Electron's focus flickers between the strip view and the pop,
|
||
// which is what happened in 0.3.106–0.3.119 — the pop briefly rendered then
|
||
// vanished and users saw the Pick-AI button as a no-op.
|
||
api.onState(render);
|
||
api.ready();
|
||
// Report rendered size so the main process can size the WebContentsView
|
||
// to match the pop's natural content. Measuring documentElement's scroll
|
||
// size — the pop's own getBoundingClientRect may still be clipped by the
|
||
// view bounds until the host resizes to our ask.
|
||
function reportSize() {
|
||
const w = document.documentElement.scrollWidth;
|
||
const h = document.documentElement.scrollHeight;
|
||
api.resize(w, h);
|
||
}
|
||
new ResizeObserver(reportSize).observe(document.body);
|
||
requestAnimationFrame(reportSize);
|
||
</script>
|
||
</body>
|
||
</html>
|