FreeLLMAPI / desktop /renderer /popover.html
Nryn215's picture
Upload folder using huggingface_hub
077865a verified
Raw
History Blame Contribute Delete
10.3 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
/* Theme variables: dark by default (deep black glass), body.light flips
them. The theme is driven by the dashboard's toggle via main.ts. */
:root {
--fg: rgba(255, 255, 255, 0.92);
--fg-dim: rgba(255, 255, 255, 0.55);
--fg-faint: rgba(255, 255, 255, 0.32);
--hairline: rgba(255, 255, 255, 0.10);
--card: rgba(255, 255, 255, 0.055);
--card-edge: rgba(255, 255, 255, 0.10);
--ghost: rgba(255, 255, 255, 0.08);
--ghost-hover: rgba(255, 255, 255, 0.13);
--switch-off: rgba(255, 255, 255, 0.16);
--sheen: rgba(255, 255, 255, 0.12);
--zero-bar: rgba(255, 255, 255, 0.10);
--accent: #6aa6ff;
--accent-2: #8e7bff;
--ok: #34c759;
--panel-tint: linear-gradient(180deg, rgba(12,12,15,0.62), rgba(0,0,2,0.72));
--panel-solid: linear-gradient(180deg, #19191d, #0a0a0d);
}
body.light {
--fg: rgba(20, 20, 24, 0.92);
--fg-dim: rgba(20, 20, 24, 0.56);
--fg-faint: rgba(20, 20, 24, 0.34);
--hairline: rgba(0, 0, 0, 0.10);
--card: rgba(0, 0, 0, 0.05);
--card-edge: rgba(255, 255, 255, 0.45);
--ghost: rgba(0, 0, 0, 0.06);
--ghost-hover: rgba(0, 0, 0, 0.10);
--switch-off: rgba(0, 0, 0, 0.14);
--sheen: rgba(255, 255, 255, 0.55);
--zero-bar: rgba(0, 0, 0, 0.10);
--ok: #1ea83f;
--panel-tint: linear-gradient(180deg, rgba(255,255,255,0.38), rgba(244,244,247,0.46));
--panel-solid: linear-gradient(180deg, #f4f4f6, #e9e9ee);
}
* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; }
/* The window itself is the panel: native rounded corners + vibrancy show
through everywhere the page stays (near-)transparent. CSS adds only a
tint and the gloss sheen. */
html, body { background: transparent; height: 100%; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-webkit-font-smoothing: antialiased;
color: var(--fg);
}
.panel {
position: relative;
height: 100%;
background: var(--panel-tint);
box-shadow: inset 0 1px 0 var(--card-edge);
overflow: hidden;
padding: 14px 14px 12px;
}
body.no-vibrancy .panel { background: var(--panel-solid); }
/* gloss sheen */
.panel::before {
content: "";
position: absolute; inset: 0 0 auto 0; height: 110px;
background: radial-gradient(120% 100% at 50% -20%, var(--sheen), rgba(255,255,255,0) 70%);
pointer-events: none;
}
.head { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.mark {
width: 22px; height: 22px; border-radius: 7px; flex: none;
background: linear-gradient(180deg, #3a3a40, #232327);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.18);
display: grid; place-items: center;
}
.mark svg { display: block; }
.title { font-size: 13px; font-weight: 600; letter-spacing: 0.1px; }
.addr { font-size: 11px; color: var(--fg-dim); font-variant-numeric: tabular-nums; }
.status { margin-left: auto; display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--fg-dim); }
.dot {
width: 7px; height: 7px; border-radius: 50%; background: var(--ok);
box-shadow: 0 0 6px rgba(52,199,89,0.9);
animation: pulse 2.4s ease-in-out infinite;
}
@keyframes pulse { 50% { opacity: 0.55; } }
.stats { display: flex; gap: 8px; margin-bottom: 10px; }
.stat {
flex: 1; padding: 9px 11px 8px;
border-radius: 12px;
background: var(--card);
box-shadow: inset 0 0.5px 0 var(--card-edge);
}
.stat .v { font-size: 19px; font-weight: 650; letter-spacing: -0.3px; font-variant-numeric: tabular-nums; }
.stat .l { font-size: 10px; color: var(--fg-dim); margin-top: 1px; text-transform: uppercase; letter-spacing: 0.5px; }
.stat .sub { font-size: 10px; color: var(--ok); margin-top: 1px; }
.chartwrap { padding: 4px 2px 0; }
canvas { display: block; width: 100%; height: 64px; }
.axis { display: flex; justify-content: space-between; font-size: 9px; color: var(--fg-faint); padding: 3px 2px 0; }
.lastmodel {
display: flex; align-items: center; gap: 6px;
font-size: 11px; color: var(--fg-dim);
padding: 9px 2px 10px;
border-bottom: 0.5px solid var(--hairline);
margin-bottom: 10px;
}
.lastmodel b { color: var(--fg); font-weight: 500; max-width: 210px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.actions { display: flex; gap: 7px; margin-bottom: 10px; }
button {
font: inherit; color: var(--fg); border: 0; cursor: pointer;
border-radius: 10px; font-size: 11.5px; font-weight: 550;
white-space: nowrap;
transition: transform 0.06s ease, filter 0.15s ease;
}
button:active { transform: scale(0.97); }
.primary {
flex: 1.6; padding: 8px 8px;
background: linear-gradient(180deg, var(--accent), #4d86e8);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), 0 2px 8px rgba(74,134,232,0.35);
}
.primary:hover { filter: brightness(1.07); }
.ghost {
flex: 1; padding: 8px 4px;
background: var(--ghost);
box-shadow: inset 0 0.5px 0 var(--card-edge);
color: var(--fg);
}
.ghost:hover { background: var(--ghost-hover); }
.ghost.copied { color: var(--ok); }
.foot { display: flex; align-items: center; font-size: 11.5px; color: var(--fg-dim); }
.switch {
position: relative; width: 30px; height: 18px; border-radius: 9px;
background: var(--switch-off); margin-right: 7px; flex: none;
transition: background 0.18s ease; cursor: pointer;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.25);
}
.switch::after {
content: ""; position: absolute; top: 2px; left: 2px;
width: 14px; height: 14px; border-radius: 50%;
background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.3);
transition: left 0.18s ease;
}
.switch.on { background: var(--ok); }
.switch.on::after { left: 14px; }
.quit {
margin-left: auto; background: none; color: var(--fg-dim);
font-size: 11.5px; padding: 4px 6px; border-radius: 7px;
}
.quit:hover { background: rgba(255,255,255,0.08); color: var(--fg); }
</style>
</head>
<body>
<div class="panel">
<div class="head">
<div class="mark">
<svg width="13" height="13" viewBox="0 0 64 64">
<circle cx="32" cy="32" r="11" fill="#fafafa"/>
<circle cx="32" cy="32" r="22" fill="none" stroke="#fafafa" stroke-opacity=".45"
stroke-width="6" stroke-linecap="round" stroke-dasharray="104 34"
transform="rotate(-60 32 32)"/>
</svg>
</div>
<div>
<div class="title">FreeLLMAPI</div>
<div class="addr" id="addr">127.0.0.1:…</div>
</div>
<div class="status"><span class="dot"></span>running</div>
</div>
<div class="stats">
<div class="stat">
<div class="v" id="reqs">0</div>
<div class="l">Requests today</div>
<div class="sub" id="rate"></div>
</div>
<div class="stat">
<div class="v" id="tokens">0</div>
<div class="l">Tokens today</div>
</div>
</div>
<div class="chartwrap">
<canvas id="chart" width="0" height="0"></canvas>
<div class="axis"><span>24h ago</span><span id="peak"></span><span>now</span></div>
</div>
<div class="lastmodel">Last model <b id="model"></b></div>
<div class="actions">
<button class="primary" id="open">Open Dashboard</button>
<button class="ghost" id="copyurl">Copy URL</button>
<button class="ghost" id="copykey">Copy Key</button>
</div>
<div class="foot">
<div class="switch" id="login"></div>
<span>Start at login</span>
<button class="quit" id="quit">Quit</button>
</div>
</div>
<script>
const $ = (id) => document.getElementById(id);
const fmt = (n) => n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? (n / 1e3).toFixed(1) + 'k' : String(n);
function drawChart(hourly) {
const c = $('chart');
const cssW = c.clientWidth, cssH = 64, dpr = window.devicePixelRatio || 2;
c.width = cssW * dpr; c.height = cssH * dpr;
const ctx = c.getContext('2d');
ctx.scale(dpr, dpr);
const n = hourly.length, gap = 3;
const bw = (cssW - gap * (n - 1)) / n;
const max = Math.max(...hourly, 1);
const grad = ctx.createLinearGradient(0, 0, 0, cssH);
grad.addColorStop(0, 'rgba(48,209,88,0.95)');
grad.addColorStop(1, 'rgba(52,199,89,0.45)');
const zeroBar = getComputedStyle(document.body).getPropertyValue('--zero-bar').trim();
for (let i = 0; i < n; i++) {
const v = hourly[i];
const h = v === 0 ? 2 : Math.max(3, (v / max) * (cssH - 6));
const x = i * (bw + gap), y = cssH - h;
ctx.fillStyle = v === 0 ? zeroBar : grad;
ctx.beginPath();
ctx.roundRect(x, y, bw, h, Math.min(3, bw / 2));
ctx.fill();
}
$('peak').textContent = max > 1 ? 'peak ' + max + '/h' : '';
}
let loginOn = false;
async function refresh() {
const s = await window.freeapi.snapshot();
document.body.classList.toggle('light', s.theme === 'light');
$('addr').textContent = '127.0.0.1:' + s.port;
$('reqs').textContent = fmt(s.requests);
$('tokens').textContent = fmt(s.tokens);
$('rate').textContent = s.successRate == null ? '' : s.successRate + '% success';
$('model').textContent = s.lastModel;
loginOn = s.loginItem;
$('login').classList.toggle('on', loginOn);
drawChart(s.hourly);
}
function flash(btn, label) {
const orig = btn.textContent;
btn.textContent = label; btn.classList.add('copied');
setTimeout(() => { btn.textContent = orig; btn.classList.remove('copied'); }, 1100);
}
$('open').addEventListener('click', () => window.freeapi.openDashboard());
$('copyurl').addEventListener('click', (e) => { window.freeapi.copyBaseUrl(); flash(e.target, 'Copied ✓'); });
$('copykey').addEventListener('click', (e) => { window.freeapi.copyApiKey(); flash(e.target, 'Copied ✓'); });
$('quit').addEventListener('click', () => window.freeapi.quit());
$('login').addEventListener('click', () => {
loginOn = !loginOn;
$('login').classList.toggle('on', loginOn);
window.freeapi.setLoginItem(loginOn);
});
window.freeapi.onRefresh(refresh);
refresh();
</script>
</body>
</html>