finoptix14b / index.html
ccortezb's picture
feat: custom HTML frontend (dark terminal, 4 panels, typing animation, history)
f042fca verified
Raw
History Blame Contribute Delete
23 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FinOptix-14B — FinOps Command Center</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0a0e17;--card:#111827;--border:#1e293b;--green:#00D4AA;--purple:#8b5cf6;--text:#f1f5f9;--muted:#94a3b8;--dim:#64748b}
body{font-family:'JetBrains Mono','SF Mono',monospace;background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column}
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Inter:wght@400;600;700&display=swap');
/* Header */
.header{padding:16px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
.logo{display:flex;align-items:center;gap:10px}
.logo-icon{width:32px;height:32px;background:linear-gradient(135deg,var(--purple),#6366f1);border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700}
.logo-text{font-family:'Inter',sans-serif;font-weight:700;font-size:1.1rem}
.logo-sub{color:var(--dim);font-size:.7rem;font-family:'Inter',sans-serif}
.badges{display:flex;gap:6px}
.badge{padding:3px 8px;border-radius:4px;font-size:.65rem;font-weight:600;background:#1e1b4b;color:#a78bfa;border:1px solid #4c1d95}
/* Layout */
.main{display:flex;flex:1;overflow:hidden}
.sidebar{width:200px;border-right:1px solid var(--border);padding:16px 0;flex-shrink:0}
.nav-item{padding:10px 20px;cursor:pointer;font-size:.8rem;color:var(--muted);border-left:2px solid transparent;transition:all .15s}
.nav-item:hover{color:var(--text);background:rgba(255,255,255,.02)}
.nav-item.active{color:var(--green);border-left-color:var(--green);background:rgba(0,212,170,.05)}
.content{flex:1;display:flex;flex-direction:column;overflow:hidden}
/* Panels */
.panel{display:none;flex-direction:column;flex:1;overflow:hidden}
.panel.active{display:flex}
.panel-header{padding:16px 24px;border-bottom:1px solid var(--border);font-family:'Inter',sans-serif}
.panel-header h2{font-size:1rem;font-weight:600}
.panel-header p{color:var(--dim);font-size:.75rem;margin-top:4px}
/* Input area */
.input-area{padding:16px 24px;border-bottom:1px solid var(--border)}
.input-area textarea{width:100%;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;color:var(--text);font-family:'JetBrains Mono',monospace;font-size:.85rem;resize:vertical;min-height:120px;outline:none;transition:border-color .2s}
.input-area textarea:focus{border-color:var(--green)}
.input-area textarea::placeholder{color:var(--dim)}
.controls{display:flex;gap:8px;margin-top:10px;align-items:center}
.btn{padding:8px 16px;border-radius:6px;border:none;font-family:'Inter',sans-serif;font-size:.8rem;font-weight:600;cursor:pointer;transition:all .15s}
.btn-primary{background:var(--green);color:var(--bg)}
.btn-primary:hover{background:#00eabb}
.btn-primary:disabled{opacity:.5;cursor:not-allowed}
.btn-secondary{background:var(--card);color:var(--muted);border:1px solid var(--border)}
.btn-secondary:hover{border-color:var(--green);color:var(--text)}
.token-select{background:var(--card);border:1px solid var(--border);color:var(--muted);padding:6px 10px;border-radius:6px;font-size:.75rem}
/* Output area */
.output-area{flex:1;overflow-y:auto;padding:24px}
.output-content{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:20px;font-size:.85rem;line-height:1.8;white-space:pre-wrap;min-height:100px;position:relative}
.output-content.empty{color:var(--dim);display:flex;align-items:center;justify-content:center;min-height:200px}
.output-content .cursor{display:inline-block;width:8px;height:16px;background:var(--green);animation:blink 1s step-end infinite;vertical-align:middle;margin-left:2px}
@keyframes blink{50%{opacity:0}}
/* Status bar */
.status-bar{padding:8px 24px;border-top:1px solid var(--border);display:flex;justify-content:space-between;font-size:.7rem;color:var(--dim)}
.status-dot{width:6px;height:6px;border-radius:50%;display:inline-block;margin-right:6px}
.status-dot.ready{background:var(--green)}
.status-dot.loading{background:var(--purple);animation:pulse 1s ease infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
/* Examples */
.examples{padding:8px 24px 16px;display:flex;gap:6px;flex-wrap:wrap}
.example-btn{padding:4px 10px;border-radius:4px;font-size:.7rem;background:var(--card);border:1px solid var(--border);color:var(--muted);cursor:pointer;transition:all .15s}
.example-btn:hover{border-color:var(--green);color:var(--green)}
/* Responsive */
@media(max-width:768px){.sidebar{display:none}.main{flex-direction:column}}
</style>
</head>
<body>
<div class="header">
<div class="logo">
<div class="logo-icon">F</div>
<div>
<div class="logo-text">FinOptix-14B</div>
<div class="logo-sub">FinOps Command Center • Qwen 2.5 14B + QLoRA</div>
</div>
</div>
<div class="badges">
<span class="badge">Apache 2.0</span>
<span class="badge">14B params</span>
<span class="badge">GGUF ready</span>
</div>
</div>
<div class="main">
<div class="sidebar">
<div class="nav-item active" data-panel="terraform">🛠 Terraform Audit</div>
<div class="nav-item" data-panel="cost">📊 Cost Parser</div>
<div class="nav-item" data-panel="byaml">📋 BYaML Governance</div>
<div class="nav-item" data-panel="ask">💬 Ask FinOptix</div>
</div>
<div class="content">
<!-- Terraform Panel -->
<div class="panel active" id="panel-terraform">
<div class="panel-header">
<h2>Terraform HCL Auditor</h2>
<p>Paste Terraform code → get FinOps audit report + optimized code</p>
</div>
<div class="examples">
<button class="example-btn" onclick="loadExample('terraform','resource &quot;aws_instance&quot; &quot;web&quot; {\n ami = &quot;ami-0c55b159cbfafe1f0&quot;\n instance_type = &quot;m5.4xlarge&quot;\n}')">EC2 no tags</button>
<button class="example-btn" onclick="loadExample('terraform','resource &quot;aws_s3_bucket&quot; &quot;data&quot; {\n bucket = &quot;my-data-lake&quot;\n}\n\nresource &quot;aws_s3_bucket_versioning&quot; &quot;data&quot; {\n bucket = aws_s3_bucket.data.id\n versioning_configuration { status = &quot;Enabled&quot; }\n}')">S3 no lifecycle</button>
<button class="example-btn" onclick="loadExample('terraform','resource &quot;aws_db_instance&quot; &quot;main&quot; {\n engine = &quot;mysql&quot;\n instance_class = &quot;db.r5.2xlarge&quot;\n allocated_storage = 500\n multi_az = true\n}')">RDS oversized</button>
<button class="example-btn" onclick="loadExample('terraform','resource &quot;aws_lambda_function&quot; &quot;api&quot; {\n function_name = &quot;my-api&quot;\n runtime = &quot;nodejs18.x&quot;\n memory_size = 3008\n timeout = 900\n handler = &quot;index.handler&quot;\n filename = &quot;lambda.zip&quot;\n}')">Lambda over-provisioned</button>
<select class="example-btn" style="min-width:130px" onchange="if(this.value){loadExample('terraform',this.value);this.selectedIndex=0;}">
<option value="">More examples...</option>
<option value='resource "aws_ebs_volume" "data" {\n availability_zone = "us-east-1a"\n size = 1000\n type = "gp2"\n}'>EBS gp2 (should be gp3)</option>
<option value='resource "aws_nat_gateway" "main" {\n allocation_id = aws_eip.nat.id\n subnet_id = aws_subnet.public.id\n}\n\nresource "aws_nat_gateway" "backup" {\n allocation_id = aws_eip.nat2.id\n subnet_id = aws_subnet.public2.id\n}'>Duplicate NAT GW</option>
<option value='resource "aws_elasticsearch_domain" "logs" {\n domain_name = "production-logs"\n instance_type = "r5.2xlarge.elasticsearch"\n instance_count = 3\n ebs_options {\n ebs_enabled = true\n volume_size = 500\n }\n}'>OpenSearch oversized</option>
</select>
</div>
<div class="input-area">
<textarea id="input-terraform" placeholder="Paste your Terraform HCL here..."></textarea>
<div class="controls">
<button class="btn btn-primary" onclick="run('terraform')">⚡ Audit</button>
<button class="btn btn-secondary" onclick="clearPanel('terraform')">Clear</button>
<button class="btn btn-secondary" onclick="showHistory('terraform')">📜 History</button>
<select class="token-select" id="tokens-terraform"><option value="512">512 tokens</option><option value="1024" selected>1024 tokens</option><option value="2048">2048 tokens</option></select>
</div>
</div>
<div class="output-area"><div class="output-content empty" id="output-terraform">Output will appear here...</div></div>
</div>
<!-- Cost Panel -->
<div class="panel" id="panel-cost">
<div class="panel-header">
<h2>AWS Cost JSON Parser</h2>
<p>Paste Cost Explorer or Anomaly Detection JSON → get structured analysis</p>
</div>
<div class="examples">
<button class="example-btn" onclick="loadExample('cost','[{&quot;Service&quot;:&quot;Amazon Bedrock&quot;,&quot;Amount&quot;:&quot;$36.82&quot;,&quot;Delta&quot;:&quot;+30.8%&quot;},{&quot;Service&quot;:&quot;Amazon S3&quot;,&quot;Amount&quot;:&quot;$12.41&quot;,&quot;Delta&quot;:&quot;+4.3%&quot;},{&quot;Service&quot;:&quot;AWS Lambda&quot;,&quot;Amount&quot;:&quot;$8.23&quot;,&quot;Delta&quot;:&quot;+15.1%&quot;}]')">Cost by service</button>
<button class="example-btn" onclick="loadExample('cost','{&quot;anomalies&quot;:[{&quot;service&quot;:&quot;Amazon EC2&quot;,&quot;impact&quot;:&quot;$142.50&quot;,&quot;severity&quot;:&quot;HIGH&quot;,&quot;start&quot;:&quot;2026-06-01&quot;,&quot;end&quot;:&quot;2026-06-03&quot;,&quot;root_cause&quot;:&quot;Spot interruption caused fallback to on-demand m5.2xlarge x4&quot;}]}')">Anomaly (EC2 spike)</button>
<button class="example-btn" onclick="loadExample('cost','{&quot;monthly_total&quot;:&quot;$4,230&quot;,&quot;top_services&quot;:[{&quot;name&quot;:&quot;EC2&quot;,&quot;cost&quot;:&quot;$1,850&quot;,&quot;instances&quot;:12},{&quot;name&quot;:&quot;RDS&quot;,&quot;cost&quot;:&quot;$980&quot;,&quot;instances&quot;:3},{&quot;name&quot;:&quot;S3&quot;,&quot;cost&quot;:&quot;$620&quot;,&quot;buckets&quot;:45}],&quot;savings_potential&quot;:&quot;$1,100/mo&quot;}')">Monthly summary</button>
<select class="example-btn" style="min-width:130px" onchange="if(this.value){loadExample('cost',this.value);this.selectedIndex=0;}">
<option value="">More examples...</option>
<option value='{&quot;budget_alert&quot;:{&quot;name&quot;:&quot;Production&quot;,&quot;limit&quot;:&quot;$5000&quot;,&quot;actual&quot;:&quot;$4,750&quot;,&quot;forecasted&quot;:&quot;$5,800&quot;,&quot;percent&quot;:95}}'>Budget alert (95%)</option>
<option value='[{&quot;resource&quot;:&quot;i-0abc123&quot;,&quot;type&quot;:&quot;m5.xlarge&quot;,&quot;avg_cpu&quot;:&quot;3.2%&quot;,&quot;cost_monthly&quot;:&quot;$140&quot;},{&quot;resource&quot;:&quot;i-0def456&quot;,&quot;type&quot;:&quot;c5.2xlarge&quot;,&quot;avg_cpu&quot;:&quot;8.1%&quot;,&quot;cost_monthly&quot;:&quot;$245&quot;}]'>Idle instances</option>
</select>
</div>
<div class="input-area">
<textarea id="input-cost" placeholder="Paste AWS Cost Explorer JSON output..."></textarea>
<div class="controls">
<button class="btn btn-primary" onclick="run('cost')">⚡ Analyze</button>
<button class="btn btn-secondary" onclick="clearPanel('cost')">Clear</button>
<button class="btn btn-secondary" onclick="showHistory('cost')">📜 History</button>
<select class="token-select" id="tokens-cost"><option value="512">512 tokens</option><option value="1024" selected>1024 tokens</option><option value="2048">2048 tokens</option></select>
</div>
</div>
<div class="output-area"><div class="output-content empty" id="output-cost">Output will appear here...</div></div>
</div>
<!-- BYaML Panel -->
<div class="panel" id="panel-byaml">
<div class="panel-header">
<h2>BYaML Governance Validator</h2>
<p>Paste BYaML architecture YAML → get catalog validation + policy findings</p>
</div>
<div class="examples">
<button class="example-btn" onclick="loadExample('byaml','byamlVersion: &quot;0.3&quot;\narchitecture:\n id: my-api\n environment: production\ncomponents:\n - id: api-gateway\n type: aws.apigateway\n - id: backend\n type: aws.lambda\n - id: database\n type: aws.dynamodb\nrelationships:\n - from: api-gateway\n to: backend\n type: routes_to\n - from: backend\n to: database\n type: reads_writes')">API + Lambda + DynamoDB</button>
<button class="example-btn" onclick="loadExample('byaml','byamlVersion: &quot;0.3&quot;\narchitecture:\n id: ml-platform\n environment: production\ncomponents:\n - id: training\n type: aws.sagemaker\n - id: model-store\n type: aws.s3\n - id: inference\n type: aws.lambda\n - id: monitoring\n type: aws.cloudwatch\nrelationships:\n - from: training\n to: model-store\n type: stores_in\n - from: inference\n to: model-store\n type: reads_writes')">ML Platform</button>
<button class="example-btn" onclick="loadExample('byaml','byamlVersion: &quot;0.3&quot;\narchitecture:\n id: web-app\n environment: staging\ncomponents:\n - id: cdn\n type: aws.cloudfront\n - id: origin\n type: aws.s3\n - id: api\n type: aws.apigateway\n - id: compute\n type: aws.ecs')">Web App + CDN</button>
<select class="example-btn" style="min-width:130px" onchange="if(this.value){loadExample('byaml',this.value);this.selectedIndex=0;}">
<option value="">More templates...</option>
<option value="byamlVersion: &quot;0.3&quot;\narchitecture:\n id: data-lake\n environment: production\ncomponents:\n - id: ingestion\n type: aws.kinesis\n - id: storage\n type: aws.s3\n - id: catalog\n type: aws.glue\n - id: query\n type: aws.athena">Data Lake</option>
<option value="byamlVersion: &quot;0.3&quot;\narchitecture:\n id: event-driven\n environment: production\ncomponents:\n - id: bus\n type: aws.eventbridge\n - id: processor\n type: aws.lambda\n - id: queue\n type: aws.sqs\n - id: store\n type: aws.dynamodb">Event-Driven</option>
</select>
</div>
<div class="input-area">
<textarea id="input-byaml" placeholder="Paste BYaML YAML architecture file..."></textarea>
<div class="controls">
<button class="btn btn-primary" onclick="run('byaml')">⚡ Validate</button>
<button class="btn btn-secondary" onclick="clearPanel('byaml')">Clear</button>
<button class="btn btn-secondary" onclick="showHistory('byaml')">📜 History</button>
<select class="token-select" id="tokens-byaml"><option value="512">512 tokens</option><option value="1024" selected>1024 tokens</option><option value="2048">2048 tokens</option></select>
</div>
</div>
<div class="output-area"><div class="output-content empty" id="output-byaml">Output will appear here...</div></div>
</div>
<!-- Ask Panel -->
<div class="panel" id="panel-ask">
<div class="panel-header">
<h2>Ask FinOptix</h2>
<p>Ask anything about FinOps, cloud costs, governance, or optimization (EN/ES)</p>
</div>
<div class="examples">
<button class="example-btn" onclick="loadExample('ask','What are the top 5 FinOps practices for a startup spending $5K/month on AWS?')">Startup FinOps</button>
<button class="example-btn" onclick="loadExample('ask','Explain the difference between Savings Plans and Reserved Instances. When should I use each?')">SP vs RI</button>
<button class="example-btn" onclick="loadExample('ask','How do I implement a tagging strategy that enables cost allocation by team?')">Tagging strategy</button>
<button class="example-btn" onclick="loadExample('ask','¿Cómo implemento un modelo de showback para que cada equipo vea sus costos de cloud sin acceso directo a la consola de billing?')">🇪🇸 Showback</button>
<button class="example-btn" onclick="loadExample('ask','¿Cuáles son las mejores prácticas de FinOps para reducir costos de Amazon Bedrock y modelos generativos en producción?')">🇪🇸 GenAI costs</button>
<select class="example-btn" style="min-width:130px" onchange="if(this.value){loadExample('ask',this.value);this.selectedIndex=0;}">
<option value="">More questions...</option>
<option value="What's the best strategy to handle spot instance interruptions for ML training workloads?">Spot for ML</option>
<option value="How do I set up AWS Budgets with anomaly detection alerts for a multi-account organization?">Budgets + Anomaly</option>
<option value="¿Qué métricas de unit economics debería trackear para un SaaS en AWS que cobra por API call?">🇪🇸 Unit Economics</option>
<option value="¿Cómo hago un assessment de madurez FinOps para mi organización? ¿Qué framework uso?">🇪🇸 FinOps Maturity</option>
<option value="Compare ECS Fargate vs EKS with Karpenter for cost optimization. Which is better for a 50-microservice platform?">ECS vs EKS cost</option>
</select>
</div>
<div class="input-area">
<textarea id="input-ask" placeholder="Ask a FinOps question (English or Spanish)..." style="min-height:80px"></textarea>
<div class="controls">
<button class="btn btn-primary" onclick="run('ask')">⚡ Ask</button>
<button class="btn btn-secondary" onclick="clearPanel('ask')">Clear</button>
<button class="btn btn-secondary" onclick="showHistory('ask')">📜 History</button>
<select class="token-select" id="tokens-ask"><option value="512">512 tokens</option><option value="1024" selected>1024 tokens</option><option value="2048">2048 tokens</option></select>
</div>
</div>
<div class="output-area"><div class="output-content empty" id="output-ask">Output will appear here...</div></div>
</div>
</div>
</div>
<div class="status-bar">
<div><span class="status-dot ready" id="status-dot"></span><span id="status-text">Ready</span></div>
<div>Model: Qwen2.5-14B + FinOptix LoRA • Quantized 4-bit • <a href="https://huggingface.co/ccortezb/FinOptix-14B-GGUF" style="color:var(--green);text-decoration:none;">Download GGUF →</a></div>
</div>
<script type="module">
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
let client;
async function initClient() {
client = await Client.connect(window.location.origin);
}
initClient();
const INSTRUCTIONS = {
terraform: "Audit this Terraform HCL for FinOps compliance. Report violations (missing tags, oversized instances, missing lifecycle policies). Provide severity, estimated cost impact, and remediated code.",
cost: "Analyze this AWS Cost Explorer JSON output. Identify anomalies, top cost drivers, MoM trends, and provide specific optimization recommendations with estimated savings.",
byaml: "Validate this BYaML architecture file. Check component types against the catalog, validate relationships, and enforce governance policies. Report findings by severity.",
ask: ""
};
// History system
const history = { terraform: [], cost: [], byaml: [], ask: [] };
window.showHistory = function(panel) {
const output = document.getElementById(`output-${panel}`);
if (history[panel].length === 0) {
output.innerHTML = '<span style="color:var(--dim);">No history yet. Run a query first.</span>';
output.classList.remove('empty');
return;
}
let html = '<div style="font-size:.75rem;color:var(--dim);margin-bottom:12px;">📜 Session History (' + history[panel].length + ' entries)</div>';
history[panel].forEach((entry, i) => {
html += `<div style="border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:8px;">`;
html += `<div style="font-size:.7rem;color:var(--dim);margin-bottom:6px;">#${i+1}${entry.time}</div>`;
html += `<div style="font-size:.75rem;color:var(--muted);margin-bottom:6px;border-left:2px solid var(--border);padding-left:8px;max-height:40px;overflow:hidden;">${entry.input.slice(0,100)}...</div>`;
html += `<div style="font-size:.8rem;white-space:pre-wrap;">${entry.output}</div>`;
html += `</div>`;
});
output.innerHTML = html;
output.classList.remove('empty');
};
window.run = async function(panel) {
const input = document.getElementById(`input-${panel}`).value.trim();
if (!input) return;
const output = document.getElementById(`output-${panel}`);
const dot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
const maxTokens = parseInt(document.getElementById(`tokens-${panel}`).value);
output.classList.remove('empty');
output.innerHTML = '<span class="cursor"></span>';
dot.className = 'status-dot loading';
statusText.textContent = 'Generating...';
document.querySelectorAll('.btn-primary').forEach(b => b.disabled = true);
const instruction = INSTRUCTIONS[panel] ? INSTRUCTIONS[panel] : input;
const context = INSTRUCTIONS[panel] ? input : "";
try {
const result = await client.predict("/generate", { instruction, context, max_tokens: maxTokens });
const text = result.data[0];
// Save to history
history[panel].push({ input: context || instruction, output: text, time: new Date().toLocaleTimeString() });
// Typing animation
output.innerHTML = '';
let i = 0;
const typeInterval = setInterval(() => {
if (i < text.length) {
output.textContent += text[i];
i++;
output.scrollTop = output.scrollHeight;
} else {
clearInterval(typeInterval);
}
}, 8);
} catch (e) {
output.innerHTML = `<span style="color:#ef4444;">Error: ${e.message}</span>`;
}
dot.className = 'status-dot ready';
statusText.textContent = 'Ready';
document.querySelectorAll('.btn-primary').forEach(b => b.disabled = false);
};
window.clearPanel = function(panel) {
document.getElementById(`input-${panel}`).value = '';
const output = document.getElementById(`output-${panel}`);
output.innerHTML = 'Output will appear here...';
output.classList.add('empty');
};
window.loadExample = function(panel, text) {
document.getElementById(`input-${panel}`).value = text;
};
// Navigation
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
item.classList.add('active');
document.getElementById(`panel-${item.dataset.panel}`).classList.add('active');
});
});
</script>
</body>
</html>