| <!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"> |
| |
| <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 "aws_instance" "web" {\n ami = "ami-0c55b159cbfafe1f0"\n instance_type = "m5.4xlarge"\n}')">EC2 no tags</button> |
| <button class="example-btn" onclick="loadExample('terraform','resource "aws_s3_bucket" "data" {\n bucket = "my-data-lake"\n}\n\nresource "aws_s3_bucket_versioning" "data" {\n bucket = aws_s3_bucket.data.id\n versioning_configuration { status = "Enabled" }\n}')">S3 no lifecycle</button> |
| <button class="example-btn" onclick="loadExample('terraform','resource "aws_db_instance" "main" {\n engine = "mysql"\n instance_class = "db.r5.2xlarge"\n allocated_storage = 500\n multi_az = true\n}')">RDS oversized</button> |
| <button class="example-btn" onclick="loadExample('terraform','resource "aws_lambda_function" "api" {\n function_name = "my-api"\n runtime = "nodejs18.x"\n memory_size = 3008\n timeout = 900\n handler = "index.handler"\n filename = "lambda.zip"\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> |
|
|
| |
| <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','[{"Service":"Amazon Bedrock","Amount":"$36.82","Delta":"+30.8%"},{"Service":"Amazon S3","Amount":"$12.41","Delta":"+4.3%"},{"Service":"AWS Lambda","Amount":"$8.23","Delta":"+15.1%"}]')">Cost by service</button> |
| <button class="example-btn" onclick="loadExample('cost','{"anomalies":[{"service":"Amazon EC2","impact":"$142.50","severity":"HIGH","start":"2026-06-01","end":"2026-06-03","root_cause":"Spot interruption caused fallback to on-demand m5.2xlarge x4"}]}')">Anomaly (EC2 spike)</button> |
| <button class="example-btn" onclick="loadExample('cost','{"monthly_total":"$4,230","top_services":[{"name":"EC2","cost":"$1,850","instances":12},{"name":"RDS","cost":"$980","instances":3},{"name":"S3","cost":"$620","buckets":45}],"savings_potential":"$1,100/mo"}')">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='{"budget_alert":{"name":"Production","limit":"$5000","actual":"$4,750","forecasted":"$5,800","percent":95}}'>Budget alert (95%)</option> |
| <option value='[{"resource":"i-0abc123","type":"m5.xlarge","avg_cpu":"3.2%","cost_monthly":"$140"},{"resource":"i-0def456","type":"c5.2xlarge","avg_cpu":"8.1%","cost_monthly":"$245"}]'>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> |
|
|
| |
| <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: "0.3"\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: "0.3"\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: "0.3"\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: "0.3"\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: "0.3"\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> |
|
|
| |
| <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: "" |
| }; |
| |
| |
| 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]; |
| |
| history[panel].push({ input: context || instruction, output: text, time: new Date().toLocaleTimeString() }); |
| |
| 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; |
| }; |
| |
| |
| 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> |
|
|