Spaces:
Running
Running
Download token-estimator.html from Signalloom-ai/LoomLensAI: direct link, hf CLI and curl.
- Browser
- Download file 33.5 kB
-
https://huggingface.co/spaces/Signalloom-ai/LoomLensAI/resolve/main/token-estimator.html
- Command line
-
hf download hf://spaces/Signalloom-ai/LoomLensAI/token-estimator.html
-
curl -L -o token-estimator.html https://huggingface.co/spaces/Signalloom-ai/LoomLensAI/resolve/main/token-estimator.html
33.5 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>LoomLens AI™ — API Token Budget Calculator · SignalloomAI™</title> | |
| <style> | |
| :root { | |
| --bg: #0a0e1a; | |
| --surface: #111827; | |
| --surface2: #1a2235; | |
| --border: #1e2d4a; | |
| --accent: #8b5cf6; | |
| --accent-bright: #a78bfa; | |
| --text: #e8edf5; | |
| --text-dim: #8899bb; | |
| --text-muted: #4a6a9a; | |
| --green: #10b981; | |
| --yellow: #f59e0b; | |
| --red: #f87171; | |
| --orange: #fb923c; | |
| --radius: 10px; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
| min-height: 100vh; | |
| padding: 40px 20px; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 32px; | |
| max-width: 680px; | |
| margin-left: auto; | |
| margin-right: auto; | |
| } | |
| .eyebrow { | |
| font-size: 11px; | |
| letter-spacing: 3px; | |
| text-transform: uppercase; | |
| color: var(--accent); | |
| margin-bottom: 10px; | |
| font-weight: 600; | |
| } | |
| h1 { font-size: 32px; font-weight: 700; letter-spacing: -0.5px; margin-bottom: 14px; } | |
| .header p { font-size: 15px; color: var(--text-dim); line-height: 1.7; max-width: 540px; margin: 0 auto 16px; } | |
| /* Pricing banner */ | |
| .pricing-bar { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 16px; | |
| padding: 8px 20px; | |
| border-radius: 99px; | |
| background: rgba(139,92,246,0.1); | |
| border: 1px solid rgba(139,92,246,0.25); | |
| margin-bottom: 4px; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| .pricing-item { font-size: 12px; font-weight: 600; color: var(--text-dim); } | |
| .pricing-item strong { color: var(--accent-bright); } | |
| .pricing-divider { width: 1px; height: 14px; background: rgba(139,92,246,0.3); } | |
| .badge-free { | |
| background: rgba(16,185,129,0.15); | |
| border: 1px solid rgba(16,185,129,0.3); | |
| color: var(--green); | |
| padding: 2px 10px; | |
| border-radius: 99px; | |
| font-size: 11px; | |
| font-weight: 700; | |
| letter-spacing: 0.5px; | |
| } | |
| .badge-pro { | |
| background: rgba(245,158,11,0.15); | |
| border: 1px solid rgba(245,158,11,0.3); | |
| color: var(--yellow); | |
| padding: 2px 10px; | |
| border-radius: 99px; | |
| font-size: 11px; | |
| font-weight: 700; | |
| letter-spacing: 0.5px; | |
| } | |
| .layout { | |
| display: grid; | |
| grid-template-columns: 1fr 380px; | |
| gap: 24px; | |
| max-width: 1060px; | |
| margin: 0 auto; | |
| align-items: start; | |
| } | |
| @media (max-width: 820px) { .layout { grid-template-columns: 1fr; } } | |
| .card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 22px; | |
| } | |
| .card-title { | |
| font-size: 11px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 1.5px; | |
| color: var(--text-muted); | |
| margin-bottom: 16px; | |
| padding-bottom: 10px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| /* Quota meter */ | |
| .quota-bar-wrap { | |
| margin-bottom: 16px; | |
| padding: 10px 14px; | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| } | |
| .quota-label { | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 11px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| color: var(--text-muted); | |
| margin-bottom: 6px; | |
| } | |
| .quota-count { color: var(--accent-bright); } | |
| .quota-track { | |
| height: 6px; | |
| background: var(--surface); | |
| border-radius: 99px; | |
| overflow: hidden; | |
| } | |
| .quota-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--accent), var(--accent-bright)); | |
| border-radius: 99px; | |
| transition: width 0.4s ease; | |
| } | |
| .quota-fill.warn { background: linear-gradient(90deg, var(--yellow), var(--orange)); } | |
| .quota-fill.done { background: var(--green); } | |
| .quota-remaining { font-size: 11px; color: var(--text-muted); margin-top: 5px; } | |
| .quota-remaining strong { color: var(--text-dim); } | |
| /* Prompt area */ | |
| .prompt-area { | |
| width: 100%; | |
| min-height: 200px; | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| color: var(--text); | |
| font-size: 13px; | |
| font-family: 'Cascadia Code','Fira Code','SF Mono',Consolas,monospace; | |
| padding: 12px 14px; | |
| resize: vertical; | |
| outline: none; | |
| line-height: 1.6; | |
| transition: border-color 0.2s; | |
| } | |
| .prompt-area:focus { border-color: var(--accent); } | |
| .prompt-area::placeholder { color: var(--text-muted); } | |
| .prompt-area.limit-reached { opacity: 0.5; cursor: not-allowed; } | |
| .token-bar { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-top: 8px; | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| } | |
| .token-count { font-weight: 700; color: var(--accent-bright); } | |
| .token-count.high { color: var(--yellow); } | |
| .token-count.very-high { color: var(--red); } | |
| /* Field rows */ | |
| .field-row { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 14px; | |
| margin-top: 14px; | |
| } | |
| .field { display: flex; flex-direction: column; gap: 6px; } | |
| .field label { | |
| font-size: 11px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| color: var(--text-muted); | |
| } | |
| .field select { width: 100%; } | |
| .field select { | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| color: var(--text); | |
| font-size: 13px; | |
| padding: 8px 10px; | |
| font-family: inherit; | |
| cursor: pointer; | |
| outline: none; | |
| } | |
| .field select option { background: var(--surface2); } | |
| .range-wrap { position: relative; } | |
| .range-val { | |
| position: absolute; | |
| right: 0; top: -18px; | |
| font-size: 12px; | |
| font-weight: 700; | |
| color: var(--accent-bright); | |
| } | |
| input[type=range] { width: 100%; accent-color: var(--accent); cursor: pointer; margin-top: 4px; } | |
| /* Model tiers */ | |
| .tier-label { | |
| font-size: 11px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 1.5px; | |
| color: var(--text-muted); | |
| margin: 16px 0 10px; | |
| } | |
| .models-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 6px; | |
| } | |
| .model-chip { | |
| display: flex; | |
| align-items: center; | |
| gap: 7px; | |
| padding: 7px 10px; | |
| border-radius: 8px; | |
| border: 1px solid var(--border); | |
| background: var(--surface2); | |
| cursor: pointer; | |
| font-size: 12px; | |
| color: var(--text-dim); | |
| transition: all 0.15s; | |
| user-select: none; | |
| } | |
| .model-chip:hover { border-color: var(--accent); color: var(--text); } | |
| .model-chip.selected { border-color: var(--accent); background: rgba(139,92,246,0.12); color: var(--accent-bright); } | |
| .model-chip input { display: none; } | |
| .chip-dot { | |
| width: 8px; height: 8px; | |
| border-radius: 50%; | |
| border: 1.5px solid currentColor; | |
| flex-shrink: 0; | |
| } | |
| .model-chip.selected .chip-dot { background: var(--accent); border-color: var(--accent); } | |
| .provider-tag { | |
| margin-left: auto; | |
| font-size: 9px; | |
| font-weight: 700; | |
| letter-spacing: 0.5px; | |
| text-transform: uppercase; | |
| padding: 2px 5px; | |
| border-radius: 4px; | |
| opacity: 0.75; | |
| } | |
| .provider-tag.openai { background: rgba(16,163,74,0.2); color: #10b981; } | |
| .provider-tag.anthropic { background: rgba(139,92,246,0.2); color: #a78bfa; } | |
| .provider-tag.google { background: rgba(251,191,36,0.2); color: #f59e0b; } | |
| .provider-tag.deepseek { background: rgba(239,68,68,0.2); color: #f87171; } | |
| .provider-tag.xai { background: rgba(255,255,255,0.08); color: #e8edf5; } | |
| .provider-tag.minimax { background: rgba(59,130,246,0.2); color: #60a5fa; } | |
| .provider-tag.meta { background: rgba(14,116,144,0.2); color: #22d3ee; } | |
| /* Tier 2 collapsible */ | |
| .tier2-toggle { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 11px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 1.5px; | |
| color: var(--text-muted); | |
| margin: 16px 0 10px; | |
| cursor: pointer; | |
| padding: 6px 0; | |
| user-select: none; | |
| } | |
| .tier2-toggle:hover { color: var(--text-dim); } | |
| .tier2-toggle .arrow { transition: transform 0.2s; font-size: 10px; } | |
| .tier2-toggle.open .arrow { transform: rotate(90deg); } | |
| .tier2-section { display: none; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 0; } | |
| .tier2-section.open { display: grid; } | |
| /* Pro upsell bar */ | |
| .pro-bar { | |
| margin-top: 14px; | |
| padding: 10px 14px; | |
| background: rgba(245,158,11,0.06); | |
| border: 1px solid rgba(245,158,11,0.2); | |
| border-radius: 8px; | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .pro-bar-icon { font-size: 20px; flex-shrink: 0; } | |
| .pro-bar strong { color: var(--yellow); } | |
| .pro-bar a { color: var(--yellow); font-weight: 700; text-decoration: none; } | |
| .pro-bar a:hover { text-decoration: underline; } | |
| /* CTA */ | |
| .btn-estimate { | |
| width: 100%; | |
| padding: 13px; | |
| border-radius: 8px; | |
| border: none; | |
| background: var(--accent); | |
| color: #fff; | |
| font-size: 14px; | |
| font-weight: 700; | |
| cursor: pointer; | |
| margin-top: 14px; | |
| transition: background 0.2s, transform 0.1s; | |
| font-family: inherit; | |
| } | |
| .btn-estimate:hover:not(:disabled) { background: var(--accent-bright); } | |
| .btn-estimate:active:not(:disabled) { transform: scale(0.98); } | |
| .btn-estimate:disabled { opacity: 0.4; cursor: not-allowed; } | |
| .btn-estimate.loading { position: relative; color: transparent; pointer-events: none; } | |
| .btn-estimate.loading::after { | |
| content: ''; | |
| position: absolute; | |
| width: 18px; height: 18px; | |
| border: 2px solid rgba(255,255,255,0.3); | |
| border-top-color: #fff; | |
| border-radius: 50%; | |
| top: 50%; left: 50%; | |
| transform: translate(-50%,-50%); | |
| animation: spin 0.7s linear infinite; | |
| } | |
| @keyframes spin { to { transform: translate(-50%,-50%) rotate(360deg); } } | |
| .error-msg { | |
| background: rgba(248,113,113,0.08); | |
| border: 1px solid rgba(248,113,113,0.25); | |
| border-radius: 8px; | |
| padding: 10px 12px; | |
| font-size: 13px; | |
| color: var(--red); | |
| margin-top: 8px; | |
| display: none; | |
| } | |
| /* Results */ | |
| .results-panel { display: flex; flex-direction: column; gap: 10px; } | |
| .result-card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 14px 16px; | |
| } | |
| .result-card.cheapest { border-color: var(--green); box-shadow: 0 0 16px rgba(16,185,129,0.07); } | |
| .result-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } | |
| .result-model { font-size: 13px; font-weight: 700; color: var(--text-dim); } | |
| .result-card.cheapest .result-model { color: var(--green); } | |
| .cheapest-badge { | |
| font-size: 9px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; | |
| padding: 3px 9px; border-radius: 99px; | |
| background: rgba(16,185,129,0.15); border: 1px solid rgba(16,185,129,0.3); color: var(--green); | |
| } | |
| .cost-display { display: flex; align-items: baseline; gap: 5px; margin-bottom: 8px; } | |
| .cost-amount { font-size: 24px; font-weight: 700; letter-spacing: -1px; } | |
| .cost-unit { font-size: 12px; color: var(--text-muted); } | |
| .cost-bands { display: flex; gap: 5px; font-size: 11px; margin-bottom: 7px; flex-wrap: wrap; } | |
| .cost-band { padding: 2px 7px; border-radius: 6px; background: var(--surface2); } | |
| .cost-band.low { color: var(--green); } .cost-band.mid { color: var(--yellow); } .cost-band.high { color: var(--red); } | |
| .token-row { font-size: 11px; color: var(--text-muted); display: flex; gap: 10px; flex-wrap: wrap; } | |
| .token-row b { color: var(--text-dim); } | |
| .savings-banner { | |
| background: rgba(16,185,129,0.07); | |
| border: 1px solid rgba(16,185,129,0.2); | |
| border-radius: var(--radius); | |
| padding: 11px 15px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-size: 13px; | |
| color: var(--text-dim); | |
| } | |
| .savings-banner strong { color: var(--green); } | |
| .savings-icon { font-size: 20px; line-height: 1; } | |
| .postmortem { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 13px 15px; | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| line-height: 1.8; | |
| } | |
| .postmortem h4 { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: var(--text-dim); margin-bottom: 7px; } | |
| .postmortem .fact { display: flex; gap: 8px; } | |
| .postmortem .fact::before { content: '→'; color: var(--accent); min-width: 14px; } | |
| .postmortem .fact b { color: var(--text-dim); } | |
| .empty-state { text-align: center; padding: 40px 20px; color: var(--text-muted); font-size: 14px; line-height: 1.7; } | |
| .empty-icon { font-size: 36px; margin-bottom: 12px; opacity: 0.3; display: block; } | |
| .attr { text-align: center; margin-top: 28px; font-size: 12px; color: var(--text-muted); } | |
| .attr a { color: var(--accent); text-decoration: none; } | |
| .attr a:hover { text-decoration: underline; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header"> | |
| <div class="eyebrow">By SignalloomAI™ — Free Tool</div> | |
| <h1>LoomLens AI™ — API Token Budget Calculator</h1> | |
| <p>Find the best model to fit your prompt.™</p> | |
| <div class="pricing-bar"> | |
| <span class="badge-free">FREE</span> | |
| <span class="pricing-item">5 estimates / day</span> | |
| <span class="pricing-divider"></span> | |
| <span class="badge-pro">PRO</span> | |
| <span class="pricing-item"><strong>$50/month</strong> — 1,000 estimates/mo <span id="avg_savings_badge" style="color:var(--green);font-size:11px">(avg save: $1.23/est)</span></span> | |
| </div> | |
| </div> | |
| <div class="layout"> | |
| <!-- LEFT: Prompt + params --> | |
| <div> | |
| <div class="card"> | |
| <div class="card-title">Your Prompt</div> | |
| <!-- Quota meter --> | |
| <div class="quota-bar-wrap" id="quota_box"> | |
| <div class="quota-label"> | |
| <span>Daily Estimates</span> | |
| <span class="quota-count" id="quota_count">5 / 5 remaining</span> | |
| </div> | |
| <div class="quota-track"> | |
| <div class="quota-fill done" id="quota_fill" style="width:100%"></div> | |
| </div> | |
| <div class="quota-remaining" id="quota_remaining">You have <strong>5 free estimates today</strong></div> | |
| </div> | |
| <textarea id="prompt_input" class="prompt-area" | |
| placeholder="Paste your prompt here... e.g. You are an expert medical coder. Review the following clinical notes and assign the appropriate ICD-10 codes..." spellcheck="false"></textarea> | |
| <div class="token-bar"> | |
| <span id="char_count">0 chars</span> | |
| <span id="token_estimate" class="token-count">~0 tokens</span> | |
| </div> | |
| <div class="field-row"> | |
| <div class="field"> | |
| <label>Complexity</label> | |
| <select id="complexity"> | |
| <option value="simple">Simple — Q&A, extraction</option> | |
| <option value="standard" selected>Standard — drafting, chat, summarization</option> | |
| <option value="reasoning">Reasoning — analysis, chain-of-thought</option> | |
| <option value="coding">Coding — generation, review, refactor</option> | |
| </select> | |
| </div> | |
| <div class="field"> | |
| <label>Agent Turns</label> | |
| <div class="range-wrap"> | |
| <span class="range-val" id="turns_val">1</span> | |
| <input type="range" id="num_turns" min="1" max="50" value="1" /> | |
| </div> | |
| <span style="font-size:11px;color:var(--text-muted)" id="turns_hint">1 prompt round trip</span> | |
| </div> | |
| </div> | |
| <!-- Tier 1 --> | |
| <div class="tier-label">Major Models</div> | |
| <div class="models-grid" id="tier1_grid"> | |
| <label class="model-chip selected" id="chip-openai/gpt-5.3-codex"> | |
| <input type="checkbox" value="openai/gpt-5.3-codex" checked /> | |
| <span class="chip-dot"></span>GPT-5.3 Codex<span class="provider-tag openai">OpenAI</span> | |
| </label> | |
| <label class="model-chip selected" id="chip-openai/gpt-4o"> | |
| <input type="checkbox" value="openai/gpt-4o" checked /> | |
| <span class="chip-dot"></span>GPT-4o<span class="provider-tag openai">OpenAI</span> | |
| </label> | |
| <label class="model-chip selected" id="chip-openai/gpt-4o-mini"> | |
| <input type="checkbox" value="openai/gpt-4o-mini" checked /> | |
| <span class="chip-dot"></span>GPT-4o Mini<span class="provider-tag openai">OpenAI</span> | |
| </label> | |
| <label class="model-chip selected" id="chip-anthropic/claude-opus-4-6"> | |
| <input type="checkbox" value="anthropic/claude-opus-4-6" checked /> | |
| <span class="chip-dot"></span>Claude Opus 4<span class="provider-tag anthropic">Anthropic</span> | |
| </label> | |
| <label class="model-chip selected" id="chip-anthropic/claude-sonnet-4-20250514"> | |
| <input type="checkbox" value="anthropic/claude-sonnet-4-20250514" checked /> | |
| <span class="chip-dot"></span>Claude Sonnet 4<span class="provider-tag anthropic">Anthropic</span> | |
| </label> | |
| <label class="model-chip selected" id="chip-google/gemini-2.5-pro"> | |
| <input type="checkbox" value="google/gemini-2.5-pro" checked /> | |
| <span class="chip-dot"></span>Gemini 2.5 Pro<span class="provider-tag google">Google</span> | |
| </label> | |
| <label class="model-chip selected" id="chip-google/gemini-2.5-flash"> | |
| <input type="checkbox" value="google/gemini-2.5-flash" checked /> | |
| <span class="chip-dot"></span>Gemini 2.5 Flash<span class="provider-tag google">Google</span> | |
| </label> | |
| <label class="model-chip selected" id="chip-deepseek/deepseek-chat-v3"> | |
| <input type="checkbox" value="deepseek/deepseek-chat-v3" checked /> | |
| <span class="chip-dot"></span>DeepSeek Chat v3<span class="provider-tag deepseek">DeepSeek</span> | |
| </label> | |
| <label class="model-chip selected" id="chip-xai/grok-3"> | |
| <input type="checkbox" value="xai/grok-3" checked /> | |
| <span class="chip-dot"></span>xAI Grok-3<span class="provider-tag xai">xAI</span> | |
| </label> | |
| </div> | |
| <!-- Tier 2 --> | |
| <div class="tier2-toggle" id="tier2_toggle" onclick="toggleTier2()"> | |
| <span class="arrow">▶</span> More models (next tier) | |
| </div> | |
| <div class="tier2-section" id="tier2_section"> | |
| <label class="model-chip" id="chip-openai/gpt-5.4"> | |
| <input type="checkbox" value="openai/gpt-5.4" /> | |
| <span class="chip-dot"></span>GPT-5.4<span class="provider-tag openai">OpenAI</span> | |
| </label> | |
| <label class="model-chip" id="chip-openai/gpt-5.4-mini"> | |
| <input type="checkbox" value="openai/gpt-5.4-mini" /> | |
| <span class="chip-dot"></span>GPT-5.4 Mini<span class="provider-tag openai">OpenAI</span> | |
| </label> | |
| <label class="model-chip" id="chip-openai/gpt-5.4-nano"> | |
| <input type="checkbox" value="openai/gpt-5.4-nano" /> | |
| <span class="chip-dot"></span>GPT-5.4 Nano<span class="provider-tag openai">OpenAI</span> | |
| </label> | |
| <label class="model-chip" id="chip-anthropic/claude-haiku-4-20250514"> | |
| <input type="checkbox" value="anthropic/claude-haiku-4-20250514" /> | |
| <span class="chip-dot"></span>Claude Haiku 4<span class="provider-tag anthropic">Anthropic</span> | |
| </label> | |
| <label class="model-chip" id="chip-google/gemini-2.0-flash"> | |
| <input type="checkbox" value="google/gemini-2.0-flash" /> | |
| <span class="chip-dot"></span>Gemini 2.0 Flash<span class="provider-tag google">Google</span> | |
| </label> | |
| <label class="model-chip" id="chip-deepseek/deepseek-reasoner"> | |
| <input type="checkbox" value="deepseek/deepseek-reasoner" /> | |
| <span class="chip-dot"></span>DeepSeek Reasoner<span class="provider-tag deepseek">DeepSeek</span> | |
| </label> | |
| <label class="model-chip" id="chip-xai/grok-3-mini"> | |
| <input type="checkbox" value="xai/grok-3-mini" /> | |
| <span class="chip-dot"></span>xAI Grok-3 Mini<span class="provider-tag xai">xAI</span> | |
| </label> | |
| <label class="model-chip" id="chip-minimax/MiniMax-M2.7"> | |
| <input type="checkbox" value="minimax/MiniMax-M2.7" /> | |
| <span class="chip-dot"></span>MiniMax M2.7<span class="provider-tag minimax">MiniMax</span> | |
| </label> | |
| <label class="model-chip" id="chip-minimax/M2.1-Ultra"> | |
| <input type="checkbox" value="minimax/M2.1-Ultra" /> | |
| <span class="chip-dot"></span>MiniMax M2.1 Ultra<span class="provider-tag minimax">MiniMax</span> | |
| </label> | |
| <label class="model-chip" id="chip-local/llama-3.3-70b"> | |
| <input type="checkbox" value="local/llama-3.3-70b" /> | |
| <span class="chip-dot"></span>Llama 3.3 70B (local)<span class="provider-tag meta">Meta</span> | |
| </label> | |
| </div> | |
| <!-- Pro upsell (shown when quota exhausted) --> | |
| <div class="pro-bar" id="pro_bar" style="display:none"> | |
| <div class="pro-bar-icon">💎</div> | |
| <div>Out of free estimates today. <strong>1,000 estimates/mo for $50</strong> — <a href="/loomlens/pricing" target="_blank">Upgrade Now</a></div> | |
| </div> | |
| <button class="btn-estimate" id="btn_estimate" onclick="runEstimate()">Estimate Cost</button> | |
| <div class="error-msg" id="error_msg"></div> | |
| <div class="attr" style="margin-top:14px"> | |
| Powered by <a href="https://signalloomai.com" target="_blank">Signal Loom AI</a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- RIGHT: Results --> | |
| <div class="results-panel" id="results_panel"> | |
| <div class="empty-state"> | |
| <span class="empty-icon">⚡</span> | |
| <div>Paste your prompt and click <strong>Estimate Cost</strong><br/>to see cost comparison across all models</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const API_URL = 'https://api.signalloomai.com/v1/estimate'; | |
| const FREE_DAILY = 5; | |
| const STORAGE_KEY = 'sl_token_est_quota'; | |
| const CHARS_PER_TOKEN = 3.8; | |
| // ── Quota management ───────────────────────────────── | |
| function getQuota() { | |
| try { | |
| const raw = localStorage.getItem(STORAGE_KEY); | |
| if (!raw) return { count: 0, date: todayKey() }; | |
| const q = JSON.parse(raw); | |
| if (q.date !== todayKey()) return { count: 0, date: todayKey() }; // reset daily | |
| return q; | |
| } catch { return { count: 0, date: todayKey() }; } | |
| } | |
| function saveQuota(q) { | |
| try { localStorage.setItem(STORAGE_KEY, JSON.stringify(q)); } catch {} | |
| } | |
| function todayKey() { | |
| return new Date().toISOString().slice(0, 10); // YYYY-MM-DD | |
| } | |
| function useQuota() { | |
| const q = getQuota(); | |
| if (q.count >= FREE_DAILY) return false; | |
| q.count++; | |
| saveQuota(q); | |
| return true; | |
| } | |
| function remaining() { | |
| return Math.max(0, FREE_DAILY - getQuota().count); | |
| } | |
| function renderQuotaUI() { | |
| const rem = remaining(); | |
| const used = FREE_DAILY - rem; | |
| const pct = (rem / FREE_DAILY) * 100; | |
| const fill = document.getElementById('quota_fill'); | |
| const count = document.getElementById('quota_count'); | |
| const remEl = document.getElementById('quota_remaining'); | |
| const proBar = document.getElementById('pro_bar'); | |
| const btn = document.getElementById('btn_estimate'); | |
| const textarea = document.getElementById('prompt_input'); | |
| count.textContent = `${rem} / ${FREE_DAILY} remaining`; | |
| fill.style.width = pct + '%'; | |
| fill.className = 'quota-fill' + (rem === 0 ? ' done' : rem <= 1 ? ' warn' : ' done'); | |
| remEl.innerHTML = rem > 0 | |
| ? `You have <strong>${rem} free estimate${rem === 1 ? '' : 's'} today</strong>` | |
| : `<strong>No free estimates remaining today.</strong> Upgrade to Pro for unlimited.`; | |
| if (rem === 0) { | |
| proBar.style.display = 'flex'; | |
| btn.disabled = true; | |
| btn.textContent = 'Daily limit reached — Upgrade to Pro'; | |
| textarea.classList.add('limit-reached'); | |
| } else { | |
| proBar.style.display = 'none'; | |
| btn.disabled = false; | |
| btn.textContent = `Estimate Cost (${rem} left today)`; | |
| textarea.classList.remove('limit-reached'); | |
| } | |
| } | |
| // ── Prompt token counter ───────────────────────────── | |
| const promptInput = document.getElementById('prompt_input'); | |
| const charCount = document.getElementById('char_count'); | |
| const tokenEst = document.getElementById('token_estimate'); | |
| promptInput.addEventListener('input', () => { | |
| const chars = promptInput.value.length; | |
| const tokens = Math.ceil(chars / CHARS_PER_TOKEN); | |
| charCount.textContent = chars.toLocaleString() + ' chars'; | |
| tokenEst.textContent = '~' + tokens.toLocaleString() + ' tokens'; | |
| tokenEst.className = 'token-count' + (tokens > 80000 ? ' very-high' : tokens > 30000 ? ' high' : ''); | |
| }); | |
| // ── Turns slider ─────────────────────────────────── | |
| document.getElementById('num_turns').addEventListener('input', function() { | |
| document.getElementById('turns_val').textContent = this.value; | |
| document.getElementById('turns_hint').textContent = | |
| this.value == 1 ? '1 prompt round trip' : this.value + ' prompt round trips'; | |
| }); | |
| // ── Model chip toggle ──────────────────────────────── | |
| document.querySelectorAll('.model-chip').forEach(chip => { | |
| chip.addEventListener('click', () => { | |
| chip.classList.toggle('selected'); | |
| chip.querySelector('input').checked = !chip.querySelector('input').checked; | |
| }); | |
| }); | |
| // ── Tier 2 toggle ─────────────────────────────────── | |
| function toggleTier2() { | |
| const tog = document.getElementById('tier2_toggle'); | |
| const sec = document.getElementById('tier2_section'); | |
| tog.classList.toggle('open'); | |
| sec.classList.toggle('open'); | |
| } | |
| // ── Run estimate ──────────────────────────────────── | |
| async function runEstimate() { | |
| const btn = document.getElementById('btn_estimate'); | |
| const err = document.getElementById('error_msg'); | |
| err.style.display = 'none'; | |
| if (!useQuota()) { | |
| err.textContent = 'Daily limit reached. Upgrade to Pro — 1,000 estimates/mo for $50.'; | |
| err.style.display = 'block'; | |
| renderQuotaUI(); | |
| return; | |
| } | |
| const promptText = promptInput.value.trim(); | |
| if (!promptText) { | |
| err.textContent = 'Please paste a prompt first.'; err.style.display = 'block'; | |
| renderQuotaUI(); // restore quota on error | |
| return; | |
| } | |
| const baseTokens = Math.ceil(promptText.length / CHARS_PER_TOKEN); | |
| const numTurns = parseInt(document.getElementById('num_turns').value); | |
| const complexity = document.getElementById('complexity').value; | |
| const targetModels = [...document.querySelectorAll('.model-chip input:checked')].map(i => i.value); | |
| if (targetModels.length === 0) { | |
| err.textContent = 'Please select at least one model.'; err.style.display = 'block'; | |
| renderQuotaUI(); return; | |
| } | |
| const payload = { | |
| job_description: `Prompt (${baseTokens} tokens, ${numTurns} turn${numTurns > 1 ? 's' : ''})`, | |
| num_agents: 1, | |
| estimated_runtime_minutes: Math.max(1, Math.ceil(baseTokens / 1000 * numTurns * 0.3)), | |
| complexity, | |
| target_models: targetModels, | |
| include_postmortem: true, | |
| }; | |
| btn.disabled = true; btn.classList.add('loading'); | |
| try { | |
| const res = await fetch(API_URL, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload), | |
| }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.detail || data.error || 'Request failed'); | |
| renderResults(data, baseTokens, numTurns); | |
| } catch (e) { | |
| err.textContent = 'Error: ' + e.message; err.style.display = 'block'; | |
| } finally { | |
| btn.disabled = false; btn.classList.remove('loading'); | |
| renderQuotaUI(); | |
| } | |
| } | |
| // ── Render ───────────────────────────────────────── | |
| function renderResults(data, baseTokens, numTurns) { | |
| const panel = document.getElementById('results_panel'); | |
| const sorted = [...data.models].sort((a, b) => a.estimated_cost_mid - b.estimated_cost_mid); | |
| const cheapest = sorted[0].model; | |
| let html = ''; | |
| if (sorted.length > 1) { | |
| const exp = sorted[sorted.length - 1]; | |
| html += `<div class="savings-banner"> | |
| <div class="savings-icon">💡</div> | |
| <div>Switching from <strong>${fmtModel(exp.model)}</strong> to <strong>${fmtModel(cheapest)}</strong> | |
| saves <strong>$${(exp.estimated_cost_mid - sorted[0].estimated_cost_mid).toFixed(4)}</strong> per run</div> | |
| </div>`; | |
| } | |
| for (const m of sorted) { | |
| html += `<div class="result-card${m.model === cheapest ? ' cheapest' : ''}"> | |
| <div class="result-header"> | |
| <div class="result-model">${fmtModel(m.model)}</div> | |
| ${m.model === cheapest ? '<span class="cheapest-badge">★ Cheapest</span>' : ''} | |
| </div> | |
| <div class="cost-display"> | |
| <span class="cost-amount">$${m.estimated_cost_mid.toFixed(4)}</span> | |
| <span class="cost-unit">per run</span> | |
| </div> | |
| <div class="cost-bands"> | |
| <span class="cost-band low">Low $${m.estimated_cost_low.toFixed(4)}</span> | |
| <span class="cost-band mid">Mid $${m.estimated_cost_mid.toFixed(4)}</span> | |
| <span class="cost-band high">High $${m.estimated_cost_high.toFixed(4)}</span> | |
| </div> | |
| <div class="token-row"> | |
| <span>In: <b>${fmtNum(m.input_tokens_estimate)}</b></span> | |
| <span>Out: <b>${fmtNum(m.output_tokens_estimate)}</b></span> | |
| <span>Total: <b>${fmtNum(m.total_tokens_estimate)}</b></span> | |
| </div> | |
| </div>`; | |
| } | |
| if (data.postmortem) { | |
| const pm = data.postmortem; | |
| html += `<div class="postmortem"><h4>Estimate Details</h4> | |
| <div class="fact">Base prompt: <b>${baseTokens.toLocaleString()} tokens</b></div> | |
| <div class="fact">Agent turns: <b>${pm.total_agent_turns.toLocaleString()}</b></div> | |
| <div class="fact">Tokens/turn: <b>${pm.tokens_per_turn_mid_band.input.toLocaleString()} in</b> / <b>${pm.tokens_per_turn_mid_band.output.toLocaleString()} out</b></div> | |
| <div class="fact">Complexity: <b>${pm.complexity_multiplier}</b></div> | |
| <div class="fact">Models compared: <b>${pm.provider_count}</b></div> | |
| </div>`; | |
| } | |
| panel.innerHTML = html; | |
| } | |
| function fmtModel(id) { | |
| const map = { | |
| 'openai/gpt-5.3-codex': 'GPT-5.3 Codex', 'openai/gpt-4o': 'GPT-4o', | |
| 'openai/gpt-4o-mini': 'GPT-4o Mini', 'openai/gpt-5.4': 'GPT-5.4', | |
| 'openai/gpt-5.4-mini': 'GPT-5.4 Mini', 'openai/gpt-5.4-nano': 'GPT-5.4 Nano', | |
| 'anthropic/claude-opus-4-6': 'Claude Opus 4', | |
| 'anthropic/claude-sonnet-4-20250514': 'Claude Sonnet 4', | |
| 'anthropic/claude-haiku-4-20250514': 'Claude Haiku 4', | |
| 'google/gemini-2.5-pro': 'Gemini 2.5 Pro', 'google/gemini-2.5-flash': 'Gemini 2.5 Flash', | |
| 'google/gemini-2.0-flash': 'Gemini 2.0 Flash', | |
| 'deepseek/deepseek-chat-v3': 'DeepSeek Chat v3', 'deepseek/deepseek-reasoner': 'DeepSeek Reasoner', | |
| 'xai/grok-3': 'xAI Grok-3', 'xai/grok-3-mini': 'xAI Grok-3 Mini', | |
| 'minimax/MiniMax-M2.7': 'MiniMax M2.7', 'minimax/M2.1-Ultra': 'MiniMax M2.1 Ultra', | |
| 'local/llama-3.3-70b': 'Llama 3.3 70B (local)', | |
| }; | |
| return map[id] || id.split('/').pop(); | |
| } | |
| function fmtNum(n) { | |
| if (n >= 1e6) return (n/1e6).toFixed(1)+'M'; | |
| if (n >= 1e3) return (n/1e3).toFixed(0)+'k'; | |
| return n.toString(); | |
| } | |
| // ── Init ──────────────────────────────────────────── | |
| renderQuotaUI(); | |
| // ── Live avg savings badge ─────────────────────────── | |
| (async () => { | |
| try { | |
| const r = await fetch('https://api.signalloomai.com/v1/analytics/loomlens/public'); | |
| if (!r.ok) return; | |
| const d = await r.json(); | |
| const avg = d.total_estimate_jobs > 0 | |
| ? (d.total_savings_usd / d.total_estimate_jobs).toFixed(2) | |
| : '1.23'; | |
| const el = document.getElementById('avg_savings_badge'); | |
| if (el) el.textContent = `(avg save: $${avg}/est)`; | |
| } catch(e) { /* silent — badge shows default */ } | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |