pool-anything 0.1.0
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/LICENSE +202 -0
- package/README.md +280 -0
- package/bin/pool-anything.js +4 -0
- package/data/providers.json +38 -0
- package/dist/admin/branding.js +34 -0
- package/dist/admin/components.js +189 -0
- package/dist/admin/pages.js +1891 -0
- package/dist/admin/shell.js +129 -0
- package/dist/admin/tokens.js +32 -0
- package/dist/cli/nl.js +147 -0
- package/dist/cli/repl.js +470 -0
- package/dist/cli/ui.js +81 -0
- package/dist/cli.js +199 -0
- package/dist/common/http.js +55 -0
- package/dist/config/env.js +94 -0
- package/dist/config/paths.js +112 -0
- package/dist/data/providers.json +38 -0
- package/dist/db/index.js +20 -0
- package/dist/db/init.js +11 -0
- package/dist/logo.png +0 -0
- package/dist/media/capabilities.js +103 -0
- package/dist/middleware/auth.js +37 -0
- package/dist/observability/health.js +31 -0
- package/dist/pool/index.js +407 -0
- package/dist/pool/rotation.js +123 -0
- package/dist/proxy/dns.js +39 -0
- package/dist/proxy/forward.js +350 -0
- package/dist/public/logos/anthropic.svg +1 -0
- package/dist/public/logos/anyscale.svg +1 -0
- package/dist/public/logos/appwrite.svg +1 -0
- package/dist/public/logos/brave.svg +1 -0
- package/dist/public/logos/cartesia.svg +4 -0
- package/dist/public/logos/claude.svg +1 -0
- package/dist/public/logos/cloudflare.svg +1 -0
- package/dist/public/logos/cohere-command.svg +1 -0
- package/dist/public/logos/cohere.svg +1 -0
- package/dist/public/logos/deepgram.svg +1 -0
- package/dist/public/logos/elevenlabs.svg +1 -0
- package/dist/public/logos/finnhub.png +0 -0
- package/dist/public/logos/fireworks.svg +1 -0
- package/dist/public/logos/gemini.svg +1 -0
- package/dist/public/logos/google.svg +1 -0
- package/dist/public/logos/googlemaps.svg +1 -0
- package/dist/public/logos/groq.svg +1 -0
- package/dist/public/logos/huggingface.svg +1 -0
- package/dist/public/logos/mailgun.svg +1 -0
- package/dist/public/logos/manifest.json +197 -0
- package/dist/public/logos/mapbox.svg +1 -0
- package/dist/public/logos/mistral.svg +1 -0
- package/dist/public/logos/neon.svg +1 -0
- package/dist/public/logos/openai.svg +1 -0
- package/dist/public/logos/openrouter.svg +1 -0
- package/dist/public/logos/openweather.png +0 -0
- package/dist/public/logos/perplexity.svg +1 -0
- package/dist/public/logos/pool-anything.svg +111 -0
- package/dist/public/logos/postmark.svg +4 -0
- package/dist/public/logos/replicate.svg +1 -0
- package/dist/public/logos/resend.svg +1 -0
- package/dist/public/logos/scrapingbee.svg +72 -0
- package/dist/public/logos/sendgrid.svg +1 -0
- package/dist/public/logos/serper.png +0 -0
- package/dist/public/logos/supabase.svg +1 -0
- package/dist/public/logos/tavily.svg +1 -0
- package/dist/public/logos/together.svg +1 -0
- package/dist/public/logos/turso.svg +1 -0
- package/dist/public/logos/twilio.svg +1 -0
- package/dist/public/logos/upstash.svg +1 -0
- package/dist/router/api.js +20 -0
- package/dist/server.js +431 -0
- package/dist/upstream/index.js +137 -0
- package/package.json +68 -0
- package/public/logos/anthropic.svg +1 -0
- package/public/logos/anyscale.svg +1 -0
- package/public/logos/appwrite.svg +1 -0
- package/public/logos/brave.svg +1 -0
- package/public/logos/cartesia.svg +4 -0
- package/public/logos/claude.svg +1 -0
- package/public/logos/cloudflare.svg +1 -0
- package/public/logos/cohere-command.svg +1 -0
- package/public/logos/cohere.svg +1 -0
- package/public/logos/deepgram.svg +1 -0
- package/public/logos/elevenlabs.svg +1 -0
- package/public/logos/finnhub.png +0 -0
- package/public/logos/fireworks.svg +1 -0
- package/public/logos/gemini.svg +1 -0
- package/public/logos/google.svg +1 -0
- package/public/logos/googlemaps.svg +1 -0
- package/public/logos/groq.svg +1 -0
- package/public/logos/huggingface.svg +1 -0
- package/public/logos/mailgun.svg +1 -0
- package/public/logos/manifest.json +197 -0
- package/public/logos/mapbox.svg +1 -0
- package/public/logos/mistral.svg +1 -0
- package/public/logos/neon.svg +1 -0
- package/public/logos/openai.svg +1 -0
- package/public/logos/openrouter.svg +1 -0
- package/public/logos/openweather.png +0 -0
- package/public/logos/perplexity.svg +1 -0
- package/public/logos/pool-anything.svg +111 -0
- package/public/logos/postmark.svg +4 -0
- package/public/logos/replicate.svg +1 -0
- package/public/logos/resend.svg +1 -0
- package/public/logos/scrapingbee.svg +72 -0
- package/public/logos/sendgrid.svg +1 -0
- package/public/logos/serper.png +0 -0
- package/public/logos/supabase.svg +1 -0
- package/public/logos/tavily.svg +1 -0
- package/public/logos/together.svg +1 -0
- package/public/logos/turso.svg +1 -0
- package/public/logos/twilio.svg +1 -0
- package/public/logos/upstash.svg +1 -0
|
@@ -0,0 +1,1891 @@
|
|
|
1
|
+
import { LOGO_V, DOCS_URL, ICONS } from "./branding.js";
|
|
2
|
+
import { shellCss, shellNav, shellJs, sharedHeader, sharedLayoutCss } from "./shell.js";
|
|
3
|
+
import { tokensCss } from "./tokens.js";
|
|
4
|
+
import { docsComponentsCss, ddSelect } from "./components.js";
|
|
5
|
+
// Web UI pages. Server-side interpolations are LOGO_V (cache-buster), DOCS_URL,
|
|
6
|
+
// ICONS, and shell partials; all client-side JS runs in the browser.
|
|
7
|
+
// Shared client-side helpers: single source of truth for the inline <script>
|
|
8
|
+
// blocks below. Page templates interpolate these instead of redefining them,
|
|
9
|
+
// so a helper can never exist in one page and be missing in another (cf. the
|
|
10
|
+
// keysPage esc() outage). Chart helpers are a separate snippet so non-chart
|
|
11
|
+
// pages don't ship chart code. Bodies contain no backticks or ${, so they are
|
|
12
|
+
// safe to interpolate into the page template literals.
|
|
13
|
+
const clientCoreJs = `async function j(r){const t=await r.text();try{return JSON.parse(t)}catch{return t}}
|
|
14
|
+
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
|
15
|
+
const err=t=>document.getElementById('err').textContent=t||'';
|
|
16
|
+
function logoSrc(img,file){if(!img)return;if(file){img.src='/logos/'+file+'?v=${LOGO_V}';img.onerror=()=>img.remove();}else img.remove();}`;
|
|
17
|
+
// Custom dropdowns (vanilla shadcn-style menus). Each `.dd` wraps a hidden
|
|
18
|
+
// native select that stays the state source of truth: the menu is built from
|
|
19
|
+
// its options, choosing dispatches real input/change events, and ddSync(id)
|
|
20
|
+
// rebuilds the menu after options are (re)populated programmatically.
|
|
21
|
+
// Bodies contain no backticks or ${, so they are safe to interpolate.
|
|
22
|
+
const clientDropdownJs = `const DD_TICK='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 12.5 5 5 10-11"/></svg>';
|
|
23
|
+
function ddClose(id){var w=document.getElementById('dd-'+id);if(!w)return;w.classList.remove('open');var b=w.querySelector('.dd-btn');if(b)b.setAttribute('aria-expanded','false');var m=w.querySelector('.dd-menu');if(m)m.hidden=true;}
|
|
24
|
+
function ddCloseAll(except){var ws=document.querySelectorAll('.dd.open');for(var i=0;i<ws.length;i++){if(ws[i].id!=='dd-'+except)ddClose(ws[i].id.slice(3));}}
|
|
25
|
+
function ddSync(id){var s=document.getElementById(id),w=document.getElementById('dd-'+id);if(!s||!w)return;var m=w.querySelector('.dd-menu');m.innerHTML='';var cur=s.value;for(var i=0;i<s.options.length;i++){var o=s.options[i];var it=document.createElement('button');it.type='button';it.className='dd-item';it.setAttribute('role','option');var t=document.createElement('span');t.className='dd-txt';t.textContent=o.textContent;it.appendChild(t);var k=document.createElement('span');k.className='tick';k.innerHTML=DD_TICK;it.appendChild(k);if(o.value===cur)it.setAttribute('aria-selected','true');if(o.disabled)it.disabled=true;(function(v,dis){it.addEventListener('click',function(){if(dis)return;ddChoose(id,v);});})(o.value,o.disabled);m.appendChild(it);}var lab=w.querySelector('.dd-val');var sel=s.options[s.selectedIndex];lab.textContent=sel?sel.textContent:'';var trg=w.querySelector('.dd-btn');if(trg)trg.disabled=s.disabled;}
|
|
26
|
+
function ddChoose(id,v){var s=document.getElementById(id),w=document.getElementById('dd-'+id);if(!s||!w||s.disabled)return;if(s.value!==v){s.value=v;ddSync(id);s.dispatchEvent(new Event('input',{bubbles:true}));s.dispatchEvent(new Event('change',{bubbles:true}));}ddClose(id);var b=w.querySelector('.dd-btn');if(b)b.focus();}
|
|
27
|
+
function ddToggle(id){var w=document.getElementById('dd-'+id),s=document.getElementById(id);if(!w||!s||s.disabled)return;var willOpen=!w.classList.contains('open');ddCloseAll(id);var m=w.querySelector('.dd-menu'),b=w.querySelector('.dd-btn');if(willOpen){w.classList.add('open');if(b)b.setAttribute('aria-expanded','true');m.hidden=false;var cur=m.querySelector('.dd-item[aria-selected="true"]')||m.querySelector('.dd-item:not(:disabled)');if(cur)cur.focus();}else ddClose(id);}
|
|
28
|
+
function ddMove(id,dir){var w=document.getElementById('dd-'+id);if(!w)return;var items=w.querySelectorAll('.dd-item:not(:disabled)');if(!items.length)return;var at=Array.prototype.indexOf.call(items,document.activeElement);var n=dir>0?(at<0?0:(at+1)%items.length):(at<0?items.length-1:(at-1+items.length)%items.length);items[n].focus();}
|
|
29
|
+
function initDropdowns(){if(!document.__paDdOutside){document.__paDdOutside=1;document.addEventListener('pointerdown',function(e){if(!e.target.closest||!e.target.closest('.dd'))ddCloseAll('');});}var ws=document.querySelectorAll('.dd');for(var i=0;i<ws.length;i++)(function(w){if(w.dataset.ddInit)return;w.dataset.ddInit='1';var id=w.id.slice(3);var b=w.querySelector('.dd-btn'),m=w.querySelector('.dd-menu');b.addEventListener('click',function(){ddToggle(id);});b.addEventListener('keydown',function(e){if(e.key==='ArrowDown'||e.key==='ArrowUp'){e.preventDefault();if(!w.classList.contains('open'))ddToggle(id);else ddMove(id,e.key==='ArrowDown'?1:-1);}else if(e.key==='Escape')ddClose(id);});m.addEventListener('keydown',function(e){if(e.key==='ArrowDown'){e.preventDefault();ddMove(id,1);}else if(e.key==='ArrowUp'){e.preventDefault();ddMove(id,-1);}else if(e.key==='Home'){e.preventDefault();var f=m.querySelector('.dd-item:not(:disabled)');if(f)f.focus();}else if(e.key==='End'){e.preventDefault();var all=m.querySelectorAll('.dd-item:not(:disabled)');if(all.length)all[all.length-1].focus();}else if(e.key==='Escape'){e.preventDefault();ddClose(id);b.focus();}else if(e.key==='Tab')ddClose(id);});ddSync(id);})(ws[i]);}`;
|
|
30
|
+
const clientChartsJs = `function fmtCompact(n) {
|
|
31
|
+
n = Number(n) || 0;
|
|
32
|
+
if (n >= 1000) {
|
|
33
|
+
const v = n / 1000;
|
|
34
|
+
const s = v >= 100 ? String(Math.round(v)) : String(Math.round(v * 100) / 100);
|
|
35
|
+
return s + 'k';
|
|
36
|
+
}
|
|
37
|
+
return String(n);
|
|
38
|
+
}
|
|
39
|
+
function renderLine(svgId, tipId, yId, days, vals, label, color) {
|
|
40
|
+
const svg = document.getElementById(svgId);
|
|
41
|
+
const tip = document.getElementById(tipId);
|
|
42
|
+
if (!svg) return;
|
|
43
|
+
const box = svg.viewBox.baseVal;
|
|
44
|
+
const W = box.width || 600, H = box.height || 110, pad = 6;
|
|
45
|
+
const n = vals.length;
|
|
46
|
+
const max = Math.max.apply(null, vals.concat([0]));
|
|
47
|
+
const xs = vals.map((_, i) => pad + (n <= 1 ? (W - pad * 2) / 2 : i * (W - pad * 2) / (n - 1)));
|
|
48
|
+
const ys = vals.map(v => max === 0 ? H - pad - 2 : H - pad - (v / max) * (H - pad * 2));
|
|
49
|
+
svg.innerHTML = '';
|
|
50
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
51
|
+
[0.25, 0.5, 0.75].forEach(g => {
|
|
52
|
+
const y = pad + (H - pad * 2) * g;
|
|
53
|
+
const ln = document.createElementNS(ns, 'line');
|
|
54
|
+
ln.setAttribute('x1', pad); ln.setAttribute('x2', W - pad);
|
|
55
|
+
ln.setAttribute('y1', y); ln.setAttribute('y2', y);
|
|
56
|
+
ln.setAttribute('stroke', '#EFEAE1'); ln.setAttribute('stroke-width', '1');
|
|
57
|
+
svg.appendChild(ln);
|
|
58
|
+
});
|
|
59
|
+
let line = '', area = '';
|
|
60
|
+
xs.forEach((x, i) => {
|
|
61
|
+
const y = Math.round(ys[i] * 10) / 10, xr = Math.round(x * 10) / 10;
|
|
62
|
+
line += (i ? 'L' : 'M') + xr + ' ' + y;
|
|
63
|
+
area += (i ? 'L' : 'M') + xr + ' ' + y;
|
|
64
|
+
});
|
|
65
|
+
if (n > 0) area += 'L' + Math.round(xs[n - 1] * 10) / 10 + ' ' + (H - pad) + 'L' + Math.round(xs[0] * 10) / 10 + ' ' + (H - pad) + 'Z';
|
|
66
|
+
const ap = document.createElementNS(ns, 'path');
|
|
67
|
+
ap.setAttribute('d', area); ap.setAttribute('fill', color === '#ef4444' ? 'rgba(239,68,68,0.08)' : 'rgba(59,130,246,0.10)'); ap.setAttribute('stroke', 'none');
|
|
68
|
+
svg.appendChild(ap);
|
|
69
|
+
const lp = document.createElementNS(ns, 'path');
|
|
70
|
+
lp.setAttribute('d', line); lp.setAttribute('fill', 'none'); lp.setAttribute('stroke', max === 0 ? '#E8E3D9' : color);
|
|
71
|
+
lp.setAttribute('stroke-width', '1.8'); lp.setAttribute('stroke-linejoin', 'round'); lp.setAttribute('stroke-linecap', 'round');
|
|
72
|
+
svg.appendChild(lp);
|
|
73
|
+
if (yId) {
|
|
74
|
+
const y = document.getElementById(yId);
|
|
75
|
+
if (y) {
|
|
76
|
+
const f = (v) => v >= 1000 ? (Math.round(v / 100) / 10 + 'k') : String(Math.round(v));
|
|
77
|
+
y.innerHTML = max === 0 ? '<span></span><span></span><span>0</span>'
|
|
78
|
+
: '<span>' + f(max) + '</span><span>' + f(max / 2) + '</span><span>0</span>';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const wrap = svg.parentElement;
|
|
82
|
+
let nd = wrap.querySelector('.no-data');
|
|
83
|
+
if (max === 0) {
|
|
84
|
+
if (!nd) { nd = document.createElement('span'); nd.className = 'no-data'; nd.textContent = 'No data'; wrap.appendChild(nd); }
|
|
85
|
+
else nd.style.display = 'block';
|
|
86
|
+
} else if (nd) nd.style.display = 'none';
|
|
87
|
+
if (!tip || max === 0) { svg.onmousemove = null; svg.onmouseleave = null; return; }
|
|
88
|
+
const dots = xs.map((x, i) => {
|
|
89
|
+
const c = document.createElementNS(ns, 'circle');
|
|
90
|
+
c.setAttribute('cx', x); c.setAttribute('cy', ys[i]); c.setAttribute('r', '3.5');
|
|
91
|
+
c.setAttribute('fill', color); c.setAttribute('opacity', '0');
|
|
92
|
+
svg.appendChild(c); return c;
|
|
93
|
+
});
|
|
94
|
+
svg.onmousemove = (e) => {
|
|
95
|
+
const r = svg.getBoundingClientRect();
|
|
96
|
+
const mx = (e.clientX - r.left) / r.width * W;
|
|
97
|
+
let best = 0, bd = 1e9;
|
|
98
|
+
xs.forEach((x, i) => { const d = Math.abs(x - mx); if (d < bd) { bd = d; best = i; } });
|
|
99
|
+
dots.forEach((d, k) => d.setAttribute('opacity', k === best ? '1' : '0'));
|
|
100
|
+
tip.style.display = 'block';
|
|
101
|
+
tip.innerHTML = '<div class="tip-day">' + days[best] + '</div>' +
|
|
102
|
+
'<div class="tip-row"><span class="dot" style="background:' + color + '"></span><span>' + label + '</span><span class="tip-val">' + vals[best] + '</span></div>';
|
|
103
|
+
const wr = wrap.getBoundingClientRect();
|
|
104
|
+
const px = (xs[best] / W) * wr.width, py = (ys[best] / H) * wr.height;
|
|
105
|
+
const tw = tip.offsetWidth, th = tip.offsetHeight;
|
|
106
|
+
let left = px + 12; if (left + tw > wr.width - 4) left = px - tw - 12;
|
|
107
|
+
let top = py - th - 10; if (top < 4) top = py + 14;
|
|
108
|
+
tip.style.left = left + 'px'; tip.style.top = top + 'px';
|
|
109
|
+
};
|
|
110
|
+
svg.onmouseleave = () => { tip.style.display = 'none'; dots.forEach(d => d.setAttribute('opacity', '0')); };
|
|
111
|
+
}
|
|
112
|
+
function setDelta(id, pct, title) {
|
|
113
|
+
const d = document.getElementById(id);
|
|
114
|
+
if (!d) return;
|
|
115
|
+
if (pct === null || pct === undefined) { d.textContent = ''; d.className = 'ana-delta flat'; return; }
|
|
116
|
+
const v = Math.abs(pct).toFixed(1) + '%';
|
|
117
|
+
if (Math.abs(pct) < 0.05) { d.textContent = '― 0%'; d.className = 'ana-delta flat'; }
|
|
118
|
+
else if (pct >= 0) { d.textContent = '↗ ' + v; d.className = 'ana-delta up'; }
|
|
119
|
+
else { d.textContent = '↘ ' + v; d.className = 'ana-delta down'; }
|
|
120
|
+
if (title) d.title = title;
|
|
121
|
+
}`;
|
|
122
|
+
export const page = `<!doctype html>
|
|
123
|
+
<html lang="en">
|
|
124
|
+
<head>
|
|
125
|
+
<meta charset="utf-8" />
|
|
126
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
127
|
+
<title>pool-anything</title>
|
|
128
|
+
<link rel="icon" type="image/png" href="/logo.png" />
|
|
129
|
+
<style>
|
|
130
|
+
${tokensCss}
|
|
131
|
+
html { background:var(--card); }
|
|
132
|
+
html { scrollbar-gutter:stable; scrollbar-width:none; -ms-overflow-style:none; }
|
|
133
|
+
html::-webkit-scrollbar, body::-webkit-scrollbar { display:none; }
|
|
134
|
+
* { box-sizing:border-box; }
|
|
135
|
+
body { margin:0; font-family:var(--font-sans); background:var(--bg); color:var(--ink); }
|
|
136
|
+
header { height:56px; background:var(--card); border-bottom:1px solid var(--line); display:flex; align-items:center; padding:0 16px; gap:8px; position:sticky; top:0; }
|
|
137
|
+
header .spacer { margin-left:auto; display:flex; gap:4px; }
|
|
138
|
+
header button, header a { font-size:14px; padding:6px 12px; border-radius:8px; border:0; background:inherit; color:var(--ink); text-decoration:none; cursor:pointer; }
|
|
139
|
+
header button:hover, header a:hover { background:var(--paper-2); }
|
|
140
|
+
.shell { display:grid; grid-template-columns:var(--sbw,260px) 1fr; min-height:100vh; transition:grid-template-columns 250ms cubic-bezier(0.77,0,0.175,1); }
|
|
141
|
+
.shell.collapsed { --sbw:57px; }
|
|
142
|
+
.sidebar { background:var(--card); border-right:1px solid var(--line); display:flex; flex-direction:column; min-height:100vh; height:100vh; position:sticky; top:0; overflow:hidden; white-space:nowrap; }
|
|
143
|
+
.sb-header { height:58px; flex-shrink:0; display:flex; align-items:center; gap:4px; border-bottom:1px solid var(--line); padding:0 12px; overflow:hidden; }
|
|
144
|
+
.sb-logo { width:40px; height:40px; flex-shrink:0; display:grid; place-items:center; overflow:hidden; }
|
|
145
|
+
.sb-logo img { width:36px; height:36px; object-fit:contain; }
|
|
146
|
+
.sb-acct { flex:1; min-width:0; display:flex; align-items:center; justify-content:space-between; gap:8px; padding:6px 12px; border-radius:8px; border:0; background:transparent; font-size:14px; }
|
|
147
|
+
.sb-acct span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:500; }
|
|
148
|
+
.sb-nav { flex:1; min-height:0; overflow-y:auto; overflow-x:hidden; padding:12px 11px 12px 14px; scrollbar-width:none; }
|
|
149
|
+
.sb-nav::-webkit-scrollbar { display:none; }
|
|
150
|
+
.quick { display:flex; align-items:center; gap:12px; width:100%; height:30px; padding:0 12px; margin-bottom:12px; border-radius:8px; border:1px solid var(--line); background:var(--card); font-size:14px; color:var(--ink-soft); cursor:pointer; }
|
|
151
|
+
.quick:hover { background:var(--paper-2); }
|
|
152
|
+
.quick kbd { margin-left:auto; font-size:12px; color:var(--ink-soft); }
|
|
153
|
+
.mi { display:flex; align-items:center; gap:10px; width:100%; min-height:34px; padding:0 12px; border-radius:8px; border:0; background:transparent; font-size:14px; font-weight:500; color:var(--ink); cursor:pointer; text-decoration:none; text-align:left; }
|
|
154
|
+
.mi:hover { background:var(--paper-2); }
|
|
155
|
+
.mi.active { background:var(--line-soft); }
|
|
156
|
+
.mi .ic { opacity:.5; flex-shrink:0; width:16px; text-align:center; }
|
|
157
|
+
.mi .ic svg { display:block; width:18px; height:18px; }
|
|
158
|
+
.mi .ext { margin-left:auto; opacity:.45; display:inline-flex; flex-shrink:0; }
|
|
159
|
+
.mi .chev { margin-left:auto; opacity:.4; font-size:12px; }
|
|
160
|
+
.sub { position:relative; margin:0; padding:0 0 0 28px; list-style:none; display:flex; flex-direction:column; gap:1px; }
|
|
161
|
+
.sub::before { content:""; position:absolute; left:19px; top:4px; bottom:4px; width:1px; background:var(--line); }
|
|
162
|
+
.sub a { display:flex; flex-direction:column; padding:8px 11px; border-radius:8px; color:var(--ink); text-decoration:none; }
|
|
163
|
+
.sub a:hover { background:var(--paper-2); }
|
|
164
|
+
.sub b { font-size:14px; font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
165
|
+
.sub small { font-size:10.5px; color:var(--ink-soft); font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
166
|
+
.sec { margin:12px 0 8px; padding:16px 12px 8px; font-size:14px; color:var(--ink-soft); font-weight:500; border-top:1px solid var(--line); }
|
|
167
|
+
.sec:first-of-type { border-top:0; }
|
|
168
|
+
details.grp { margin:1px 0; }
|
|
169
|
+
details.grp > summary { list-style:none; }
|
|
170
|
+
details.grp > summary::-webkit-details-marker { display:none; }
|
|
171
|
+
details.grp > summary .chev { transition:transform 200ms; }
|
|
172
|
+
details.grp[open] > summary .chev { transform:rotate(90deg); }
|
|
173
|
+
.sb-footer { height:48px; flex-shrink:0; display:flex; align-items:center; padding:0 14px; border-top:1px solid var(--line); position:sticky; bottom:0; background:var(--card); }
|
|
174
|
+
.collapse-btn { width:34px; height:34px; display:grid; place-items:center; border-radius:8px; border:0; background:transparent; color:var(--ink-soft); cursor:pointer; }
|
|
175
|
+
.collapse-btn:hover { background:var(--paper-2); color:var(--ink); }
|
|
176
|
+
.shell.collapsed:not(.peeking) .lbl, .shell.collapsed:not(.peeking) .sb-acct span, .shell.collapsed:not(.peeking) .sb-acct svg, .shell.collapsed:not(.peeking) .quick span, .shell.collapsed:not(.peeking) .quick kbd, .shell.collapsed:not(.peeking) .mi .chev, .shell.collapsed:not(.peeking) .mi .ext, .shell.collapsed:not(.peeking) .sec, .shell.collapsed:not(.peeking) .sub { display:none; }
|
|
177
|
+
.shell.collapsed:not(.peeking) .sb-header { padding:0 8px; justify-content:center; }
|
|
178
|
+
.shell.collapsed:not(.peeking) .sb-acct { display:none; }
|
|
179
|
+
.shell.collapsed:not(.peeking) .mi, .shell.collapsed:not(.peeking) .quick { justify-content:center; padding:0; }
|
|
180
|
+
.shell.peeking { --sbw:260px; }
|
|
181
|
+
.shell .lbl, .shell .sb-acct, .shell .mi .chev, .shell .sec, .shell .sub { opacity:1; transition:opacity 150ms ease; }
|
|
182
|
+
.shell.closing .lbl, .shell.closing .sb-acct, .shell.closing .mi .chev, .shell.closing .sec, .shell.closing .sub { opacity:0; }
|
|
183
|
+
.content { flex:1; min-width:0; display:flex; flex-direction:column; animation:pageIn .3s ease both; }
|
|
184
|
+
@keyframes pageIn { from { opacity:0; transform:translateY(-6px); } to { opacity:1; transform:none; } }
|
|
185
|
+
@media (max-width:720px) { .shell { --sbw:57px; } .lbl, .ext { display:none; } }
|
|
186
|
+
main { min-height:calc(100vh - 56px); display:flex; flex-direction:column; align-items:center; justify-content:flex-start; padding:16vh 16px 24px; }
|
|
187
|
+
.wrap { width:100%; max-width:1400px; margin:0 auto; display:flex; flex-direction:column; align-items:stretch; gap:24px; padding:0 8px; }
|
|
188
|
+
.hero-row, .search-card { width:100%; max-width:640px; margin-left:auto; margin-right:auto; }
|
|
189
|
+
.home-grid { width:100%; display:grid; grid-template-columns:minmax(0,1fr) minmax(320px,380px); gap:24px; align-items:start; }
|
|
190
|
+
@media (min-width:901px) { .home-grid .analytics { position:sticky; top:72px; } }
|
|
191
|
+
@media (max-width:900px) {
|
|
192
|
+
.home-grid { grid-template-columns:1fr; }
|
|
193
|
+
}
|
|
194
|
+
.hero-logo { width:40px; height:40px; object-fit:contain; flex-shrink:0; }
|
|
195
|
+
.hero-row { display:flex; align-items:center; justify-content:center; gap:4px; }
|
|
196
|
+
.search-card { width:100%; background:var(--card); border:1px solid var(--line); border-radius:12px; padding:6px; box-shadow:none; overflow:hidden; }
|
|
197
|
+
.search-card:focus-within { border-color:var(--ink-faint); box-shadow:none; }
|
|
198
|
+
.search-box { display:flex; align-items:center; gap:0; background:var(--paper-2); border:1px solid var(--line); border-radius:8px; height:var(--ctl-xl); padding:0 4px 0 10px; box-shadow:none; overflow:hidden; }
|
|
199
|
+
.search-box svg { flex-shrink:0; color:var(--ink-soft); }
|
|
200
|
+
.search-box input { flex:1; min-width:0; height:100%; border:0; outline:0; background:transparent; font-size:14px; font-weight:500; padding:0 16px; }
|
|
201
|
+
.kbd { display:flex; align-items:center; gap:4px; padding-right:10px; }
|
|
202
|
+
.kbd kbd { height:20px; min-width:20px; display:inline-flex; align-items:center; justify-content:center; padding:0 4px; font-size:12px; font-weight:500; font-family:inherit; background:var(--card); color:var(--ink-soft); border:1px solid var(--line); border-radius:5px; }
|
|
203
|
+
#results { width:100%; display:grid; grid-template-columns:repeat(auto-fill,minmax(96px,1fr)); gap:8px; }
|
|
204
|
+
.prov { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px; width:100%; min-height:76px; background:var(--card); border:1px solid var(--line); border-radius:10px; padding:10px 6px; cursor:pointer; font-size:11px; font-weight:500; text-align:center; animation:fadeSlide .28s cubic-bezier(.2,.7,.3,1) both; }
|
|
205
|
+
.prov:hover { background:var(--paper-2); }
|
|
206
|
+
.prov img { width:24px; height:24px; }
|
|
207
|
+
.prov span { max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
208
|
+
.prov small { display:none; }
|
|
209
|
+
@keyframes fadeSlide { from { opacity:0; transform:translateY(-8px); } to { opacity:1; transform:none; } }
|
|
210
|
+
dialog { border:1px solid var(--line); border-radius:12px; padding:0; max-width:640px; width:calc(100vw - 48px); font-family:inherit; overflow:hidden; }
|
|
211
|
+
dialog::backdrop { background:var(--scrim); }
|
|
212
|
+
.p-head { display:flex; align-items:center; gap:10px; padding:16px 20px 12px; border-bottom:1px solid var(--line); }
|
|
213
|
+
.p-head img { width:28px; height:28px; }
|
|
214
|
+
.p-head b { font-size:16px; }
|
|
215
|
+
.pill { margin-left:auto; font-size:11px; font-weight:600; background:var(--paper-2); border-radius:999px; padding:3px 10px; white-space:nowrap; }
|
|
216
|
+
.p-body { padding:12px 20px 16px; display:flex; flex-direction:column; gap:8px; }
|
|
217
|
+
.p-note { font-size:12px; color:var(--ink-soft); }
|
|
218
|
+
.krow { display:flex; align-items:center; gap:8px; background:var(--paper-2); border-radius:10px; padding:8px 10px; font-size:13px; animation:fadeSlide .25s ease both; }
|
|
219
|
+
.krow .meta { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
220
|
+
.krow .use { font-size:11px; color:var(--ink-soft); }
|
|
221
|
+
.krow button, .p-foot button { border:1px solid var(--line); background:var(--card); color:var(--ink); border-radius:8px; height:30px; padding:0 12px; font-size:13px; cursor:pointer; }
|
|
222
|
+
.slotrow { display:flex; gap:8px; }
|
|
223
|
+
.slotrow input { flex:1; min-width:0; border:1px solid var(--line); border-radius:8px; height:34px; padding:0 12px; font-size:14px; }
|
|
224
|
+
.slotrow button { border:1px solid var(--ink); background:var(--ink); color:var(--card); border-radius:8px; height:34px; padding:0 12px; font-size:14px; cursor:pointer; }
|
|
225
|
+
.p-foot { display:flex; align-items:center; gap:8px; padding:12px 20px 16px; border-top:1px solid var(--line); font-size:12px; color:var(--ink-soft); }
|
|
226
|
+
.p-foot button { margin-left:auto; }
|
|
227
|
+
.perr { color:var(--danger-ink); font-size:13px; min-height:18px; }
|
|
228
|
+
button:focus-visible, a:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
|
229
|
+
.search-card:focus-within { border-color:var(--ink-faint); box-shadow:none; }
|
|
230
|
+
.hero-row, .search-card { max-width:400px; }
|
|
231
|
+
.hero-row h1, .hero-row { gap:2px; }
|
|
232
|
+
h1 { font-family:var(--font-serif); font-size:clamp(20px, 4vw, 26px); font-weight:500; }
|
|
233
|
+
.sec-label { font-size:11px; font-weight:600; letter-spacing:.08em; text-transform:uppercase; color:var(--ink-soft); margin:0 0 8px; }
|
|
234
|
+
.conn-grid { width:100%; display:flex; flex-direction:column; gap:8px; }
|
|
235
|
+
.prov-lg { display:flex; align-items:center; gap:10px; width:100%; background:var(--card); border:1px solid var(--line); border-radius:12px; padding:10px 12px; cursor:pointer; font-size:13px; font-weight:600; text-align:left; animation:fadeSlide .28s cubic-bezier(.2,.7,.3,1) both; }
|
|
236
|
+
.prov-lg:hover { background:var(--paper-2); }
|
|
237
|
+
.prov-lg .top { display:flex; align-items:center; gap:10px; flex:1; min-width:0; }
|
|
238
|
+
.prov-lg img { width:28px; height:28px; flex-shrink:0; }
|
|
239
|
+
.prov-lg .nm { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
240
|
+
.prov-lg .meta { font-size:11px; color:var(--ink-soft); font-weight:500; font-variant-numeric:tabular-nums; white-space:nowrap; flex-shrink:0; }
|
|
241
|
+
.meter { height:4px; border-radius:999px; background:var(--line); width:64px; flex-shrink:0; overflow:hidden; }
|
|
242
|
+
.meter i { display:block; height:100%; background:var(--chart-blue); border-radius:999px; }
|
|
243
|
+
details.browse { width:100%; background:transparent; border:0; }
|
|
244
|
+
details.browse > summary { list-style:none; cursor:pointer; display:flex; align-items:center; gap:8px; font-size:13px; font-weight:600; color:var(--ink); padding:10px 2px; user-select:none; }
|
|
245
|
+
details.browse > summary::-webkit-details-marker { display:none; }
|
|
246
|
+
details.browse > summary .chev { color:var(--ink-soft); font-size:11px; transition:transform 200ms; }
|
|
247
|
+
details.browse[open] > summary .chev { transform:rotate(90deg); }
|
|
248
|
+
details.browse > summary .count { font-size:11px; font-weight:500; color:var(--ink-soft); }
|
|
249
|
+
details.cat { width:100%; margin:2px 0 10px; }
|
|
250
|
+
details.cat > summary { list-style:none; cursor:pointer; display:flex; align-items:center; gap:8px; padding:8px 2px; user-select:none; }
|
|
251
|
+
details.cat > summary::-webkit-details-marker { display:none; }
|
|
252
|
+
details.cat > summary .chev { color:var(--ink-soft); font-size:10px; transition:transform 200ms; }
|
|
253
|
+
details.cat[open] > summary .chev { transform:rotate(90deg); }
|
|
254
|
+
details.cat > summary:hover, details.browse > summary:hover { background:var(--paper-2); border-radius:8px; }
|
|
255
|
+
details.cat > summary:focus-visible, details.browse > summary:focus-visible, .tips-banner > summary:focus-visible { outline:2px solid var(--accent); outline-offset:2px; border-radius:8px; }
|
|
256
|
+
.cat-grid { width:100%; display:grid; grid-template-columns:repeat(auto-fill,minmax(96px,1fr)); gap:8px; padding-bottom:4px; }
|
|
257
|
+
.cat-empty { font-size:12px; color:var(--ink-soft); padding:4px 2px 12px; }
|
|
258
|
+
.conn-empty { font-size:13px; color:var(--ink-soft); background:var(--card); border:1px dashed var(--line); border-radius:12px; padding:14px; }
|
|
259
|
+
.conn-empty b { color:var(--ink); }
|
|
260
|
+
@media (max-width:520px) { .hero-row { flex-wrap:wrap; text-align:center; } h1 { font-size:22px; } .analytics-head { flex-wrap:wrap; row-gap:8px; } }
|
|
261
|
+
@media (prefers-reduced-motion: reduce) {
|
|
262
|
+
*, *::before, *::after { animation:none !important; transition:none !important; }
|
|
263
|
+
}
|
|
264
|
+
.analytics { width:100%; display:flex; flex-direction:column; gap:10px; }
|
|
265
|
+
.home-grid { row-gap:0; }
|
|
266
|
+
#homeLeft { gap:0 !important; background:var(--card); border:1px solid var(--line); border-radius:12px; padding:18px; align-self:start; }
|
|
267
|
+
#homeLeft > section { margin-bottom:18px; }
|
|
268
|
+
#homeLeft > section:last-child { margin-bottom:0; }
|
|
269
|
+
#browseAll { border-top:1px solid var(--paper-2); padding-top:6px; }
|
|
270
|
+
section[aria-label="Quick links"] { border-top:1px solid var(--line); margin:0 -18px -18px; padding:12px 18px 16px; background:var(--paper-2); border-radius:0 0 12px 12px; }
|
|
271
|
+
.qlinks { display:flex; flex-direction:column; border-top:1px solid var(--paper-2); padding-top:6px; }
|
|
272
|
+
.qlinks a { display:flex; align-items:center; gap:10px; padding:9px 2px; font-size:13.5px; font-weight:500; color:var(--ink); text-decoration:none; border-radius:8px; }
|
|
273
|
+
.qlinks a:hover { background:var(--line-soft); }
|
|
274
|
+
.qlinks a:hover .t { text-decoration:underline; text-underline-offset:2px; }
|
|
275
|
+
.qlinks a .go { margin-left:auto; color:var(--ink-soft); font-size:12px; transition:transform 150ms ease; }
|
|
276
|
+
.qlinks a:hover .go, .qlinks a:focus-visible .go { transform:translateX(4px); color:var(--ink); }
|
|
277
|
+
.qlinks a:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
|
|
278
|
+
.qlinks small { color:var(--ink-soft); font-weight:400; }
|
|
279
|
+
.analytics-head { display:flex; align-items:center; gap:10px; }
|
|
280
|
+
.analytics-head h2 { font-size:15px; font-weight:600; margin:0; }
|
|
281
|
+
.analytics-controls { margin-left:auto; display:flex; align-items:center; gap:8px; }
|
|
282
|
+
.range-pill { display:inline-flex; align-items:center; gap:6px; font-size:12.5px; font-weight:500; background:var(--card); border:1px solid var(--line); border-radius:999px; padding:6px 12px; color:var(--ink); white-space:nowrap; }
|
|
283
|
+
.icon-btn { width:30px; height:30px; display:grid; place-items:center; border-radius:8px; border:1px solid transparent; background:transparent; color:var(--ink-soft); cursor:pointer; font-size:15px; }
|
|
284
|
+
.icon-btn:hover { background:var(--paper-2); color:var(--ink); }
|
|
285
|
+
.analytics-grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
|
286
|
+
.ana-card { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:12px 14px 6px; min-width:0; overflow:hidden; transition:border-color 150ms; }
|
|
287
|
+
.ana-card:hover { border-color:var(--underline); }
|
|
288
|
+
.ana-card.small { padding-bottom:2px; }
|
|
289
|
+
.ana-top { display:flex; align-items:center; gap:6px; }
|
|
290
|
+
.ana-title { font-size:12px; color:var(--ink-soft); font-weight:400; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
291
|
+
.ana-dots { margin-left:auto; color:var(--ink-faint); font-size:14px; letter-spacing:1px; user-select:none; }
|
|
292
|
+
.ana-value { display:flex; align-items:baseline; gap:8px; margin-top:2px; }
|
|
293
|
+
.ana-value b { font-size:22px; font-weight:600; line-height:1.2; font-variant-numeric:tabular-nums; }
|
|
294
|
+
.ana-card.small .ana-value b { font-size:20px; }
|
|
295
|
+
.ana-delta { font-size:12.5px; font-weight:600; font-variant-numeric:tabular-nums; }
|
|
296
|
+
.ana-delta.up { color:var(--green-ink); }
|
|
297
|
+
.ana-delta.down { color:var(--danger-ink); }
|
|
298
|
+
.ana-delta.flat { color:var(--ink-soft); font-weight:500; }
|
|
299
|
+
.ana-chart { position:relative; height:110px; margin:4px -6px 0; }
|
|
300
|
+
.ana-card.small .ana-chart { height:78px; }
|
|
301
|
+
.ana-chart svg { display:block; width:100%; height:100%; overflow:visible; }
|
|
302
|
+
.ana-y { position:absolute; right:0; top:0; bottom:0; display:flex; flex-direction:column; justify-content:space-between; font-size:10px; color:var(--ink-faint); padding:2px 0 14px; pointer-events:none; font-variant-numeric:tabular-nums; }
|
|
303
|
+
.chart-tip { position:absolute; display:none; z-index:5; pointer-events:none; background:var(--card); border:1px solid var(--line); border-radius:10px; box-shadow:var(--shadow-md); padding:8px 10px; min-width:150px; }
|
|
304
|
+
.chart-tip .tip-day { font-size:12px; font-weight:600; margin-bottom:4px; font-variant-numeric:tabular-nums; }
|
|
305
|
+
.chart-tip .tip-row { display:flex; align-items:center; gap:8px; font-size:12px; }
|
|
306
|
+
.chart-tip .dot { width:10px; height:10px; border-radius:999px; background:var(--chart-blue); flex-shrink:0; }
|
|
307
|
+
.chart-tip .tip-val { margin-left:auto; font-weight:600; font-variant-numeric:tabular-nums; padding-left:12px; }
|
|
308
|
+
.no-data { position:absolute; top:38%; left:50%; transform:translate(-50%,-50%); font-size:11px; color:var(--ink-soft); background:var(--card); border:1px solid var(--line); border-radius:999px; padding:3px 10px; white-space:nowrap; }
|
|
309
|
+
.ana-card.primary .ana-value b { font-size:26px; }
|
|
310
|
+
.ana-card.primary .ana-chart { height:96px; }
|
|
311
|
+
.ana-cap { font-size:11px; color:var(--ink-soft); margin-top:2px; }
|
|
312
|
+
.stat-list { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:4px 14px; }
|
|
313
|
+
.stat-list .srow { display:flex; align-items:baseline; gap:8px; padding:7px 0; border-top:1px solid var(--paper-2); font-size:13px; }
|
|
314
|
+
.stat-list .srow:first-child { border-top:0; }
|
|
315
|
+
.stat-list .sl { color:var(--ink-soft); }
|
|
316
|
+
.stat-list .sv { margin-left:auto; font-weight:600; font-variant-numeric:tabular-nums; }
|
|
317
|
+
.stat-list .sv.calm { color:var(--ink-soft); font-weight:500; }
|
|
318
|
+
.stat-list .sd { font-size:12px; color:var(--ink-soft); }
|
|
319
|
+
.tips { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:12px 14px; font-size:13px; display:flex; flex-direction:column; gap:8px; }
|
|
320
|
+
.tips b { font-size:13px; }
|
|
321
|
+
.tips ol { margin:0; padding-left:18px; display:flex; flex-direction:column; gap:4px; color:var(--body-copy); }
|
|
322
|
+
.tips small { color:var(--ink-soft); }
|
|
323
|
+
.tips-banner { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:4px 14px; font-size:13px; }
|
|
324
|
+
.tips-banner > summary { list-style:none; cursor:pointer; display:flex; align-items:center; gap:8px; padding:8px 0; user-select:none; }
|
|
325
|
+
.tips-banner > summary::-webkit-details-marker { display:none; }
|
|
326
|
+
.tips-banner > summary .chev { color:var(--ink-soft); font-size:10px; transition:transform 200ms; }
|
|
327
|
+
.tips-banner[open] > summary .chev { transform:rotate(90deg); }
|
|
328
|
+
.tips-banner ol { margin:4px 0 10px; padding-left:18px; display:flex; flex-direction:column; gap:4px; color:var(--body-copy); }
|
|
329
|
+
.gs-x { margin-left:auto; border:0; background:transparent; color:var(--ink-soft); cursor:pointer; font-size:12px; padding:2px 6px; border-radius:6px; }
|
|
330
|
+
.gs-x:hover { background:var(--paper-2); color:var(--ink); }
|
|
331
|
+
.stat-row { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; }
|
|
332
|
+
.stat-mini { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:12px 14px; display:flex; flex-direction:column; gap:2px; }
|
|
333
|
+
.stat-mini b { font-size:18px; font-weight:600; font-variant-numeric:tabular-nums; }
|
|
334
|
+
@media (max-width:640px) { .analytics-grid { grid-template-columns:1fr 1fr; } .ana-card.span2 { grid-column:1 / -1; } }
|
|
335
|
+
@media (max-width:520px) { .stat-row { grid-template-columns:1fr 1fr; } }
|
|
336
|
+
${docsComponentsCss}
|
|
337
|
+
</style>
|
|
338
|
+
</head>
|
|
339
|
+
<body>
|
|
340
|
+
<div class="shell" id="shell">
|
|
341
|
+
<aside class="sidebar">
|
|
342
|
+
<div class="sb-header">
|
|
343
|
+
<a class="sb-logo" aria-label="pool-anything home" href="/"><img src="/logo.png" alt="pool-anything" width="36" height="36" /></a>
|
|
344
|
+
<div class="sb-acct" title="Local account"><span>Local account</span></div>
|
|
345
|
+
</div>
|
|
346
|
+
<nav class="sb-nav">
|
|
347
|
+
<a class="mi" href="/"><span class="ic">${ICONS.home}</span><span class="lbl">Home</span></a>
|
|
348
|
+
<a class="mi" href="/pools"><span class="ic">${ICONS.pools}</span><span class="lbl">Pools</span></a>
|
|
349
|
+
<a class="mi" href="/keys"><span class="ic">${ICONS.keys}</span><span class="lbl">API key manager</span></a>
|
|
350
|
+
<a class="mi" href="/playground"><span class="ic">${ICONS.playground}</span><span class="lbl">Playground</span></a>
|
|
351
|
+
<a class="mi" href="/analytics"><span class="ic">${ICONS.analytics}</span><span class="lbl">Analytics</span></a>
|
|
352
|
+
<a class="mi" href="${DOCS_URL}" target="_blank" rel="noopener" title="Opens external docs in a new tab"><span class="ic">${ICONS.docs}</span><span class="lbl">Docs</span><span class="ext">${ICONS.external}</span></a>
|
|
353
|
+
</nav>
|
|
354
|
+
<div class="sb-footer"><button class="collapse-btn" id="collapseBtn" type="button" data-sidebar="trigger" aria-expanded="true" aria-label="Collapse sidebar"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M21.25 6.72v10.56a2.97 2.97 0 0 1-2.97 2.97H5.72a2.97 2.97 0 0 1-2.97-2.97V6.72a2.97 2.97 0 0 1 2.97-2.97h12.56a2.97 2.97 0 0 1 2.97 2.97"></path><path d="M6.25 7.25v9.5"></path></svg></button></div>
|
|
355
|
+
</aside>
|
|
356
|
+
<script>
|
|
357
|
+
(function () {
|
|
358
|
+
try {
|
|
359
|
+
var sh = document.getElementById('shell');
|
|
360
|
+
if (localStorage.getItem('sb-collapsed') !== '1') return;
|
|
361
|
+
sh.classList.add('collapsed');
|
|
362
|
+
var cb = document.getElementById('collapseBtn');
|
|
363
|
+
cb.setAttribute('aria-expanded', 'false');
|
|
364
|
+
cb.setAttribute('aria-label', 'Expand sidebar');
|
|
365
|
+
var sb = sh.querySelector('.sidebar');
|
|
366
|
+
if (sb && sb.matches(':hover')) sh.classList.add('peeking');
|
|
367
|
+
} catch (e) {}
|
|
368
|
+
})();
|
|
369
|
+
</script>
|
|
370
|
+
<div class="content">
|
|
371
|
+
<header>
|
|
372
|
+
<div></div>
|
|
373
|
+
<div class="spacer">
|
|
374
|
+
<a href="${DOCS_URL}" target="_blank" rel="noopener">Docs</a>
|
|
375
|
+
</div>
|
|
376
|
+
</header>
|
|
377
|
+
<main>
|
|
378
|
+
<div class="wrap">
|
|
379
|
+
<div class="hero-row"><img class="hero-logo" src="/logo.png" alt="pool-anything logo" width="40" height="40" /><h1>What do you want to pool</h1></div>
|
|
380
|
+
<div class="search-card">
|
|
381
|
+
<div class="search-box">
|
|
382
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"></path></svg>
|
|
383
|
+
<input id="search" aria-label="Search" placeholder="Search" autocomplete="off" />
|
|
384
|
+
<span class="kbd"><kbd>⌘</kbd><kbd>K</kbd></span>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
<div class="home-grid" id="homeGrid">
|
|
388
|
+
<div id="homeLeft" style="width:100%;display:flex;flex-direction:column;gap:20px;min-width:0">
|
|
389
|
+
<section id="connectedSection" aria-label="Connected providers">
|
|
390
|
+
<p class="sec-label">Connected providers</p>
|
|
391
|
+
<div class="conn-grid" id="connected"></div>
|
|
392
|
+
</section>
|
|
393
|
+
<details class="browse" id="browseAll">
|
|
394
|
+
<summary><span class="chev">▶</span>Browse all providers <span class="count" id="browseCount"></span></summary>
|
|
395
|
+
<div id="browseCats"></div>
|
|
396
|
+
</details>
|
|
397
|
+
<section aria-label="Quick links">
|
|
398
|
+
<p class="sec-label">Quick links</p>
|
|
399
|
+
<div class="qlinks">
|
|
400
|
+
<a href="/pools"><span class="t">Pools</span> <small>usage per pool</small><span class="go">→</span></a>
|
|
401
|
+
<a href="/keys"><span class="t">API key manager</span> <small>add · view · rotate</small><span class="go">→</span></a>
|
|
402
|
+
<a href="/playground"><span class="t">Playground</span> <small>try a pooled key</small><span class="go">→</span></a>
|
|
403
|
+
</div>
|
|
404
|
+
</section>
|
|
405
|
+
</div>
|
|
406
|
+
<div class="analytics" id="analytics">
|
|
407
|
+
<div class="analytics-head"><h2>Analytics</h2><div class="analytics-controls"><span class="range-pill">◷ Last 14 days</span><button class="icon-btn" id="gsReopen" type="button" title="Show getting started" aria-label="Show getting started" hidden>?</button><button class="icon-btn" id="anaRefresh" type="button" title="Refresh analytics" aria-label="Refresh analytics">↻</button></div></div>
|
|
408
|
+
<details class="tips-banner" id="getStarted" hidden>
|
|
409
|
+
<summary><span class="chev">▶</span><b>Getting started</b><button class="gs-x" id="gsHide" type="button" title="Dismiss" aria-label="Dismiss getting started">✕</button></summary>
|
|
410
|
+
<ol>
|
|
411
|
+
<li>Gather keys for a provider above — one per free-tier account.</li>
|
|
412
|
+
<li>Point your client at <small>/api/pools/:id/proxy</small> and send a request.</li>
|
|
413
|
+
<li>Come back here: usage per key shows up automatically.</li>
|
|
414
|
+
</ol>
|
|
415
|
+
</details>
|
|
416
|
+
<div class="analytics-grid">
|
|
417
|
+
<div class="ana-card primary"><div class="ana-top"><span class="ana-title">Total requests</span><span class="ana-dots">…</span></div><div class="ana-value"><b id="statRequests">—</b><span class="ana-delta flat" id="statDeltaReq"></span></div><div class="ana-chart"><svg id="chartRequests" viewBox="0 0 600 120" preserveAspectRatio="none" role="img" aria-label="Total requests"></svg><div class="ana-y" id="yRequests"></div><div class="chart-tip" id="tipRequests"></div></div><div class="ana-cap">Requests served through your pools</div></div>
|
|
418
|
+
<div class="ana-card primary"><div class="ana-top"><span class="ana-title">Keys pooled</span><span class="ana-dots">…</span></div><div class="ana-value"><b id="statKeys">—</b></div><div class="ana-chart"><svg id="chartKeys" viewBox="0 0 600 120" preserveAspectRatio="none" role="img" aria-label="Keys pooled"></svg><div class="chart-tip" id="tipKeys"></div></div><div class="ana-cap">API keys rotating across pools</div></div>
|
|
419
|
+
</div>
|
|
420
|
+
<div class="stat-list" id="statList">
|
|
421
|
+
<div class="srow"><span class="sl">Tokens tracked</span><span class="ana-delta flat" id="statDeltaTok"></span><span class="sv" id="statTokens">—</span></div>
|
|
422
|
+
<div class="srow"><span class="sl">Pools</span><span class="sv" id="statPools">—</span></div>
|
|
423
|
+
<div class="srow"><span class="sl">Keys cooling</span><span class="sd" id="statCoolingNote"></span><span class="sv" id="statCooling">—</span></div>
|
|
424
|
+
<div class="srow"><span class="sl">Avg tokens / req</span><span class="sv" id="statAvg">—</span></div>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
</main>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
<script>
|
|
433
|
+
const shell = document.getElementById('shell');
|
|
434
|
+
document.querySelectorAll('.sb-nav .mi').forEach(a => {
|
|
435
|
+
if (a.getAttribute('href') === location.pathname) a.classList.add('active');
|
|
436
|
+
});
|
|
437
|
+
const collapseBtn = document.getElementById('collapseBtn');
|
|
438
|
+
try { if (localStorage.getItem('sb-collapsed') === '1') { shell.classList.add('collapsed'); collapseBtn.setAttribute('aria-expanded', 'false'); collapseBtn.setAttribute('aria-label', 'Expand sidebar'); } } catch {}
|
|
439
|
+
const saveSb = () => { try { localStorage.setItem('sb-collapsed', shell.classList.contains('collapsed') ? '1' : '0'); } catch {} };
|
|
440
|
+
const finishCollapse = (apply) => {
|
|
441
|
+
shell.classList.add('closing');
|
|
442
|
+
clearTimeout(finishCollapse.t);
|
|
443
|
+
finishCollapse.t = setTimeout(() => {
|
|
444
|
+
apply();
|
|
445
|
+
shell.classList.remove('closing');
|
|
446
|
+
saveSb();
|
|
447
|
+
}, 160);
|
|
448
|
+
};
|
|
449
|
+
collapseBtn.addEventListener('click', () => {
|
|
450
|
+
if (shell.classList.contains('collapsed')) {
|
|
451
|
+
shell.classList.remove('collapsed');
|
|
452
|
+
shell.classList.remove('peeking');
|
|
453
|
+
collapseBtn.setAttribute('aria-expanded', 'true');
|
|
454
|
+
collapseBtn.setAttribute('aria-label', 'Collapse sidebar');
|
|
455
|
+
saveSb();
|
|
456
|
+
} else {
|
|
457
|
+
finishCollapse(() => {
|
|
458
|
+
shell.classList.add('collapsed');
|
|
459
|
+
shell.classList.remove('peeking');
|
|
460
|
+
collapseBtn.setAttribute('aria-expanded', 'false');
|
|
461
|
+
collapseBtn.setAttribute('aria-label', 'Expand sidebar');
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
const sidebar = document.querySelector('.sidebar');
|
|
466
|
+
if (shell.classList.contains('collapsed') && sidebar.matches(':hover')) shell.classList.add('peeking');
|
|
467
|
+
let peekTimer;
|
|
468
|
+
sidebar.addEventListener('mouseenter', () => {
|
|
469
|
+
if (!shell.classList.contains('collapsed')) return;
|
|
470
|
+
clearTimeout(peekTimer);
|
|
471
|
+
clearTimeout(finishCollapse.t);
|
|
472
|
+
shell.classList.remove('closing');
|
|
473
|
+
peekTimer = setTimeout(() => shell.classList.add('peeking'), 150);
|
|
474
|
+
});
|
|
475
|
+
sidebar.addEventListener('mouseleave', () => {
|
|
476
|
+
clearTimeout(peekTimer);
|
|
477
|
+
if (!shell.classList.contains('peeking')) return;
|
|
478
|
+
finishCollapse(() => shell.classList.remove('peeking'));
|
|
479
|
+
});
|
|
480
|
+
const input = document.getElementById('search');
|
|
481
|
+
const connBox = document.getElementById('connected');
|
|
482
|
+
const browseAll = document.getElementById('browseAll');
|
|
483
|
+
const browseCats = document.getElementById('browseCats');
|
|
484
|
+
const browseCount = document.getElementById('browseCount');
|
|
485
|
+
let providers = [];
|
|
486
|
+
const CATS = [
|
|
487
|
+
['AI Models', ['groq','openrouter','gemini','mistral','cohere','fireworks','huggingface','together','replicate','openai','anthropic','perplexity','anyscale']],
|
|
488
|
+
['Voice & Speech', ['elevenlabs','deepgram','cartesia']],
|
|
489
|
+
['Search & Data Retrieval', ['tavily','brave-search','serper','scrapingbee']],
|
|
490
|
+
['Email & Messaging', ['resend','sendgrid','mailgun','postmark','twilio']],
|
|
491
|
+
['Business / Maps / Weather', ['finnhub','mapbox','googlemaps','openweather']],
|
|
492
|
+
['Infrastructure / Database', ['supabase','neon','upstash','turso','appwrite','cloudflare']],
|
|
493
|
+
];
|
|
494
|
+
const CUSTOM_ID = 'custom';
|
|
495
|
+
let connectedIds = new Set();
|
|
496
|
+
let connStats = {};
|
|
497
|
+
${clientCoreJs}
|
|
498
|
+
function provById(id) { return providers.find(p => p.id === id); }
|
|
499
|
+
function matches(p, f) {
|
|
500
|
+
if (!f) return true;
|
|
501
|
+
return p.name.toLowerCase().includes(f) || p.id.includes(f);
|
|
502
|
+
}
|
|
503
|
+
function logoImg(p, size) {
|
|
504
|
+
if (!p.logoFile) return null;
|
|
505
|
+
const img = document.createElement('img'); img.alt = '';
|
|
506
|
+
logoSrc(img, p.logoFile);
|
|
507
|
+
return img;
|
|
508
|
+
}
|
|
509
|
+
async function loadProviders() {
|
|
510
|
+
providers = await j(await fetch('/api/providers'));
|
|
511
|
+
// Connected = providers with pools holding keys (+ per-provider usage for status).
|
|
512
|
+
try {
|
|
513
|
+
const pools = await j(await fetch('/api/pools'));
|
|
514
|
+
const withKeys = [];
|
|
515
|
+
for (const pl of (Array.isArray(pools) ? pools : [])) {
|
|
516
|
+
try {
|
|
517
|
+
const s = await j(await fetch('/api/pools/' + pl.id));
|
|
518
|
+
if (s && !s.error && (s.keys || 0) > 0) withKeys.push(s);
|
|
519
|
+
} catch {}
|
|
520
|
+
}
|
|
521
|
+
connectedIds = new Set(withKeys.map(s => s.provider));
|
|
522
|
+
connStats = {};
|
|
523
|
+
for (const st of withKeys) {
|
|
524
|
+
const cur = connStats[st.provider] || { keys: 0, used: 0, quota: null };
|
|
525
|
+
cur.keys += st.keys || 0;
|
|
526
|
+
cur.used += (st.usedInWindow ?? st.used) || 0;
|
|
527
|
+
if (st.quota !== null && st.quota !== undefined) cur.quota = (cur.quota || 0) + st.quota * (st.keys || 0);
|
|
528
|
+
connStats[st.provider] = cur;
|
|
529
|
+
}
|
|
530
|
+
} catch { connectedIds = new Set(); connStats = {}; }
|
|
531
|
+
// Browse stays collapsed by default; only expand on explicit user action (caret click or typing in search).
|
|
532
|
+
browseAll.open = false;
|
|
533
|
+
renderHome('');
|
|
534
|
+
// Exception: /#browseAll deep-link (e.g. provider-page "← all providers" crumb) opens the canonical list.
|
|
535
|
+
if (location.hash === '#browseAll') {
|
|
536
|
+
browseAll.open = true;
|
|
537
|
+
setTimeout(() => document.getElementById('browseAll').scrollIntoView({ block: 'start' }), 100);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function cardButton(p, i, large) {
|
|
541
|
+
const b = document.createElement('button');
|
|
542
|
+
b.className = large ? 'prov-lg' : 'prov'; b.type = 'button'; b.title = p.name;
|
|
543
|
+
b.style.animationDelay = Math.min(i * 35, 350) + 'ms';
|
|
544
|
+
if (large) {
|
|
545
|
+
const top = document.createElement('div'); top.className = 'top';
|
|
546
|
+
const im = logoImg(p); if (im) top.appendChild(im);
|
|
547
|
+
const nm = document.createElement('span'); nm.className = 'nm'; nm.textContent = p.name; top.appendChild(nm);
|
|
548
|
+
b.appendChild(top);
|
|
549
|
+
const st = connStats[p.id] || { keys: 0, used: 0, quota: null };
|
|
550
|
+
const meta = document.createElement('span'); meta.className = 'meta';
|
|
551
|
+
meta.textContent = st.keys + (st.keys === 1 ? ' key' : ' keys') + (st.quota ? ' · ' + st.used + ' / ' + st.quota + ' used' : st.used ? ' · ' + st.used + ' tracked' : '');
|
|
552
|
+
b.appendChild(meta);
|
|
553
|
+
if (st.quota) {
|
|
554
|
+
const m = document.createElement('div'); m.className = 'meter';
|
|
555
|
+
const fill = document.createElement('i');
|
|
556
|
+
fill.style.width = Math.min(100, Math.round(st.used / st.quota * 100)) + '%';
|
|
557
|
+
m.appendChild(fill); b.appendChild(m);
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
const im = logoImg(p); if (im) b.appendChild(im);
|
|
561
|
+
const n = document.createElement('span'); n.textContent = p.name; b.appendChild(n);
|
|
562
|
+
}
|
|
563
|
+
b.onclick = () => { location.href = '/provider/' + p.id; };
|
|
564
|
+
return b;
|
|
565
|
+
}
|
|
566
|
+
function renderHome(f) {
|
|
567
|
+
f = (f || '').toLowerCase();
|
|
568
|
+
// Connected section.
|
|
569
|
+
connBox.innerHTML = '';
|
|
570
|
+
const connList = providers.filter(p => connectedIds.has(p.id) && matches(p, f));
|
|
571
|
+
if (connList.length === 0) {
|
|
572
|
+
const d = document.createElement('div'); d.className = 'conn-empty';
|
|
573
|
+
d.innerHTML = f ? 'No connected providers match.' : '<b>No providers connected yet.</b> Pick one below to pool your first keys.';
|
|
574
|
+
connBox.appendChild(d);
|
|
575
|
+
} else {
|
|
576
|
+
connList.forEach((p, i) => connBox.appendChild(cardButton(p, i, true)));
|
|
577
|
+
}
|
|
578
|
+
// Browse-all categories (custom rendered as its own tile group).
|
|
579
|
+
browseCats.innerHTML = '';
|
|
580
|
+
let total = 0, shown = 0;
|
|
581
|
+
const groups = CATS.map(([label, ids]) => [label, ids.map(provById).filter(Boolean)]);
|
|
582
|
+
const custom = provById(CUSTOM_ID);
|
|
583
|
+
if (custom) groups.push(['Custom', [custom]]);
|
|
584
|
+
const known = new Set(); groups.forEach(([, ps]) => ps.forEach(p => known.add(p.id)));
|
|
585
|
+
const other = providers.filter(p => !known.has(p.id));
|
|
586
|
+
if (other.length) groups.push(['Other', other]);
|
|
587
|
+
groups.forEach(([label, ps]) => {
|
|
588
|
+
const vis = ps.filter(p => matches(p, f));
|
|
589
|
+
total += ps.length; shown += vis.length;
|
|
590
|
+
if (f && vis.length === 0) return;
|
|
591
|
+
const det = document.createElement('details'); det.className = 'cat';
|
|
592
|
+
// Collapsed by default; expand only when filtering so matches are visible, otherwise wait for explicit caret click.
|
|
593
|
+
det.open = Boolean(f);
|
|
594
|
+
const sum = document.createElement('summary');
|
|
595
|
+
const chev = document.createElement('span'); chev.className = 'chev'; chev.textContent = '▶';
|
|
596
|
+
sum.appendChild(chev);
|
|
597
|
+
const lab = document.createElement('span'); lab.className = 'sec-label'; lab.style.margin = '0'; lab.textContent = label;
|
|
598
|
+
sum.appendChild(lab);
|
|
599
|
+
const cnt = document.createElement('span'); cnt.className = 'count'; cnt.style.cssText = 'font-size:11px;color:var(--ink-soft)';
|
|
600
|
+
cnt.textContent = vis.length + ' / ' + ps.length; sum.appendChild(cnt);
|
|
601
|
+
det.appendChild(sum);
|
|
602
|
+
if (vis.length === 0) {
|
|
603
|
+
const e = document.createElement('div'); e.className = 'cat-empty'; e.textContent = 'Nothing here yet.'; det.appendChild(e);
|
|
604
|
+
} else {
|
|
605
|
+
const grid = document.createElement('div'); grid.className = 'cat-grid';
|
|
606
|
+
vis.forEach((p, i) => grid.appendChild(cardButton(p, i, false)));
|
|
607
|
+
det.appendChild(grid);
|
|
608
|
+
}
|
|
609
|
+
browseCats.appendChild(det);
|
|
610
|
+
});
|
|
611
|
+
browseCount.textContent = f ? shown + ' of ' + total : total + ' providers';
|
|
612
|
+
}
|
|
613
|
+
document.addEventListener('keydown', (e) => {
|
|
614
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); input.focus(); }
|
|
615
|
+
});
|
|
616
|
+
let t;
|
|
617
|
+
input.addEventListener('input', () => {
|
|
618
|
+
clearTimeout(t);
|
|
619
|
+
t = setTimeout(() => {
|
|
620
|
+
const v = input.value.trim();
|
|
621
|
+
if (v) browseAll.open = true; // typing expands browse so matches are visible
|
|
622
|
+
renderHome(v);
|
|
623
|
+
}, 120);
|
|
624
|
+
});
|
|
625
|
+
input.addEventListener('keydown', (e) => {
|
|
626
|
+
if (e.key === 'Enter') {
|
|
627
|
+
const first = document.querySelector('#connected .prov-lg, #browseCats .prov');
|
|
628
|
+
if (first) first.click();
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
${clientChartsJs}
|
|
632
|
+
async function loadAnalytics() {
|
|
633
|
+
try {
|
|
634
|
+
const a = await j(await fetch('/api/analytics'));
|
|
635
|
+
const grid = document.getElementById('homeGrid');
|
|
636
|
+
const gs = document.getElementById('getStarted');
|
|
637
|
+
const gsReopen = document.getElementById('gsReopen');
|
|
638
|
+
const req0 = (a.requests || 0) === 0;
|
|
639
|
+
const dismissed = (() => { try { return localStorage.getItem('gs-dismissed') === '1'; } catch { return false; } })();
|
|
640
|
+
// Zero usage always shows it (fresh workspace recovers automatically);
|
|
641
|
+
// otherwise a collapsed banner under 10 requests, hidden beyond that.
|
|
642
|
+
// Dismissal persists in localStorage; the ? control reopens it anytime.
|
|
643
|
+
if (gs) {
|
|
644
|
+
if (req0) { gs.hidden = false; gs.open = true; }
|
|
645
|
+
else if (dismissed || (a.requests || 0) >= 10) gs.hidden = true;
|
|
646
|
+
else { gs.hidden = false; gs.open = false; }
|
|
647
|
+
}
|
|
648
|
+
if (gsReopen) {
|
|
649
|
+
gsReopen.hidden = !(!req0 && gs.hidden);
|
|
650
|
+
gsReopen.onclick = () => {
|
|
651
|
+
try { localStorage.removeItem('gs-dismissed'); } catch {}
|
|
652
|
+
gs.hidden = false; gs.open = true;
|
|
653
|
+
gsReopen.hidden = true;
|
|
654
|
+
gs.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
const gsHide = document.getElementById('gsHide');
|
|
658
|
+
if (gsHide) gsHide.onclick = (e) => {
|
|
659
|
+
e.preventDefault();
|
|
660
|
+
try { localStorage.setItem('gs-dismissed', '1'); } catch {}
|
|
661
|
+
document.getElementById('getStarted').hidden = true;
|
|
662
|
+
};
|
|
663
|
+
if (grid) grid.style.display = '';
|
|
664
|
+
const BLUE = '#3b82f6';
|
|
665
|
+
document.getElementById('statRequests').textContent = fmtCompact(a.requests);
|
|
666
|
+
document.getElementById('statRequests').title = String(a.requests);
|
|
667
|
+
const st = document.getElementById('statTokens');
|
|
668
|
+
st.textContent = fmtCompact(a.tokens); st.title = String(a.tokens);
|
|
669
|
+
document.getElementById('statPools').textContent = String(a.pools);
|
|
670
|
+
document.getElementById('statKeys').textContent = String(a.keys);
|
|
671
|
+
document.getElementById('statKeys').title = String(a.keys);
|
|
672
|
+
const cool = document.getElementById('statCooling');
|
|
673
|
+
const cn = document.getElementById('statCoolingNote');
|
|
674
|
+
if ((a.cooling || 0) > 0) {
|
|
675
|
+
cool.textContent = String(a.cooling); cool.className = 'sv';
|
|
676
|
+
cn.textContent = 'in backoff'; cn.className = 'sd';
|
|
677
|
+
} else {
|
|
678
|
+
cool.textContent = '—'; cool.className = 'sv calm'; cool.title = '0';
|
|
679
|
+
cn.textContent = 'all healthy'; cn.className = 'sd';
|
|
680
|
+
}
|
|
681
|
+
document.getElementById('statAvg').textContent = String(a.avgTokens || 0);
|
|
682
|
+
setDelta('statDeltaReq', a.deltaRequestsPct, 'requests, last 7 days vs prior 7 days');
|
|
683
|
+
setDelta('statDeltaTok', a.deltaTokensPct, 'tokens, last 7 days vs prior 7 days');
|
|
684
|
+
const days = (a.series || []).map(p => p.day);
|
|
685
|
+
renderLine('chartRequests', 'tipRequests', 'yRequests', days, (a.series || []).map(p => p.requests), 'Total requests', BLUE);
|
|
686
|
+
renderLine('chartKeys', 'tipKeys', null, (a.keysSeries || []).map(p => p.day), (a.keysSeries || []).map(p => p.count), 'Keys pooled', BLUE);
|
|
687
|
+
} catch (e) { /* analytics is best-effort; search still works */ }
|
|
688
|
+
}
|
|
689
|
+
const anaRefresh = document.getElementById('anaRefresh');
|
|
690
|
+
if (anaRefresh) anaRefresh.onclick = () => loadAnalytics();
|
|
691
|
+
// Refresh when returning from a provider page (covers back-nav and tab switch) so newly gathered keys show up.
|
|
692
|
+
window.addEventListener('pageshow', () => setTimeout(loadAnalytics, 200));
|
|
693
|
+
window.addEventListener('focus', () => setTimeout(loadAnalytics, 200));
|
|
694
|
+
// Deep-link from provider pages: /#browseAll opens the canonical provider list.
|
|
695
|
+
window.addEventListener('hashchange', () => {
|
|
696
|
+
if (location.hash === '#browseAll' && browseAll) {
|
|
697
|
+
browseAll.open = true;
|
|
698
|
+
setTimeout(() => document.getElementById('browseAll').scrollIntoView({ block: 'start' }), 100);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
loadProviders();
|
|
702
|
+
loadAnalytics();
|
|
703
|
+
</script>
|
|
704
|
+
</body>
|
|
705
|
+
</html>`;
|
|
706
|
+
export function keysPage() {
|
|
707
|
+
return `<!doctype html>
|
|
708
|
+
<html lang="en"><head><meta charset="utf-8"/>
|
|
709
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
710
|
+
<title>API key manager — pool-anything</title>
|
|
711
|
+
<link rel="icon" type="image/png" href="/logo.png" />
|
|
712
|
+
<style>
|
|
713
|
+
*{box-sizing:border-box}body{margin:0;font-family:var(--font-sans);background:var(--paper-2);color:var(--ink)}
|
|
714
|
+
${shellCss}
|
|
715
|
+
${sharedLayoutCss}
|
|
716
|
+
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px}
|
|
717
|
+
.chead{display:flex;align-items:center;gap:10px}.chead img{width:24px;height:24px}
|
|
718
|
+
.chead b{font-size:15px}.pill{margin-left:auto;font-size:11px;font-weight:600;background:var(--paper-2);border-radius:999px;padding:3px 10px;white-space:nowrap}
|
|
719
|
+
.row{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
|
|
720
|
+
input,select{border:1px solid var(--line);border-radius:8px;min-height:34px;padding:0 10px;font-size:14px;flex:1;min-width:140px}
|
|
721
|
+
button{border:1px solid var(--ink);background:var(--ink);color:var(--card);border-radius:8px;min-height:34px;padding:0 12px;font-size:14px;cursor:pointer}
|
|
722
|
+
button.ghost{background:var(--card);color:var(--ink)}button.sm{min-height:30px;font-size:12px}
|
|
723
|
+
ul{margin:10px 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:6px}
|
|
724
|
+
li{background:var(--paper-2);border-radius:8px;padding:8px 10px;font-size:13px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
725
|
+
li span{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:120px}
|
|
726
|
+
li small{color:var(--ink-soft)}.err{color:var(--danger-ink);font-size:13px;min-height:18px}
|
|
727
|
+
@media (max-width:520px){.row>*{flex:1 1 100%}}
|
|
728
|
+
${docsComponentsCss}
|
|
729
|
+
</style></head><body><div class="shell" id="shell">${shellNav("/keys")}<div class="content">${sharedHeader}<main>
|
|
730
|
+
<div class="wrap">
|
|
731
|
+
<div class="page-head"><a href="/"><img src="/logo.png" alt="pool-anything" width="28" height="28"/></a><h1>API key manager</h1></div>
|
|
732
|
+
<a class="back" href="/">← pool search</a>
|
|
733
|
+
<div class="err" id="err"></div>
|
|
734
|
+
<div class="card"><div class="row">${ddSelect("prov", "Provider", "", "dd-block")}<input id="pname" placeholder="Pool name" aria-label="Pool name"/></div><div class="row"><input id="pbase" placeholder="base_url override (custom / self-host)" aria-label="Base URL override"/><input id="phead" placeholder="key header (default X-API-Key)" aria-label="Key header"/><input id="pprefix" placeholder="key prefix, e.g. Bearer " aria-label="Key prefix"/></div><div class="row"><button id="create">New pool</button></div></div>
|
|
735
|
+
<div id="pools" style="display:flex;flex-direction:column;gap:12px"></div>
|
|
736
|
+
</div>
|
|
737
|
+
</main><script>
|
|
738
|
+
let provs=[];
|
|
739
|
+
${clientCoreJs}
|
|
740
|
+
${clientDropdownJs}
|
|
741
|
+
async function init(){
|
|
742
|
+
initDropdowns();
|
|
743
|
+
provs=await j(await fetch('/api/providers'));
|
|
744
|
+
document.getElementById('prov').innerHTML=provs.map(p=>'<option value="'+p.id+'">'+p.name+'</option>').join('');
|
|
745
|
+
ddSync('prov');
|
|
746
|
+
refresh();
|
|
747
|
+
}
|
|
748
|
+
async function refresh(){
|
|
749
|
+
err('');
|
|
750
|
+
const pools=await j(await fetch('/api/pools'));
|
|
751
|
+
const box=document.getElementById('pools');box.innerHTML='';
|
|
752
|
+
for(const p of pools){
|
|
753
|
+
const prov=provs.find(x=>x.id===p.provider)||{name:p.provider,quota:'',logoFile:p.provider+'.svg'};
|
|
754
|
+
const [ks,us]=await Promise.all([j(await fetch('/api/pools/'+p.id+'/keys')),j(await fetch('/api/pools/'+p.id+'/usage'))]);
|
|
755
|
+
const card=document.createElement('div');card.className='card';
|
|
756
|
+
const fields=(prov.keyFields&&prov.keyFields.length?prov.keyFields:['api_key']);
|
|
757
|
+
const multi=!(fields.length===1&&(fields[0]==='api_key'||fields[0]==='apiToken'));
|
|
758
|
+
const rowEl=document.createElement('div');rowEl.className='row';
|
|
759
|
+
const inputs=fields.map(f=>{const i=document.createElement('input');i.type='password';i.autocomplete='off';i.placeholder='key '+(ks.length+1)+' — '+f;i.setAttribute('aria-label',f);rowEl.appendChild(i);return i;});
|
|
760
|
+
const gatherBtn=document.createElement('button');gatherBtn.textContent='Gather';rowEl.appendChild(gatherBtn);
|
|
761
|
+
const delBtn=document.createElement('button');delBtn.textContent='Delete pool';delBtn.className='ghost sm';rowEl.appendChild(delBtn);
|
|
762
|
+
card.innerHTML='<div class="chead"><img alt=""/><b></b><span class="pill"></span></div><ul></ul>';
|
|
763
|
+
card.appendChild(rowEl);
|
|
764
|
+
logoSrc(card.querySelector('img'),prov.logoFile);
|
|
765
|
+
card.querySelector('b').textContent=prov.name+' · #'+p.id;
|
|
766
|
+
card.querySelector('.pill').textContent='used '+((us.quota?(us.usedInWindow ?? us.used):us.used)||0)+(us.quota?' / '+(us.quota*ks.length):'');
|
|
767
|
+
const ul=card.querySelector('ul');
|
|
768
|
+
ks.forEach(k=>{
|
|
769
|
+
const li=document.createElement('li');
|
|
770
|
+
li.innerHTML='<span></span><small></small>';
|
|
771
|
+
li.querySelector('span').textContent=k.label+' · '+k.masked;
|
|
772
|
+
li.querySelector('small').textContent='used '+((us.perKey||[]).find(x=>x.id===k.id)?.used||0);
|
|
773
|
+
const v=document.createElement('button');v.textContent='View';v.className='ghost sm';
|
|
774
|
+
v.onclick=async()=>{
|
|
775
|
+
if(v.dataset.open){
|
|
776
|
+
delete v.dataset.open;v.textContent='View';
|
|
777
|
+
li.querySelector('span').textContent=k.label+' · '+k.masked;return;
|
|
778
|
+
}
|
|
779
|
+
const full=await j(await fetch('/api/pools/'+p.id+'/keys/'+k.id));
|
|
780
|
+
if(full.error){err(full.error);return;}
|
|
781
|
+
v.dataset.open='1';v.textContent='Hide';
|
|
782
|
+
li.querySelector('span').textContent=k.label+' · '+full.api_key+(full.credentials?' · '+Object.entries(full.credentials).map(([a,b])=>a+'='+b).join(' '):'');
|
|
783
|
+
};
|
|
784
|
+
const e=document.createElement('button');e.textContent='Edit';e.className='ghost sm';
|
|
785
|
+
e.onclick=async()=>{
|
|
786
|
+
const full=await j(await fetch('/api/pools/'+p.id+'/keys/'+k.id));
|
|
787
|
+
if(full.error){err(full.error);return;}
|
|
788
|
+
li.innerHTML='';
|
|
789
|
+
const f=document.createElement('div');f.style.cssText='display:flex;gap:6px;flex:1;flex-wrap:wrap';
|
|
790
|
+
f.innerHTML='<input value="'+esc(full.label)+'" style="flex:1;min-width:80px"/><input value="'+esc(full.info||'')+'" placeholder="info" style="flex:1;min-width:80px"/>';
|
|
791
|
+
const credFields=(full.credentials&&Object.keys(full.credentials).length?Object.keys(full.credentials):(k.credentials&&Object.keys(k.credentials).length?Object.keys(k.credentials):['api_key']));
|
|
792
|
+
const kins=credFields.map(cf=>{const i=document.createElement('input');i.type='password';i.placeholder=cf;i.value=(full.credentials&&full.credentials[cf])||(cf==='api_key'?full.api_key:'');i.style.cssText='flex:2;min-width:110px';f.appendChild(i);return i;});
|
|
793
|
+
const [il,ii]=f.querySelectorAll('input');
|
|
794
|
+
const sv=document.createElement('button');sv.textContent='Save';sv.className='sm';
|
|
795
|
+
sv.onclick=async()=>{
|
|
796
|
+
const patch={label:il.value,info:ii.value};
|
|
797
|
+
if(credFields.length===1&&credFields[0]==='api_key')patch.api_key=kins[0].value;
|
|
798
|
+
else patch.credentials=Object.fromEntries(credFields.map((cf,i)=>[cf,kins[i].value]));
|
|
799
|
+
const r=await j(await fetch('/api/pools/'+p.id+'/keys/'+k.id,{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify(patch)}));
|
|
800
|
+
if(r.error){err(r.error);return;}
|
|
801
|
+
refresh();
|
|
802
|
+
};
|
|
803
|
+
const c=document.createElement('button');c.textContent='Cancel';c.className='ghost sm';
|
|
804
|
+
c.onclick=()=>refresh();
|
|
805
|
+
li.appendChild(f);li.appendChild(sv);li.appendChild(c);
|
|
806
|
+
};
|
|
807
|
+
const d=document.createElement('button');d.textContent='Remove';d.className='ghost sm';
|
|
808
|
+
d.onclick=async()=>{await fetch('/api/pools/'+p.id+'/keys/'+k.id,{method:'DELETE'});refresh();};
|
|
809
|
+
li.appendChild(v);li.appendChild(e);li.appendChild(d);ul.appendChild(li);
|
|
810
|
+
});
|
|
811
|
+
const del=delBtn,gather=gatherBtn;
|
|
812
|
+
gather.onclick=async()=>{
|
|
813
|
+
err('');
|
|
814
|
+
const vals=inputs.map(i=>i.value.trim());
|
|
815
|
+
if(vals.some(v=>!v)){err('Paste '+(multi?'all '+fields.join(', '):'an API key')+' first.');return;}
|
|
816
|
+
const payload=multi?{label:'key '+(ks.length+1),credentials:Object.fromEntries(fields.map((f,i)=>[f,vals[i]]))}:{label:'key '+(ks.length+1),api_key:vals[0]};
|
|
817
|
+
const r=await j(await fetch('/api/pools/'+p.id+'/keys',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(payload)}));
|
|
818
|
+
if(r.error){err(r.error);return;}
|
|
819
|
+
refresh();
|
|
820
|
+
};
|
|
821
|
+
inputs.forEach(i=>i.addEventListener('keydown',(e)=>{if(e.key==='Enter'){e.preventDefault();gather.onclick();}}));
|
|
822
|
+
del.onclick=async()=>{if(!confirm('Delete pool "'+p.name+'" and all its keys + usage?'))return;await fetch('/api/pools/'+p.id,{method:'DELETE'});refresh();};
|
|
823
|
+
box.appendChild(card);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
document.getElementById('create').onclick=async()=>{
|
|
827
|
+
err('');
|
|
828
|
+
const provider=document.getElementById('prov').value,name=document.getElementById('pname').value||'pool';
|
|
829
|
+
const base_url=document.getElementById('pbase').value.trim(),key_header=document.getElementById('phead').value.trim(),key_prefix=document.getElementById('pprefix').value;
|
|
830
|
+
const p=await j(await fetch('/api/pools',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({provider,name,base_url,key_header,key_prefix})}));
|
|
831
|
+
if(p.error){err(p.error);return;}
|
|
832
|
+
document.getElementById('pname').value='';
|
|
833
|
+
document.getElementById('pbase').value='';
|
|
834
|
+
document.getElementById('phead').value='';
|
|
835
|
+
document.getElementById('pprefix').value='';
|
|
836
|
+
refresh();
|
|
837
|
+
};
|
|
838
|
+
init();
|
|
839
|
+
</script></div></div>${shellJs}</body></html>`;
|
|
840
|
+
}
|
|
841
|
+
export function analyticsPage() {
|
|
842
|
+
return `<!doctype html>
|
|
843
|
+
<html lang="en"><head><meta charset="utf-8"/>
|
|
844
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
845
|
+
<title>Analytics — pool-anything</title>
|
|
846
|
+
<link rel="icon" type="image/png" href="/logo.png" />
|
|
847
|
+
<style>
|
|
848
|
+
*{box-sizing:border-box}body{margin:0;font-family:var(--font-sans);background:var(--paper-2);color:var(--ink)}
|
|
849
|
+
${shellCss}
|
|
850
|
+
${sharedLayoutCss}
|
|
851
|
+
.row{display:flex;gap:8px;align-items:center}
|
|
852
|
+
.analytics { width:100%; display:flex; flex-direction:column; gap:10px; }
|
|
853
|
+
.analytics-head { display:flex; align-items:center; gap:10px; }
|
|
854
|
+
.analytics-head h2 { font-size:15px; font-weight:600; margin:0; }
|
|
855
|
+
.analytics-controls { margin-left:auto; display:flex; align-items:center; gap:8px; }
|
|
856
|
+
.range-pill { display:inline-flex; align-items:center; gap:6px; font-size:12.5px; font-weight:500; background:var(--card); border:1px solid var(--line); border-radius:999px; padding:6px 12px; color:var(--ink); white-space:nowrap; }
|
|
857
|
+
.icon-btn { width:30px; height:30px; display:grid; place-items:center; border-radius:8px; border:1px solid transparent; background:transparent; color:var(--ink-soft); cursor:pointer; font-size:15px; }
|
|
858
|
+
.icon-btn:hover { background:var(--paper-2); color:var(--ink); }
|
|
859
|
+
.analytics-grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
|
860
|
+
.ana-card { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:12px 14px 6px; min-width:0; overflow:hidden; transition:border-color 150ms; }
|
|
861
|
+
.ana-card:hover { border-color:var(--underline); }
|
|
862
|
+
.ana-top { display:flex; align-items:center; gap:6px; }
|
|
863
|
+
.ana-title { font-size:12px; color:var(--ink-soft); font-weight:400; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
864
|
+
.ana-dots { margin-left:auto; color:var(--ink-faint); font-size:14px; letter-spacing:1px; user-select:none; }
|
|
865
|
+
.ana-value { display:flex; align-items:baseline; gap:8px; margin-top:2px; }
|
|
866
|
+
.ana-value b { font-size:22px; font-weight:600; line-height:1.2; font-variant-numeric:tabular-nums; }
|
|
867
|
+
.ana-delta { font-size:12.5px; font-weight:600; font-variant-numeric:tabular-nums; }
|
|
868
|
+
.ana-delta.up { color:var(--green-ink); }
|
|
869
|
+
.ana-delta.down { color:var(--danger-ink); }
|
|
870
|
+
.ana-delta.flat { color:var(--ink-soft); font-weight:500; }
|
|
871
|
+
.ana-chart { position:relative; height:110px; margin:4px -6px 0; }
|
|
872
|
+
.ana-chart svg { display:block; width:100%; height:100%; overflow:visible; }
|
|
873
|
+
.ana-y { position:absolute; right:0; top:0; bottom:0; display:flex; flex-direction:column; justify-content:space-between; font-size:10px; color:var(--ink-faint); padding:2px 0 14px; pointer-events:none; font-variant-numeric:tabular-nums; }
|
|
874
|
+
.chart-tip { position:absolute; display:none; z-index:5; pointer-events:none; background:var(--card); border:1px solid var(--line); border-radius:10px; box-shadow:var(--shadow-md); padding:8px 10px; min-width:150px; }
|
|
875
|
+
.chart-tip .tip-day { font-size:12px; font-weight:600; margin-bottom:4px; font-variant-numeric:tabular-nums; }
|
|
876
|
+
.chart-tip .tip-row { display:flex; align-items:center; gap:8px; font-size:12px; }
|
|
877
|
+
.chart-tip .dot { width:10px; height:10px; border-radius:999px; background:var(--chart-blue); flex-shrink:0; }
|
|
878
|
+
.chart-tip .tip-val { margin-left:auto; font-weight:600; font-variant-numeric:tabular-nums; padding-left:12px; }
|
|
879
|
+
.no-data { position:absolute; top:38%; left:50%; transform:translate(-50%,-50%); font-size:11px; color:var(--ink-soft); background:var(--card); border:1px solid var(--line); border-radius:999px; padding:3px 10px; white-space:nowrap; }
|
|
880
|
+
.ana-card.primary .ana-value b { font-size:26px; }
|
|
881
|
+
.ana-card.primary .ana-chart { height:96px; }
|
|
882
|
+
.ana-cap { font-size:11px; color:var(--ink-soft); margin-top:2px; }
|
|
883
|
+
.stat-list { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:4px 14px; }
|
|
884
|
+
.stat-list .srow { display:flex; align-items:baseline; gap:8px; padding:7px 0; border-top:1px solid var(--paper-2); font-size:13px; }
|
|
885
|
+
.stat-list .srow:first-child { border-top:0; }
|
|
886
|
+
.stat-list .sl { color:var(--ink-soft); }
|
|
887
|
+
.stat-list .sv { margin-left:auto; font-weight:600; font-variant-numeric:tabular-nums; }
|
|
888
|
+
.stat-list .sv.calm { color:var(--ink-soft); font-weight:500; }
|
|
889
|
+
.stat-list .sd { font-size:12px; color:var(--ink-soft); }
|
|
890
|
+
.tips-banner { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:4px 14px; font-size:13px; }
|
|
891
|
+
.tips-banner > summary { list-style:none; cursor:pointer; display:flex; align-items:center; gap:8px; padding:8px 0; user-select:none; }
|
|
892
|
+
.tips-banner > summary::-webkit-details-marker { display:none; }
|
|
893
|
+
.tips-banner > summary .chev { color:var(--ink-soft); font-size:10px; transition:transform 200ms; }
|
|
894
|
+
.tips-banner[open] > summary .chev { transform:rotate(90deg); }
|
|
895
|
+
.tips-banner ol { margin:4px 0 10px; padding-left:18px; display:flex; flex-direction:column; gap:4px; color:var(--body-copy); }
|
|
896
|
+
.gs-x { margin-left:auto; border:0; background:transparent; color:var(--ink-soft); cursor:pointer; font-size:12px; padding:2px 6px; border-radius:6px; }
|
|
897
|
+
.gs-x:hover { background:var(--paper-2); color:var(--ink); }
|
|
898
|
+
@media (max-width:640px) { .analytics-grid { grid-template-columns:1fr 1fr; } }
|
|
899
|
+
${docsComponentsCss}
|
|
900
|
+
</style></head><body><div class="shell" id="shell">${shellNav("/analytics")}<div class="content">${sharedHeader}<main>
|
|
901
|
+
<div class="wrap">
|
|
902
|
+
<div class="page-head"><a href="/"><img src="/logo.png" alt="pool-anything" width="28" height="28"/></a><h1>Analytics</h1></div>
|
|
903
|
+
<a class="back" href="/">← pool search</a>
|
|
904
|
+
<div class="analytics" id="analytics">
|
|
905
|
+
<div class="analytics-head"><div class="analytics-controls"><span class="range-pill">◷ Last 14 days</span><button class="icon-btn" id="gsReopen" type="button" title="Show getting started" aria-label="Show getting started" hidden>?</button><button class="icon-btn" id="anaRefresh" type="button" title="Refresh analytics" aria-label="Refresh analytics">↻</button></div></div>
|
|
906
|
+
<details class="tips-banner" id="getStarted" hidden>
|
|
907
|
+
<summary><span class="chev">▶</span><b>Getting started</b><button class="gs-x" id="gsHide" type="button" title="Dismiss" aria-label="Dismiss getting started">✕</button></summary>
|
|
908
|
+
<ol>
|
|
909
|
+
<li>Gather keys for a provider on <a href="/">home</a> — one per free-tier account.</li>
|
|
910
|
+
<li>Point your client at <small>/api/pools/:id/proxy</small> and send a request.</li>
|
|
911
|
+
<li>Come back here: usage per key shows up automatically.</li>
|
|
912
|
+
</ol>
|
|
913
|
+
</details>
|
|
914
|
+
<div class="analytics-grid">
|
|
915
|
+
<div class="ana-card primary"><div class="ana-top"><span class="ana-title">Total requests</span><span class="ana-dots">…</span></div><div class="ana-value"><b id="statRequests">—</b><span class="ana-delta flat" id="statDeltaReq"></span></div><div class="ana-chart"><svg id="chartRequests" viewBox="0 0 600 120" preserveAspectRatio="none" role="img" aria-label="Total requests"></svg><div class="ana-y" id="yRequests"></div><div class="chart-tip" id="tipRequests"></div></div><div class="ana-cap">Requests served through your pools</div></div>
|
|
916
|
+
<div class="ana-card primary"><div class="ana-top"><span class="ana-title">Keys pooled</span><span class="ana-dots">…</span></div><div class="ana-value"><b id="statKeys">—</b></div><div class="ana-chart"><svg id="chartKeys" viewBox="0 0 600 120" preserveAspectRatio="none" role="img" aria-label="Keys pooled"></svg><div class="chart-tip" id="tipKeys"></div></div><div class="ana-cap">API keys rotating across pools</div></div>
|
|
917
|
+
</div>
|
|
918
|
+
<div class="stat-list" id="statList">
|
|
919
|
+
<div class="srow"><span class="sl">Tokens tracked</span><span class="ana-delta flat" id="statDeltaTok"></span><span class="sv" id="statTokens">—</span></div>
|
|
920
|
+
<div class="srow"><span class="sl">Pools</span><span class="sv" id="statPools">—</span></div>
|
|
921
|
+
<div class="srow"><span class="sl">Keys cooling</span><span class="sd" id="statCoolingNote"></span><span class="sv" id="statCooling">—</span></div>
|
|
922
|
+
<div class="srow"><span class="sl">Avg tokens / req</span><span class="sv" id="statAvg">—</span></div>
|
|
923
|
+
</div>
|
|
924
|
+
</div>
|
|
925
|
+
</div>
|
|
926
|
+
</main><script>
|
|
927
|
+
${clientCoreJs}
|
|
928
|
+
${clientChartsJs}
|
|
929
|
+
async function loadAnalytics() {
|
|
930
|
+
try {
|
|
931
|
+
const a = await j(await fetch('/api/analytics'));
|
|
932
|
+
const gs = document.getElementById('getStarted');
|
|
933
|
+
const gsReopen = document.getElementById('gsReopen');
|
|
934
|
+
const req0 = (a.requests || 0) === 0;
|
|
935
|
+
const dismissed = (() => { try { return localStorage.getItem('gs-dismissed') === '1'; } catch { return false; } })();
|
|
936
|
+
if (gs) {
|
|
937
|
+
if (req0) { gs.hidden = false; gs.open = true; }
|
|
938
|
+
else if (dismissed || (a.requests || 0) >= 10) gs.hidden = true;
|
|
939
|
+
else { gs.hidden = false; gs.open = false; }
|
|
940
|
+
}
|
|
941
|
+
if (gsReopen) {
|
|
942
|
+
gsReopen.hidden = !(!req0 && gs.hidden);
|
|
943
|
+
gsReopen.onclick = () => {
|
|
944
|
+
try { localStorage.removeItem('gs-dismissed'); } catch {}
|
|
945
|
+
gs.hidden = false; gs.open = true;
|
|
946
|
+
gsReopen.hidden = true;
|
|
947
|
+
gs.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
const gsHide = document.getElementById('gsHide');
|
|
951
|
+
if (gsHide) gsHide.onclick = (e) => {
|
|
952
|
+
e.preventDefault();
|
|
953
|
+
try { localStorage.setItem('gs-dismissed', '1'); } catch {}
|
|
954
|
+
document.getElementById('getStarted').hidden = true;
|
|
955
|
+
};
|
|
956
|
+
const BLUE = '#3b82f6';
|
|
957
|
+
document.getElementById('statRequests').textContent = fmtCompact(a.requests);
|
|
958
|
+
document.getElementById('statRequests').title = String(a.requests);
|
|
959
|
+
const st = document.getElementById('statTokens');
|
|
960
|
+
st.textContent = fmtCompact(a.tokens); st.title = String(a.tokens);
|
|
961
|
+
document.getElementById('statPools').textContent = String(a.pools);
|
|
962
|
+
document.getElementById('statKeys').textContent = String(a.keys);
|
|
963
|
+
document.getElementById('statKeys').title = String(a.keys);
|
|
964
|
+
const cool = document.getElementById('statCooling');
|
|
965
|
+
const cn = document.getElementById('statCoolingNote');
|
|
966
|
+
if ((a.cooling || 0) > 0) {
|
|
967
|
+
cool.textContent = String(a.cooling); cool.className = 'sv';
|
|
968
|
+
cn.textContent = 'in backoff'; cn.className = 'sd';
|
|
969
|
+
} else {
|
|
970
|
+
cool.textContent = '—'; cool.className = 'sv calm'; cool.title = '0';
|
|
971
|
+
cn.textContent = 'all healthy'; cn.className = 'sd';
|
|
972
|
+
}
|
|
973
|
+
document.getElementById('statAvg').textContent = String(a.avgTokens || 0);
|
|
974
|
+
setDelta('statDeltaReq', a.deltaRequestsPct, 'requests, last 7 days vs prior 7 days');
|
|
975
|
+
setDelta('statDeltaTok', a.deltaTokensPct, 'tokens, last 7 days vs prior 7 days');
|
|
976
|
+
const days = (a.series || []).map(p => p.day);
|
|
977
|
+
renderLine('chartRequests', 'tipRequests', 'yRequests', days, (a.series || []).map(p => p.requests), 'Total requests', BLUE);
|
|
978
|
+
renderLine('chartKeys', 'tipKeys', null, (a.keysSeries || []).map(p => p.day), (a.keysSeries || []).map(p => p.count), 'Keys pooled', BLUE);
|
|
979
|
+
} catch (e) { /* analytics is best-effort */ }
|
|
980
|
+
}
|
|
981
|
+
const anaRefresh = document.getElementById('anaRefresh');
|
|
982
|
+
if (anaRefresh) anaRefresh.onclick = () => loadAnalytics();
|
|
983
|
+
window.addEventListener('pageshow', () => setTimeout(loadAnalytics, 200));
|
|
984
|
+
window.addEventListener('focus', () => setTimeout(loadAnalytics, 200));
|
|
985
|
+
loadAnalytics();
|
|
986
|
+
</script></div></div>${shellJs}</body></html>`;
|
|
987
|
+
}
|
|
988
|
+
export function poolsPage() {
|
|
989
|
+
return `<!doctype html>
|
|
990
|
+
<html lang="en"><head><meta charset="utf-8"/>
|
|
991
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
992
|
+
<title>Pools — pool-anything</title>
|
|
993
|
+
<link rel="icon" type="image/png" href="/logo.png" />
|
|
994
|
+
<style>
|
|
995
|
+
*{box-sizing:border-box}body{margin:0;font-family:var(--font-sans);background:var(--paper-2);color:var(--ink)}
|
|
996
|
+
${shellCss}
|
|
997
|
+
${sharedLayoutCss}
|
|
998
|
+
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px;animation:fadeSlide .28s cubic-bezier(.2,.7,.3,1) both}
|
|
999
|
+
.chead{display:flex;align-items:center;gap:10px}.chead img{width:24px;height:24px}
|
|
1000
|
+
.chead b{font-size:15px}.pill{margin-left:auto;font-size:11px;font-weight:600;background:var(--paper-2);border-radius:999px;padding:3px 10px;white-space:nowrap}
|
|
1001
|
+
.row{display:flex;gap:8px;align-items:center}
|
|
1002
|
+
ul{margin:10px 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:6px}
|
|
1003
|
+
li{background:var(--paper-2);border-radius:8px;padding:8px 10px;font-size:13px;display:flex;gap:8px;align-items:center}
|
|
1004
|
+
li span{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1005
|
+
li small{color:var(--ink-soft)}
|
|
1006
|
+
a.manage{font-size:13px}
|
|
1007
|
+
.empty{color:var(--ink-soft);font-size:14px;text-align:center;padding:24px 0}
|
|
1008
|
+
${docsComponentsCss}
|
|
1009
|
+
</style></head><body><div class="shell" id="shell">${shellNav("/pools")}<div class="content">${sharedHeader}<main>
|
|
1010
|
+
<div class="wrap">
|
|
1011
|
+
<div class="page-head"><a href="/"><img src="/logo.png" alt="pool-anything" width="28" height="28"/></a><h1>Pools</h1></div>
|
|
1012
|
+
<a class="back" href="/">← pool search</a>
|
|
1013
|
+
<div id="pools" style="display:flex;flex-direction:column;gap:12px"></div>
|
|
1014
|
+
</div>
|
|
1015
|
+
</main><script>
|
|
1016
|
+
${clientCoreJs}
|
|
1017
|
+
async function init(){
|
|
1018
|
+
const provs=await j(await fetch('/api/providers'));
|
|
1019
|
+
const pools=await j(await fetch('/api/pools'));
|
|
1020
|
+
const box=document.getElementById('pools');box.innerHTML='';
|
|
1021
|
+
let shown=0;
|
|
1022
|
+
for(const [i,p] of pools.entries()){
|
|
1023
|
+
const ks=await j(await fetch('/api/pools/'+p.id+'/keys'));
|
|
1024
|
+
if(!ks.length) continue;
|
|
1025
|
+
shown++;
|
|
1026
|
+
const us=await j(await fetch('/api/pools/'+p.id+'/usage'));
|
|
1027
|
+
const prov=provs.find(x=>x.id===p.provider)||{name:p.provider,quota:'',logoFile:p.provider+'.svg'};
|
|
1028
|
+
const card=document.createElement('div');card.className='card';
|
|
1029
|
+
card.style.animationDelay=Math.min(i*40,320)+'ms';
|
|
1030
|
+
card.innerHTML='<div class="chead"><img alt=""/><b></b><span class="pill"></span></div><ul></ul><div class="row" style="margin-top:10px"><a class="manage" href="/keys">Manage keys →</a></div>';
|
|
1031
|
+
logoSrc(card.querySelector('img'),prov.logoFile);
|
|
1032
|
+
card.querySelector('b').textContent=prov.name+' · '+p.name;
|
|
1033
|
+
card.querySelector('.pill').textContent=ks.length+' key(s) · used '+(us.used||0);
|
|
1034
|
+
const ul=card.querySelector('ul');
|
|
1035
|
+
ks.forEach(k=>{
|
|
1036
|
+
const li=document.createElement('li');
|
|
1037
|
+
li.innerHTML='<span></span><small></small>';
|
|
1038
|
+
li.querySelector('span').textContent=k.label+' · '+k.masked;
|
|
1039
|
+
li.querySelector('small').textContent='used '+((us.perKey||[]).find(x=>x.id===k.id)?.used||0);
|
|
1040
|
+
ul.appendChild(li);
|
|
1041
|
+
});
|
|
1042
|
+
box.appendChild(card);
|
|
1043
|
+
}
|
|
1044
|
+
if(!shown) box.innerHTML='<div class="empty">No pools with keys yet — <a href="/">search providers and gather some</a>.</div>';
|
|
1045
|
+
}
|
|
1046
|
+
init();
|
|
1047
|
+
</script></div></div>${shellJs}</body></html>`;
|
|
1048
|
+
}
|
|
1049
|
+
export function playgroundPage() {
|
|
1050
|
+
return `<!doctype html>
|
|
1051
|
+
<html lang="en"><head><meta charset="utf-8"/>
|
|
1052
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
1053
|
+
<title>Playground — pool-anything</title>
|
|
1054
|
+
<link rel="icon" type="image/png" href="/logo.png" />
|
|
1055
|
+
<style>
|
|
1056
|
+
*{box-sizing:border-box}body{margin:0;font-family:var(--font-sans);background:var(--paper-2);color:var(--ink)}
|
|
1057
|
+
${shellCss}
|
|
1058
|
+
${sharedLayoutCss}
|
|
1059
|
+
.pg-top{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
1060
|
+
.pg-top .spacer{flex:1}
|
|
1061
|
+
.seg{display:inline-flex;background:var(--line-soft);border-radius:999px;padding:2px}
|
|
1062
|
+
.seg button{border:0;background:none;border-radius:999px;padding:5px 16px;font-size:13px;cursor:pointer;color:var(--ink-soft);font-family:inherit}
|
|
1063
|
+
.seg button.on{background:var(--card);box-shadow:var(--shadow-sm);color:var(--ink);font-weight:600}
|
|
1064
|
+
.ctl{border:1px solid var(--line);border-radius:8px;min-height:34px;padding:0 10px;font-size:13px;background:var(--card);color:var(--ink);font-family:inherit}
|
|
1065
|
+
select.ctl{max-width:220px}input.ctl.model{min-width:220px}
|
|
1066
|
+
.btn{border:1px solid var(--line);background:var(--card);border-radius:8px;min-height:34px;padding:0 12px;font-size:13px;cursor:pointer;color:var(--ink);font-family:inherit;display:inline-flex;align-items:center;gap:6px}
|
|
1067
|
+
.btn:hover{background:var(--paper-2)}
|
|
1068
|
+
.pg-grid{display:grid;grid-template-columns:minmax(0,1fr) 400px;gap:16px;align-items:start;min-height:560px}
|
|
1069
|
+
.pg-grid.nocode{grid-template-columns:minmax(0,1fr)}
|
|
1070
|
+
.chat{display:flex;flex-direction:column;gap:12px;min-width:0;background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px;min-height:560px;animation:fadeSlide .28s cubic-bezier(.2,.7,.3,1) both}
|
|
1071
|
+
.code{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px;display:flex;flex-direction:column;min-width:0;min-height:560px;animation:fadeSlide .28s cubic-bezier(.2,.7,.3,1) both}
|
|
1072
|
+
.pg-grid.nocode .code{display:none}
|
|
1073
|
+
.pg-grid[hidden],.studio[hidden]{display:none}
|
|
1074
|
+
@media (max-width:900px){.pg-grid{grid-template-columns:1fr}}
|
|
1075
|
+
.fld label{display:block;font-size:11px;letter-spacing:.05em;color:var(--ink-soft);font-weight:600;margin-bottom:4px}
|
|
1076
|
+
.ghost{width:100%;border:0;background:transparent;outline:0;resize:vertical;font:inherit;font-size:14px;color:var(--ink);padding:2px 0;min-height:24px}
|
|
1077
|
+
.msg{background:var(--paper-2);border-radius:12px;padding:12px 14px}
|
|
1078
|
+
.msg.assistant{background:var(--card);border:1px solid var(--line)}
|
|
1079
|
+
.msg.assistant.err{border-color:var(--danger-line);background:var(--danger-bg)}
|
|
1080
|
+
.msg .body{font-size:14px;white-space:pre-wrap;word-break:break-word}
|
|
1081
|
+
.msg .who{display:block;font-size:11px;letter-spacing:.05em;color:var(--ink-soft);font-weight:600;margin-bottom:4px}
|
|
1082
|
+
.chat-foot{display:flex;align-items:center;gap:8px;margin-top:auto;padding-top:12px}
|
|
1083
|
+
.iconbtn{border:1px solid var(--line);background:var(--card);border-radius:8px;min-width:32px;height:30px;cursor:pointer;font-size:15px;color:var(--ink)}
|
|
1084
|
+
.submit{margin:0 auto;border:1px solid var(--danger-ink);background:var(--card);border-radius:999px;padding:8px 24px;font-weight:600;font-size:14px;cursor:pointer;color:var(--ink);box-shadow:0 0 0 3px var(--danger-ring);font-family:inherit;display:inline-flex;align-items:center;gap:8px}
|
|
1085
|
+
.submit:disabled{opacity:.5;cursor:wait}
|
|
1086
|
+
.submit .hint{font-size:11px;color:var(--ink-faint);font-weight:400}
|
|
1087
|
+
details.params{font-size:13px}
|
|
1088
|
+
details.params summary{cursor:pointer;color:var(--ink-soft);font-size:12px}
|
|
1089
|
+
.params-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;margin-top:8px}
|
|
1090
|
+
.params-grid label{font-size:11px;color:var(--ink-soft);display:flex;flex-direction:column;gap:4px}
|
|
1091
|
+
.code-head{display:flex;align-items:center;gap:8px;padding:0 0 10px;border-bottom:1px solid var(--line);margin-bottom:12px}
|
|
1092
|
+
.code-head .spacer{flex:1}
|
|
1093
|
+
.code pre{margin:0;padding:0;font-family:var(--font-mono);font-size:12.5px;line-height:1.65;overflow:auto;flex:1;white-space:pre-wrap;word-break:break-word}
|
|
1094
|
+
.k{color:var(--tok-kw)}.s{color:var(--tok-str)}.n{color:var(--warn-ink)}.fn{color:var(--link-ink)}.c{color:var(--code-com)}
|
|
1095
|
+
.err{color:var(--danger-ink);font-size:13px;min-height:18px}
|
|
1096
|
+
.studio{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px;animation:fadeSlide .28s cubic-bezier(.2,.7,.3,1) both}
|
|
1097
|
+
.gal{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin-top:4px}
|
|
1098
|
+
.gal figure{margin:0;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--paper-2)}
|
|
1099
|
+
.gal img{width:100%;display:block;aspect-ratio:1;object-fit:cover}
|
|
1100
|
+
.gal figcaption{font-size:11px;color:var(--ink-soft);padding:8px 10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1101
|
+
.attach-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
1102
|
+
.attach-row .filebtn{border:1px solid var(--line);background:var(--card);border-radius:8px;min-height:30px;padding:0 10px;font-size:12.5px;cursor:pointer;color:var(--ink);font-family:inherit;display:inline-flex;align-items:center;gap:6px}
|
|
1103
|
+
.attach-row .filebtn:hover{background:var(--paper-2)}
|
|
1104
|
+
.attach-row .filebtn.off{opacity:.4;cursor:not-allowed}
|
|
1105
|
+
.attach-row .filebtn svg{width:14px;height:14px;flex-shrink:0;display:block}
|
|
1106
|
+
.attach-row .filebtn.recording{border-color:var(--danger-line);background:var(--danger-bg);color:var(--danger-ink)}
|
|
1107
|
+
.attach-row .filebtn.recording svg{animation:recPulse 1.2s ease-in-out infinite}
|
|
1108
|
+
@keyframes recPulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
1109
|
+
.cap-note{font-size:12.5px;color:var(--warn-ink);background:var(--warn-bg);border:1px solid var(--warn-line);border-radius:8px;padding:8px 10px}
|
|
1110
|
+
.cap-note.ok{color:var(--green-ink);background:var(--green-bg);border-color:var(--green-line)}
|
|
1111
|
+
.mprev{display:flex;gap:8px;flex-wrap:wrap}
|
|
1112
|
+
.mprev .thumb{position:relative;width:88px;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--paper-2)}
|
|
1113
|
+
.mprev .thumb img{width:100%;height:64px;object-fit:cover;display:block}
|
|
1114
|
+
.mprev .thumb .tag{font-size:10px;color:var(--ink-soft);padding:4px 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1115
|
+
.mprev .thumb audio{width:100%;height:30px}
|
|
1116
|
+
.mprev .thumb button{position:absolute;top:2px;right:2px;border:0;background:var(--overlay-strong);color:var(--card);border-radius:6px;font-size:11px;cursor:pointer;padding:1px 6px}
|
|
1117
|
+
.modes{display:inline-flex;background:var(--line-soft);border-radius:999px;padding:2px}
|
|
1118
|
+
.modes button{border:0;background:none;border-radius:999px;padding:5px 14px;font-size:12.5px;cursor:pointer;color:var(--ink-soft);font-family:inherit}
|
|
1119
|
+
.modes button.on{background:var(--card);box-shadow:var(--shadow-sm);color:var(--ink);font-weight:600}
|
|
1120
|
+
.modes button.off{opacity:.4;cursor:not-allowed}
|
|
1121
|
+
.media-out{margin-top:12px;display:flex;flex-direction:column;gap:10px}
|
|
1122
|
+
.media-out audio,.media-out video{width:100%;border-radius:10px;background:var(--code-dark)}
|
|
1123
|
+
.media-out .dl{font-size:13px;color:var(--ink)}
|
|
1124
|
+
${docsComponentsCss}
|
|
1125
|
+
</style></head><body><div class="shell" id="shell">${shellNav("/playground")}<div class="content">${sharedHeader}<main>
|
|
1126
|
+
<div class="wrap">
|
|
1127
|
+
<div class="page-head"><a href="/"><img src="/logo.png" alt="pool-anything" width="28" height="28"/></a><h1>Playground</h1></div>
|
|
1128
|
+
<a class="back" href="/">← pool search</a>
|
|
1129
|
+
<div class="pg-top">
|
|
1130
|
+
<div class="seg"><button id="tabChat" class="on" type="button">Chat</button><button id="tabStudio" type="button">Studio</button></div>
|
|
1131
|
+
<div class="spacer"></div>
|
|
1132
|
+
${ddSelect("pool", "Pool")}
|
|
1133
|
+
<input class="ctl model" id="model" value="" placeholder="model id" aria-label="Model" spellcheck="false" autocomplete="off"/>
|
|
1134
|
+
<button class="btn" id="copyTop" type="button" title="Copy code">⧉</button>
|
|
1135
|
+
<button class="btn" id="hideCode" type="button"></> Hide code</button>
|
|
1136
|
+
</div>
|
|
1137
|
+
<div class="err" id="err"></div>
|
|
1138
|
+
<div class="pg-grid" id="grid">
|
|
1139
|
+
<div class="chat" id="chatPane">
|
|
1140
|
+
<div class="fld"><label>SYSTEM</label><textarea class="ghost" id="system" rows="1" placeholder="Enter system message (Optional)"></textarea></div>
|
|
1141
|
+
<div id="turns" style="display:flex;flex-direction:column;gap:12px"></div>
|
|
1142
|
+
<details class="params"><summary>Parameters</summary><div class="params-grid">
|
|
1143
|
+
<label>Path<input class="ctl" id="path" value="/chat/completions" spellcheck="false" autocomplete="off"/></label>
|
|
1144
|
+
<label>Temperature<input class="ctl" id="temp" value="1" inputmode="decimal"/></label>
|
|
1145
|
+
<label>Max tokens<input class="ctl" id="maxtokens" value="2048" inputmode="numeric"/></label>
|
|
1146
|
+
<label>Top P<input class="ctl" id="topp" value="1" inputmode="decimal"/></label>
|
|
1147
|
+
</div></details>
|
|
1148
|
+
<div class="chat-foot">
|
|
1149
|
+
<button class="iconbtn" id="addMsg" type="button" title="Add message">+</button>
|
|
1150
|
+
<button class="btn" id="clear" type="button">Clear</button>
|
|
1151
|
+
<button class="submit" id="submit" type="button">Submit <span class="hint">⌘↵</span></button>
|
|
1152
|
+
</div>
|
|
1153
|
+
<div class="attach-row" id="attachRow">
|
|
1154
|
+
<button class="filebtn" id="attachImg" type="button" title="Attach image (png/jpg/webp)">${ICONS.image}Image</button>
|
|
1155
|
+
<button class="filebtn" id="attachAudio" type="button" title="Attach audio">${ICONS.audio}Audio</button>
|
|
1156
|
+
<button class="filebtn" id="attachVideo" type="button" title="Attach video (Gemini video models)">${ICONS.video}Video</button>
|
|
1157
|
+
<button class="filebtn" id="recAudio" type="button" title="Record audio with microphone">${ICONS.rec}Rec</button>
|
|
1158
|
+
<input type="file" id="fileImg" accept="image/png,image/jpeg,image/webp" hidden/>
|
|
1159
|
+
<input type="file" id="fileAudio" accept="audio/*" hidden/>
|
|
1160
|
+
<input type="file" id="fileVideo" accept="video/mp4,video/webm" hidden/>
|
|
1161
|
+
</div>
|
|
1162
|
+
<div class="mprev" id="mediaPrev"></div>
|
|
1163
|
+
<div class="cap-note" id="capNote" hidden></div>
|
|
1164
|
+
</div>
|
|
1165
|
+
<div class="code" id="codePane">
|
|
1166
|
+
<div class="code-head">
|
|
1167
|
+
${ddSelect("codeLang", "Code language", '<option value="python">Python</option><option value="curl">cURL</option>', "dd-ghost")}
|
|
1168
|
+
<div class="spacer"></div>
|
|
1169
|
+
<button class="btn" id="copyCode" type="button" style="border:0">⧉ Copy</button>
|
|
1170
|
+
</div>
|
|
1171
|
+
<pre id="code"></pre>
|
|
1172
|
+
</div>
|
|
1173
|
+
</div>
|
|
1174
|
+
<div class="studio" id="studioPane" hidden>
|
|
1175
|
+
<div class="modes" id="studioModes" role="tablist" aria-label="Studio mode">
|
|
1176
|
+
<button id="modeImage" class="on" type="button">Image</button><button id="modeAudio" type="button">Audio</button><button id="modeVideo" type="button">Video</button>
|
|
1177
|
+
</div>
|
|
1178
|
+
<div class="cap-note" id="studioNote" hidden style="margin-top:10px"></div>
|
|
1179
|
+
<div id="studioImage">
|
|
1180
|
+
<div class="fld" style="margin-top:12px"><label>PROMPT</label><textarea class="ghost" id="imgPrompt" rows="2" placeholder="Describe the image you want to generate..."></textarea></div>
|
|
1181
|
+
<div class="params-grid" style="margin-top:12px">
|
|
1182
|
+
<label>Model<input class="ctl" id="imgModel" value="" placeholder="image model id" spellcheck="false" autocomplete="off"/></label>
|
|
1183
|
+
<label>Size${ddSelect("imgSize", "Size", "<option>1024x1024</option><option>1792x1024</option><option>1024x1792</option><option>512x512</option>", "dd-block")}</label>
|
|
1184
|
+
</div>
|
|
1185
|
+
<div class="chat-foot" style="justify-content:center">
|
|
1186
|
+
<button class="submit" id="generate" type="button">Generate <span class="hint">⌘↵</span></button>
|
|
1187
|
+
</div>
|
|
1188
|
+
<div class="gal" id="gallery"></div>
|
|
1189
|
+
</div>
|
|
1190
|
+
<div id="studioAudio" hidden>
|
|
1191
|
+
<div class="fld" style="margin-top:12px"><label>TEXT TO SPEAK</label><textarea class="ghost" id="audText" rows="2" placeholder="Text for TTS / audio generation..."></textarea></div>
|
|
1192
|
+
<div class="params-grid" style="margin-top:12px">
|
|
1193
|
+
<label>Model<input class="ctl" id="audModel" value="" placeholder="audio model id" spellcheck="false" autocomplete="off"/></label>
|
|
1194
|
+
<label>Voice<input class="ctl" id="audVoice" value="alloy" spellcheck="false" autocomplete="off"/></label>
|
|
1195
|
+
<label>Format${ddSelect("audFormat", "Format", "<option>mp3</option><option>wav</option><option>opus</option>", "dd-block")}</label>
|
|
1196
|
+
</div>
|
|
1197
|
+
<div class="chat-foot" style="justify-content:center">
|
|
1198
|
+
<button class="submit" id="genAudio" type="button">Synthesize <span class="hint">⌘↵</span></button>
|
|
1199
|
+
</div>
|
|
1200
|
+
<div class="media-out" id="audioOut"></div>
|
|
1201
|
+
</div>
|
|
1202
|
+
<div id="studioVideo" hidden>
|
|
1203
|
+
<div class="fld" style="margin-top:12px"><label>PROMPT</label><textarea class="ghost" id="vidPrompt" rows="2" placeholder="Describe the video you want to generate..."></textarea></div>
|
|
1204
|
+
<div class="params-grid" style="margin-top:12px">
|
|
1205
|
+
<label>Model<input class="ctl" id="vidModel" value="" placeholder="video model id" spellcheck="false" autocomplete="off"/></label>
|
|
1206
|
+
<label>Duration${ddSelect("vidDur", "Duration", "<option>4s</option><option>8s</option><option>12s</option>", "dd-block")}</label>
|
|
1207
|
+
<label>Resolution${ddSelect("vidRes", "Resolution", "<option>720p</option><option>1080p</option><option>480p</option>", "dd-block")}</label>
|
|
1208
|
+
</div>
|
|
1209
|
+
<div class="chat-foot" style="justify-content:center">
|
|
1210
|
+
<button class="submit" id="genVideo" type="button">Generate video <span class="hint">⌘↵</span></button>
|
|
1211
|
+
</div>
|
|
1212
|
+
<div class="media-out" id="videoOut"></div>
|
|
1213
|
+
</div>
|
|
1214
|
+
</div>
|
|
1215
|
+
</div>
|
|
1216
|
+
</main><script>
|
|
1217
|
+
${clientCoreJs}
|
|
1218
|
+
${clientDropdownJs}
|
|
1219
|
+
const MODELS={groq:'openai/gpt-oss-120b',openai:'gpt-4o-mini',openrouter:'meta-llama/llama-3.3-70b-instruct:free',mistral:'mistral-small-latest',fireworks:'accounts/fireworks/models/llama-v3p3-70b-instruct',together:'meta-llama/Llama-3.3-70B-Instruct-Turbo',gemini:'gemini-2.0-flash',anthropic:'claude-sonnet-4-20250514',perplexity:'sonar',cohere:'command-r',replicate:'ibm-granite/granite-3.3-8b-instruct',huggingface:'meta-llama/Llama-3.3-70B-Instruct',deepgram:'nova-3',elevenlabs:'eleven_multilingual_v2',cartesia:'sonic-2',tavily:'tavily-search',anyscale:'meta-llama/Llama-3.3-70B-Instruct'};
|
|
1220
|
+
const IMG_MODELS={openai:'gpt-image-1',together:'FLUX.1-schnell',fireworks:'accounts/fireworks/models/flux-1-schnell',replicate:'black-forest-labs/flux-schnell',huggingface:'black-forest-labs/FLUX.1-schnell',openrouter:'black-forest-labs/flux-1-schnell:free',gemini:'imagen-3.0-generate-002',mistral:'mistral-medium'};
|
|
1221
|
+
const AUD_MODELS={openai:'tts-1',elevenlabs:'eleven_multilingual_v2',cartesia:'sonic-2',deepgram:'aura-2-thalia-en',gemini:'gemini-2.5-flash-preview-tts'};
|
|
1222
|
+
const VID_MODELS={gemini:'veo-3.0-generate-001',replicate:'stability-ai/stable-video-diffusion'};
|
|
1223
|
+
const CAPS={openai:{vision:1,audioIn:1,videoIn:0,imageOut:1,audioOut:1,videoOut:0},anthropic:{vision:1,audioIn:0,videoIn:0,imageOut:0,audioOut:0,videoOut:0},gemini:{vision:1,audioIn:1,videoIn:1,imageOut:1,audioOut:1,videoOut:1},groq:{vision:1,audioIn:1,videoIn:0,imageOut:0,audioOut:0,videoOut:0},openrouter:{vision:1,audioIn:0,videoIn:0,imageOut:1,audioOut:0,videoOut:0},mistral:{vision:1,audioIn:0,videoIn:0,imageOut:0,audioOut:0,videoOut:0},fireworks:{vision:1,audioIn:0,videoIn:0,imageOut:1,audioOut:0,videoOut:0},together:{vision:1,audioIn:0,videoIn:0,imageOut:1,audioOut:0,videoOut:0},huggingface:{vision:1,audioIn:0,videoIn:0,imageOut:1,audioOut:0,videoOut:0},replicate:{vision:1,audioIn:0,videoIn:0,imageOut:1,audioOut:0,videoOut:1},perplexity:{vision:1,audioIn:0,videoIn:0,imageOut:0,audioOut:0,videoOut:0},cohere:{vision:1,audioIn:0,videoIn:0,imageOut:0,audioOut:0,videoOut:0},elevenlabs:{vision:0,audioIn:0,videoIn:0,imageOut:0,audioOut:1,videoOut:0},deepgram:{vision:0,audioIn:1,videoIn:0,imageOut:0,audioOut:1,videoOut:0},cartesia:{vision:0,audioIn:0,videoIn:0,imageOut:0,audioOut:1,videoOut:0}};
|
|
1224
|
+
const NO_VISION_RE=/tts|whisper|transcri|embed|image|flux|diffusion|imagen|dall|sonic|nova|stt|audit/i;
|
|
1225
|
+
function capsFor(prov,model){
|
|
1226
|
+
const b=CAPS[prov]||{vision:0,audioIn:0,videoIn:0,imageOut:0,audioOut:0,videoOut:0};
|
|
1227
|
+
const c={vision:b.vision,audioIn:b.audioIn,videoIn:b.videoIn,imageOut:b.imageOut,audioOut:b.audioOut,videoOut:b.videoOut};
|
|
1228
|
+
if(!model)return c;
|
|
1229
|
+
const m=String(model).toLowerCase();
|
|
1230
|
+
if(/embed/.test(m)){c.vision=0;c.audioIn=0;c.videoIn=0;c.imageOut=0;c.audioOut=0;c.videoOut=0;return c;}
|
|
1231
|
+
if(/image|flux|diffusion|imagen|dall|stable|midjourney/.test(m)){c.vision=0;c.audioIn=0;c.videoIn=0;return c;}
|
|
1232
|
+
if(/video|veo|pika|runway|sora/.test(m)){c.vision=0;c.audioIn=0;c.videoIn=0;return c;}
|
|
1233
|
+
if(/tts|sonic|eleven/.test(m)){c.vision=0;c.audioIn=0;c.videoIn=0;return c;}
|
|
1234
|
+
if(/whisper|transcri|^nova|stt/.test(m)){c.vision=0;c.videoIn=0;return c;}
|
|
1235
|
+
if(NO_VISION_RE.test(m))c.vision=0;
|
|
1236
|
+
return c;
|
|
1237
|
+
}
|
|
1238
|
+
function curProv(){return window._pgPmap ? (window._pgPmap[document.getElementById('pool').value]||'') : '';}
|
|
1239
|
+
function gateNote(cap,prov,model){
|
|
1240
|
+
const label=cap==='vision'?'image input':cap==='audioIn'?'audio input':cap==='videoIn'?'video input':cap==='imageOut'?'image output':cap==='audioOut'?'audio output':'video output';
|
|
1241
|
+
return 'this model doesn'+String.fromCharCode(39)+'t support '+label+' ('+prov+' '+(model||'default model')+')';
|
|
1242
|
+
}
|
|
1243
|
+
const IMG_MIMES=['image/png','image/jpeg','image/webp'];
|
|
1244
|
+
const AUD_MIMES=['audio/mpeg','audio/wav','audio/webm','audio/ogg','audio/mp4'];
|
|
1245
|
+
const VID_MIMES=['video/mp4','video/webm'];
|
|
1246
|
+
const MAX_MEDIA_BYTES=8000000;
|
|
1247
|
+
let pendingMedia=[];
|
|
1248
|
+
function fmtKB(n){return Math.round(n/1024)+'KB';}
|
|
1249
|
+
function renderMediaPrev(){
|
|
1250
|
+
const box=document.getElementById('mediaPrev');box.innerHTML='';
|
|
1251
|
+
pendingMedia.forEach(function(a,i){
|
|
1252
|
+
const d=document.createElement('div');d.className='thumb';
|
|
1253
|
+
if(a.kind==='image'){const im=document.createElement('img');im.src='data:'+a.mime+';base64,'+a.b64;im.alt=a.name;d.appendChild(im);}
|
|
1254
|
+
else if(a.kind==='audio'){const au=document.createElement('audio');au.controls=true;au.src='data:'+a.mime+';base64,'+a.b64;d.appendChild(au);}
|
|
1255
|
+
else{const v=document.createElement('video');v.controls=true;v.muted=true;v.preload='metadata';v.src='data:'+a.mime+';base64,'+a.b64;v.style.cssText='width:100%;height:64px;object-fit:cover;display:block';d.appendChild(v);}
|
|
1256
|
+
const t=document.createElement('div');t.className='tag';t.textContent=a.kind+' '+fmtKB(Math.round(a.b64.length*0.75));d.appendChild(t);
|
|
1257
|
+
const x=document.createElement('button');x.type='button';x.textContent='x';x.title='Remove';
|
|
1258
|
+
x.onclick=function(){pendingMedia.splice(i,1);renderMediaPrev();updateMediaUI();updateCode();};
|
|
1259
|
+
d.appendChild(x);box.appendChild(d);
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
function addMediaFile(file,kind){
|
|
1263
|
+
err('');
|
|
1264
|
+
const okMimes=kind==='image'?IMG_MIMES:kind==='audio'?AUD_MIMES:VID_MIMES;
|
|
1265
|
+
if(okMimes.indexOf(file.type)<0){err('Unsupported '+kind+' type: '+(file.type||'unknown')+'.');return;}
|
|
1266
|
+
if(file.size>MAX_MEDIA_BYTES){err(kind+' file too large (max 8MB).');return;}
|
|
1267
|
+
const rd=new FileReader();
|
|
1268
|
+
rd.onload=function(){
|
|
1269
|
+
const s=String(rd.result||'');
|
|
1270
|
+
const b64=s.indexOf(',')>=0?s.slice(s.indexOf(',')+1):s;
|
|
1271
|
+
pendingMedia.push({kind:kind,mime:file.type,b64:b64,name:file.name});
|
|
1272
|
+
renderMediaPrev();updateMediaUI();updateCode();
|
|
1273
|
+
};
|
|
1274
|
+
rd.onerror=function(){err('Could not read '+kind+' file.');};
|
|
1275
|
+
rd.readAsDataURL(file);
|
|
1276
|
+
}
|
|
1277
|
+
function updateMediaUI(){
|
|
1278
|
+
const prov=curProv(),model=document.getElementById('model').value.trim();
|
|
1279
|
+
const c=capsFor(prov,model);
|
|
1280
|
+
const note=document.getElementById('capNote');
|
|
1281
|
+
const bi=document.getElementById('attachImg'),ba=document.getElementById('attachAudio'),bv=document.getElementById('attachVideo'),br=document.getElementById('recAudio');
|
|
1282
|
+
const setBtn=function(b,on){b.classList.toggle('off',!on);b.disabled=!on;};
|
|
1283
|
+
setBtn(bi,!!c.vision);setBtn(ba,!!c.audioIn);setBtn(bv,!!c.videoIn);setBtn(br,!!c.audioIn);
|
|
1284
|
+
const bad=pendingMedia.filter(function(a){return (a.kind==='image'&&!c.vision)||(a.kind==='audio'&&!c.audioIn)||(a.kind==='video'&&!c.videoIn);});
|
|
1285
|
+
if(bad.length){
|
|
1286
|
+
note.hidden=false;note.className='cap-note';
|
|
1287
|
+
note.textContent=gateNote(bad[0].kind==='image'?'vision':bad[0].kind==='audio'?'audioIn':'videoIn',prov,model);
|
|
1288
|
+
}else if(pendingMedia.length){
|
|
1289
|
+
note.hidden=false;note.className='cap-note ok';
|
|
1290
|
+
note.textContent=pendingMedia.length+' attachment(s) ready — included in request + code snippet.';
|
|
1291
|
+
}else if(!c.vision&&!c.audioIn&&!c.videoIn){
|
|
1292
|
+
note.hidden=false;note.className='cap-note';
|
|
1293
|
+
note.textContent=gateNote('vision',prov,model)+' — text-only for this model.';
|
|
1294
|
+
}else{note.hidden=true;note.textContent='';}
|
|
1295
|
+
}
|
|
1296
|
+
function openAiParts(text,media){
|
|
1297
|
+
const parts=[{type:'text',text:text}];
|
|
1298
|
+
media.forEach(function(a){
|
|
1299
|
+
if(a.kind==='image')parts.push({type:'image_url',image_url:{url:'data:'+a.mime+';base64,'+a.b64}});
|
|
1300
|
+
else if(a.kind==='audio'){const f=a.mime.indexOf('wav')>=0?'wav':'mp3';parts.push({type:'input_audio',input_audio:{data:a.b64,format:f}});}
|
|
1301
|
+
else parts.push({type:'text',text:'[video '+a.mime+' base64 length '+a.b64.length+']'});
|
|
1302
|
+
});
|
|
1303
|
+
return parts;
|
|
1304
|
+
}
|
|
1305
|
+
function anthropicBlocks(text,media){
|
|
1306
|
+
const blocks=[{type:'text',text:text}];
|
|
1307
|
+
media.forEach(function(a){
|
|
1308
|
+
if(a.kind==='image')blocks.push({type:'image',source:{type:'base64',media_type:a.mime,data:a.b64}});
|
|
1309
|
+
});
|
|
1310
|
+
return blocks;
|
|
1311
|
+
}
|
|
1312
|
+
function chatContentFor(prov,text,media){
|
|
1313
|
+
if(!media.length)return text;
|
|
1314
|
+
if(prov==='anthropic'){
|
|
1315
|
+
const nonImg=media.filter(function(a){return a.kind!=='image';});
|
|
1316
|
+
if(nonImg.length)return null;
|
|
1317
|
+
return anthropicBlocks(text,media);
|
|
1318
|
+
}
|
|
1319
|
+
return openAiParts(text,media);
|
|
1320
|
+
}
|
|
1321
|
+
let lastDefault='';
|
|
1322
|
+
let lastImgDefault='⌁';
|
|
1323
|
+
let lastAudDefault='⌁';
|
|
1324
|
+
let lastVidDefault='⌁';
|
|
1325
|
+
let mediaRecorder=null,recChunks=[],recording=false;
|
|
1326
|
+
const REC_SVG='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.2"/><circle cx="12" cy="12" r="3" fill="#B3261E" stroke="#B3261E"/></svg>';
|
|
1327
|
+
const STOP_SVG='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.2"/><rect x="9.6" y="9.6" width="4.8" height="4.8" rx="1.2" fill="#B3261E" stroke="#B3261E"/></svg>';
|
|
1328
|
+
async function toggleRec(){
|
|
1329
|
+
const btn=document.getElementById('recAudio');
|
|
1330
|
+
if(recording&&mediaRecorder){mediaRecorder.stop();return;}
|
|
1331
|
+
if(!navigator.mediaDevices||!navigator.mediaDevices.getUserMedia){err('Microphone not available in this browser.');return;}
|
|
1332
|
+
if(!window.MediaRecorder){err('MediaRecorder not supported — upload an audio file instead.');return;}
|
|
1333
|
+
try{
|
|
1334
|
+
const stream=await navigator.mediaDevices.getUserMedia({audio:true});
|
|
1335
|
+
recChunks=[];
|
|
1336
|
+
mediaRecorder=new MediaRecorder(stream);
|
|
1337
|
+
mediaRecorder.ondataavailable=e=>{if(e.data&&e.data.size)recChunks.push(e.data);};
|
|
1338
|
+
mediaRecorder.onstop=()=>{
|
|
1339
|
+
recording=false;btn.classList.remove('recording');btn.innerHTML=REC_SVG+'Rec';
|
|
1340
|
+
stream.getTracks().forEach(t=>t.stop());
|
|
1341
|
+
const blob=new Blob(recChunks,{type:mediaRecorder.mimeType||'audio/webm'});
|
|
1342
|
+
const f=new File([blob],'recording.'+(blob.type.indexOf('mp4')>=0?'mp4':'webm'),{type:blob.type||'audio/webm'});
|
|
1343
|
+
addMediaFile(f,'audio');
|
|
1344
|
+
mediaRecorder=null;
|
|
1345
|
+
};
|
|
1346
|
+
mediaRecorder.start();recording=true;btn.classList.add('recording');btn.innerHTML=STOP_SVG+'Stop';
|
|
1347
|
+
err('');
|
|
1348
|
+
}catch{err('Microphone permission denied.');}
|
|
1349
|
+
}
|
|
1350
|
+
function params(){
|
|
1351
|
+
return {
|
|
1352
|
+
path:document.getElementById('path').value.trim()||'/chat/completions',
|
|
1353
|
+
temp:parseFloat(document.getElementById('temp').value)||0,
|
|
1354
|
+
maxtokens:parseInt(document.getElementById('maxtokens').value,10)||2048,
|
|
1355
|
+
topp:parseFloat(document.getElementById('topp').value)||0,
|
|
1356
|
+
model:document.getElementById('model').value.trim(),
|
|
1357
|
+
system:document.getElementById('system').value
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
function collectMessages(){
|
|
1361
|
+
const p=params(),out=[];
|
|
1362
|
+
if(p.system.trim())out.push({role:'system',content:p.system});
|
|
1363
|
+
const prov=curProv();
|
|
1364
|
+
let mediaAttached=false;
|
|
1365
|
+
document.getElementById('turns').childNodes.forEach(n=>{
|
|
1366
|
+
if(n.className&&n.className.indexOf('uturn')>=0){
|
|
1367
|
+
const t=n.dataset.locked?(n.dataset.content||''):n.querySelector('textarea').value;
|
|
1368
|
+
if(!t.trim()&&!pendingMedia.length)return;
|
|
1369
|
+
if(!t.trim()){out.push({role:'user',content:chatContentFor(prov,'(attached media)',pendingMedia)});mediaAttached=true;return;}
|
|
1370
|
+
if(pendingMedia.length&&!mediaAttached){
|
|
1371
|
+
const c=chatContentFor(prov,t,pendingMedia);
|
|
1372
|
+
out.push({role:'user',content:c===null?t:c});
|
|
1373
|
+
mediaAttached=true;
|
|
1374
|
+
}
|
|
1375
|
+
else if(t.trim())out.push({role:'user',content:t});
|
|
1376
|
+
}else if(n.className&&n.className.indexOf('aturn')>=0){
|
|
1377
|
+
out.push({role:'assistant',content:n.dataset.content||''});
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
return out;
|
|
1381
|
+
}
|
|
1382
|
+
function addUserTurn(text){
|
|
1383
|
+
const d=document.createElement('div');d.className='msg uturn';
|
|
1384
|
+
d.innerHTML='<span class="who">USER</span>';
|
|
1385
|
+
const t=document.createElement('textarea');t.className='ghost';t.rows=2;t.placeholder='Enter user message...';t.value=text||'';
|
|
1386
|
+
t.addEventListener('input',updateCode);
|
|
1387
|
+
d.appendChild(t);
|
|
1388
|
+
document.getElementById('turns').appendChild(d);
|
|
1389
|
+
return t;
|
|
1390
|
+
}
|
|
1391
|
+
function lockComposers(){
|
|
1392
|
+
document.getElementById('turns').childNodes.forEach(n=>{
|
|
1393
|
+
if(n.className&&n.className.indexOf('uturn')>=0&&!n.dataset.locked){
|
|
1394
|
+
const t=n.querySelector('textarea');if(!t)return;
|
|
1395
|
+
n.dataset.locked='1';n.dataset.content=t.value;
|
|
1396
|
+
n.removeChild(t);
|
|
1397
|
+
const b=document.createElement('div');b.className='body';b.textContent=n.dataset.content;
|
|
1398
|
+
n.appendChild(b);
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
function addAssistantTurn(text,isErr){
|
|
1403
|
+
const d=document.createElement('div');d.className='msg assistant aturn'+(isErr?' err':'');
|
|
1404
|
+
d.dataset.content=text;
|
|
1405
|
+
d.innerHTML='<span class="who">ASSISTANT</span>';
|
|
1406
|
+
const b=document.createElement('div');b.className='body';b.textContent=text;
|
|
1407
|
+
d.appendChild(b);
|
|
1408
|
+
document.getElementById('turns').appendChild(d);
|
|
1409
|
+
d.scrollIntoView({block:'nearest'});
|
|
1410
|
+
}
|
|
1411
|
+
let rawSnippet='';
|
|
1412
|
+
function pyStr(s){return esc(JSON.stringify(s));}
|
|
1413
|
+
function isStudio(){return document.getElementById('tabStudio').classList.contains('on');}
|
|
1414
|
+
function updateCode(){
|
|
1415
|
+
if(isStudio()){updateStudioCode();return;}
|
|
1416
|
+
const p=params(),msgs=collectMessages();
|
|
1417
|
+
const pid=document.getElementById('pool').value||'1';
|
|
1418
|
+
const url=location.origin+'/api/pools/'+pid+'/proxy';
|
|
1419
|
+
const lang=document.getElementById('codeLang').value;
|
|
1420
|
+
const prov=curProv(),model=p.model;
|
|
1421
|
+
const c=capsFor(prov,model);
|
|
1422
|
+
const bad=pendingMedia.filter(function(a){return (a.kind==='image'&&!c.vision)||(a.kind==='audio'&&!c.audioIn)||(a.kind==='video'&&!c.videoIn);});
|
|
1423
|
+
if(pendingMedia.length&&bad.length){
|
|
1424
|
+
const note=gateNote(bad[0].kind==='image'?'vision':bad[0].kind==='audio'?'audioIn':'videoIn',prov,model);
|
|
1425
|
+
rawSnippet='# '+note+'\\n# Remove the '+bad[0].kind+' attachment or switch to a capable model.\\n';
|
|
1426
|
+
document.getElementById('code').innerHTML='<span class="c">'+esc(rawSnippet)+'</span>';
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
const body={model:p.model,messages:msgs,temperature:p.temp,max_tokens:p.maxtokens,top_p:p.topp};
|
|
1430
|
+
const shortMsgs=JSON.stringify(msgs,function(k,v){return k==='data'&&typeof v==='string'&&v.length>64?v.slice(0,64)+'…<base64 '+v.length+' chars>':k==='url'&&typeof v==='string'&&v.indexOf('base64,')>=0?v.slice(0,80)+'…<base64>':v;});
|
|
1431
|
+
if(lang==='curl'){
|
|
1432
|
+
rawSnippet='curl -X POST '+url+' \\\\\\n -H "Content-Type: application/json" \\\\\\n -d '+pyStr(JSON.stringify({path:p.path,method:'POST',body}))+';\\n';
|
|
1433
|
+
document.getElementById('code').innerHTML='<span class="c"># runs through your pooled keys — auth injected server-side</span>\\n'+esc(rawSnippet);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
const lines=[];
|
|
1437
|
+
lines.push('<span class="k">import</span> requests\\n');
|
|
1438
|
+
lines.push('resp = requests.<span class="fn">post</span>(');
|
|
1439
|
+
lines.push(' '+pyStr(url)+',');
|
|
1440
|
+
lines.push(' json={');
|
|
1441
|
+
lines.push(' '+pyStr('path')+': '+pyStr(p.path)+',');
|
|
1442
|
+
lines.push(' '+pyStr('method')+': '+pyStr('POST')+',');
|
|
1443
|
+
lines.push(' '+pyStr('body')+': {');
|
|
1444
|
+
lines.push(' '+pyStr('model')+': '+pyStr(p.model)+',');
|
|
1445
|
+
lines.push(' '+pyStr('messages')+': '+esc(shortMsgs)+',');
|
|
1446
|
+
lines.push(' '+pyStr('temperature')+': <span class="n">'+p.temp+'</span>,');
|
|
1447
|
+
lines.push(' '+pyStr('max_tokens')+': <span class="n">'+p.maxtokens+'</span>,');
|
|
1448
|
+
lines.push(' '+pyStr('top_p')+': <span class="n">'+p.topp+'</span>,');
|
|
1449
|
+
lines.push(' },');
|
|
1450
|
+
lines.push(' },');
|
|
1451
|
+
lines.push(' timeout=<span class="n">120</span>,');
|
|
1452
|
+
lines.push(')');
|
|
1453
|
+
lines.push('data = resp.<span class="fn">json</span>()');
|
|
1454
|
+
lines.push('<span class="fn">print</span>(data[<span class="s">'+pyStr('status')+'</span>])');
|
|
1455
|
+
lines.push('<span class="fn">print</span>(data[<span class="s">'+pyStr('body')+'</span>])');
|
|
1456
|
+
document.getElementById('code').innerHTML=lines.join('\\n');
|
|
1457
|
+
rawSnippet='import requests\\n\\nresp = requests.post(\\n '+JSON.stringify(url)+',\\n json='+JSON.stringify({path:p.path,method:'POST',body},null,4)+',\\n timeout=120,\\n)\\ndata = resp.json()\\nprint(data["status"])\\nprint(data["body"])\\n';
|
|
1458
|
+
}
|
|
1459
|
+
function studioMode(){if(document.getElementById('modeAudio').classList.contains('on'))return 'audio';if(document.getElementById('modeVideo').classList.contains('on'))return 'video';return 'image';}
|
|
1460
|
+
function setStudioMode(m){
|
|
1461
|
+
document.getElementById('modeImage').classList.toggle('on',m==='image');
|
|
1462
|
+
document.getElementById('modeAudio').classList.toggle('on',m==='audio');
|
|
1463
|
+
document.getElementById('modeVideo').classList.toggle('on',m==='video');
|
|
1464
|
+
document.getElementById('studioImage').hidden=m!=='image';
|
|
1465
|
+
document.getElementById('studioAudio').hidden=m!=='audio';
|
|
1466
|
+
document.getElementById('studioVideo').hidden=m!=='video';
|
|
1467
|
+
updateStudioModes();updateCode();
|
|
1468
|
+
}
|
|
1469
|
+
function updateStudioModes(){
|
|
1470
|
+
const prov=curProv();
|
|
1471
|
+
const im=document.getElementById('imgModel').value.trim()||AUD_MODELS[prov]||'';
|
|
1472
|
+
const c=capsFor(prov,im);
|
|
1473
|
+
const mi=document.getElementById('modeImage'),ma=document.getElementById('modeAudio'),mv=document.getElementById('modeVideo');
|
|
1474
|
+
const note=document.getElementById('studioNote');
|
|
1475
|
+
mi.classList.toggle('off',!c.imageOut);ma.classList.toggle('off',!c.audioOut);mv.classList.toggle('off',!c.videoOut);
|
|
1476
|
+
mi.disabled=!c.imageOut;ma.disabled=!c.audioOut;mv.disabled=!c.videoOut;
|
|
1477
|
+
const m=studioMode();
|
|
1478
|
+
const need=m==='image'?'imageOut':m==='audio'?'audioOut':'videoOut';
|
|
1479
|
+
if(!c[need]){
|
|
1480
|
+
note.hidden=false;note.className='cap-note';
|
|
1481
|
+
note.textContent=gateNote(need,prov,im)+' — switch studio mode or pool.';
|
|
1482
|
+
}else{note.hidden=true;note.textContent='';}
|
|
1483
|
+
if(!c.imageOut&&!c.audioOut&&!c.videoOut){note.hidden=false;note.className='cap-note';note.textContent=gateNote('imageOut',prov,im)+' — this pool has no generation output.';}
|
|
1484
|
+
}
|
|
1485
|
+
function studioParams(){
|
|
1486
|
+
const m=studioMode();
|
|
1487
|
+
if(m==='audio')return {mode:m,prompt:document.getElementById('audText').value,model:document.getElementById('audModel').value.trim(),voice:document.getElementById('audVoice').value.trim()||'alloy',format:document.getElementById('audFormat').value};
|
|
1488
|
+
if(m==='video')return {mode:m,prompt:document.getElementById('vidPrompt').value,model:document.getElementById('vidModel').value.trim(),duration:document.getElementById('vidDur').value,resolution:document.getElementById('vidRes').value};
|
|
1489
|
+
return {
|
|
1490
|
+
mode:m,
|
|
1491
|
+
prompt:document.getElementById('imgPrompt').value,
|
|
1492
|
+
model:document.getElementById('imgModel').value.trim(),
|
|
1493
|
+
size:document.getElementById('imgSize').value
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
function studioPayload(s){
|
|
1497
|
+
if(s.mode==='audio')return {path:'/audio/speech',body:{model:s.model,input:s.prompt,voice:s.voice,response_format:s.format}};
|
|
1498
|
+
if(s.mode==='video')return {path:'/videos/generations',body:{model:s.model,prompt:s.prompt,duration:s.duration,resolution:s.resolution,n:1}};
|
|
1499
|
+
return {path:'/images/generations',body:{model:s.model,prompt:s.prompt,size:s.size,n:1}};
|
|
1500
|
+
}
|
|
1501
|
+
function updateStudioCode(){
|
|
1502
|
+
const s=studioParams();
|
|
1503
|
+
const pid=document.getElementById('pool').value||'1';
|
|
1504
|
+
const url=location.origin+'/api/pools/'+pid+'/proxy';
|
|
1505
|
+
const lang=document.getElementById('codeLang').value;
|
|
1506
|
+
const pl=studioPayload(s);
|
|
1507
|
+
const body=pl.body;
|
|
1508
|
+
if(lang==='curl'){
|
|
1509
|
+
rawSnippet='curl -X POST '+url+' \\\\\\n -H "Content-Type: application/json" \\\\\\n -d '+pyStr(JSON.stringify({path:pl.path,method:'POST',body}))+';\\n';
|
|
1510
|
+
document.getElementById('code').innerHTML='<span class="c"># runs through your pooled keys — auth injected server-side</span>\\n'+esc(rawSnippet);
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
const lines=[];
|
|
1514
|
+
lines.push('<span class="k">import</span> requests\\n');
|
|
1515
|
+
lines.push('resp = requests.<span class="fn">post</span>(');
|
|
1516
|
+
lines.push(' '+pyStr(url)+',');
|
|
1517
|
+
lines.push(' json={');
|
|
1518
|
+
lines.push(' '+pyStr('path')+': '+pyStr(pl.path)+',');
|
|
1519
|
+
lines.push(' '+pyStr('method')+': '+pyStr('POST')+',');
|
|
1520
|
+
lines.push(' '+pyStr('body')+': '+esc(JSON.stringify(body,null,4))+',');
|
|
1521
|
+
lines.push(' },');
|
|
1522
|
+
lines.push(' timeout=<span class="n">120</span>,');
|
|
1523
|
+
lines.push(')');
|
|
1524
|
+
lines.push('data = resp.<span class="fn">json</span>()');
|
|
1525
|
+
lines.push('<span class="fn">print</span>(data[<span class="s">'+pyStr('status')+'</span>])');
|
|
1526
|
+
lines.push('<span class="fn">print</span>(data[<span class="s">'+pyStr('body')+'</span>])');
|
|
1527
|
+
document.getElementById('code').innerHTML=lines.join('\\n');
|
|
1528
|
+
rawSnippet='import requests\\n\\nresp = requests.post(\\n '+JSON.stringify(url)+',\\n json='+JSON.stringify({path:pl.path,method:'POST',body},null,4)+',\\n timeout=120,\\n)\\ndata = resp.json()\\nprint(data["status"])\\nprint(data["body"])\\n';
|
|
1529
|
+
}
|
|
1530
|
+
function mediaResultBox(id){return document.getElementById(id);}
|
|
1531
|
+
function appendMediaOut(boxId,kind,src,mime,caption,downloadName){
|
|
1532
|
+
const box=mediaResultBox(boxId);
|
|
1533
|
+
const wrap=document.createElement('div');
|
|
1534
|
+
const el=document.createElement(kind==='audio'?'audio':'video');
|
|
1535
|
+
el.controls=true;el.src=src;
|
|
1536
|
+
if(kind==='video'){el.preload='metadata';}
|
|
1537
|
+
wrap.appendChild(el);
|
|
1538
|
+
const cap=document.createElement('div');cap.style.cssText='font-size:12px;color:var(--ink-soft)';cap.textContent=caption;
|
|
1539
|
+
wrap.appendChild(cap);
|
|
1540
|
+
const a=document.createElement('a');a.className='dl';a.href=src;a.download=downloadName;a.textContent='Download '+kind;
|
|
1541
|
+
wrap.appendChild(a);
|
|
1542
|
+
box.prepend(wrap);
|
|
1543
|
+
}
|
|
1544
|
+
function audioMimeFor(fmt){return fmt==='wav'?'audio/wav':fmt==='opus'?'audio/ogg':'audio/mpeg';}
|
|
1545
|
+
function detectMediaInBody(r){
|
|
1546
|
+
if(r.encoding==='base64'&&r.body){
|
|
1547
|
+
const ct=String(r.contentType||'');
|
|
1548
|
+
if(/audio/i.test(ct))return {kind:'audio',src:'data:'+ct+';base64,'+r.body,mime:ct};
|
|
1549
|
+
if(/video/i.test(ct))return {kind:'video',src:'data:'+ct+';base64,'+r.body,mime:ct};
|
|
1550
|
+
if(/image/i.test(ct))return {kind:'image',src:'data:'+ct+';base64,'+r.body,mime:ct};
|
|
1551
|
+
return {kind:'audio',src:'data:audio/mpeg;base64,'+r.body,mime:'audio/mpeg'};
|
|
1552
|
+
}
|
|
1553
|
+
try{
|
|
1554
|
+
const d=JSON.parse(r.body);
|
|
1555
|
+
const first=d.data&&d.data[0];
|
|
1556
|
+
if(first){
|
|
1557
|
+
if(first.b64_json){
|
|
1558
|
+
const kind=studioMode()==='audio'?'audio':studioMode()==='video'?'video':'image';
|
|
1559
|
+
const mime=kind==='audio'?'audio/mpeg':kind==='video'?'video/mp4':'image/png';
|
|
1560
|
+
return {kind:kind,src:'data:'+mime+';base64,'+first.b64_json,mime:mime,json:true};
|
|
1561
|
+
}
|
|
1562
|
+
if(first.url)return {kind:studioMode(),src:first.url,mime:'',json:true,caption:first.revised_prompt||''};
|
|
1563
|
+
}
|
|
1564
|
+
if(d.audio||d.audio_base64||d.data_base64){
|
|
1565
|
+
const b=d.audio||d.audio_base64||d.data_base64;
|
|
1566
|
+
return {kind:'audio',src:'data:audio/mpeg;base64,'+b,mime:'audio/mpeg',json:true};
|
|
1567
|
+
}
|
|
1568
|
+
}catch{}
|
|
1569
|
+
return null;
|
|
1570
|
+
}
|
|
1571
|
+
async function sendStudio(){
|
|
1572
|
+
err('');
|
|
1573
|
+
const s=studioParams();
|
|
1574
|
+
const pl=studioPayload(s);
|
|
1575
|
+
if(s.mode==='image'&&!s.prompt.trim()){err('Describe the image first.');return;}
|
|
1576
|
+
if(s.mode!=='image'&&!s.prompt.trim()){err(s.mode==='audio'?'Type text to synthesize first.':'Describe the video first.');return;}
|
|
1577
|
+
if(!s.model){err('Enter a model id.');return;}
|
|
1578
|
+
const prov=curProv();
|
|
1579
|
+
const need=s.mode==='image'?'imageOut':s.mode==='audio'?'audioOut':'videoOut';
|
|
1580
|
+
if(!capsFor(prov,s.model)[need]){err(gateNote(need,prov,s.model));return;}
|
|
1581
|
+
const pid=Number(document.getElementById('pool').value);
|
|
1582
|
+
const btn=s.mode==='image'?document.getElementById('generate'):s.mode==='audio'?document.getElementById('genAudio'):document.getElementById('genVideo');
|
|
1583
|
+
const label=btn.innerHTML;
|
|
1584
|
+
btn.disabled=true;btn.textContent=s.mode==='image'?'Generating…':s.mode==='audio'?'Synthesizing…':'Rendering…';
|
|
1585
|
+
try{
|
|
1586
|
+
const r=await j(await fetch('/api/pools/'+pid+'/proxy',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({path:pl.path,method:'POST',body:pl.body})}));
|
|
1587
|
+
if(r.error){err(r.error);return;}
|
|
1588
|
+
const hit=detectMediaInBody(r);
|
|
1589
|
+
if(!hit){err('No media in upstream response (status '+r.status+').');return;}
|
|
1590
|
+
if(s.mode==='image'){
|
|
1591
|
+
let src=hit.src;
|
|
1592
|
+
if(hit.kind!=='image'&&!hit.json){err('Unexpected media kind in response.');return;}
|
|
1593
|
+
const gal=document.getElementById('gallery');
|
|
1594
|
+
const fig=document.createElement('figure');
|
|
1595
|
+
const img=document.createElement('img');img.src=src;img.alt=s.prompt;img.loading='lazy';
|
|
1596
|
+
const cap=document.createElement('figcaption');cap.textContent=s.model+' · '+(hit.caption||s.prompt);
|
|
1597
|
+
fig.appendChild(img);fig.appendChild(cap);
|
|
1598
|
+
gal.prepend(fig);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
appendMediaOut(s.mode==='audio'?'audioOut':'videoOut',s.mode,hit.src,hit.mime,s.model+' · '+s.prompt.slice(0,60),s.mode+'-output');
|
|
1602
|
+
}finally{btn.disabled=false;btn.innerHTML=label;updateCode();}
|
|
1603
|
+
}
|
|
1604
|
+
async function send(){
|
|
1605
|
+
err('');
|
|
1606
|
+
const btn=document.getElementById('submit');
|
|
1607
|
+
const p=params(),msgs=collectMessages();
|
|
1608
|
+
if(!msgs.some(m=>m.role==='user')){err('Type a user message first.');return;}
|
|
1609
|
+
if(!p.model){err('Enter a model id.');return;}
|
|
1610
|
+
const prov=curProv();
|
|
1611
|
+
const c=capsFor(prov,p.model);
|
|
1612
|
+
const bad=pendingMedia.filter(function(a){return (a.kind==='image'&&!c.vision)||(a.kind==='audio'&&!c.audioIn)||(a.kind==='video'&&!c.videoIn);});
|
|
1613
|
+
if(bad.length){err(gateNote(bad[0].kind==='image'?'vision':bad[0].kind==='audio'?'audioIn':'videoIn',prov,p.model));return;}
|
|
1614
|
+
const pid=Number(document.getElementById('pool').value);
|
|
1615
|
+
btn.disabled=true;
|
|
1616
|
+
lockComposers();
|
|
1617
|
+
try{
|
|
1618
|
+
const r=await j(await fetch('/api/pools/'+pid+'/proxy',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({path:p.path,method:'POST',body:{model:p.model,messages:msgs,temperature:p.temp,max_tokens:p.maxtokens,top_p:p.topp}})}));
|
|
1619
|
+
pendingMedia=[];renderMediaPrev();updateMediaUI();
|
|
1620
|
+
if(r.error){addAssistantTurn(r.error,true);addUserTurn('').focus();return;}
|
|
1621
|
+
let text=r.body||'';
|
|
1622
|
+
try{
|
|
1623
|
+
const d=JSON.parse(text);
|
|
1624
|
+
text=(d.choices&&d.choices[0]&&(d.choices[0].message||{}).content)||(d.content&&d.content[0]&&d.content[0].text)||text;
|
|
1625
|
+
if(typeof text!=='string')text=JSON.stringify(text,null,2);
|
|
1626
|
+
}catch{}
|
|
1627
|
+
addAssistantTurn(text,r.status<200||r.status>=300);
|
|
1628
|
+
addUserTurn('').focus();
|
|
1629
|
+
}finally{btn.disabled=false;updateCode();}
|
|
1630
|
+
}
|
|
1631
|
+
async function init(){
|
|
1632
|
+
initDropdowns();
|
|
1633
|
+
const provs=await j(await fetch('/api/providers'));
|
|
1634
|
+
const pools=await j(await fetch('/api/pools'));
|
|
1635
|
+
const sel=document.getElementById('pool');
|
|
1636
|
+
sel.innerHTML='';
|
|
1637
|
+
window._pgPmap={};
|
|
1638
|
+
const pmap=window._pgPmap;
|
|
1639
|
+
for(const p of pools){
|
|
1640
|
+
const ks=await j(await fetch('/api/pools/'+p.id+'/keys'));
|
|
1641
|
+
const prov=provs.find(x=>x.id===p.provider);
|
|
1642
|
+
pmap[p.id]=p.provider;
|
|
1643
|
+
const o=document.createElement('option');
|
|
1644
|
+
o.value=p.id;
|
|
1645
|
+
o.textContent=(prov?prov.name:p.provider)+' · '+p.name+' ('+ks.length+' keys)';
|
|
1646
|
+
o.disabled=ks.length===0;
|
|
1647
|
+
sel.appendChild(o);
|
|
1648
|
+
}
|
|
1649
|
+
if(!sel.options.length){err('No pools yet — gather keys for a provider first.');document.getElementById('submit').disabled=true;return;}
|
|
1650
|
+
const pre=new URLSearchParams(location.search).get('pool');
|
|
1651
|
+
if(pre)sel.value=pre;
|
|
1652
|
+
if(sel.selectedIndex<0||sel.options[sel.selectedIndex].disabled)sel.selectedIndex=[...sel.options].findIndex(o=>!o.disabled);
|
|
1653
|
+
ddSync('pool');
|
|
1654
|
+
const applyDefault=()=>{
|
|
1655
|
+
const m=document.getElementById('model');
|
|
1656
|
+
if(m.value===lastDefault){m.value=MODELS[pmap[sel.value]]||'';lastDefault=m.value;}
|
|
1657
|
+
};
|
|
1658
|
+
const applyImgDefault=()=>{
|
|
1659
|
+
const m=document.getElementById('imgModel');
|
|
1660
|
+
if(m.value===lastImgDefault){m.value=IMG_MODELS[pmap[sel.value]]||'';lastImgDefault=m.value;}
|
|
1661
|
+
};
|
|
1662
|
+
const applyAudDefault=()=>{
|
|
1663
|
+
const m=document.getElementById('audModel');
|
|
1664
|
+
if(m.value===lastAudDefault){m.value=AUD_MODELS[pmap[sel.value]]||'';lastAudDefault=m.value;}
|
|
1665
|
+
};
|
|
1666
|
+
const applyVidDefault=()=>{
|
|
1667
|
+
const m=document.getElementById('vidModel');
|
|
1668
|
+
if(m.value===lastVidDefault){m.value=VID_MODELS[pmap[sel.value]]||'';lastVidDefault=m.value;}
|
|
1669
|
+
};
|
|
1670
|
+
applyDefault();
|
|
1671
|
+
applyImgDefault();
|
|
1672
|
+
applyAudDefault();
|
|
1673
|
+
applyVidDefault();
|
|
1674
|
+
lastDefault=document.getElementById('model').value;
|
|
1675
|
+
lastImgDefault=document.getElementById('imgModel').value;
|
|
1676
|
+
lastAudDefault=document.getElementById('audModel').value;
|
|
1677
|
+
lastVidDefault=document.getElementById('vidModel').value;
|
|
1678
|
+
sel.onchange=()=>{applyDefault();applyImgDefault();applyAudDefault();applyVidDefault();lastDefault=document.getElementById('model').value;lastImgDefault=document.getElementById('imgModel').value;lastAudDefault=document.getElementById('audModel').value;lastVidDefault=document.getElementById('vidModel').value;updateMediaUI();updateStudioModes();updateCode();};
|
|
1679
|
+
addUserTurn('');
|
|
1680
|
+
['system','model','path','temp','maxtokens','topp'].forEach(id=>document.getElementById(id).addEventListener('input',()=>{if(id==='model')lastDefault='⌁';if(id==='model'){updateMediaUI();updateStudioModes();}updateCode();}));
|
|
1681
|
+
['imgPrompt','imgModel','imgSize'].forEach(id=>document.getElementById(id).addEventListener('input',()=>{if(id==='imgModel')lastImgDefault='⌁';updateStudioModes();updateCode();}));
|
|
1682
|
+
['audText','audModel','audVoice','audFormat'].forEach(id=>document.getElementById(id).addEventListener('input',()=>{if(id==='audModel')lastAudDefault='⌁';updateStudioModes();updateCode();}));
|
|
1683
|
+
['vidPrompt','vidModel','vidDur','vidRes'].forEach(id=>document.getElementById(id).addEventListener('input',()=>{if(id==='vidModel')lastVidDefault='⌁';updateStudioModes();updateCode();}));
|
|
1684
|
+
document.getElementById('generate').onclick=()=>sendStudio();
|
|
1685
|
+
document.getElementById('genAudio').onclick=()=>sendStudio();
|
|
1686
|
+
document.getElementById('genVideo').onclick=()=>sendStudio();
|
|
1687
|
+
document.getElementById('modeImage').onclick=()=>setStudioMode('image');
|
|
1688
|
+
document.getElementById('modeAudio').onclick=function(){if(!this.disabled)setStudioMode('audio');};
|
|
1689
|
+
document.getElementById('modeVideo').onclick=function(){if(!this.disabled)setStudioMode('video');};
|
|
1690
|
+
document.getElementById('attachImg').onclick=()=>document.getElementById('fileImg').click();
|
|
1691
|
+
document.getElementById('attachAudio').onclick=()=>document.getElementById('fileAudio').click();
|
|
1692
|
+
document.getElementById('attachVideo').onclick=()=>document.getElementById('fileVideo').click();
|
|
1693
|
+
document.getElementById('fileImg').onchange=e=>{if(e.target.files[0])addMediaFile(e.target.files[0],'image');e.target.value='';};
|
|
1694
|
+
document.getElementById('fileAudio').onchange=e=>{if(e.target.files[0])addMediaFile(e.target.files[0],'audio');e.target.value='';};
|
|
1695
|
+
document.getElementById('fileVideo').onchange=e=>{if(e.target.files[0])addMediaFile(e.target.files[0],'video');e.target.value='';};
|
|
1696
|
+
document.getElementById('recAudio').onclick=()=>toggleRec();
|
|
1697
|
+
document.getElementById('clear').onclick=()=>{document.getElementById('turns').innerHTML='';pendingMedia=[];renderMediaPrev();addUserTurn('');updateMediaUI();updateCode();};
|
|
1698
|
+
document.getElementById('codeLang').onchange=updateCode;
|
|
1699
|
+
document.getElementById('submit').onclick=send;
|
|
1700
|
+
document.getElementById('addMsg').onclick=()=>{addUserTurn('').focus();};
|
|
1701
|
+
const copy=()=>{navigator.clipboard.writeText(rawSnippet).catch(()=>{});};
|
|
1702
|
+
document.getElementById('copyCode').onclick=copy;
|
|
1703
|
+
document.getElementById('copyTop').onclick=copy;
|
|
1704
|
+
document.getElementById('hideCode').onclick=e=>{const g=document.getElementById('grid');g.classList.toggle('nocode');e.target.textContent=g.classList.contains('nocode')?'</> Show code':'</> Hide code';};
|
|
1705
|
+
document.getElementById('tabChat').onclick=()=>setTab(true);
|
|
1706
|
+
document.getElementById('tabStudio').onclick=()=>setTab(false);
|
|
1707
|
+
document.addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){e.preventDefault();if(document.getElementById('tabStudio').classList.contains('on'))sendStudio();else send();}});
|
|
1708
|
+
updateMediaUI();updateStudioModes();updateCode();
|
|
1709
|
+
}
|
|
1710
|
+
function setTab(chat){
|
|
1711
|
+
document.getElementById('tabChat').classList.toggle('on',chat);
|
|
1712
|
+
document.getElementById('tabStudio').classList.toggle('on',!chat);
|
|
1713
|
+
document.getElementById('grid').hidden=!chat;
|
|
1714
|
+
document.getElementById('studioPane').hidden=chat;
|
|
1715
|
+
updateCode();
|
|
1716
|
+
}
|
|
1717
|
+
init();
|
|
1718
|
+
</script></div></div>${shellJs}</body></html>`;
|
|
1719
|
+
}
|
|
1720
|
+
export function providerPage() {
|
|
1721
|
+
return `<!doctype html>
|
|
1722
|
+
<html lang="en"><head><meta charset="utf-8"/>
|
|
1723
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
1724
|
+
<title>Provider — pool-anything</title>
|
|
1725
|
+
<link rel="icon" type="image/png" href="/logo.png" />
|
|
1726
|
+
<style>
|
|
1727
|
+
*{box-sizing:border-box}body{margin:0;font-family:var(--font-sans);background:var(--paper-2);color:var(--ink)}
|
|
1728
|
+
${shellCss}
|
|
1729
|
+
${sharedLayoutCss}
|
|
1730
|
+
.chead{display:flex;align-items:center;gap:10px}.chead img{width:28px;height:28px}
|
|
1731
|
+
.title-wrap{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
|
|
1732
|
+
.title-wrap b{font-size:16px;line-height:1.25}
|
|
1733
|
+
.use-stat{font-size:12.5px;color:var(--ink-soft);font-variant-numeric:tabular-nums}
|
|
1734
|
+
.pill{margin-left:auto;font-size:11px;font-weight:600;background:var(--paper-2);border-radius:999px;padding:3px 10px;white-space:nowrap;flex-shrink:0}
|
|
1735
|
+
.hint{font-size:13px;color:var(--ink-soft);margin:10px 0 0}
|
|
1736
|
+
.row{display:flex;gap:8px;align-items:center}
|
|
1737
|
+
.err{color:var(--danger-ink);font-size:13px;min-height:18px}
|
|
1738
|
+
.krow{display:flex;align-items:center;gap:8px;background:var(--paper-2);border-radius:10px;padding:8px 10px;font-size:13px;animation:fadeSlide .25s ease both}
|
|
1739
|
+
.krow .meta{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1740
|
+
.krow .use{font-size:11px;color:var(--ink-soft);margin:0;white-space:nowrap}
|
|
1741
|
+
.krow button{border:1px solid var(--line);background:var(--card);color:var(--ink);border-radius:8px;min-height:30px;padding:0 12px;font-size:13px;cursor:pointer}
|
|
1742
|
+
.slotrow{display:flex;gap:8px}
|
|
1743
|
+
.slotrow input{flex:1;min-width:0;border:1px solid var(--line);border-radius:8px;min-height:34px;padding:0 12px;font-size:14px}
|
|
1744
|
+
.slotrow button{border:1px solid var(--ink);background:var(--ink);color:var(--card);border-radius:8px;min-height:34px;padding:0 12px;font-size:14px;cursor:pointer}
|
|
1745
|
+
.pmore{border:0;background:none;cursor:pointer;font-size:13px;color:var(--ink-soft);padding:0}
|
|
1746
|
+
${docsComponentsCss}
|
|
1747
|
+
</style></head><body><div class="shell" id="shell">${shellNav("")}<div class="content">${sharedHeader}<main>
|
|
1748
|
+
<div class="wrap">
|
|
1749
|
+
<div class="page-head"><a href="/"><img src="/logo.png" alt="pool-anything" width="28" height="28"/></a><h1 id="pTitle">Provider</h1></div>
|
|
1750
|
+
<a class="back" id="crumbAll" href="/#browseAll">← all providers</a>
|
|
1751
|
+
<div class="err" id="err"></div>
|
|
1752
|
+
<div class="page-card" id="pcard" hidden>
|
|
1753
|
+
<div class="chead"><img id="pLogo" alt=""/><div class="title-wrap"><b id="pName"></b><span class="use-stat" id="pUse"></span></div><span class="pill" id="pQuota"></span></div>
|
|
1754
|
+
<p class="hint" id="pHint"></p>
|
|
1755
|
+
<p class="sec-label" id="keyLabel" style="margin:12px 0 8px">Keys</p>
|
|
1756
|
+
<div id="krows" style="display:flex;flex-direction:column;gap:6px"></div>
|
|
1757
|
+
<div id="pslots" style="display:flex;flex-direction:column;gap:8px;margin-top:10px"></div>
|
|
1758
|
+
<div style="margin-top:10px"><button class="pmore" id="pmore" type="button">+ key slot</button></div>
|
|
1759
|
+
</div>
|
|
1760
|
+
<div class="support-grid" id="supportGrid" hidden>
|
|
1761
|
+
<div class="support-card"><p class="sec-label">Related pools</p><div id="related"></div></div>
|
|
1762
|
+
<div class="support-card"><p class="sec-label">Use this pool</p><code id="proxySnippet"></code><div style="display:flex;gap:12px;flex-wrap:wrap"><a id="tryPlay" href="/playground">Try in Playground →</a><a href="/keys">Manage all keys →</a><a href="/analytics">View analytics →</a></div><small>Requests rotate across this provider's keys automatically.</small></div>
|
|
1763
|
+
</div>
|
|
1764
|
+
</div>
|
|
1765
|
+
</main><script>
|
|
1766
|
+
${clientCoreJs}
|
|
1767
|
+
const id=(location.pathname.split('/')[2]||'');
|
|
1768
|
+
let pool=null,keyTotal=0,usageMap={},cur=null,allProvs=[],allPools=[];
|
|
1769
|
+
async function init(){
|
|
1770
|
+
if(!/^[a-z0-9-]+$/.test(id)){err('Unknown provider.');return;}
|
|
1771
|
+
const provs=await j(await fetch('/api/providers'));
|
|
1772
|
+
allProvs=provs;
|
|
1773
|
+
const p=provs.find(x=>x.id===id);
|
|
1774
|
+
cur=p;
|
|
1775
|
+
if(!p){err('Unknown provider.');return;}
|
|
1776
|
+
document.title=p.name+' — pool-anything';
|
|
1777
|
+
document.getElementById('pTitle').textContent=p.name;
|
|
1778
|
+
const logo=document.getElementById('pLogo');
|
|
1779
|
+
logoSrc(logo,p.logoFile);
|
|
1780
|
+
document.getElementById('pName').textContent=p.name;
|
|
1781
|
+
document.getElementById('pQuota').textContent=p.quota;
|
|
1782
|
+
document.getElementById('pHint').textContent=p.hint+(p.poolable===false?' · control-plane only, not poolable for data':'');
|
|
1783
|
+
document.getElementById('pcard').hidden=false;
|
|
1784
|
+
const pools=await j(await fetch('/api/pools'));
|
|
1785
|
+
allPools=Array.isArray(pools)?pools:[];
|
|
1786
|
+
pool=allPools.find(x=>x.provider===p.id);
|
|
1787
|
+
if(!pool)pool=await j(await fetch('/api/pools',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({provider:p.id,name:p.name+' pool'})}));
|
|
1788
|
+
if(!pool||pool.error){err((pool&&pool.error)||'Could not load pool.');return;}
|
|
1789
|
+
refresh();
|
|
1790
|
+
}
|
|
1791
|
+
async function refresh(){
|
|
1792
|
+
const [ks,us]=await Promise.all([j(await fetch('/api/pools/'+pool.id+'/keys')),j(await fetch('/api/pools/'+pool.id+'/usage'))]);
|
|
1793
|
+
usageMap={};
|
|
1794
|
+
(us.perKey||[]).forEach(k=>{usageMap[k.id]=k.used;});
|
|
1795
|
+
const used=us.quota?(us.usedInWindow ?? us.used):us.used;
|
|
1796
|
+
const nKeys=ks.length;
|
|
1797
|
+
const keyWord=nKeys===1?'key':'keys';
|
|
1798
|
+
document.getElementById('pUse').textContent=us.quota
|
|
1799
|
+
?used+' / '+(us.quota*nKeys)+' tokens used across '+nKeys+' '+keyWord+' this period'
|
|
1800
|
+
:used+' tokens used across '+nKeys+' '+keyWord+' this period';
|
|
1801
|
+
document.getElementById('keyLabel').textContent='Keys · '+nKeys;
|
|
1802
|
+
renderSupport(nKeys);
|
|
1803
|
+
const box=document.getElementById('krows');box.innerHTML='';
|
|
1804
|
+
ks.forEach((k,i)=>{
|
|
1805
|
+
const row=document.createElement('div');
|
|
1806
|
+
row.className='krow';row.style.animationDelay=Math.min(i*40,300)+'ms';
|
|
1807
|
+
const m=document.createElement('span');m.className='meta';m.textContent=k.label+' · '+k.masked;row.appendChild(m);
|
|
1808
|
+
const u=document.createElement('span');u.className='use';u.textContent=(usageMap[k.id]||0)+' tokens';row.appendChild(u);
|
|
1809
|
+
const v=document.createElement('button');v.textContent='View';v.type='button';
|
|
1810
|
+
v.onclick=async()=>{
|
|
1811
|
+
if(v.dataset.open){delete v.dataset.open;v.textContent='View';m.textContent=k.label+' · '+k.masked;return;}
|
|
1812
|
+
const full=await j(await fetch('/api/pools/'+pool.id+'/keys/'+k.id));
|
|
1813
|
+
if(full.error){err(full.error);return;}
|
|
1814
|
+
v.dataset.open='1';v.textContent='Hide';
|
|
1815
|
+
m.textContent=k.label+' · '+full.api_key+(full.credentials?' · '+Object.entries(full.credentials).map(([a,b])=>a+'='+b).join(' '):'');
|
|
1816
|
+
};
|
|
1817
|
+
const e=document.createElement('button');e.textContent='Edit';e.type='button';
|
|
1818
|
+
e.onclick=async()=>{
|
|
1819
|
+
const full=await j(await fetch('/api/pools/'+pool.id+'/keys/'+k.id));
|
|
1820
|
+
if(full.error){err(full.error);return;}
|
|
1821
|
+
row.innerHTML='';
|
|
1822
|
+
const f=document.createElement('div');f.style.cssText='display:flex;gap:6px;flex:1;flex-wrap:wrap';
|
|
1823
|
+
f.innerHTML='<input value="'+esc(full.label)+'" style="flex:1;min-width:70px"/>';
|
|
1824
|
+
const credFields=(full.credentials&&Object.keys(full.credentials).length?Object.keys(full.credentials):(k.credentials&&Object.keys(k.credentials).length?Object.keys(k.credentials):((cur.keyFields&&cur.keyFields.length?cur.keyFields:['api_key']))));
|
|
1825
|
+
const kins=credFields.map(cf=>{const i=document.createElement('input');i.type='password';i.placeholder=cf;i.value=(full.credentials&&full.credentials[cf])||(cf==='api_key'?full.api_key:'');i.style.cssText='flex:2;min-width:100px';f.appendChild(i);return i;});
|
|
1826
|
+
const [il]=f.querySelectorAll('input');
|
|
1827
|
+
const sv=document.createElement('button');sv.textContent='Save';sv.type='button';
|
|
1828
|
+
sv.onclick=async()=>{
|
|
1829
|
+
const patch={label:il.value};
|
|
1830
|
+
if(credFields.length===1&&credFields[0]==='api_key')patch.api_key=kins[0].value;
|
|
1831
|
+
else patch.credentials=Object.fromEntries(credFields.map((cf,i)=>[cf,kins[i].value]));
|
|
1832
|
+
const r=await j(await fetch('/api/pools/'+pool.id+'/keys/'+k.id,{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify(patch)}));
|
|
1833
|
+
if(r.error){err(r.error);return;}
|
|
1834
|
+
refresh();
|
|
1835
|
+
};
|
|
1836
|
+
const c=document.createElement('button');c.textContent='Cancel';c.type='button';
|
|
1837
|
+
c.onclick=()=>refresh();
|
|
1838
|
+
row.appendChild(f);row.appendChild(sv);row.appendChild(c);
|
|
1839
|
+
};
|
|
1840
|
+
const d=document.createElement('button');d.textContent='Remove';d.type='button';
|
|
1841
|
+
d.onclick=async()=>{await fetch('/api/pools/'+pool.id+'/keys/'+k.id,{method:'DELETE'});refresh();};
|
|
1842
|
+
row.appendChild(v);row.appendChild(e);row.appendChild(d);box.appendChild(row);
|
|
1843
|
+
});
|
|
1844
|
+
keyTotal=ks.length;
|
|
1845
|
+
if(!document.querySelector('#pslots .slotrow'))addSlot();
|
|
1846
|
+
}
|
|
1847
|
+
function renderSupport(nKeys){
|
|
1848
|
+
try{
|
|
1849
|
+
document.getElementById('supportGrid').hidden=false;
|
|
1850
|
+
document.getElementById('proxySnippet').textContent='POST /api/pools/'+pool.id+'/proxy';
|
|
1851
|
+
document.getElementById('tryPlay').href='/playground?pool='+pool.id;
|
|
1852
|
+
const rel=document.getElementById('related');rel.innerHTML='';
|
|
1853
|
+
const others=allPools.filter(x=>x.provider!==id);
|
|
1854
|
+
if(!others.length){
|
|
1855
|
+
const d=document.createElement('div');d.style.cssText='font-size:13px;color:var(--ink-soft)';
|
|
1856
|
+
d.textContent='No other pools yet — gather keys for another provider from Home.';
|
|
1857
|
+
rel.appendChild(d);
|
|
1858
|
+
}else{
|
|
1859
|
+
others.slice(0,5).forEach(x=>{
|
|
1860
|
+
const prov=allProvs.find(v=>v.id===x.provider);
|
|
1861
|
+
const row=document.createElement('div');row.className='rel-row';
|
|
1862
|
+
const a=document.createElement('a');a.href='/provider/'+x.provider;a.textContent=(prov?prov.name:x.provider)+' · '+x.name;
|
|
1863
|
+
row.appendChild(a);
|
|
1864
|
+
rel.appendChild(row);
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
}catch{}
|
|
1868
|
+
}
|
|
1869
|
+
function addSlot(){
|
|
1870
|
+
const n=keyTotal+document.querySelectorAll('#pslots .slotrow').length+1;
|
|
1871
|
+
const fields=(cur.keyFields&&cur.keyFields.length?cur.keyFields:['api_key']);
|
|
1872
|
+
const multi=!(fields.length===1&&(fields[0]==='api_key'||fields[0]==='apiToken'));
|
|
1873
|
+
const form=document.createElement('form');form.className='slotrow';
|
|
1874
|
+
const inputs=fields.map(f=>{const i=document.createElement('input');i.type='password';i.autocomplete='off';i.placeholder='key '+n+' — '+f;i.setAttribute('aria-label',f+' '+n);form.appendChild(i);return i;});
|
|
1875
|
+
const btn=document.createElement('button');btn.type='submit';btn.textContent='Gather';form.appendChild(btn);
|
|
1876
|
+
form.onsubmit=async(e)=>{
|
|
1877
|
+
e.preventDefault();
|
|
1878
|
+
err('');
|
|
1879
|
+
const vals=inputs.map(i=>i.value.trim());
|
|
1880
|
+
if(vals.some(v=>!v)){err(multi?'Paste all '+fields.join(', ')+' first.':'Paste an API key first.');return;}
|
|
1881
|
+
const payload=multi?{label:'key '+n,credentials:Object.fromEntries(fields.map((f,i)=>[f,vals[i]]))}:{label:'key '+n,api_key:vals[0]};
|
|
1882
|
+
const r=await j(await fetch('/api/pools/'+pool.id+'/keys',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(payload)}));
|
|
1883
|
+
if(r.error){err(r.error);return;}
|
|
1884
|
+
form.remove();refresh();
|
|
1885
|
+
};
|
|
1886
|
+
document.getElementById('pslots').appendChild(form);inputs[0].focus();
|
|
1887
|
+
}
|
|
1888
|
+
document.getElementById('pmore').onclick=()=>addSlot();
|
|
1889
|
+
init();
|
|
1890
|
+
</script></div></main></div></div>${shellJs}</body></html>`;
|
|
1891
|
+
}
|