HuggingClaw / env-builder.html
Anurag
Fix gateway/telegram config bugs and stabilize WhatsApp + key rotator flows
c8a2bdb
raw
history blame
23.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HuggingClaw · ENV Builder</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Syne:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
/* ── Reset & Tokens ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0b0c0f;
--bg2: #111318;
--bg3: #181c23;
--bg4: #1e2330;
--border: #252b38;
--border2: #2e3648;
--amber: #f5a623;
--amber2: #ffbe55;
--amber-dim: rgba(245,166,35,.12);
--amber-glow:rgba(245,166,35,.22);
--green: #3dd68c;
--red: #f05f5f;
--blue: #5b8af5;
--text: #e4e8f0;
--text2: #8d97ad;
--text3: #535f76;
--mono: 'JetBrains Mono', monospace;
--sans: 'Syne', sans-serif;
--r: 8px;
--r2: 12px;
--sidebar-w: 220px;
--panel-w: 340px;
}
html { scroll-behavior: smooth; height: 100%; }
body {
font-family: var(--sans);
background: var(--bg);
color: var(--text);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* ── Topbar ── */
.topbar {
position: sticky;
top: 0;
z-index: 100;
height: 52px;
background: rgba(11,12,15,.9);
backdrop-filter: blur(14px);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 20px;
gap: 16px;
flex-shrink: 0;
}
.topbar-logo {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.topbar-logo svg { width: 26px; height: 26px; }
.topbar-wordmark {
font-weight: 800;
font-size: 14px;
letter-spacing: -.2px;
color: var(--text);
white-space: nowrap;
}
.topbar-wordmark em {
color: var(--amber);
font-style: normal;
}
.topbar-divider {
width: 1px;
height: 22px;
background: var(--border2);
}
.topbar-title {
font-size: 12px;
font-weight: 600;
color: var(--text2);
letter-spacing: .5px;
text-transform: uppercase;
}
.topbar-spacer { flex: 1; }
.topbar-pill {
font-family: var(--mono);
font-size: 10px;
color: var(--amber);
background: var(--amber-dim);
border: 1px solid var(--amber-glow);
border-radius: 20px;
padding: 3px 10px;
letter-spacing: .5px;
}
/* ── Layout ── */
.layout {
display: flex;
flex: 1;
min-height: 0;
height: calc(100vh - 52px);
}
/* ── Sidebar ── */
.sidebar-wrap {
width: var(--sidebar-w);
flex-shrink: 0;
border-right: 1px solid var(--border);
background: var(--bg2);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-scroll {
flex: 1;
overflow-y: auto;
padding: 14px 10px;
}
.sidebar-scroll::-webkit-scrollbar { width: 4px; }
.sidebar-scroll::-webkit-scrollbar-track { background: transparent; }
.sidebar-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
/* sb-label rendered by JS */
.sb-label {
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--text3);
padding: 0 8px 10px;
}
.nav-btn {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border: none;
background: transparent;
cursor: pointer;
border-radius: var(--r);
text-align: left;
color: var(--text2);
font-family: var(--sans);
font-size: 12.5px;
font-weight: 500;
transition: background .15s, color .15s;
margin-bottom: 2px;
}
.nav-btn:hover { background: var(--bg3); color: var(--text); }
.nav-btn.active {
background: var(--amber-dim);
color: var(--amber);
border: 1px solid var(--amber-glow);
}
.nav-icon { font-size: 13px; flex-shrink: 0; }
.nav-label { flex: 1; }
.nav-count {
font-family: var(--mono);
font-size: 10px;
font-weight: 600;
color: var(--text3);
background: var(--bg3);
border-radius: 10px;
padding: 1px 6px;
min-width: 20px;
text-align: center;
transition: background .2s, color .2s;
}
.nav-btn.active .nav-count {
background: var(--amber-glow);
color: var(--amber2);
}
/* ── Main ── */
.main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
/* ── Toolbar ── */
.toolbar {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 20px;
border-bottom: 1px solid var(--border);
background: var(--bg2);
flex-shrink: 0;
flex-wrap: wrap;
}
.search-wrap {
position: relative;
flex: 1;
min-width: 160px;
max-width: 340px;
}
.search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: var(--text3);
pointer-events: none;
font-size: 12px;
}
#search {
width: 100%;
background: var(--bg3);
border: 1px solid var(--border2);
border-radius: var(--r);
padding: 7px 10px 7px 30px;
font-family: var(--mono);
font-size: 12px;
color: var(--text);
outline: none;
transition: border-color .15s;
}
#search:focus { border-color: var(--amber); }
#search::placeholder { color: var(--text3); }
.tb-sep {
width: 1px;
height: 24px;
background: var(--border2);
}
.btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 13px;
border-radius: var(--r);
border: 1px solid var(--border2);
background: var(--bg3);
color: var(--text2);
font-family: var(--sans);
font-size: 11.5px;
font-weight: 600;
cursor: pointer;
transition: all .15s;
white-space: nowrap;
}
.btn:hover { background: var(--bg4); color: var(--text); border-color: var(--border2); }
.btn-amber {
background: var(--amber);
color: #0b0c0f;
border-color: var(--amber);
}
.btn-amber:hover { background: var(--amber2); border-color: var(--amber2); }
.btn-ghost {
background: transparent;
border-color: transparent;
color: var(--text3);
}
.btn-ghost:hover { background: var(--bg3); color: var(--text2); border-color: var(--border2); }
/* ── Content area ── */
.content-wrap {
flex: 1;
display: flex;
min-height: 0;
overflow: hidden;
}
/* ── Sections scroll ── */
.sections-scroll {
flex: 1;
overflow-y: auto;
padding: 16px 20px 80px;
min-width: 0;
}
.sections-scroll::-webkit-scrollbar { width: 5px; }
.sections-scroll::-webkit-scrollbar-track { background: transparent; }
.sections-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
/* ── Section ── */
.sec { margin-bottom: 28px; }
.sec.sec-hidden { display: none !important; }
.sec-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.sec-icon { font-size: 14px; }
.sec-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--text3);
}
.sec-count {
font-family: var(--mono);
font-size: 10px;
color: var(--text3);
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1px 7px;
}
.sec-line {
flex: 1;
height: 1px;
background: var(--border);
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 9px;
}
/* ── ENV Card ── */
.env-card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--r2);
padding: 12px;
transition: border-color .2s, background .2s;
}
.env-card:hover { border-color: var(--border2); }
.env-card.hidden { display: none; }
.env-card.selected {
border-color: var(--amber-glow);
background: linear-gradient(135deg, var(--bg2) 80%, rgba(245,166,35,.04));
}
/* Critical cards get a red left-border accent */
.env-card:has(.badge-critical) {
border-left: 3px solid rgba(240,80,80,.4);
}
.env-card:has(.badge-critical):hover {
border-left-color: rgba(240,80,80,.7);
}
.env-card:has(.badge-critical).selected {
border-left-color: #f05f5f;
}
/* Credential cards get an amber left-border accent */
.env-card:has(.badge-credential) {
border-left: 3px solid rgba(220,140,60,.3);
}
.card-top {
display: flex;
align-items: flex-start;
gap: 9px;
margin-bottom: 9px;
}
.card-check {
width: 15px;
height: 15px;
accent-color: var(--amber);
flex-shrink: 0;
margin-top: 2px;
cursor: pointer;
}
.card-info { flex: 1; min-width: 0; }
.card-key {
font-family: var(--mono);
font-size: 11.5px;
font-weight: 600;
color: var(--text);
letter-spacing: .3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-lbl {
font-size: 11px;
color: var(--text3);
margin-top: 2px;
line-height: 1.35;
}
.badge {
flex-shrink: 0;
font-family: var(--mono);
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .6px;
padding: 2px 7px;
border-radius: 20px;
}
/* critical — space breaks without this */
.badge-critical {
background: rgba(240,80,80,.14);
color: #f05f5f;
border: 1px solid rgba(240,80,80,.3);
}
/* credential — sensitive key / token */
.badge-credential {
background: rgba(220,140,60,.13);
color: #e09040;
border: 1px solid rgba(220,140,60,.28);
}
/* feature — enables an optional feature */
.badge-feature {
background: rgba(70,140,250,.12);
color: #5a9eff;
border: 1px solid rgba(70,140,250,.25);
}
/* optional — safe to change freely */
.badge-optional {
background: rgba(61,214,140,.10);
color: #3dd68c;
border: 1px solid rgba(61,214,140,.22);
}
/* advanced — power-user, risky if wrong */
.badge-advanced {
background: rgba(160,100,230,.12);
color: #b07ae0;
border: 1px solid rgba(160,100,230,.25);
}
/* build-time — needs HF Space rebuild */
.badge-build {
background: rgba(240,185,60,.12);
color: #e0b030;
border: 1px solid rgba(240,185,60,.28);
}
/* ── Card inputs ── */
.card-input { position: relative; }
.card-input input[type="text"],
.card-input input[type="password"],
.card-input input[type="number"],
.card-input textarea,
.card-input select {
width: 100%;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 7px 10px;
font-family: var(--mono);
font-size: 11.5px;
color: var(--text);
outline: none;
transition: border-color .15s;
resize: vertical;
}
.card-input input[type="text"]:focus,
.card-input input[type="password"]:focus,
.card-input input[type="number"]:focus,
.card-input textarea:focus,
.card-input select:focus {
border-color: var(--amber);
}
.card-input textarea { min-height: 64px; }
.card-input select {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238d97ad' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
padding-right: 28px;
}
.card-input optgroup { color: var(--text2); font-weight: 600; }
.card-input option { color: var(--text); background: var(--bg3); }
/* ── Toggle ── */
.toggle-shell { display: flex; align-items: center; gap: 8px; }
.tog {
padding: 5px 14px;
border-radius: 20px;
border: 1px solid var(--border2);
background: var(--bg3);
color: var(--text3);
font-family: var(--mono);
font-size: 11px;
font-weight: 700;
cursor: pointer;
transition: all .18s;
letter-spacing: .5px;
}
.tog.on {
background: rgba(61,214,140,.15);
border-color: rgba(61,214,140,.4);
color: var(--green);
}
/* ── Picker shell ── */
.picker-shell { display: flex; flex-direction: column; gap: 6px; }
.picker-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.picker-select {
flex: 1;
min-width: 0;
padding: 6px 28px 6px 8px !important;
font-size: 11px !important;
}
.mini-btn {
padding: 5px 9px;
border-radius: var(--r);
border: 1px solid var(--border2);
background: var(--bg3);
color: var(--text2);
font-family: var(--mono);
font-size: 10px;
font-weight: 600;
cursor: pointer;
transition: all .15s;
white-space: nowrap;
}
.mini-btn:hover { background: var(--bg4); color: var(--text); }
/* ── Right panel ── */
.right-panel {
width: var(--panel-w);
flex-shrink: 0;
border-left: 1px solid var(--border);
background: var(--bg2);
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-scroll {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.panel-scroll::-webkit-scrollbar { width: 4px; }
.panel-scroll::-webkit-scrollbar-track { background: transparent; }
.panel-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
/* ── Panel block ── */
.pblock {
background: var(--bg3);
border: 1px solid var(--border);
border-radius: var(--r2);
overflow: hidden;
}
.pblock-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid var(--border);
}
.pblock-title {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text3);
display: flex;
align-items: center;
gap: 6px;
}
.pblock-body { padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
.pblock-body textarea,
.pblock-body input[type="text"] {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 8px 10px;
font-family: var(--mono);
font-size: 10.5px;
color: var(--text2);
outline: none;
resize: vertical;
transition: border-color .15s;
}
.pblock-body textarea:focus,
.pblock-body input[type="text"]:focus {
border-color: var(--amber);
color: var(--text);
}
#importText { min-height: 80px; }
#bundleOut { min-height: 60px; color: var(--amber2); }
#envLineOut { font-size: 10px; }
.row-btns { display: flex; gap: 6px; flex-wrap: wrap; }
/* ── Summary ── */
#summary {
font-size: 11.5px;
color: var(--text2);
line-height: 1.6;
}
#summary strong {
font-size: 15px;
color: var(--amber);
font-family: var(--mono);
}
.sum-keys {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.sum-key {
font-family: var(--mono);
font-size: 9.5px;
color: var(--text2);
background: var(--bg4);
border: 1px solid var(--border2);
border-radius: 4px;
padding: 2px 6px;
}
/* ── Custom Env section ── */
#customSec {
margin-top: 8px;
}
.custom-row {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.custom-row input {
flex: 1;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 7px 10px;
font-family: var(--mono);
font-size: 11px;
color: var(--text);
outline: none;
transition: border-color .15s;
min-width: 0;
}
.custom-row input:focus { border-color: var(--amber); }
.custom-row input:first-child { flex: 0 0 40%; }
/* ── Toast ── */
#toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: var(--bg4);
border: 1px solid var(--border2);
color: var(--amber);
font-family: var(--mono);
font-size: 12px;
font-weight: 600;
padding: 9px 20px;
border-radius: 30px;
z-index: 9999;
opacity: 0;
transition: opacity .2s, transform .2s;
pointer-events: none;
box-shadow: 0 8px 32px rgba(0,0,0,.5);
}
#toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ── Scrollbar global ── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
/* ── Responsive ── */
@media (max-width: 900px) {
:root { --panel-w: 280px; --sidebar-w: 180px; }
}
@media (max-width: 700px) {
.right-panel { display: none; }
:root { --sidebar-w: 160px; }
}
@media (max-width: 520px) {
.sidebar-wrap { display: none; }
.topbar-divider, .topbar-title { display: none; }
}
/* ── Tag Legend (collapsible) ── */
.tag-legend {
margin-bottom: 14px;
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--r);
overflow: hidden;
}
.legend-summary {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 12px;
cursor: pointer;
list-style: none;
user-select: none;
outline: none;
}
.legend-summary::-webkit-details-marker { display: none; }
.legend-chips { display: flex; gap: 5px; flex-wrap: wrap; flex: 1; }
.legend-hint {
font-size: 10px;
color: var(--text3);
white-space: nowrap;
flex-shrink: 0;
}
.tag-legend[open] .legend-hint { opacity: 0; }
.legend-body {
padding: 8px 12px 10px;
border-top: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 6px;
}
.legend-row {
display: flex;
align-items: center;
gap: 10px;
font-size: 11px;
color: var(--text2);
}
.legend-row .badge { flex-shrink: 0; width: 74px; text-align: center; }
.legend-tip {
font-size: 9.5px;
color: var(--text3);
margin-top: 4px;
padding-top: 6px;
border-top: 1px solid var(--border);
}
</style>
</head>
<body>
<!-- ── Topbar ── -->
<header class="topbar">
<div class="topbar-logo">
<svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" rx="14" fill="#1a1f2e"/>
<text x="10" y="46" font-size="40" font-family="monospace">🤗</text>
<circle cx="46" cy="20" r="10" fill="#f5a623"/>
<text x="40" y="25" font-size="14" font-family="monospace" fill="#0b0c0f"></text>
</svg>
<span class="topbar-wordmark">Hugging<em>Claw</em></span>
</div>
<div class="topbar-divider"></div>
<span class="topbar-title">ENV Builder</span>
<div class="topbar-spacer"></div>
<span class="topbar-pill">v2025</span>
</header>
<!-- ── Layout ── -->
<div class="layout">
<!-- ── Sidebar ── -->
<aside class="sidebar-wrap">
<div class="sidebar-scroll">
<div id="sidebar"></div>
</div>
</aside>
<!-- ── Main ── -->
<main class="main">
<!-- toolbar -->
<div class="toolbar">
<div class="search-wrap">
<span class="search-icon"></span>
<input id="search" type="text" placeholder="Search variables…" autocomplete="off" spellcheck="false">
</div>
<div class="tb-sep"></div>
<button id="selectCommon" class="btn">★ Common</button>
<button id="selectVisible" class="btn">☑ Visible</button>
<button id="clearAll" class="btn btn-ghost">✕ Clear</button>
</div>
<!-- content row -->
<div class="content-wrap">
<!-- sections -->
<div class="sections-scroll">
<!-- Tag Legend (compact collapsible) -->
<details class="tag-legend" id="tagLegend">
<summary class="legend-summary">
<span class="legend-chips">
<span class="badge badge-critical">critical</span>
<span class="badge badge-credential">credential</span>
<span class="badge badge-feature">feature</span>
<span class="badge badge-optional">optional</span>
<span class="badge badge-advanced">advanced</span>
<span class="badge badge-build">build-time</span>
</span>
<span class="legend-hint">what do these mean?</span>
</summary>
<div class="legend-body">
<div class="legend-row"><span class="badge badge-critical">critical</span><span>Space won't work without this</span></div>
<div class="legend-row"><span class="badge badge-credential">credential</span><span>Sensitive key or token — never share</span></div>
<div class="legend-row"><span class="badge badge-feature">feature</span><span>Enables a specific optional feature</span></div>
<div class="legend-row"><span class="badge badge-optional">optional</span><span>Safe to change — nothing breaks</span></div>
<div class="legend-row"><span class="badge badge-advanced">advanced</span><span>Power-user setting — wrong value causes issues</span></div>
<div class="legend-row"><span class="badge badge-build">build-time</span><span>Needs HF Space rebuild to fully take effect</span></div>
<div class="legend-tip">💡 Type a tag name in search to filter by it</div>
</div>
</details>
<div id="sections"></div>
<!-- Custom Env section -->
<div id="customSec" class="sec" data-section="Custom Env">
<div class="sec-header">
<span class="sec-icon">🔧</span>
<span class="sec-title">Custom Env</span>
<div class="sec-line"></div>
</div>
<div id="customRows"></div>
<button id="addCustom" class="btn" style="margin-top:6px;">+ Add variable</button>
</div>
</div>
<!-- right panel -->
<aside class="right-panel">
<div class="panel-scroll">
<!-- Summary -->
<div class="pblock">
<div class="pblock-head">
<span class="pblock-title">📊 Summary</span>
</div>
<div class="pblock-body">
<div id="summary">No variables selected yet.</div>
</div>
</div>
<!-- Output -->
<div class="pblock">
<div class="pblock-head">
<span class="pblock-title">📦 Bundle Output</span>
</div>
<div class="pblock-body">
<button id="generateBundle" class="btn btn-amber" style="width:100%;font-size:12.5px;"># Generate Bundle</button>
<textarea id="bundleOut" placeholder="Click # Generate to build your bundle…" readonly spellcheck="false"></textarea>
<input type="text" id="envLineOut" placeholder="HUGGINGCLAW_ENV_BUNDLE=…" readonly spellcheck="false">
<div class="row-btns">
<button id="copyBundle" class="btn btn-amber">⎘ Bundle</button>
<button id="copyEnvLine" class="btn">⎘ Env Line</button>
<button id="copyJson" class="btn">⎘ JSON</button>
<button id="applyBundle" class="btn btn-ghost">↺ Apply</button>
</div>
</div>
</div>
<!-- Import -->
<div class="pblock">
<div class="pblock-head">
<span class="pblock-title">📥 Import</span>
</div>
<div class="pblock-body">
<textarea id="importText" placeholder="Paste .env, JSON, or HUGGINGCLAW_ENV_BUNDLE=… here" spellcheck="false"></textarea>
<button id="applyImport" class="btn btn-amber" style="width:100%;">↓ Import & Apply</button>
</div>
</div>
</div>
</aside>
</div><!-- /content-wrap -->
</main>
</div><!-- /layout -->
<!-- Toast -->
<div id="toast">Copied ✓</div>
<!-- env-builder.js provides MODEL_CATALOGS, FIELDS, ICONS and all logic -->
<script src="env-builder.js"></script>
</body>
</html>