← Back to all documents

cai-exos-systems/daveadmin-exos-demo:assets/js/demo.js

gitea 16,847 words Source ↗
assets/js/demo.js ```text /** * Exos Agentic BSS — Demo Chat UI * VS Code–style 4-panel layout */ const AGENT_PROMPTS = { billing: [ "Show me the latest invoice and break down the main charges", "How does this month's invoice compare to last month — what's driving the change?", "Which line items on the latest invoice are unusually high, and why?", "Based on this account's usage profile, what products would you recommend?", ], product: [ "Show the full product catalogue with current pricing", "Which products in the catalogue are end-of-life or being phased out?", "List all enterprise connectivity products and their pricing tiers", "Add a new SD-WAN Enterprise product to the catalogue at $4,500/month", ], order: [ "Show all failed orders — what went wrong with each one?", "Which orders are currently blocked and what needs to happen to unblock them?", "What is the combined revenue impact of all failed and stalled orders?", "Place a new order for SD-WAN Enterprise service for this account", ], care: [ "Show the full customer profile for this account", "List all open support tickets — highlight any P1 or P2 incidents", "Who is the account manager and what is the escalation path for urgent issues?", "Create a P2 support ticket for a connectivity issue on the primary circuit", ], super: [ "Map the strongest TM Forum aligned BSS story for {account} and explain how EXOS should sell it today", "Build a super-mode order-to-bill plan for {account} with the key TM Forum APIs and agent steps", "Show how CaveauAI, tool orchestration, and TM Forum retrieval make {account} a credible EXOS demo before the fine-tune lands", "Suggest the best improvements before the EXOS telecom fine-tune for {account}", ], creator: [ "Create a customer rescue agent for billing disputes, failed orders, and urgent tickets.", "Design a quote-to-order agent that uses TMF648, TMF679, TMF622, and TMF641.", "Create a product lifecycle agent for catalogue, inventory, and retirement risk.", "Build a field care agent that opens standards-compliant tickets and cites CaveauAI evidence.", ], toolcreator: [ "Generate the MCP tools for the customer rescue agent and call the Factory.", "Create a Factory-ready Estate Manifest for billing, order, product, customer, and support tools.", "Build a Copilot-ready tool digest for Exosphere customer operations.", "Create a read-only toolset for customer profile, catalogue, bills, and tickets.", ], quote: [ "Create a quote for {account}.", "Check billing and tickets before ordering for {account}.", "Finish the safe parts and route exceptions for {account}.", "Show the standards mapping for {account}.", ], order_sherpa: [ "Why is ORD-70021 stuck? Walk me through the full fallout chain.", "Check warehouse inventory for DOCSIS 3.1 modems — any alternatives to Oslo?", "What is the SLA risk on ORD-70021 and what needs to happen today?", "Show me the field ops status and recommend the fastest path to resolution.", ], enterprise_billing: [ "Roll up all subsidiary spend for March 2026 — consolidated total across Nordics, UK, and EMEA.", "Detect billing anomalies across the Acme Holdings group and flag anything actionable.", "Show me the contract compliance status for each Acme subsidiary.", "Break down March spend by cost centre across the three subsidiaries.", ], };
assets/js/demo.js on ORD-70021 and what needs to happen today?", "Show me the field ops status and recommend the fastest path to resolution.", ], enterprise_billing: [ "Roll up all subsidiary spend for March 2026 — consolidated total across Nordics, UK, and EMEA.", "Detect billing anomalies across the Acme Holdings group and flag anything actionable.", "Show me the contract compliance status for each Acme subsidiary.", "Break down March spend by cost centre across the three subsidiaries.", ], }; // Zava-specific prompts — reference real MCP data injected by agent.php // Master account: 100-445782 (Family Bundle — Adults + Teens sub-accounts) const ZAVA_PROMPTS = { billing: [ "Show the latest consolidated bill for account 100-445782", "Compare this month's bill to last month — what's driving any changes on the family bundle?", "Break down the charges across the Adults and Teens sub-accounts", "Which lines on the family bundle have roaming charges this month?", ], product: [ "Show all active products in the Zava catalogue with current pricing", "Which product categories does Zava Telecom offer — fetch live from the MCP server", "List active 5G offerings with their contract terms and pricing", "Which products include global roaming and what are the add-on options?", ], order: [ "Show all failed orders — what went wrong with each one?", "Explain the failure on Order 1999 (Unified Connect+) and the resolution path", "What happened with the Satellite TV Installation Package failure?", "What is the resolution recommended for each failed order right now?", ], care: [ "Show the available service categories from the live Zava MCP connection", "What support categories are available for Zava Telecom customers?", "List the service categories from the MCP server and map them to TM Forum domains", "Which service categories cover 5G connectivity issues in the Dhaka region?", ], }; // ── Stack constants ──────────────────────────────────────────────────────────── let currentStack = null; // 'bnl' | 'zava' // All tools exposed by the Zava MCP servers, grouped by agent domain const ZAVA_TOOL_CATALOG = { billing: [ { name: 'show-latest-bill', label: 'Latest Bill', params: ['accountNumber'], write: false }, { name: 'compare-latest-bill', label: 'Compare Bills', params: ['accountNumber'], write: false }, { name: 'latest-bill-high-reason', label: 'High Bill Reason', params: ['accountNumber'], write: false }, { name: 'product-offering-suggestion', label: 'Product Suggestions', params: ['name'], write: false }, { name: 'place-a-new-order', label: 'Place New Order', params: ['category','main_product','customer_name','add_ons'], write: true }, ], product: [ { name: 'fetch-product-price-lifecyclestatus', label: 'Product Catalogue', params: ['offset','limit','lifecycleStatus'], write: false }, { name: 'get-category', label: 'Categories', params: ['offset','limit'], write: false }, { name: 'get-product-specification-by-id', label: 'Product Spec', params: ['id'], write: false }, { name: 'pricing-add-offering', label: 'Add Product', params: ['name','description','priceValue','priceUnit','taxAmountValue','taxAmountUnit','timeAmount','timeUnit'], write: true }, { name: 'product-update', label: 'Update Product', params: ['id','name','description','priceValue','priceUnit','locationName','durationAmount','durationUnit'], write: true }, { name: 'category-management-add', label: 'Add Category', params: ['categoryName','description','subCategoryNumber','subCategoryName'], write: true }, { name: 'delete-product-with-price', label: 'Delete Product', params: ['id'], write: true }, ], order: [ { name: 'product-order-failure', label: 'Failed Orders', params: [], write: false }, { name: 'order-failure-resolution', label: 'Order Resolution', params: ['orderId'], write: false }, { name: 'cancel-order', label: 'Cancel Order', params: ['orderId'], write: true }, { name: 'recreate-order-for-failed-orders', label: 'Recreate Order', params: ['main_product','add_ons'], write: true }, ], care: [ { name: 'get-category', label: 'Service Categories', params: ['offset','limit'], write: false }, ], };
assets/js/demo.js }, { name: 'delete-product-with-price', label: 'Delete Product', params: ['id'], write: true }, ], order: [ { name: 'product-order-failure', label: 'Failed Orders', params: [], write: false }, { name: 'order-failure-resolution', label: 'Order Resolution', params: ['orderId'], write: false }, { name: 'cancel-order', label: 'Cancel Order', params: ['orderId'], write: true }, { name: 'recreate-order-for-failed-orders', label: 'Recreate Order', params: ['main_product','add_ons'], write: true }, ], care: [ { name: 'get-category', label: 'Service Categories', params: ['offset','limit'], write: false }, ], }; const AGENT_META = { billing: { icon: '💶', label: 'Billing Expert', tmf: 'TMF678 Customer Bill Management', tools: ['get_invoice_summary', 'compare_bills', 'search_standards_corpus'], welcome: `Demonstrates the <em>TMF678 Customer Bill Management</em> API pattern on scaffolded BSS data. Each tool call shows the exact endpoint contract and response shape — then hit <strong>Build →</strong> in the Tool Trace to generate the real <strong>FOSSbilling</strong> integration code.<br><br> Powered by a locally fine-tuned telecom LLM (<strong>telecom-bss-v1</strong>) with TM Forum corpus RAG for standards evidence.<br><br> <span style="color:var(--text-muted);font-size:0.8rem">← Click any example · hit Build → in the trace to generate the real tool</span>`, }, product: { icon: '📦', label: 'Product Expert', tmf: 'TMF620 Product Catalog Management', tools: ['get_product_catalogue', 'search_standards_corpus'], welcome: `Demonstrates the <em>TMF620 Product Catalog Management</em> API pattern on scaffolded BSS data. See lifecycle status, MRR, and capacity flags as a real agent would — then hit <strong>Build →</strong> in the Tool Trace to generate the real <strong>FOSSbilling</strong> catalogue integration.<br><br> Powered by a locally fine-tuned telecom LLM with TM Forum corpus RAG.<br><br> <span style="color:var(--text-muted);font-size:0.8rem">← Click any example · hit Build → in the trace to generate the real tool</span>`, }, order: { icon: '📋', label: 'Order Expert', tmf: 'TMF622 Product Ordering Management', tools: ['list_orders', 'diagnose_order', 'search_standards_corpus'], welcome: `Demonstrates the <em>TMF622 Product Ordering Management</em> API pattern on scaffolded BSS data. See failed orders, root-cause diagnosis, and ARR at risk as a real agent would — then hit <strong>Build →</strong> in the Tool Trace to generate the real <strong>FOSSbilling</strong> order integration.<br><br> Powered by a locally fine-tuned telecom LLM with TM Forum corpus RAG.<br><br> <span style="color:var(--text-muted);font-size:0.8rem">← Click any example · hit Build → in the trace to generate the real tool</span>`, }, care: { icon: '🤝', label: 'Care Agent', tmf: 'TMF629/TMF621 Customer &amp; Trouble Ticket', tools: ['get_customer_profile', 'list_tickets', 'create_ticket', 'search_standards_corpus'], welcome: `Demonstrates the <em>TMF629 Customer Management</em> and <em>TMF621 Trouble Ticket</em> API patterns on scaffolded BSS data. See P1–P4 ticket triage and customer profiles as a real agent would — then hit <strong>Build →</strong> in the Tool Trace to generate the real <strong>SuiteCRM</strong> integration.<br><br> Powered by a locally fine-tuned telecom LLM with TM Forum corpus RAG.<br><br> <span style="color:var(--text-muted);font-size:0.8rem">← Click any example · hit Build → in the trace to generate the real tool</span>`, }, super: { icon: 'TMF', label: 'Super Mode', welcome: 'Run the TM Forum aware EXOS super-mode flow with CaveauAI retrieval, tool traces, and runtime routing.' }, creator: { icon: 'AC', label: 'Agent Creator', welcome: 'Compose a standards-first EXOS agent with TM Forum APIs, CaveauAI evidence, guardrails, and a tool manifest.' }, toolcreator: { icon: 'TC', label: 'Tool Creator', welcome: 'Turn an agent intent into a Factory-ready Estate Manifest, MCP tool spec, and Copilot digest.' }, quote: { icon: 'Q2O', label: 'Quote-to-Order', welcome: 'Pick a sample customer and run the whole FOSSBilling-backed path: onboarding, qualification, quote, order, invoice, provisioning, and tickets.' }, order_sherpa: { icon: '🚚', label: 'Order Sherpa', tmf: 'TMF622 Product Ordering\nTMF638 Resource Inventory', tools: ['get_order_status', 'get_fallout_details', 'check_warehouse_inventory', 'get_field_ops', 'assess_sla_risk'], welcome: `Demonstrates MSO/cable <em>order fallout management</em> using TMF622 Product Ordering and TMF638 Resource Inventory patterns.<br><br> Greenfield Apartments (ORD-70021) is suspended due to a DOCSIS 3.1 modem shortage at the Oslo warehouse. The agent diagnoses the fallout chain, checks alternate depots (Bergen: 12 available), assesses SLA risk, and recommends a resolution path.<br><br> <span style="color:var(--text-muted);font-size:0.8rem">← Click an example to start · tool trace shows each step in the Explorer panel</span>`, }, enterprise_billing: { icon: '🏢', label: 'Enterprise Billing Expert', tmf: 'TMF678 Customer Bill\nTMF629 Customer Management', tools: ['get_enterprise_hierarchy', 'get_consolidated_bill', 'detect_billing_anomalies', 'check_contracts', 'get_cost_allocation'], welcome: `Demonstrates <em>enterprise telco billing</em> with parent/child account hierarchies using TMF678 Customer Bill and TMF629 Customer Management patterns.<br><br> Acme Holdings consolidates spend across Nordics (EUR 6,842), UK (EUR 5,931), and EMEA (EUR 9,774) for a March 2026 total of EUR 22,547. Two anomalies are live: UK has 31 unused PBX extensions (EUR 372/mo), EMEA has a cloud compute overage of +32%. SuiteCRM provides the account hierarchy and contract data.<br><br> <span style="color:var(--text-muted);font-size:0.8rem">← Click an example · hierarchy + contracts pulled from SuiteCRM · billing from FOSSBilling</span>`, }, home: { icon: 'EX', label: 'Exos Agentic BSS', welcome: '' }, };
assets/js/demo.js Nordics (EUR 6,842), UK (EUR 5,931), and EMEA (EUR 9,774) for a March 2026 total of EUR 22,547. Two anomalies are live: UK has 31 unused PBX extensions (EUR 372/mo), EMEA has a cloud compute overage of +32%. SuiteCRM provides the account hierarchy and contract data.<br><br> <span style="color:var(--text-muted);font-size:0.8rem">← Click an example · hierarchy + contracts pulled from SuiteCRM · billing from FOSSBilling</span>`, }, home: { icon: 'EX', label: 'Exos Agentic BSS', welcome: '' }, }; AGENT_META.care.welcome = `Uses the live <strong>FOSSBilling via Blue Note</strong> test BSS for Customer Care. The Care agent can list open tickets, create P1-P4 support tickets, and verify created tickets by reading them back from FOSSBilling through the TMF621 Trouble Ticket pattern.<br><br> Powered by <strong>telecom-bss-v1</strong> with TM Forum corpus RAG, with Sure Telecom P1/P2 incidents seeded in the FOSSBilling support table for a real demo path.<br><br> <span style="color:var(--text-muted);font-size:0.8rem">Click an example or ask for ticket IDs; results should cite persisted FOSSBilling ticket records.</span>`; const CREATOR_TMF_APIS = { TMF620: { label: 'Product Catalog', detail: 'offers, prices, lifecycle' }, TMF648: { label: 'Quote Management', detail: 'quote proposals' }, TMF679: { label: 'Offering Qualification', detail: 'eligibility checks' }, TMF622: { label: 'Product Ordering', detail: 'order capture and tracking' }, TMF641: { label: 'Service Ordering', detail: 'activation and fulfillment' }, TMF629: { label: 'Customer Management', detail: 'profile and account context' }, TMF621: { label: 'Trouble Ticket', detail: 'incidents and support' }, TMF678: { label: 'Customer Bill', detail: 'invoices and bill items' }, TMF637: { label: 'Product Inventory', detail: 'installed products' }, }; const DIFY_APP_URLS = { billing: 'https://dify.bluenotelogic.com/app/94893e3e-7713-44a5-be17-8106922c4da0/configuration', care: 'https://dify.bluenotelogic.com/app/192babc1-35a9-4cf1-8a5b-225f7af0e33e/configuration', order: 'https://dify.bluenotelogic.com/app/313bec2d-17e8-4504-85d4-6067b5d92ea5/configuration', product: 'https://dify.bluenotelogic.com/app/f9523582-caa3-459f-b859-693e49eb8e21/configuration', }; const ZAVA_MCP_CONNECTORS = [ { agent: 'billing', title: 'MCP_Billing_Expert', tmf: 'TMF678', host: '119.148.10.57', path: '/mcp-server-billing-faster/mcp', protocol: 'mcp-streamable-1.0', file: 'BillingExpertlatest.swagger.json', desc: 'Latest bill, month comparison, anomaly detection', }, { agent: 'order', title: 'Order_Expert', tmf: 'TMF622', host: '119.148.10.57', path: '/mcp-order-expert-faster/mcp', protocol: 'mcp-streamable-1.0', file: 'OrderExpert.swagger.json', desc: 'Order lookup, failed orders, cancel, place order', }, { agent: 'product', title: 'Product_Expert', tmf: 'TMF620', host: '119.148.10.57', path: '/mcp-order-expert-faster/mcp', protocol: 'mcp-streamable-1.0', file: 'ProductExpert.swagger.json', desc: 'Catalogue, categories, add/update/delete product', }, ]; const TOOL_CREATOR_DOMAINS = { billing: { label: 'Billing', detail: 'latest bill, compare, high-bill reason', tmf: 'TMF678' }, product: { label: 'Product', detail: 'catalogue, lifecycle, price actions', tmf: 'TMF620' }, order: { label: 'Order', detail: 'failed orders and order creation', tmf: 'TMF622' }, customer: { label: 'Customer', detail: 'profile and account manager context', tmf: 'TMF629' }, case: { label: 'Trouble', detail: 'tickets, incidents, support cases', tmf: 'TMF621' }, }; const ACCOUNT_META = { '1': 'Sure Telecom', '2': 'Telesur Suriname', '3': 'CW Seychelles', '4': 'Digicel Pacific', '5': 'Vodacom Mozambique', '14': 'Zava Telecom — Dhaka', '17': 'Greenfield Apartments', '18': 'Acme Holdings', '19': 'Acme Nordics', '20': 'Acme UK', '21': 'Acme EMEA', '6': 'Carter Household - Philadelphia', '7': 'Harborview Apartments', '8': 'Northstar Studios', '9': 'Meridian Health Clinics', '10': 'Lakeside Family', '11': 'Bayfront Dental Group', '12': 'Parkside Theater', '13': 'Summit Property Management', };
assets/js/demo.js support cases', tmf: 'TMF621' }, }; const ACCOUNT_META = { '1': 'Sure Telecom', '2': 'Telesur Suriname', '3': 'CW Seychelles', '4': 'Digicel Pacific', '5': 'Vodacom Mozambique', '14': 'Zava Telecom — Dhaka', '17': 'Greenfield Apartments', '18': 'Acme Holdings', '19': 'Acme Nordics', '20': 'Acme UK', '21': 'Acme EMEA', '6': 'Carter Household - Philadelphia', '7': 'Harborview Apartments', '8': 'Northstar Studios', '9': 'Meridian Health Clinics', '10': 'Lakeside Family', '11': 'Bayfront Dental Group', '12': 'Parkside Theater', '13': 'Summit Property Management', }; const QTO_CUSTOMERS = { '6': { shortName: 'Carter Household', need: 'Existing household wants broadband, mobile, streaming, and a Universal experience under $140/month.', invoice: 'QTO-100 - $137/mo', ticket: 'Universal approval + streaming remediation', }, '7': { shortName: 'Harborview Apartments', need: 'New MDU customer needs onboarding, service qualification, install scheduling, and resident streaming.', invoice: 'QTO-201 - $167/mo', ticket: 'Building access onboarding', }, '8': { shortName: 'Northstar Studios', need: 'Media customer needs connectivity plus screening-room streaming with a rights approval gate.', invoice: 'QTO-202 - $148/mo', ticket: 'Rights + partner approval', }, '9': { shortName: 'Meridian Health Clinics', need: 'New branch rollout needs resilient broadband, mobile failover, and finance approval for discount.', invoice: 'QTO-203 - $164/mo', ticket: 'Discount + billing contact', }, '10': { shortName: 'Lakeside Family', need: 'Simple household quote: broadband plus streaming under $120/month, no blocking ticket.', invoice: 'QTO-204 - $108/mo', ticket: 'No blocker', }, '11': { shortName: 'Bayfront Dental Group', need: 'New clinic branch needs broadband and mobile failover after billing contact verification.', invoice: 'QTO-205 - $146/mo', ticket: 'Billing contact verification', }, '12': { shortName: 'Parkside Theater', need: 'Venue needs connectivity plus streaming/event entitlement before opening weekend.', invoice: 'QTO-206 - $132/mo', ticket: 'Entitlement approval', }, '13': { shortName: 'Summit Property Management', need: 'Property manager needs an MDU quote with building access scheduling and pending setup.', invoice: 'QTO-207 - $158/mo', ticket: 'Install access scheduling', }, };
assets/js/demo.js { level: 'warn', text: 'Cloud commitment expires May 2026 — renegotiate to cover analytics baseline' }, ], }, billing: { metrics: [ { icon: '💶', label: 'Mar 2026', value: 'EUR 9,774' }, { icon: '☁️', label: 'Cloud overage', value: '+EUR 1,248 vs commitment' }, { icon: '📈', label: 'Month-on-month', value: '+32% (analytics workload)' }, ], alerts: [ { level: 'critical', text: 'Cloud compute overage will recur unless commitment is renegotiated' }, ], }, }, }; const TOOL_LABELS = { 'show-latest-bill': 'fetching latest invoice', 'compare-latest-bill': 'comparing with previous bill', 'latest-bill-high-reason': 'analysing high charges', 'product-offering-suggestion': 'building product recommendations', 'get-product-catalogue-with-price': 'loading product catalogue', 'get-product-catalogue-lifecycle-status': 'checking product lifecycle', 'add-product-with-price': 'adding product to catalogue', 'product-order-failure': 'diagnosing order failures', 'place-a-new-order': 'placing new order', 'get-customer-profile': 'loading customer profile', 'list-support-tickets': 'fetching support tickets', 'create-support-ticket': 'creating support ticket', 'get-account-manager': 'looking up account manager', 'route_super_mode': 'routing the super-mode runtime', 'retrieve_corpus_context': 'retrieving TM Forum and EXOS corpus evidence', 'draft_super_mode_response': 'drafting the TM Forum aware EXOS answer', 'sense_customer_intent': 'reading the customer intent', 'directus_catalog_search': 'loading Directus offer catalog', 'opa_policy_decision': 'checking OPA guardrails', 'fossbilling_customer_context': 'reading FOSSBilling customer/order/ticket context', 'fossbilling_quote_invoice': 'rating bundle from FOSSBilling invoice items', 'flexprice_quote_rating': 'rating quote in the legacy pricing sidecar', 'caveauai_tmf_context': 'mapping CaveauAI TMF context', 'provision_broadband': 'scheduling broadband activation', 'activate_streaming_entitlement': 'activating streaming entitlement', 'route_experience_approval': 'routing approval task', 'caveauai_search_corpus': 'searching CaveauAI corpus evidence', 'get_order_status': 'fetching order status from FOSSBilling', 'get_fallout_details': 'retrieving fallout reason and task chain', 'check_warehouse_inventory': 'checking warehouse DOCSIS modem stock', 'get_field_ops': 'loading field technician assignment', 'assess_sla_risk': 'calculating SLA risk and days remaining', 'get_enterprise_hierarchy': 'loading Acme Holdings account hierarchy from SuiteCRM', 'get_consolidated_bill': 'rolling up FOSSBilling invoices across subsidiaries', 'detect_billing_anomalies': 'scanning for billing anomalies and overage flags', 'check_contracts': 'fetching SuiteCRM contracts and expiry status', 'get_cost_allocation': 'allocating costs by cost centre', }; const PATIENCE_MSGS = [ 'Thinking… running on BNL private GPU', 'Still working… model processing your request', 'Querying BSS backend tools…', 'Running on a shared dev GPU — hang tight!', 'Almost there… (or ask us about an API key 😅)', ]; // ── State ────────────────────────────────────────────────── let currentAgent = 'home'; let currentAccount = '6'; let conversationId = null; let isStreaming = false; let useRag = false; let patienceTimer = null; let pendingJsonAnswer = null; let traceCardStack = {}; let traceSeq = 0; let creatorProfile = { name: 'EXOS Customer Rescue Agent', domain: 'telecom BSS customer operations', mission: 'Resolve billing disputes, failed orders, and support issues with TM Forum compliant tools and CaveauAI evidence.', channel: 'EXOS demo console and future Copilot Studio', guardrails: 'Read before write, cite evidence, require approval for irreversible changes, and open TMF621 tickets for exceptions.', corpus: 'exos_bss', selected_apis: ['TMF678', 'TMF629', 'TMF622', 'TMF621', 'TMF620'], }; let toolProfile = { name: 'EXOS Customer Rescue Toolset', domain: 'telecom BSS customer operations', mission: 'Generate a governed MCP layer for billing disputes, failed orders, product suggestions, customer profile, and support tickets.', customer_name: 'Sure Telecom', environment: 'demo', adapter_target: 'fossbilling', write_policy: 'approval_gated', corpus: 'exos_bss', target_domains: ['billing', 'product', 'order', 'customer', 'case'], }; // ── DOM refs ─────────────────────────────────────────────── const messagesEl = document.getElementById('chat-messages'); const inputEl = document.getElementById('chat-input'); const sendBtn = document.getElementById('send-btn'); const promptsEl = document.getElementById('prompts-list'); const agentLabel = document.getElementById('agent-label'); // ── Initialise ───────────────────────────────────────────── // ── RAG / mode toggle ────────────────────────────────────── function setMode(mode) { useRag = (mode === 'ai'); localStorage.setItem('exos-mode', useRag ? 'ai' : 'standard'); document.getElementById('btn-mode-rag')?.classList.toggle('rag-pill-active', useRag); document.getElementById('btn-mode-std')?.classList.toggle('rag-pill-active', !useRag); // When switching to standard, default to 7B; to AI, default to fine-tuned if (useRag) { selectLlm('telecom-bss-v1'); } else { selectLlm('qwen2.5:7b'); } const sbRag = document.getElementById('sb-rag'); if (sbRag) sbRag.textContent = useRag ? '🧠 Corpus RAG on' : '⚡ Standard'; clearChat(); renderWelcome(); }
assets/js/demo.js const agentLabel = document.getElementById('agent-label'); // ── Initialise ───────────────────────────────────────────── // ── RAG / mode toggle ────────────────────────────────────── function setMode(mode) { useRag = (mode === 'ai'); localStorage.setItem('exos-mode', useRag ? 'ai' : 'standard'); document.getElementById('btn-mode-rag')?.classList.toggle('rag-pill-active', useRag); document.getElementById('btn-mode-std')?.classList.toggle('rag-pill-active', !useRag); // When switching to standard, default to 7B; to AI, default to fine-tuned if (useRag) { selectLlm('telecom-bss-v1'); } else { selectLlm('qwen2.5:7b'); } const sbRag = document.getElementById('sb-rag'); if (sbRag) sbRag.textContent = useRag ? '🧠 Corpus RAG on' : '⚡ Standard'; clearChat(); renderWelcome(); } function initModeToggle() { useRag = false; document.getElementById('btn-mode-rag')?.classList.remove('rag-pill-active'); document.getElementById('btn-mode-std')?.classList.add('rag-pill-active'); const sbRag = document.getElementById('sb-rag'); if (sbRag) sbRag.textContent = '⚡ Standard'; selectLlm('qwen2.5:7b'); } function init() { initAllResizers(); initModelParams(); initModeToggle(); collapseAdvancedPanels(); initStack(); // must come after DOM is ready, sets currentStack + applies mode } // ── Stack management ─────────────────────────────────────────────────────────── function initStack() { updateStackSplashCopy(); const saved = localStorage.getItem('exos-stack'); const splash = document.getElementById('stack-splash'); if (saved === 'bnl' || saved === 'zava') { currentStack = saved; applyStack(false); splash?.classList.remove('stack-splash-fadeout', 'stack-splash-gone'); return; } currentStack = 'bnl'; applyStack(false); splash?.classList.remove('stack-splash-fadeout', 'stack-splash-gone'); // If no stack saved, splash stays visible so the user must choose. } function updateStackSplashCopy() { const bnlCard = document.querySelector('.stack-card-bnl'); if (!bnlCard) return; const desc = bnlCard.querySelector('.stack-card-desc'); const tags = bnlCard.querySelector('.stack-card-tags'); const cta = bnlCard.querySelector('.stack-card-cta'); if (desc) { desc.innerHTML = 'Live FOSSBilling test BSS with Customer Care writes<br>TMF621 tickets: list, create, verify by read-back'; } if (tags) { tags.innerHTML = '<span>Live support tickets</span><span>P1/P2 incidents</span><span>FOSSBilling write-back</span><span>CaveauAI RAG</span>'; } if (cta) { cta.textContent = 'Open Blue Note FOSSBilling ->'; } } function selectStack(stack) { currentStack = stack; localStorage.setItem('exos-stack', stack); const splash = document.getElementById('stack-splash'); if (splash) { splash.classList.add('stack-splash-fadeout'); setTimeout(() => splash.classList.add('stack-splash-gone'), 380); } applyStack(true); } function switchStack(targetStack) { if (currentStack === targetStack) return; const name = targetStack === 'zava' ? 'Zava Telecom via Copilot + Dhaka MCP' : 'FOSSBilling via Blue Note'; if (!confirm(`Switch to ${name}?\n\nYour current conversation will be cleared. Changes you made to the Zava MCP server remain on the live server.`)) return; selectStack(targetStack); } function applyStack(doRender) { document.body.dataset.stack = currentStack ?? ''; if (currentStack === 'zava') { // Zava mode: always account 14, no Q2O or Super currentAccount = '14'; if (['quote', 'super'].includes(currentAgent)) currentAgent = 'billing'; document.querySelectorAll('.carrier-select').forEach(s => { s.value = '14'; }); } else { // BNL mode: reset to Sure Telecom if on Zava if (currentAccount === '14') { currentAccount = '1'; document.querySelectorAll('.carrier-select').forEach(s => { s.value = '1'; }); } } updateStackIndicator(); syncAccountSelects(currentAccount); document.querySelectorAll('.agent-tab, .mode-tab').forEach(t => { t.classList.toggle('active', t.dataset.agent === currentAgent); }); if (agentLabel) agentLabel.textContent = AGENT_META[currentAgent]?.label ?? currentAgent; updateQtoCustomerNeed(); renderAgentCockpit(); renderPrompts(); renderSidebar(); if (doRender) clearChat(); renderWelcome(); updateStatusBar(); } function updateStackIndicator() { const el = document.getElementById('stack-indicator'); if (!el) return; if (currentStack === 'zava') { el.innerHTML = `<button class="stack-pill stack-pill-zava stack-pill-active" title="Zava Telecom MCP — click to switch">` + `<span class="mcp-conn-dot" style="display:inline-block;width:6px;height:6px;margin-right:0.3rem;vertical-align:middle"></span>Zava MCP</button>` + `<button class="stack-pill stack-pill-bnl" onclick="switchStack('bnl')" title="Switch to BNL Stack">BNL</button>`; } else { el.innerHTML = `<button class="stack-pill stack-pill-bnl stack-pill-active" title="BNL / FOSSBilling">BNL</button>` + `<button class="stack-pill stack-pill-zava" onclick="switchStack('zava')" title="Switch to Zava MCP">` + `<span class="mcp-conn-dot" style="display:inline-block;width:6px;height:6px;margin-right:0.3rem;vertical-align:middle;opacity:0.7"></span>Zava MCP</button>`; } } // ── Write confirm modal ──────────────────────────────────────────────────────── let _pendingWriteFn = null; function showWriteConfirm(tool, args, onConfirm) { const modal = document.getElementById('write-confirm-modal'); const tEl = document.getElementById('write-modal-tool'); const argsEl = document.getElementById('write-modal-args'); if (!modal) { onConfirm(); return; } _pendingWriteFn = onConfirm; if (tEl) tEl.textContent = tool; if (argsEl) argsEl.textContent = JSON.stringify(args, null, 2); modal.classList.remove('modal-hidden'); } function cancelWrite() { _pendingWriteFn = null; document.getElementById('write-confirm-modal')?.classList.add('modal-hidden'); }
assets/js/demo.js FOSSBilling">BNL</button>` + `<button class="stack-pill stack-pill-zava" onclick="switchStack('zava')" title="Switch to Zava MCP">` + `<span class="mcp-conn-dot" style="display:inline-block;width:6px;height:6px;margin-right:0.3rem;vertical-align:middle;opacity:0.7"></span>Zava MCP</button>`; } } // ── Write confirm modal ──────────────────────────────────────────────────────── let _pendingWriteFn = null; function showWriteConfirm(tool, args, onConfirm) { const modal = document.getElementById('write-confirm-modal'); const tEl = document.getElementById('write-modal-tool'); const argsEl = document.getElementById('write-modal-args'); if (!modal) { onConfirm(); return; } _pendingWriteFn = onConfirm; if (tEl) tEl.textContent = tool; if (argsEl) argsEl.textContent = JSON.stringify(args, null, 2); modal.classList.remove('modal-hidden'); } function cancelWrite() { _pendingWriteFn = null; document.getElementById('write-confirm-modal')?.classList.add('modal-hidden'); } function confirmWrite() { document.getElementById('write-confirm-modal')?.classList.add('modal-hidden'); if (_pendingWriteFn) { _pendingWriteFn(); _pendingWriteFn = null; } } // ── Zava write action executor ───────────────────────────────────────────────── async function executeZavaWrite(server, tool, args) { const seq = ++toolCallCount; appendTraceCard({ event: 'tool_start', tool, is_mcp: true, mcp_server: '119.148.10.57', input: args, seq }); const badge = document.getElementById('tool-call-count'); if (badge) { badge.textContent = toolCallCount; badge.style.display = ''; } try { const resp = await fetch('/api/mcp-zava.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ server, tool, args }), }); const data = await resp.json(); updateTraceCard({ event: 'tool_result', tool, output: data.text || JSON.stringify(data.result), latency_ms: data.latency_ms }); appendAiMsg(`**${tool}** executed.\n\n${data.text || JSON.stringify(data.result, null, 2)}`); } catch (err) { updateTraceCard({ event: 'tool_result', tool, output: 'Error: ' + err.message, latency_ms: 0 }); appendError('Write operation failed: ' + err.message); } } function collapseAdvancedPanels() { const bottom = document.getElementById('panel-bottom'); if (bottom) { bottom.classList.add('panel-bottom-collapsed'); const btn = bottom.querySelector('.panel-collapse-btn'); if (btn) btn.textContent = '^'; } document.getElementById('thinking-panel')?.classList.add('panel-side-collapsed'); } // ── Panel resize ─────────────────────────────────────────── function initResize(handleId, cssVar, direction, invertDelta) { const handle = document.getElementById(handleId); if (!handle) return; let startPos, startSize, newSize; handle.addEventListener('mousedown', e => { e.preventDefault(); startPos = direction === 'h' ? e.clientX : e.clientY; startSize = parseInt(getComputedStyle(document.documentElement).getPropertyValue(cssVar)) || 0; handle.classList.add('dragging'); const onMove = e => { const delta = (direction === 'h' ? e.clientX : e.clientY) - startPos; const sign = invertDelta ? -1 : 1; newSize = Math.max(120, Math.min(600, startSize + sign * delta)); document.documentElement.style.setProperty(cssVar, newSize + 'px'); }; const onUp = () => { handle.classList.remove('dragging'); document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); if (newSize != null) localStorage.setItem('exos-' + cssVar, newSize); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); } function initAllResizers() { // Restore saved sizes ['--left-panel-w', '--right-panel-w', '--bottom-panel-h'].forEach(v => { const saved = localStorage.getItem('exos-' + v); if (saved) document.documentElement.style.setProperty(v, saved + 'px'); }); initResize('rh-left', '--left-panel-w', 'h', false); // drag right → expand left panel initResize('rh-right', '--right-panel-w', 'h', true); // drag left → expand right panel initResize('rh-bottom', '--bottom-panel-h', 'v', true); // drag up → expand bottom panel } // ── Left panel tab switching ─────────────────────────────── let leftTab = 'context'; function switchLeftTab(tab) { leftTab = tab; document.getElementById('left-context-tab').style.display = tab === 'context' ? '' : 'none'; document.getElementById('left-tools-tab').style.display = tab === 'tools' ? '' : 'none'; document.getElementById('ptab-context').classList.toggle('ptab-active', tab === 'context'); document.getElementById('ptab-tools').classList.toggle('ptab-active', tab === 'tools'); } // ── Tool results panel ───────────────────────────────────── let toolCallCount = 0; let traceStartTime = 0; function getOrCreateTraceStatus() { let el = document.getElementById('trace-agent-status'); if (!el) { el = document.createElement('div'); el.id = 'trace-agent-status'; el.className = 'trace-agent-status'; const tab = document.getElementById('left-tools-tab'); if (tab) tab.prepend(el); } return el; } function updateTraceStatus(text, state = 'running') { const el = getOrCreateTraceStatus(); el.className = 'trace-agent-status tas-' + state; el.innerHTML = text; el.style.display = text ? '' : 'none'; } function clearToolResults() { const log = document.getElementById('tool-results-log'); if (log) log.innerHTML = '<div class="panel-empty">Tool calls will appear here during inference</div>'; const badge = document.getElementById('tool-call-count'); if (badge) { badge.textContent = ''; badge.style.display = 'none'; } toolCallCount = 0; traceCardStack = {}; traceSeq = 0; pendingJsonAnswer = null; traceStartTime = 0; updateTraceStatus('', 'idle'); }
assets/js/demo.js = 'running') { const el = getOrCreateTraceStatus(); el.className = 'trace-agent-status tas-' + state; el.innerHTML = text; el.style.display = text ? '' : 'none'; } function clearToolResults() { const log = document.getElementById('tool-results-log'); if (log) log.innerHTML = '<div class="panel-empty">Tool calls will appear here during inference</div>'; const badge = document.getElementById('tool-call-count'); if (badge) { badge.textContent = ''; badge.style.display = 'none'; } toolCallCount = 0; traceCardStack = {}; traceSeq = 0; pendingJsonAnswer = null; traceStartTime = 0; updateTraceStatus('', 'idle'); } // Legacy adapter: called by agent_thought events from super.php function appendToolResult(toolName, toolInput, observation) { if (!toolName && !toolInput && !observation) return; const log = document.getElementById('tool-results-log'); if (!log) return; log.querySelector('.panel-empty')?.remove(); traceSeq++; const seq = traceSeq; let inputObj = null, outputObj = null; try { inputObj = toolInput ? JSON.parse(toolInput) : null; } catch { inputObj = toolInput || null; } try { outputObj = observation ? JSON.parse(observation) : null; } catch { outputObj = observation ? String(observation).slice(0, 800) : null; } const el = _buildTraceCardEl({ seq, tool: toolName || 'tool', tmf_api: null, is_mcp: false, system: null, input: inputObj || toolInput, output: outputObj || observation, pending: false, }); log.appendChild(el); log.scrollTop = log.scrollHeight; toolCallCount++; const badge = document.getElementById('tool-call-count'); if (badge) { badge.textContent = toolCallCount; badge.style.display = ''; } } // ── Trace card helpers ───────────────────────────────────── function syntaxJson(obj) { try { const raw = typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2); const parts = []; let last = 0; const re = /("(?:\\u[0-9a-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g; let m; while ((m = re.exec(raw)) !== null) { if (m.index > last) parts.push(escHtml(raw.slice(last, m.index))); let cls; if (m[0].startsWith('"')) { cls = /:$/.test(m[0]) ? 'jk' : 'js'; } else if (m[0] === 'true' || m[0] === 'false') { cls = 'jb'; } else if (m[0] === 'null') { cls = 'jn'; } else { cls = 'ji'; } parts.push(`<span class="${cls}">${escHtml(m[0])}</span>`); last = m.index + m[0].length; } if (last < raw.length) parts.push(escHtml(raw.slice(last))); return parts.join(''); } catch { return escHtml(typeof obj === 'string' ? obj : String(obj)); } } function latencyLabel(ms) { if (!ms) return ''; return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`; } function _buildTraceCardEl({ seq, tool, tmf_api, is_mcp, mcp_server, collection, system, input, latency_ms, output, pending }) { const el = document.createElement('div'); el.className = 'trace-card' + (is_mcp ? ' mcp' : '') + (pending ? ' pending' : ' done'); el.id = 'trace-card-' + seq; const tmfBadge = !is_mcp && tmf_api ? `<span class="trace-badge tmf">${escHtml(tmf_api)}</span>` : ''; const mcpBadge = is_mcp ? `<span class="trace-badge mcp">${escHtml(mcp_server || 'MCP')}</span>` : ''; const sysBadge = !is_mcp && system ? `<span class="trace-badge sys">${escHtml(system)}</span>` : ''; const latHtml = latency_ms ? `<span class="trace-latency">${latencyLabel(latency_ms)}</span>` : (pending ? `<span class="trace-latency" id="trace-lat-${seq}">…</span>` : ''); const toolLabel = (tool || 'tool').replace(/_/g, ' '); const collHtml = collection ? `<div class="trace-collection">Collection: ${escHtml(collection)}</div>` : ''; // "Build →" button for scaffolded BSS tools only (not corpus search) const _tmfSystems = {'TMF678':'FOSSbilling','TMF620':'FOSSbilling','TMF622':'FOSSbilling','TMF629':'SuiteCRM','TMF621':'SuiteCRM'}; const buildBtn = !is_mcp && tool && tmf_api ? `<button class="trace-build-btn" onclick="buildToolFromTrace(${escHtml(JSON.stringify(tool))},${escHtml(JSON.stringify(tmf_api))},${escHtml(JSON.stringify(_tmfSystems[tmf_api]??'BSS'))})">Build →</button>` : ''; const inputHtml = (input !== null && input !== undefined && input !== '') ? `<div class="trace-section"><div class="trace-sec-hdr" onclick="this.parentElement.classList.toggle('open')">▶ Input</div><pre class="trace-json">${syntaxJson(input)}</pre></div>` : ''; const outputHtml = (output !== null && output !== undefined && output !== '') ? `<div class="trace-section open"><div class="trace-sec-hdr" onclick="this.parentElement.classList.toggle('open')">▼ Output</div><pre class="trace-json">${syntaxJson(output)}</pre></div>` : ''; const pendingOutHtml = pending ? `<div id="trace-out-${seq}"></div>` : ''; el.innerHTML = ` <div class="trace-header"> ${tmfBadge}${mcpBadge} <span class="trace-tool-name">${escHtml(toolLabel)}</span> ${sysBadge} ${latHtml} ${buildBtn} </div> ${collHtml}${inputHtml}${pendingOutHtml}${outputHtml}`; return el; }
assets/js/demo.js →</button>` : ''; const inputHtml = (input !== null && input !== undefined && input !== '') ? `<div class="trace-section"><div class="trace-sec-hdr" onclick="this.parentElement.classList.toggle('open')">▶ Input</div><pre class="trace-json">${syntaxJson(input)}</pre></div>` : ''; const outputHtml = (output !== null && output !== undefined && output !== '') ? `<div class="trace-section open"><div class="trace-sec-hdr" onclick="this.parentElement.classList.toggle('open')">▼ Output</div><pre class="trace-json">${syntaxJson(output)}</pre></div>` : ''; const pendingOutHtml = pending ? `<div id="trace-out-${seq}"></div>` : ''; el.innerHTML = ` <div class="trace-header"> ${tmfBadge}${mcpBadge} <span class="trace-tool-name">${escHtml(toolLabel)}</span> ${sysBadge} ${latHtml} ${buildBtn} </div> ${collHtml}${inputHtml}${pendingOutHtml}${outputHtml}`; return el; } function appendTraceCard(evt) { const log = document.getElementById('tool-results-log'); if (!log) return; log.querySelector('.panel-empty')?.remove(); traceSeq++; const seq = traceSeq; traceCardStack['last:' + evt.tool] = seq; const el = _buildTraceCardEl({ seq, tool: evt.tool, tmf_api: evt.tmf_api ?? null, is_mcp: !!evt.is_mcp, mcp_server: evt.mcp_server ?? null, collection: evt.collection ?? null, system: evt.system ?? null, input: evt.input ?? null, pending: true, }); log.appendChild(el); log.scrollTop = log.scrollHeight; toolCallCount++; const badge = document.getElementById('tool-call-count'); if (badge) { badge.textContent = toolCallCount; badge.style.display = ''; } } function updateTraceCard(evt) { const seq = traceCardStack['last:' + evt.tool]; if (!seq) return; const card = document.getElementById('trace-card-' + seq); if (!card) return; card.classList.remove('pending'); card.classList.add('done'); const latEl = document.getElementById('trace-lat-' + seq); if (latEl && evt.latency_ms) latEl.textContent = latencyLabel(evt.latency_ms); const outSlot = document.getElementById('trace-out-' + seq); if (outSlot && evt.output != null) { const section = document.createElement('div'); section.className = 'trace-section open'; section.innerHTML = `<div class="trace-sec-hdr" onclick="this.parentElement.classList.toggle('open')">▼ Output</div><pre class="trace-json">${syntaxJson(evt.output)}</pre>`; outSlot.replaceWith(section); } const log = document.getElementById('tool-results-log'); if (log) log.scrollTop = log.scrollHeight; } function renderJsonAnswerPanel(data) { const statusClasses = { critical: 'jap-status-critical', attention: 'jap-status-attention', healthy: 'jap-status-healthy' }; const status = (data.status ?? 'info').toLowerCase(); const statusCls = statusClasses[status] || 'jap-status-attention'; const valueText = (value) => { if (value == null) return ''; if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value); if (Array.isArray(value)) return value.map(valueText).filter(Boolean).join(', '); if (typeof value === 'object') { const bits = []; if (value.api) bits.push(value.api); if (value.title) bits.push(value.title); if (value.use) bits.push(value.use); if (value.excerpt) bits.push(value.excerpt); if (value.source) bits.push(value.source); return bits.filter(Boolean).join(' - '); } return String(value); }; const renderReference = (r) => { if (!r || typeof r !== 'object' || Array.isArray(r)) { return `<li><em>${escHtml(valueText(r))}</em></li>`; } const api = r.api ? `<span class="trace-badge tmf">${escHtml(r.api)}</span>` : ''; const title = r.title ? `<strong>${escHtml(r.title)}</strong>` : ''; const use = r.use ? `<span>${escHtml(r.use)}</span>` : ''; const excerpt = r.excerpt ? `<small>${escHtml(r.excerpt)}</small>` : ''; const source = r.source ? `<a href="${escHtml(r.source)}" target="_blank" rel="noopener">${escHtml(r.source)}</a>` : ''; return `<li class="jap-ref-object">${api}<div>${title}${use ? `<p>${use}</p>` : ''}${excerpt ? `<p>${excerpt}</p>` : ''}${source ? `<p>${source}</p>` : ''}</div></li>`; }; const renderSections = (sections) => { if (!sections || typeof sections !== 'object') return ''; return Object.entries(sections).map(([title, value], i) => { const text = valueText(value); return text ? `<details class="jap-section jap-section-collapsible" ${i < 2 ? 'open' : ''}><summary>${escHtml(title)}</summary><p class="jap-text">${escHtml(text)}</p></details>` : ''; }).join(''); };
assets/js/demo.js r.excerpt ? `<small>${escHtml(r.excerpt)}</small>` : ''; const source = r.source ? `<a href="${escHtml(r.source)}" target="_blank" rel="noopener">${escHtml(r.source)}</a>` : ''; return `<li class="jap-ref-object">${api}<div>${title}${use ? `<p>${use}</p>` : ''}${excerpt ? `<p>${excerpt}</p>` : ''}${source ? `<p>${source}</p>` : ''}</div></li>`; }; const renderSections = (sections) => { if (!sections || typeof sections !== 'object') return ''; return Object.entries(sections).map(([title, value], i) => { const text = valueText(value); return text ? `<details class="jap-section jap-section-collapsible" ${i < 2 ? 'open' : ''}><summary>${escHtml(title)}</summary><p class="jap-text">${escHtml(text)}</p></details>` : ''; }).join(''); }; if (data.customer_summary || data.quote_summary || data.tickets_summary) { const customer = data.customer_summary ?? {}; const quote = data.quote_summary ?? {}; const billing = data.billing_summary ?? {}; const orders = data.orders_summary ?? {}; const tickets = data.tickets_summary ?? {}; const selectedTicket = data.selected_ticket && typeof data.selected_ticket === 'object' ? data.selected_ticket : null; const hidden = data.hidden_details ?? {}; const refs = hidden.standards_references ?? data.standards_references ?? []; const showStandards = data.mode === 'tmf_map' || data.show_standards === true; const refItems = showStandards ? refs.map(renderReference).join('') : ''; const itemRows = (quote.items ?? []).slice(0, 5).map(item => { const title = valueText(item.title ?? item.name ?? item.sku ?? 'Item'); const price = item.price ?? item.monthly_price; return `<li><span>${escHtml(title)}</span>${price != null ? `<strong>$${escHtml(Number(price).toFixed(2))}</strong>` : ''}</li>`; }).join(''); const visibleTickets = selectedTicket ? [selectedTicket] : (tickets.items ?? []).slice(0, 4); const ticketRows = visibleTickets.map(t => ( `<li><span>${escHtml(valueText(t.subject ?? t.title ?? 'Ticket'))}</span><strong>${escHtml(valueText(t.status ?? 'open'))}</strong></li>` )).join(''); const orderRows = (orders.items ?? []).slice(0, 4).map(o => ( `<li><span>${escHtml(valueText(o.title ?? 'Order'))}</span><strong>${escHtml(valueText(o.status ?? ''))}</strong></li>` )).join(''); const ticketTask = selectedTicket ? valueText(selectedTicket.rel_task ?? selectedTicket.task ?? selectedTicket.reason ?? selectedTicket.content ?? 'Review before release.') : ''; const focusTitle = selectedTicket ? `Ticket #${valueText(selectedTicket.id ?? selectedTicket.ticket_id ?? '')}` : 'Orders & Tickets'; const focusStrong = selectedTicket ? valueText(selectedTicket.subject ?? selectedTicket.title ?? 'FOSSBilling ticket') : valueText(tickets.headline ?? `${tickets.open ?? 0} open tickets`); const focusBody = selectedTicket ? `${valueText(selectedTicket.status ?? 'open')}${ticketTask ? ` · ${ticketTask}` : ''}` : valueText(orders.headline ?? 'Safe order work continues; exceptions stay in tickets.'); return `<div class="json-answer-panel qto-result-panel"> <div class="jap-header qto-result-header"> <span class="jap-status-badge ${statusCls}">${escHtml(status.toUpperCase())}</span> <span class="jap-label">${escHtml(valueText(data.mode_label ?? 'Quote-to-Order result'))}</span> </div> <div class="qto-result-grid"> <section class="qto-result-card"> <span>Customer</span> <strong>${escHtml(valueText(customer.name ?? customer.company ?? 'Selected customer'))}</strong> <p>${escHtml(valueText(customer.need ?? customer.status ?? 'FOSSBilling customer loaded.'))}</p> <small>${escHtml(valueText(customer.record ?? customer.email ?? 'FOSSBilling client'))}</small> </section> <section class="qto-result-card"> <span>Quote</span> <strong>${quote.total != null ? `$${escHtml(Number(quote.total).toFixed(2))}/mo` : escHtml(valueText(quote.status ?? 'Draft quote'))}</strong> <p>${escHtml(valueText(quote.title ?? 'Draft quote created from invoice items.'))}</p> <small>${escHtml(valueText(quote.invoice ?? billing.invoice ?? 'Draft invoice'))}</small> </section> <section class="qto-result-card"> <span>${escHtml(focusTitle)}</span> <strong>${escHtml(focusStrong)}</strong> <p>${escHtml(focusBody)}</p> <small>${escHtml(valueText(data.next_action ?? 'Review the next action.'))}</small> </section> </div> ${itemRows ? `<details class="jap-section qto-detail-fold"><summary>Quote items</summary><ul class="qto-compact-list">${itemRows}</ul></details>` : ''} ${orderRows || ticketRows ? `<details class="jap-section qto-detail-fold"><summary>Orders and tickets</summary>${orderRows ? `<ul class="qto-compact-list">${orderRows}</ul>` : ''}${ticketRows ? `<ul class="qto-compact-list">${ticketRows}</ul>` : ''}</details>` : ''} <details class="jap-section qto-detail-fold"> <summary>Decision details</summary> <p class="jap-text">${escHtml(valueText(hidden.summary ?? 'The agent read FOSSBilling, checked policy and pricing, executed safe steps, and kept exceptions visible as tickets.'))}</p> </details> ${refItems ? `<details class="jap-section qto-detail-fold"><summary>Standards evidence</summary><ul class="jap-list jap-refs">${refItems}</ul></details>` : ''} </div>`; }
assets/js/demo.js (TOOL_LABELS[tool] ?? tool.replace(/-/g, ' ')) : 'reasoning'; const text = (thought ?? '').trim().slice(0, 400); const el = document.createElement('div'); el.className = 'thought-step'; el.innerHTML = `<div class="thought-tool">${escHtml(label)}</div>` + (text ? `<div class="thought-text">${escHtml(text)}</div>` : ''); log.appendChild(el); log.scrollTop = log.scrollHeight; } function finishThinkingPanel() { const panel = document.getElementById('thinking-panel'); if (panel) { panel.classList.remove('thinking-active'); panel.classList.add('thinking-done'); } } // ── Model params ─────────────────────────────────────────── function getModelParams() { return { temperature: parseFloat(document.getElementById('param-temperature')?.value ?? 0.7), max_tokens: parseInt(document.getElementById('param-max-tokens')?.value ?? 2048), top_p: parseFloat(document.getElementById('param-top-p')?.value ?? 0.9), }; } function getAgentEndpoint() { if (currentAgent === 'super') return '/api/super.php'; if (currentAgent === 'quote') return '/api/quote-order.php'; if (currentAgent === 'creator') return '/api/agent-creator.php'; if (currentAgent === 'toolcreator') return '/api/tool-creator.php'; return '/api/agent.php'; } function getSelectedModel() { const pill = document.querySelector('.llm-pill.active'); return pill?.dataset.model ?? document.getElementById('model-select')?.value ?? 'qwen2.5:7b'; } function selectLlm(model) { document.querySelectorAll('.llm-pill').forEach(b => { b.classList.toggle('active', b.dataset.model === model); }); const sel = document.getElementById('model-select'); if (sel) sel.value = model; updateStatusBar(); } function getAgentPayload(message) { const payload = { message: message, conversation_id: conversationId ?? '', account: currentAccount, model: getSelectedModel(), use_rag: useRag, ...getModelParams(), }; if (currentAgent === 'super') { payload.prefer_boost = true; } else if (currentAgent === 'creator') { payload.agent = 'creator'; payload.model = 'bnl-telecom'; payload.creator_profile = getCreatorProfile(); } else if (currentAgent === 'toolcreator') { payload.agent = 'toolcreator'; payload.model = 'bnl-telecom'; payload.tool_profile = getToolCreatorProfile(); } else { payload.agent = currentAgent; if (currentAgent === 'quote') payload.scenario = 'autonomous_quote_to_order_v2'; } return payload; } function getCreatorProfile() { const root = document.getElementById('creator-builder'); if (!root) return { ...creatorProfile }; const selected = [...root.querySelectorAll('input[name="creator-api"]:checked')] .map(input => input.value); creatorProfile = { name: root.querySelector('[data-creator-field="name"]')?.value.trim() || creatorProfile.name, domain: root.querySelector('[data-creator-field="domain"]')?.value.trim() || creatorProfile.domain, mission: root.querySelector('[data-creator-field="mission"]')?.value.trim() || creatorProfile.mission, channel: root.querySelector('[data-creator-field="channel"]')?.value.trim() || creatorProfile.channel, guardrails: root.querySelector('[data-creator-field="guardrails"]')?.value.trim() || creatorProfile.guardrails, corpus: root.querySelector('[data-creator-field="corpus"]')?.value.trim() || creatorProfile.corpus, selected_apis: selected.length ? selected : creatorProfile.selected_apis, }; return { ...creatorProfile, selected_apis: [...creatorProfile.selected_apis] }; } function setCreatorTemplate(kind) { const templates = { rescue: { name: 'EXOS Customer Rescue Agent', domain: 'telecom BSS customer operations', mission: 'Resolve billing disputes, failed orders, and urgent support issues using TM Forum resources and CaveauAI evidence.', selected_apis: ['TMF678', 'TMF629', 'TMF622', 'TMF621', 'TMF620'], }, qto: { name: 'EXOS Quote-to-Order Agent', domain: 'telecom quote and fulfillment operations', mission: 'Qualify offers, create quotes, convert accepted quotes to product orders, and route fulfillment through service orders.', selected_apis: ['TMF648', 'TMF679', 'TMF620', 'TMF622', 'TMF641', 'TMF629'], }, product: { name: 'EXOS Product Lifecycle Agent', domain: 'telecom product catalogue and inventory operations', mission: 'Manage product catalogue insight, installed product risk, lifecycle actions, and migration recommendations.', selected_apis: ['TMF620', 'TMF637', 'TMF679', 'TMF622'], }, }; creatorProfile = { ...creatorProfile, ...(templates[kind] ?? templates.rescue) }; renderAgentCockpit(); } function runAgentCreator() { const profile = getCreatorProfile(); const prompt = `Create ${profile.name} for ${profile.domain}. Mission: ${profile.mission}. Use CorpusAI corpus ${profile.corpus} and export both Dify YAML and Copilot Studio config.`; sendMessage(prompt); } function getToolCreatorProfile() { const root = document.getElementById('tool-creator-builder'); if (!root) return { ...toolProfile, target_domains: [...toolProfile.target_domains] }; const selected = [...root.querySelectorAll('input[name="tool-domain"]:checked')] .map(input => input.value);
assets/js/demo.js lifecycle actions, and migration recommendations.', selected_apis: ['TMF620', 'TMF637', 'TMF679', 'TMF622'], }, }; creatorProfile = { ...creatorProfile, ...(templates[kind] ?? templates.rescue) }; renderAgentCockpit(); } function runAgentCreator() { const profile = getCreatorProfile(); const prompt = `Create ${profile.name} for ${profile.domain}. Mission: ${profile.mission}. Use CorpusAI corpus ${profile.corpus} and export both Dify YAML and Copilot Studio config.`; sendMessage(prompt); } function getToolCreatorProfile() { const root = document.getElementById('tool-creator-builder'); if (!root) return { ...toolProfile, target_domains: [...toolProfile.target_domains] }; const selected = [...root.querySelectorAll('input[name="tool-domain"]:checked')] .map(input => input.value); toolProfile = { name: root.querySelector('[data-tool-field="name"]')?.value.trim() || toolProfile.name, domain: root.querySelector('[data-tool-field="domain"]')?.value.trim() || toolProfile.domain, mission: root.querySelector('[data-tool-field="mission"]')?.value.trim() || toolProfile.mission, customer_name: root.querySelector('[data-tool-field="customer_name"]')?.value.trim() || toolProfile.customer_name, environment: root.querySelector('[data-tool-field="environment"]')?.value || toolProfile.environment, adapter_target: root.querySelector('[data-tool-field="adapter_target"]')?.value || toolProfile.adapter_target, write_policy: root.querySelector('[data-tool-field="write_policy"]')?.value || toolProfile.write_policy, corpus: root.querySelector('[data-tool-field="corpus"]')?.value.trim() || toolProfile.corpus, target_domains: selected.length ? selected : toolProfile.target_domains, }; return { ...toolProfile, target_domains: [...toolProfile.target_domains] }; } function setToolCreatorTemplate(kind) { const templates = { rescue: { name: 'EXOS Customer Rescue Toolset', domain: 'telecom BSS customer operations', mission: 'Generate a governed MCP layer for billing disputes, failed orders, product suggestions, customer profile, and support tickets.', target_domains: ['billing', 'product', 'order', 'customer', 'case'], write_policy: 'approval_gated', }, readonly: { name: 'EXOS Read-Only Discovery Toolset', domain: 'telecom BSS discovery and assurance', mission: 'Expose safe read-only tools for bills, product catalogue, order fallout, customer context, and support status.', target_domains: ['billing', 'product', 'order', 'customer', 'case'], write_policy: 'read_only', }, copilot: { name: 'EXOS Copilot Action Toolset', domain: 'enterprise Copilot Studio BSS operations', mission: 'Create a Copilot-ready action digest and MCP manifest for governed BSS actions through Exosphere.', target_domains: ['customer', 'billing', 'order', 'case'], write_policy: 'demo_safe', }, }; toolProfile = { ...toolProfile, ...(templates[kind] ?? templates.rescue) }; renderAgentCockpit(); } function runToolCreator() { const profile = getToolCreatorProfile(); const prompt = `Create MCP tools for ${profile.name}. Mission: ${profile.mission}. Generate a Factory-ready Estate Manifest, tool spec, Copilot digest, and call the MCP Factory.`; sendMessage(prompt); } // ── Build real tool from trace card / build_hint ─────────────────────────── function buildToolFromTrace(toolName, tmfApi, targetSystem) { const domainMap = { get_invoice_summary: 'billing', compare_bills: 'billing', get_product_catalogue: 'product', list_orders: 'order', diagnose_order: 'order', get_customer_profile: 'customer', list_tickets: 'case', create_ticket: 'case', }; const domain = domainMap[toolName] ?? 'billing'; const humanName = toolName.replace(/_/g, ' '); const isWrite = /^(create|place|update|delete|cancel)_/.test(toolName); const adapterKey = (targetSystem ?? 'fossbilling').toLowerCase().replace(/\W/g, ''); toolProfile = { ...toolProfile, name: `${humanName} — ${tmfApi ?? 'TM Forum'} Tool`, domain: 'telecom BSS operations', mission: `Build a real MCP implementation of \`${toolName}\` (${tmfApi ?? ''}) with a ${targetSystem ?? 'FOSSbilling'} adapter, input validation, error handling, and governed write policy.`, adapter_target: adapterKey, target_domains: [domain], write_policy: isWrite ? 'approval_gated' : 'read_only', customer_name: ACCOUNT_META[currentAccount] ?? 'Sure Telecom', }; selectAgent('toolcreator'); const acctName = ACCOUNT_META[currentAccount] ?? 'this account'; const prompt = `Build the real MCP implementation for \`${toolName}\` (${tmfApi ?? 'TM Forum'}). Target system: ${targetSystem ?? 'FOSSbilling'} TM Forum API: ${tmfApi ?? 'not specified'} Demo account context: ${acctName} Generate: 1. The full MCP tool schema (name, description, inputSchema with all parameters and required fields) 2. The ${targetSystem ?? 'FOSSbilling'} adapter function — show the actual API call with endpoint, headers, and payload 3. Input validation and error handling (missing fields, upstream errors) 4. Write policy: ${isWrite ? 'approval-gated — requires human confirmation before execution' : 'read-only — safe, no side effects'} 5. A complete Factory-ready tool definition ready to register in the MCP server`; setTimeout(() => sendMessage(prompt), 150); }
assets/js/demo.js schema (name, description, inputSchema with all parameters and required fields) 2. The ${targetSystem ?? 'FOSSbilling'} adapter function — show the actual API call with endpoint, headers, and payload 3. Input validation and error handling (missing fields, upstream errors) 4. Write policy: ${isWrite ? 'approval-gated — requires human confirmation before execution' : 'read-only — safe, no side effects'} 5. A complete Factory-ready tool definition ready to register in the MCP server`; setTimeout(() => sendMessage(prompt), 150); } function appendBuildHint(tools) { const container = document.createElement('div'); container.className = 'build-hint'; const chips = tools.map(t => `<button class="build-hint-tool" onclick="buildToolFromTrace(${escHtml(JSON.stringify(t.tool))},${escHtml(JSON.stringify(t.tmf_api))},${escHtml(JSON.stringify(t.target))})"><span class="build-hint-tool-name">${escHtml(t.tool.replace(/_/g, ' '))}</span><span class="build-hint-tmf">${escHtml(t.tmf_api??'')} → ${escHtml(t.target??'')}</span></button>` ).join(''); container.innerHTML = `<span class="build-hint-label">🔧 Build real implementation</span>${chips}`; const msgs = messagesEl.querySelectorAll('.msg-ai'); const last = msgs[msgs.length - 1]; if (last) last.appendChild(container); else messagesEl.appendChild(container); } const MODEL_LABELS = { 'telecom-bss-v1': 'Telecom BSS v1 · Fine-Tuned', 'qwen2.5:7b': 'Qwen 7B · Baseline', 'qwen2.5:14b': 'Qwen 14B · Balanced', 'qwen3:8b': 'Qwen3 8B · Thinker', }; function getModelSurface() { if (currentAgent === 'super') { return { panelName: 'telecom-bss-v1', panelBadge: 'Cuttlefish', status: 'telecom-bss-v1 @ Cuttlefish', note: 'Super Mode runs through CaveauAI retrieval and tool orchestration.', }; } if (currentAgent === 'quote') { return { panelName: 'telecom-bss-v1 + tools', panelBadge: 'Cuttlefish', status: 'telecom-bss-v1 @ Cuttlefish', note: 'Quote-to-Order uses live FOSSBilling client/order/invoice/ticket data, OPA policy, Directus catalog support, and provisioning.', }; } if (currentAgent === 'creator') { return { panelName: 'bnl-telecom', panelBadge: 'LiteLLM', status: 'bnl-telecom @ LiteLLM', note: 'Creator targets LiteLLM at 10.0.1.10:4000 for failover, Langfuse tracing, and virtual-key budget control.', }; } if (currentAgent === 'toolcreator') { return { panelName: 'bnl-telecom + MCP Factory', panelBadge: 'LiteLLM', status: 'bnl-telecom -> Estate Manifest -> Factory', note: 'Tool Creator uses CaveauAI and LiteLLM to produce Factory-ready MCP manifests and Copilot action digests.', }; } const model = getSelectedModel(); const label = MODEL_LABELS[model] ?? model; return { panelName: label, panelBadge: 'Cuttlefish', status: `${model} @ Cuttlefish`, note: "Running on BNL private GPU · Cuttlefish (SimplePod) · first response may take 5–15s", }; } function initModelParams() { const params = [ { id: 'temperature', decimals: 2 }, { id: 'max-tokens', decimals: 0 }, { id: 'top-p', decimals: 2 }, ]; params.forEach(({ id, decimals }) => { const input = document.getElementById(`param-${id}`); const val = document.getElementById(`param-${id}-val`); if (!input || !val) return; input.addEventListener('input', () => { val.textContent = parseFloat(input.value).toFixed(decimals); }); }); } // ── Status bar ───────────────────────────────────────────── function toggleBottomPanel() { const panel = document.getElementById('panel-bottom'); if (!panel) return; panel.classList.toggle('panel-bottom-collapsed'); const btn = panel.querySelector('.panel-collapse-btn'); if (btn) btn.textContent = panel.classList.contains('panel-bottom-collapsed') ? '^' : 'v'; } function updateStatusBar() { const sbAgent = document.getElementById('sb-agent'); const sbAccount = document.getElementById('sb-account'); const sbModel = document.getElementById('sb-model'); const modelName = document.getElementById('param-model-name'); const modelBadge = document.getElementById('param-model-badge'); const runtimeNote = document.getElementById('runtime-note'); const surface = getModelSurface(); if (sbAgent) sbAgent.textContent = (AGENT_META[currentAgent]?.icon ?? '') + ' ' + (AGENT_META[currentAgent]?.label ?? currentAgent); const isZavaMcpActive = currentAccount === '14' && ['billing','product','order','care'].includes(currentAgent); if (sbAccount) sbAccount.innerHTML = escHtml(ACCOUNT_META[currentAccount] ?? '') + (isZavaMcpActive ? ' <span class="sb-mcp-live">MCP Live</span>' : ''); if (sbModel) sbModel.textContent = surface.status; if (modelName) modelName.textContent = surface.panelName; if (modelBadge) modelBadge.textContent = surface.panelBadge; if (runtimeNote) runtimeNote.textContent = surface.note; } function setStreamingStatus(active) { const el = document.getElementById('sb-stream'); if (el) el.textContent = active ? '⬤ streaming' : ''; } // ── Account switching ────────────────────────────────────── function syncAccountSelects(accountId) { document.querySelectorAll('.account-select, .usecase-customer-select, .carrier-select').forEach(sel => { if ([...sel.options].some(opt => opt.value === accountId)) { sel.value = accountId; } }); } function selectAccount(accountId) { currentAccount = accountId; conversationId = null; syncAccountSelects(accountId); renderAgentCockpit(); renderPrompts(); renderSidebar(); updateQtoCustomerNeed(); clearChat(); renderWelcome(); updateStatusBar(); if (accountId === '14') loadZavaLiveData(); } // ── Agent switching ──────────────────────────────────────── const QTO_AGENTS = new Set(['quote']); const CARRIER_AGENTS = new Set(['billing', 'product', 'order', 'care', 'order_sherpa', 'enterprise_billing', 'super', 'creator', 'toolcreator']);
assets/js/demo.js : ''; } // ── Account switching ────────────────────────────────────── function syncAccountSelects(accountId) { document.querySelectorAll('.account-select, .usecase-customer-select, .carrier-select').forEach(sel => { if ([...sel.options].some(opt => opt.value === accountId)) { sel.value = accountId; } }); } function selectAccount(accountId) { currentAccount = accountId; conversationId = null; syncAccountSelects(accountId); renderAgentCockpit(); renderPrompts(); renderSidebar(); updateQtoCustomerNeed(); clearChat(); renderWelcome(); updateStatusBar(); if (accountId === '14') loadZavaLiveData(); } // ── Agent switching ──────────────────────────────────────── const QTO_AGENTS = new Set(['quote']); const CARRIER_AGENTS = new Set(['billing', 'product', 'order', 'care', 'order_sherpa', 'enterprise_billing', 'super', 'creator', 'toolcreator']); function selectAgent(agent) { const wasCarrier = CARRIER_AGENTS.has(currentAgent); const isCarrier = CARRIER_AGENTS.has(agent); currentAgent = agent; conversationId = null; // ── Layout toggle ── const body = document.getElementById('vsc-body'); if (body) { body.classList.toggle('qto-focused', agent === 'quote'); body.classList.toggle('agent-focused', isCarrier); body.classList.toggle('creator-focused', agent === 'creator' || agent === 'toolcreator'); body.classList.toggle('toolcreator-focused', agent === 'toolcreator'); body.classList.toggle('home-focused', agent === 'home'); } // ── Left panel: open for carrier agents, collapsed for QTO ── const leftPanel = document.getElementById('panel-left'); if (leftPanel) { if (isCarrier) { leftPanel.classList.remove('panel-side-collapsed'); } else { leftPanel.classList.add('panel-side-collapsed'); } } // ── Auto-switch accounts across mode boundaries ── const acctNum = Number(currentAccount); if (agent === 'order_sherpa') { currentAccount = '17'; syncAccountSelects('17'); } else if (agent === 'enterprise_billing') { currentAccount = '18'; syncAccountSelects('18'); } else if (isCarrier && QTO_CUSTOMERS[currentAccount]) { currentAccount = '1'; syncAccountSelects('1'); } else if (agent === 'quote' && !QTO_CUSTOMERS[currentAccount]) { currentAccount = '6'; syncAccountSelects('6'); } // ── Nav badge ── const navBadge = document.getElementById('nav-badge'); if (navBadge) { if (agent === 'home') { navBadge.textContent = 'Agentic BSS'; } else { navBadge.textContent = isCarrier ? (AGENT_META[agent]?.label ?? agent) : 'FOSSBilling QTO'; } } // ── Chat input hint ── const hintEl = document.getElementById('agent-hint-text'); if (hintEl) { if (agent === 'home') { hintEl.textContent = 'Select an agent or creator from the cards above'; } else { hintEl.textContent = isCarrier ? 'Carrier agent — tool calls shown in Explorer panel' : 'FOSSBilling is the visible system of record'; } } // ── Chat input placeholder ── if (hintEl && agent === 'creator') { hintEl.textContent = 'Agent creator - TM Forum APIs, CaveauAI evidence, guardrails, and tool contracts'; } if (hintEl && agent === 'toolcreator') { hintEl.textContent = 'Tool creator - Estate Manifest, MCP tools, Factory package, and Copilot digest'; } const inputEl2 = document.getElementById('chat-input'); if (inputEl2) { const placeholders = { home: 'Choose an agent above, or type to start…', billing: 'Ask about invoices, bill comparisons, or anomalies…', product: 'Ask about the product catalogue, pricing, or add offerings…', order: 'Ask about order status, failures, or place new orders…', care: 'Look up customer profile, tickets, or account manager…', super: 'Run a TM Forum–aware BSS analysis across all domains…', order_sherpa: 'Ask about ORD-70021 fallout, warehouse stock, or SLA risk…', enterprise_billing: 'Ask about consolidated spend, anomalies, or contract compliance…', }; inputEl2.placeholder = placeholders[agent] ?? 'Ask for a quote, check billing/tickets…'; } // ── Tabs (both .agent-tab and .mode-tab) ── document.querySelectorAll('.agent-tab, .mode-tab').forEach(t => { t.classList.toggle('active', t.dataset.agent === agent); }); if (agentLabel) agentLabel.textContent = AGENT_META[agent]?.label ?? agent; renderAgentCockpit(); renderPrompts(); renderSidebar(); clearChat(); renderWelcome(); updateStatusBar(); updateQtoCustomerNeed(); if (agent === 'creator') loadCreatorRegistry(); if (agent === 'toolcreator') loadToolCreatorRegistry(); if (currentAccount === '14' && ['billing','order','product','care'].includes(agent)) loadZavaLiveData(); } // ── Prompts ──────────────────────────────────────────────── function interpolatePrompt(text) { return text.replace(/\{account\}/g, ACCOUNT_META[currentAccount] ?? 'this account'); } function getActivePrompts() { if (currentAccount === '14' && ZAVA_PROMPTS[currentAgent]) { return ZAVA_PROMPTS[currentAgent]; } return AGENT_PROMPTS[currentAgent] ?? []; } function renderPrompts() { if (!promptsEl) return; const prompts = getActivePrompts(); promptsEl.innerHTML = prompts.map((p, i) => `<button class="prompt-chip" data-idx="${i}">${escHtml(interpolatePrompt(p))}</button>` ).join(''); promptsEl.querySelectorAll('.prompt-chip').forEach(btn => { btn.addEventListener('click', () => { const list = getActivePrompts(); sendMessage(interpolatePrompt(list[Number(btn.dataset.idx)])); }); }); }
assets/js/demo.js loadToolCreatorRegistry(); if (currentAccount === '14' && ['billing','order','product','care'].includes(agent)) loadZavaLiveData(); } // ── Prompts ──────────────────────────────────────────────── function interpolatePrompt(text) { return text.replace(/\{account\}/g, ACCOUNT_META[currentAccount] ?? 'this account'); } function getActivePrompts() { if (currentAccount === '14' && ZAVA_PROMPTS[currentAgent]) { return ZAVA_PROMPTS[currentAgent]; } return AGENT_PROMPTS[currentAgent] ?? []; } function renderPrompts() { if (!promptsEl) return; const prompts = getActivePrompts(); promptsEl.innerHTML = prompts.map((p, i) => `<button class="prompt-chip" data-idx="${i}">${escHtml(interpolatePrompt(p))}</button>` ).join(''); promptsEl.querySelectorAll('.prompt-chip').forEach(btn => { btn.addEventListener('click', () => { const list = getActivePrompts(); sendMessage(interpolatePrompt(list[Number(btn.dataset.idx)])); }); }); } // ── Agent cockpit (carrier mode) ─────────────────────────── const TMF_API_MAP = { billing: 'TMF678 Customer Bill', product: 'TMF620 Product Catalog', order: 'TMF622 Product Ordering', care: 'TMF629 Customer Mgmt\nTMF621 Trouble Ticket', order_sherpa: 'TMF622 Product Ordering\nTMF638 Resource Inventory', enterprise_billing: 'TMF678 Customer Bill\nTMF629 Customer Management', super: 'Full TM Forum Suite', creator: 'TMF Agent Factory', toolcreator: 'Estate Manifest\nMCP Factory', }; function renderCreatorCockpit(el) { const apiCards = Object.entries(CREATOR_TMF_APIS).map(([api, meta]) => { const checked = (creatorProfile.selected_apis ?? []).includes(api) ? 'checked' : ''; return ` <label class="creator-api-chip"> <input type="checkbox" name="creator-api" value="${api}" ${checked}> <span> <strong>${api}</strong> <em>${escHtml(meta.label)}</em> <small>${escHtml(meta.detail)}</small> </span> </label>`; }).join(''); el.innerHTML = ` <div class="creator-cockpit" id="creator-builder"> <div class="creator-head"> <div> <div class="usecase-kicker">Agent Creator</div> <h2>TM Forum compliant agent factory</h2> <p>Design the agent contract first: TMF scope, CaveauAI evidence, LiteLLM runtime, tools, guardrails, tests, and exports.</p> </div> <div class="creator-actions"> <button type="button" class="qto-run-btn" onclick="runAgentCreator()">Create agent</button> </div> </div> <div class="creator-workbench" id="creator-workbench" aria-label="Agent creator workbench"> <aside class="creator-pane creator-template-pane" data-creator-pane="templates"> <div class="creator-pane-title" draggable="true"> <span>Templates</span> <small>drag panel</small> </div> <div class="creator-template-stack" role="group" aria-label="Creator templates"> <button type="button" onclick="setCreatorTemplate('rescue')"><strong>Rescue</strong><span>Billing, order fallout, and care recovery</span></button> <button type="button" onclick="setCreatorTemplate('qto')"><strong>Q2O</strong><span>Quote, product order, service order, ticket</span></button> <button type="button" onclick="setCreatorTemplate('product')"><strong>Product</strong><span>Catalog lifecycle and approval-gated updates</span></button> </div> <div class="creator-runtime-card"> <span>Runtime</span> <strong>bnl-telecom</strong> <small>LiteLLM at 10.0.1.10:4000 with failover and Langfuse tracing.</small> </div> </aside> <section class="creator-pane creator-editor-pane" data-creator-pane="contract"> <div class="creator-pane-title" draggable="true"> <span>Agent contract</span> <small>TMF first</small> </div> <div class="creator-grid"> <label> <span>Agent name</span> <input data-creator-field="name" value="${escHtml(creatorProfile.name)}"> </label> <label> <span>Domain</span> <input data-creator-field="domain" value="${escHtml(creatorProfile.domain)}"> </label> <label class="creator-wide"> <span>Mission</span> <textarea data-creator-field="mission" rows="3">${escHtml(creatorProfile.mission)}</textarea> </label> <label> <span>Target channel</span> <input data-creator-field="channel" value="${escHtml(creatorProfile.channel)}"> </label> <label> <span>Guardrails</span> <input data-creator-field="guardrails" value="${escHtml(creatorProfile.guardrails)}"> </label> <label> <span>CorpusAI corpus</span> <input data-creator-field="corpus" value="${escHtml(creatorProfile.corpus)}"> </label> </div> <div class="creator-api-grid">${apiCards}</div> </section>
assets/js/demo.js approval-gated updates</span></button> </div> <div class="creator-runtime-card"> <span>Runtime</span> <strong>bnl-telecom</strong> <small>LiteLLM at 10.0.1.10:4000 with failover and Langfuse tracing.</small> </div> </aside> <section class="creator-pane creator-editor-pane" data-creator-pane="contract"> <div class="creator-pane-title" draggable="true"> <span>Agent contract</span> <small>TMF first</small> </div> <div class="creator-grid"> <label> <span>Agent name</span> <input data-creator-field="name" value="${escHtml(creatorProfile.name)}"> </label> <label> <span>Domain</span> <input data-creator-field="domain" value="${escHtml(creatorProfile.domain)}"> </label> <label class="creator-wide"> <span>Mission</span> <textarea data-creator-field="mission" rows="3">${escHtml(creatorProfile.mission)}</textarea> </label> <label> <span>Target channel</span> <input data-creator-field="channel" value="${escHtml(creatorProfile.channel)}"> </label> <label> <span>Guardrails</span> <input data-creator-field="guardrails" value="${escHtml(creatorProfile.guardrails)}"> </label> <label> <span>CorpusAI corpus</span> <input data-creator-field="corpus" value="${escHtml(creatorProfile.corpus)}"> </label> </div> <div class="creator-api-grid">${apiCards}</div> </section> <aside class="creator-pane creator-inspector-pane" data-creator-pane="inspector"> <div class="creator-pane-title" draggable="true"> <span>Inspector</span> <small>exports</small> </div> <div class="creator-export-flags" aria-label="Agent exports"> <span>Dify YAML</span> <span>Copilot Studio</span> <span>Registry</span> <small>Writes: quote/order/ticket demo-safe; product/customer updates require approval.</small> </div> <div class="creator-registry" id="creator-registry"> <div class="creator-registry-empty">Generated agents will appear here with exports.</div> </div> <details class="copilot-import-panel" id="copilot-import-panel"> <summary class="copilot-import-summary">↑ Import from Copilot Studio</summary> <div class="copilot-import-body"> <p class="copilot-import-hint">Drop a Copilot Studio Teams App <code>.zip</code> here to import the agent into the EXOS registry.</p> <div class="copilot-import-drop" id="copilot-import-drop" role="button" tabindex="0" aria-label="Drop zone for Copilot Studio zip package"> <span class="copilot-drop-icon">📦</span> <span>Drop zip or <label class="copilot-drop-browse">browse<input type="file" id="copilot-import-file" accept=".zip" style="display:none"></label></span> </div> <div class="copilot-import-status" id="copilot-import-status" aria-live="polite"></div> </div> </details> </aside> </div> </div>`; initCreatorPaneDrag('creator-workbench', 'exos-creator-pane-order'); loadCreatorRegistry(); initCopilotImport(); } // ── Copilot Studio import panel ───────────────────────────── function initCopilotImport() { const drop = document.getElementById('copilot-import-drop'); const fileIn = document.getElementById('copilot-import-file'); const status = document.getElementById('copilot-import-status'); if (!drop || !fileIn || !status) return; async function handleFile(file) { if (!file || !file.name.endsWith('.zip')) { status.innerHTML = '<span class="copilot-import-err">Please select a .zip file exported from Copilot Studio.</span>'; return; } status.innerHTML = '<span class="copilot-import-working">Uploading and parsing…</span>'; drop.classList.add('copilot-drop-loading'); const fd = new FormData(); fd.append('package', file); try { const res = await fetch('/api/copilot-import.php', { method: 'POST', body: fd }); const data = await res.json(); if (!res.ok || data.error) { status.innerHTML = `<span class="copilot-import-err">Error: ${escHtml(data.error || 'Import failed')}</span>`; return; } status.innerHTML = `<span class="copilot-import-ok"> ✓ Imported <strong>${escHtml(data.agent_name)}</strong> — ${data.tools_imported} tool${data.tools_imported !== 1 ? 's' : ''}, ${(data.tmf_apis_inferred ?? []).join(', ')} <a class="creator-export-link" href="${escHtml(data.export_url)}" title="Re-export as Copilot Studio zip">Copilot Studio ↓</a> </span>`; loadCreatorRegistry(); } catch (err) { status.innerHTML = `<span class="copilot-import-err">Network error: ${escHtml(String(err))}</span>`; } finally { drop.classList.remove('copilot-drop-loading'); } } drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('copilot-drop-over'); }); drop.addEventListener('dragleave', () => drop.classList.remove('copilot-drop-over')); drop.addEventListener('drop', e => { e.preventDefault(); drop.classList.remove('copilot-drop-over'); handleFile(e.dataTransfer?.files?.[0]); }); drop.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') fileIn.click(); }); fileIn.addEventListener('change', () => handleFile(fileIn.files?.[0])); } function initCreatorPaneDrag(workbenchId = 'creator-workbench', storageKey = 'exos-creator-pane-order') { const workbench = document.getElementById(workbenchId); if (!workbench) return; const saved = localStorage.getItem(storageKey); if (saved) { saved.split(',').forEach(name => { const pane = workbench.querySelector(`[data-creator-pane="${CSS.escape(name)}"]`); if (pane) workbench.appendChild(pane); }); }
assets/js/demo.js } } drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('copilot-drop-over'); }); drop.addEventListener('dragleave', () => drop.classList.remove('copilot-drop-over')); drop.addEventListener('drop', e => { e.preventDefault(); drop.classList.remove('copilot-drop-over'); handleFile(e.dataTransfer?.files?.[0]); }); drop.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') fileIn.click(); }); fileIn.addEventListener('change', () => handleFile(fileIn.files?.[0])); } function initCreatorPaneDrag(workbenchId = 'creator-workbench', storageKey = 'exos-creator-pane-order') { const workbench = document.getElementById(workbenchId); if (!workbench) return; const saved = localStorage.getItem(storageKey); if (saved) { saved.split(',').forEach(name => { const pane = workbench.querySelector(`[data-creator-pane="${CSS.escape(name)}"]`); if (pane) workbench.appendChild(pane); }); } let dragged = null; workbench.querySelectorAll('.creator-pane').forEach(pane => { const handle = pane.querySelector('.creator-pane-title'); if (!handle) return; handle.addEventListener('dragstart', event => { dragged = pane; pane.classList.add('creator-pane-dragging'); event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/plain', pane.dataset.creatorPane || ''); }); handle.addEventListener('dragend', () => { pane.classList.remove('creator-pane-dragging'); workbench.querySelectorAll('.creator-pane-drop-target').forEach(el => el.classList.remove('creator-pane-drop-target')); dragged = null; const order = [...workbench.querySelectorAll('.creator-pane')] .map(el => el.dataset.creatorPane) .filter(Boolean) .join(','); localStorage.setItem(storageKey, order); }); pane.addEventListener('dragover', event => { if (!dragged || dragged === pane) return; event.preventDefault(); pane.classList.add('creator-pane-drop-target'); event.dataTransfer.dropEffect = 'move'; }); pane.addEventListener('dragleave', () => pane.classList.remove('creator-pane-drop-target')); pane.addEventListener('drop', event => { if (!dragged || dragged === pane) return; event.preventDefault(); pane.classList.remove('creator-pane-drop-target'); const panes = [...workbench.querySelectorAll('.creator-pane')]; const targetIndex = panes.indexOf(pane); const draggedIndex = panes.indexOf(dragged); if (draggedIndex < targetIndex) { pane.after(dragged); } else { pane.before(dragged); } }); }); } async function loadCreatorRegistry() { const slot = document.getElementById('creator-registry'); if (!slot) return; try { const res = await fetch('/api/agent-registry.php', { headers: { 'Accept': 'application/json' } }); if (!res.ok) throw new Error(`registry ${res.status}`); const payload = await res.json(); const items = payload.data ?? []; if (!items.length) { slot.innerHTML = '<div class="creator-registry-empty">Generated agents will appear here with exports.</div>'; return; } slot.innerHTML = items.slice(0, 4).map(item => { const apis = (item.tmf_apis_used ?? []).map(api => `<span>${escHtml(api)}</span>`).join(''); const exports = item.exports ?? {}; return `<article class="creator-registry-card"> <div> <strong>${escHtml(item.agent_name ?? 'Generated agent')}</strong> <small>${escHtml(item.created_at ?? '')} · ${escHtml(item.model ?? 'bnl-telecom')}</small> </div> <p>${escHtml(item.mission ?? '')}</p> <div class="creator-registry-apis">${apis}</div> <div class="creator-export-row"> ${exports.dify_yaml ? `<a class="creator-export-link" href="${escHtml(exports.dify_yaml)}" title="Download Dify YAML">Dify YAML ↓</a>` : ''} ${exports.copilot_config ? `<a class="creator-export-link creator-export-link-copilot" href="${escHtml(exports.copilot_config)}" title="Download Teams App zip for Copilot Studio">Copilot Studio ↓</a>` : ''} </div> </article>`; }).join(''); } catch (err) { slot.innerHTML = '<div class="creator-registry-empty">Registry is not available yet.</div>'; } } function renderToolCreatorCockpit(el) { const domainCards = Object.entries(TOOL_CREATOR_DOMAINS).map(([domain, meta]) => { const checked = (toolProfile.target_domains ?? []).includes(domain) ? 'checked' : ''; return ` <label class="creator-api-chip tool-domain-chip"> <input type="checkbox" name="tool-domain" value="${domain}" ${checked}> <span> <strong>${escHtml(meta.tmf)}</strong> <em>${escHtml(meta.label)}</em> <small>${escHtml(meta.detail)}</small> </span> </label>`; }).join('');
assets/js/demo.js YAML ↓</a>` : ''} ${exports.copilot_config ? `<a class="creator-export-link creator-export-link-copilot" href="${escHtml(exports.copilot_config)}" title="Download Teams App zip for Copilot Studio">Copilot Studio ↓</a>` : ''} </div> </article>`; }).join(''); } catch (err) { slot.innerHTML = '<div class="creator-registry-empty">Registry is not available yet.</div>'; } } function renderToolCreatorCockpit(el) { const domainCards = Object.entries(TOOL_CREATOR_DOMAINS).map(([domain, meta]) => { const checked = (toolProfile.target_domains ?? []).includes(domain) ? 'checked' : ''; return ` <label class="creator-api-chip tool-domain-chip"> <input type="checkbox" name="tool-domain" value="${domain}" ${checked}> <span> <strong>${escHtml(meta.tmf)}</strong> <em>${escHtml(meta.label)}</em> <small>${escHtml(meta.detail)}</small> </span> </label>`; }).join(''); el.innerHTML = ` <div class="creator-cockpit tool-creator-cockpit" id="tool-creator-builder"> <div class="creator-head"> <div> <div class="usecase-kicker">Tool Creator</div> <h2>Agent-to-MCP Factory bridge</h2> <p>Turn an agent mission into a Factory-ready Estate Manifest, canonical MCP tool spec, Copilot digest, and generated package metadata.</p> </div> <div class="creator-actions"> <button type="button" class="qto-run-btn" onclick="runToolCreator()">Create tools</button> </div> </div> <div class="creator-workbench tool-creator-workbench" id="tool-creator-workbench" aria-label="Tool creator workbench"> <aside class="creator-pane creator-template-pane" data-creator-pane="intent"> <div class="creator-pane-title" draggable="true"> <span>Intent</span> <small>agent to tools</small> </div> <div class="creator-template-stack" role="group" aria-label="Tool Creator templates"> <button type="button" onclick="setToolCreatorTemplate('rescue')"><strong>Rescue</strong><span>Full customer rescue MCP surface</span></button> <button type="button" onclick="setToolCreatorTemplate('readonly')"><strong>Read only</strong><span>Discovery-safe BSS tools</span></button> <button type="button" onclick="setToolCreatorTemplate('copilot')"><strong>Copilot</strong><span>Enterprise action digest first</span></button> </div> <div class="creator-runtime-card"> <span>Pipeline</span> <strong>bnl-telecom -> Factory</strong> <small>CaveauAI evidence plus LiteLLM creates the manifest; MCP Factory generates the package.</small> </div> </aside> <section class="creator-pane creator-editor-pane" data-creator-pane="contract"> <div class="creator-pane-title" draggable="true"> <span>Tool contract</span> <small>manifest input</small> </div> <div class="creator-grid"> <label> <span>Toolset name</span> <input data-tool-field="name" value="${escHtml(toolProfile.name)}"> </label> <label> <span>Domain</span> <input data-tool-field="domain" value="${escHtml(toolProfile.domain)}"> </label> <label class="creator-wide"> <span>Mission</span> <textarea data-tool-field="mission" rows="3">${escHtml(toolProfile.mission)}</textarea> </label> <label> <span>Customer / operator</span> <input data-tool-field="customer_name" value="${escHtml(toolProfile.customer_name)}"> </label> <label> <span>CorpusAI corpus</span> <input data-tool-field="corpus" value="${escHtml(toolProfile.corpus)}"> </label> <label> <span>Adapter target</span> <select data-tool-field="adapter_target"> <option value="fossbilling" ${toolProfile.adapter_target === 'fossbilling' ? 'selected' : ''}>FOSSBilling + SuiteCRM</option> <option value="mock" ${toolProfile.adapter_target === 'mock' ? 'selected' : ''}>Mock BSS</option> <option value="customer-placeholder" ${toolProfile.adapter_target === 'customer-placeholder' ? 'selected' : ''}>Customer placeholders</option> </select> </label> <label> <span>Write policy</span> <select data-tool-field="write_policy"> <option value="approval_gated" ${toolProfile.write_policy === 'approval_gated' ? 'selected' : ''}>Approval-gated writes</option> <option value="demo_safe" ${toolProfile.write_policy === 'demo_safe' ? 'selected' : ''}>Demo-safe writes</option> <option value="read_only" ${toolProfile.write_policy === 'read_only' ? 'selected' : ''}>Read-only</option> </select> </label> <label> <span>Environment</span> <select data-tool-field="environment"> <option value="demo" ${toolProfile.environment === 'demo' ? 'selected' : ''}>Demo</option> <option value="staging" ${toolProfile.environment === 'staging' ? 'selected' : ''}>Staging</option> <option value="production" ${toolProfile.environment === 'production' ? 'selected' : ''}>Production</option> </select> </label> </div> <div class="creator-api-grid">${domainCards}</div> </section> <aside class="creator-pane creator-inspector-pane" data-creator-pane="factory"> <div class="creator-pane-title" draggable="true"> <span>Factory output</span> <small>exports</small> </div> <div class="creator-export-flags tool-export-flags" aria-label="Tool Creator exports"> <span>Manifest</span> <span>Tool spec</span> <span>Copilot</span> <span>Factory</span> <small>Artifacts are stored; generated MCP servers are not auto-deployed in v1.</small> </div> <div class="creator-registry" id="tool-creator-registry"> <div class="creator-registry-empty">Generated toolsets will appear here with export links.</div> </div> </aside> </div> </div>`; initCreatorPaneDrag('tool-creator-workbench', 'exos-tool-creator-pane-order'); loadToolCreatorRegistry(); }
assets/js/demo.js === 'staging' ? 'selected' : ''}>Staging</option> <option value="production" ${toolProfile.environment === 'production' ? 'selected' : ''}>Production</option> </select> </label> </div> <div class="creator-api-grid">${domainCards}</div> </section> <aside class="creator-pane creator-inspector-pane" data-creator-pane="factory"> <div class="creator-pane-title" draggable="true"> <span>Factory output</span> <small>exports</small> </div> <div class="creator-export-flags tool-export-flags" aria-label="Tool Creator exports"> <span>Manifest</span> <span>Tool spec</span> <span>Copilot</span> <span>Factory</span> <small>Artifacts are stored; generated MCP servers are not auto-deployed in v1.</small> </div> <div class="creator-registry" id="tool-creator-registry"> <div class="creator-registry-empty">Generated toolsets will appear here with export links.</div> </div> </aside> </div> </div>`; initCreatorPaneDrag('tool-creator-workbench', 'exos-tool-creator-pane-order'); loadToolCreatorRegistry(); } async function loadToolCreatorRegistry() { const slot = document.getElementById('tool-creator-registry'); if (!slot) return; try { const res = await fetch('/api/tool-registry.php', { headers: { 'Accept': 'application/json' } }); if (!res.ok) throw new Error(`registry ${res.status}`); const payload = await res.json(); const items = payload.data ?? []; if (!items.length) { slot.innerHTML = '<div class="creator-registry-empty">Generated toolsets will appear here with export links.</div>'; return; } slot.innerHTML = items.slice(0, 4).map(item => { const apis = (item.tmf_apis_used ?? []).map(api => `<span>${escHtml(api)}</span>`).join(''); const exports = item.exports ?? {}; const stats = item.factory_stats ? `${escHtml(String(item.factory_stats.toolCount ?? item.tool_count ?? 0))} tools · ${escHtml(String(item.factory_stats.adapterCount ?? item.adapter_count ?? 0))} adapters` : `${escHtml(String(item.tool_count ?? 0))} tools · ${escHtml(String(item.adapter_count ?? 0))} adapters`; return `<article class="creator-registry-card tool-registry-card"> <div> <strong>${escHtml(item.toolset_name ?? 'Generated toolset')}</strong> <small>${escHtml(item.created_at ?? '')} · ${escHtml(item.factory_status ?? 'artifact-ready')}</small> </div> <p>${escHtml(item.mission ?? stats)}</p> <div class="creator-registry-apis">${apis}</div> <div class="creator-export-row"> ${exports.estate_manifest ? `<a class="creator-export-link" href="${escHtml(exports.estate_manifest)}" target="_blank" rel="noopener">Manifest</a>` : ''} ${exports.tool_spec ? `<a class="creator-export-link" href="${escHtml(exports.tool_spec)}" target="_blank" rel="noopener">Tools</a>` : ''} ${exports.copilot_digest ? `<a class="creator-export-link" href="${escHtml(exports.copilot_digest)}" target="_blank" rel="noopener">Copilot</a>` : ''} ${exports.factory_response ? `<a class="creator-export-link" href="${escHtml(exports.factory_response)}" target="_blank" rel="noopener">Factory</a>` : ''} </div> </article>`; }).join(''); } catch (err) { slot.innerHTML = '<div class="creator-registry-empty">Tool registry is not available yet.</div>'; } } function renderAgentCockpit() { const el = document.getElementById('agent-cockpit-content'); if (!el) return; if (!CARRIER_AGENTS.has(currentAgent)) { el.innerHTML = ''; return; } if (currentAgent === 'creator') { renderCreatorCockpit(el); return; } if (currentAgent === 'toolcreator') { renderToolCreatorCockpit(el); return; } const acct = ACCOUNT_DATA[currentAccount]; if (!acct) { el.innerHTML = ''; return; } const section = acct[currentAgent] ?? acct.billing; const healthKey = acct.health; const healthLabel = { red: 'Critical', amber: 'Attention', green: 'Healthy' }[healthKey] ?? healthKey; const tmfApi = TMF_API_MAP[currentAgent] ?? ''; const metricsHtml = (section.metrics ?? []).map(m => ` <div class="ck-metric"> <span class="ck-metric-icon">${m.icon}</span> <span class="ck-metric-label">${escHtml(m.label)}</span> <span class="ck-metric-value">${escHtml(m.value)}</span> </div>`).join(''); const alertsHtml = (section.alerts ?? []).slice(0, 3).map(a => ` <div class="ck-alert ck-alert-${a.level === 'critical' ? 'critical' : 'warn'}"> <span class="ck-alert-dot">${a.level === 'critical' ? '⬤' : '▲'}</span> <span>${escHtml(a.text)}</span> </div>`).join(''); el.innerHTML = ` <div class="ck-account"> <span class="ck-flag">${acct.flag}</span> <div> <div class="ck-account-name">${escHtml(acct.name)}</div> <div class="ck-account-region">${escHtml(acct.region)}</div> <div class="ck-health-wrap"> <span class="health-badge health-${healthKey}" style="font-size:0.6rem;padding:0.12rem 0.45rem;border-radius:4px;font-weight:700;letter-spacing:0.06em">${healthLabel}</span> </div> </div> </div> <div class="ck-metrics">${metricsHtml}</div> <div class="ck-alerts">${alertsHtml || '<span style="font-size:0.65rem;color:var(--text-dim)">No active alerts</span>'}</div> <div class="ck-tmf"> <span class="ck-tmf-label">TM Forum API</span> <span class="ck-tmf-api">${escHtml(tmfApi).replace('\n', '<br>')}</span> </div>`;