theseus/llm-picker.html

107 lines
5.2 KiB
HTML
Raw Normal View History

<!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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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(); });
window.addEventListener("blur", () => api.close());
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>