884 lines
34 KiB
HTML
884 lines
34 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>TIP — Transceiver Intelligence Platform</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0a0e17;
|
|
--surface: #111827;
|
|
--surface2: #1f2937;
|
|
--border: #374151;
|
|
--text: #e5e7eb;
|
|
--text-dim: #9ca3af;
|
|
--accent: #3b82f6;
|
|
--accent-glow: rgba(59,130,246,0.15);
|
|
--green: #10b981;
|
|
--yellow: #f59e0b;
|
|
--red: #ef4444;
|
|
--purple: #8b5cf6;
|
|
}
|
|
* { margin:0; padding:0; box-sizing:border-box; }
|
|
body {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
min-height: 100vh;
|
|
}
|
|
.header {
|
|
background: linear-gradient(135deg, #111827 0%, #1e1b4b 100%);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 1.5rem 2rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.header h1 {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
.header .status {
|
|
display: flex;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
font-size: 0.85rem;
|
|
color: var(--text-dim);
|
|
}
|
|
.status-dot {
|
|
width: 8px; height: 8px; border-radius: 50%;
|
|
display: inline-block;
|
|
margin-right: 4px;
|
|
}
|
|
.status-dot.green { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
.status-dot.red { background: var(--red); }
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: 0;
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 0 2rem;
|
|
background: var(--surface);
|
|
}
|
|
.tab {
|
|
padding: 0.75rem 1.25rem;
|
|
cursor: pointer;
|
|
border-bottom: 2px solid transparent;
|
|
color: var(--text-dim);
|
|
font-size: 0.9rem;
|
|
transition: all 0.2s;
|
|
}
|
|
.tab:hover { color: var(--text); }
|
|
.tab.active {
|
|
color: var(--accent);
|
|
border-bottom-color: var(--accent);
|
|
}
|
|
|
|
.main { padding: 1.5rem 2rem; max-width: 1400px; margin: 0 auto; }
|
|
|
|
.grid { display: grid; gap: 1rem; }
|
|
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
|
|
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
|
|
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); }
|
|
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 1.25rem;
|
|
}
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.card-title {
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--text-dim);
|
|
}
|
|
.card-value {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
color: var(--text);
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
.badge-blue { background: rgba(59,130,246,0.2); color: #60a5fa; }
|
|
.badge-green { background: rgba(16,185,129,0.2); color: #34d399; }
|
|
.badge-yellow { background: rgba(245,158,11,0.2); color: #fbbf24; }
|
|
.badge-red { background: rgba(239,68,68,0.2); color: #f87171; }
|
|
.badge-purple { background: rgba(139,92,246,0.2); color: #a78bfa; }
|
|
|
|
.search-box {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.search-box input {
|
|
flex: 1;
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 0.75rem 1rem;
|
|
color: var(--text);
|
|
font-size: 0.95rem;
|
|
}
|
|
.search-box input:focus { outline: none; border-color: var(--accent); }
|
|
.search-box select {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 0.5rem 0.75rem;
|
|
color: var(--text);
|
|
}
|
|
.search-box button {
|
|
background: var(--accent);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 0.75rem 1.5rem;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
}
|
|
.search-box button:hover { opacity: 0.9; }
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.9rem;
|
|
}
|
|
th {
|
|
text-align: left;
|
|
padding: 0.6rem 0.75rem;
|
|
border-bottom: 1px solid var(--border);
|
|
color: var(--text-dim);
|
|
font-weight: 600;
|
|
font-size: 0.8rem;
|
|
text-transform: uppercase;
|
|
}
|
|
td {
|
|
padding: 0.6rem 0.75rem;
|
|
border-bottom: 1px solid rgba(55,65,81,0.5);
|
|
}
|
|
tr:hover td { background: var(--accent-glow); }
|
|
|
|
.hype-bar {
|
|
height: 8px;
|
|
background: var(--surface2);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
width: 100%;
|
|
}
|
|
.hype-fill {
|
|
height: 100%;
|
|
border-radius: 4px;
|
|
transition: width 0.5s;
|
|
}
|
|
|
|
.result-item {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 1rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
.result-item h4 { margin-bottom: 0.5rem; }
|
|
.result-score {
|
|
font-size: 0.8rem;
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
.result-meta { font-size: 0.8rem; color: var(--text-dim); margin-top: 0.5rem; }
|
|
|
|
.section-title {
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
margin: 1.5rem 0 1rem;
|
|
}
|
|
|
|
.hidden { display: none !important; }
|
|
.loading { text-align: center; padding: 2rem; color: var(--text-dim); }
|
|
|
|
/* Hype Cycle Visualization */
|
|
.hype-viz { position: relative; }
|
|
.hype-svg-wrap {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
overflow-x: auto;
|
|
}
|
|
.hype-svg-wrap svg { display: block; margin: 0 auto; }
|
|
.hype-dot {
|
|
cursor: pointer;
|
|
transition: r 0.2s, filter 0.2s;
|
|
}
|
|
.hype-dot:hover { filter: brightness(1.3); }
|
|
.hype-label {
|
|
font-size: 11px;
|
|
fill: var(--text);
|
|
pointer-events: none;
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
.hype-phase-label {
|
|
font-size: 10px;
|
|
fill: var(--text-dim);
|
|
text-anchor: middle;
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
.hype-detail-panel {
|
|
position: fixed;
|
|
top: 0; right: 0;
|
|
width: 420px;
|
|
height: 100vh;
|
|
background: var(--surface);
|
|
border-left: 1px solid var(--border);
|
|
box-shadow: -8px 0 32px rgba(0,0,0,0.5);
|
|
z-index: 1000;
|
|
overflow-y: auto;
|
|
transform: translateX(100%);
|
|
transition: transform 0.3s ease;
|
|
padding: 1.5rem;
|
|
}
|
|
.hype-detail-panel.open { transform: translateX(0); }
|
|
.hype-detail-close {
|
|
position: absolute;
|
|
top: 1rem; right: 1rem;
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
width: 32px; height: 32px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 1.1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.hype-detail-close:hover { background: var(--border); }
|
|
.hype-detail-title { font-size: 1.3rem; font-weight: 700; margin-bottom: 0.25rem; }
|
|
.hype-detail-phase { font-size: 0.85rem; margin-bottom: 1.5rem; }
|
|
.hype-stat-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 0.75rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.hype-stat {
|
|
background: var(--surface2);
|
|
border-radius: 6px;
|
|
padding: 0.75rem;
|
|
}
|
|
.hype-stat-label { font-size: 0.7rem; text-transform: uppercase; color: var(--text-dim); letter-spacing: 0.05em; }
|
|
.hype-stat-value { font-size: 1.4rem; font-weight: 700; margin-top: 0.25rem; }
|
|
.hype-forecast-title { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; color: var(--text-dim); margin-bottom: 0.75rem; }
|
|
.hype-forecast-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.4rem;
|
|
font-size: 0.85rem;
|
|
}
|
|
.hype-forecast-bar .year { width: 40px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
|
|
.hype-forecast-bar .bar-track { flex: 1; height: 6px; background: var(--surface2); border-radius: 3px; overflow: hidden; }
|
|
.hype-forecast-bar .bar-fill { height: 100%; border-radius: 3px; transition: width 0.5s; }
|
|
.hype-forecast-bar .pct { width: 45px; text-align: right; font-variant-numeric: tabular-nums; color: var(--text-dim); }
|
|
|
|
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.5; } }
|
|
.animate-pulse { animation: pulse 2s infinite; }
|
|
|
|
.toast {
|
|
position: fixed;
|
|
top: 1.5rem;
|
|
right: 1.5rem;
|
|
z-index: 9999;
|
|
background: var(--surface);
|
|
border: 1px solid var(--green);
|
|
border-radius: 8px;
|
|
padding: 1rem 1.5rem;
|
|
max-width: 420px;
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
transform: translateX(120%);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
.toast.show { transform: translateX(0); }
|
|
.toast.error { border-color: var(--red); }
|
|
.toast-title { font-weight: 600; font-size: 0.9rem; margin-bottom: 0.25rem; }
|
|
.toast-body { font-size: 0.8rem; color: var(--text-dim); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="toast" class="toast"><div class="toast-title"></div><div class="toast-body"></div></div>
|
|
|
|
<div class="header">
|
|
<h1>TIP — Transceiver Intelligence Platform</h1>
|
|
<div class="status">
|
|
<span><span class="status-dot green" id="api-status"></span> API</span>
|
|
<span><span class="status-dot green" id="db-status"></span> Database</span>
|
|
<span><span class="status-dot green" id="qdrant-status"></span> Qdrant</span>
|
|
<span id="version-label"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tabs">
|
|
<div class="tab active" data-tab="overview">Overview</div>
|
|
<div class="tab" data-tab="search">Semantic Search</div>
|
|
<div class="tab" data-tab="hype">Hype Cycle</div>
|
|
<div class="tab" data-tab="transceivers">Transceivers</div>
|
|
<div class="tab" data-tab="news">News</div>
|
|
<div class="tab" data-tab="blog">Blog Engine</div>
|
|
</div>
|
|
|
|
<div class="main">
|
|
<!-- OVERVIEW TAB -->
|
|
<div id="tab-overview">
|
|
<div class="grid grid-4" id="stats-grid" style="margin-bottom:1.5rem">
|
|
<div class="card"><div class="card-title">Transceivers</div><div class="card-value" id="stat-transceivers">—</div></div>
|
|
<div class="card"><div class="card-title">Vendors</div><div class="card-value" id="stat-vendors">—</div></div>
|
|
<div class="card"><div class="card-title">Standards</div><div class="card-value" id="stat-standards">—</div></div>
|
|
<div class="card"><div class="card-title">News Articles</div><div class="card-value" id="stat-news">—</div></div>
|
|
</div>
|
|
|
|
<div class="grid grid-2">
|
|
<div class="card">
|
|
<div class="card-title" style="margin-bottom:1rem">Vector Collections</div>
|
|
<div id="collections-list"></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title" style="margin-bottom:1rem">Recent News</div>
|
|
<div id="recent-news"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top:1rem">
|
|
<div class="card-title" style="margin-bottom:1rem">API Endpoints</div>
|
|
<div id="endpoints-list" style="font-family:monospace;font-size:0.85rem;color:var(--text-dim);line-height:1.8"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SEARCH TAB -->
|
|
<div id="tab-search" class="hidden">
|
|
<div class="search-box">
|
|
<input type="text" id="search-input" placeholder="Search transceivers, datasheets, FAQ, troubleshooting...">
|
|
<select id="search-collection">
|
|
<option value="product_embeddings">Products</option>
|
|
<option value="faq_embeddings">FAQ</option>
|
|
<option value="troubleshooting_embeddings">Troubleshooting</option>
|
|
<option value="datasheet_chunks">Datasheets</option>
|
|
<option value="news_embeddings">News</option>
|
|
</select>
|
|
<button id="search-btn">Search</button>
|
|
</div>
|
|
<div id="search-results"></div>
|
|
</div>
|
|
|
|
<!-- HYPE CYCLE TAB -->
|
|
<div id="tab-hype" class="hidden">
|
|
<div class="hype-svg-wrap">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
|
|
<div>
|
|
<div style="font-size:1.1rem;font-weight:700">Optical Transceiver Hype Cycle <span id="hype-year" style="color:var(--accent)">2026</span></div>
|
|
<div style="font-size:0.8rem;color:var(--text-dim);margin-top:0.25rem">Norton-Bass Multigenerational Diffusion Model — Click any technology for details</div>
|
|
</div>
|
|
<div style="display:flex;gap:1rem;font-size:0.75rem;color:var(--text-dim)">
|
|
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#3b82f6;vertical-align:middle;margin-right:4px"></span>Innovation</span>
|
|
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#f59e0b;vertical-align:middle;margin-right:4px"></span>Peak</span>
|
|
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#ef4444;vertical-align:middle;margin-right:4px"></span>Trough</span>
|
|
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#8b5cf6;vertical-align:middle;margin-right:4px"></span>Slope</span>
|
|
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#10b981;vertical-align:middle;margin-right:4px"></span>Plateau</span>
|
|
</div>
|
|
</div>
|
|
<div id="hype-svg-container"></div>
|
|
</div>
|
|
<div style="margin-top:1rem">
|
|
<div class="card">
|
|
<div class="card-title" style="margin-bottom:0.75rem">All Technologies</div>
|
|
<table>
|
|
<thead>
|
|
<tr><th>Technology</th><th>Phase</th><th>Position</th><th>Adoption</th><th>Peak Year</th><th>Years to Plateau</th></tr>
|
|
</thead>
|
|
<tbody id="hype-table"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- HYPE CYCLE DETAIL PANEL (slides in from right) -->
|
|
<div id="hype-detail" class="hype-detail-panel">
|
|
<button class="hype-detail-close" id="hype-detail-close">×</button>
|
|
<div id="hype-detail-content"></div>
|
|
</div>
|
|
|
|
<!-- TRANSCEIVERS TAB -->
|
|
<div id="tab-transceivers" class="hidden">
|
|
<div class="search-box">
|
|
<input type="text" id="tx-search" placeholder="Search transceivers (e.g. 100G LR4, QSFP28, coherent)...">
|
|
<button id="tx-search-btn">Search</button>
|
|
</div>
|
|
<div class="card">
|
|
<table>
|
|
<thead>
|
|
<tr><th>Name</th><th>Form Factor</th><th>Speed</th><th>Reach</th><th>Fiber</th><th>Connector</th><th>WDM</th></tr>
|
|
</thead>
|
|
<tbody id="tx-table"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- NEWS TAB -->
|
|
<div id="tab-news" class="hidden">
|
|
<div id="news-list"></div>
|
|
</div>
|
|
|
|
<!-- BLOG TAB -->
|
|
<div id="tab-blog" class="hidden">
|
|
<div class="grid grid-3" style="margin-bottom:1.5rem">
|
|
<div class="card" style="cursor:pointer" id="gen-hype">
|
|
<div class="card-title">Generate</div>
|
|
<div style="font-weight:600;margin-top:0.5rem">Hype Cycle Analysis</div>
|
|
<div style="font-size:0.8rem;color:var(--text-dim);margin-top:0.25rem">800G technology position article</div>
|
|
</div>
|
|
<div class="card" style="cursor:pointer" id="gen-comparison">
|
|
<div class="card-title">Generate</div>
|
|
<div style="font-weight:600;margin-top:0.5rem">Product Comparison</div>
|
|
<div style="font-size:0.8rem;color:var(--text-dim);margin-top:0.25rem">400G transceiver comparison</div>
|
|
</div>
|
|
<div class="card" style="cursor:pointer" id="gen-tutorial">
|
|
<div class="card-title">Generate</div>
|
|
<div style="font-weight:600;margin-top:0.5rem">Tutorial</div>
|
|
<div style="font-size:0.8rem;color:var(--text-dim);margin-top:0.25rem">Transceiver troubleshooting guide</div>
|
|
</div>
|
|
</div>
|
|
<div class="section-title">Draft History</div>
|
|
<div id="blog-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API = window.location.hostname === 'localhost'
|
|
? 'http://localhost:3201'
|
|
: window.location.origin;
|
|
|
|
// Safe text escaping to prevent XSS
|
|
function esc(str) {
|
|
if (str == null) return '';
|
|
const d = document.createElement('div');
|
|
d.textContent = String(str);
|
|
return d.innerHTML;
|
|
}
|
|
|
|
async function api(path) {
|
|
const r = await fetch(API + path);
|
|
return r.json();
|
|
}
|
|
|
|
function el(id) { return document.getElementById(id); }
|
|
|
|
function showToast(title, body, isError) {
|
|
var t = el('toast');
|
|
t.querySelector('.toast-title').textContent = title;
|
|
t.querySelector('.toast-body').textContent = body;
|
|
t.className = 'toast' + (isError ? ' error' : '');
|
|
void t.offsetWidth;
|
|
t.classList.add('show');
|
|
clearTimeout(showToast._timer);
|
|
showToast._timer = setTimeout(function() { t.classList.remove('show'); }, 4000);
|
|
}
|
|
|
|
function buildDOM(parent, html) {
|
|
// Using textContent where possible, innerHTML only with escaped data
|
|
parent.textContent = '';
|
|
const t = document.createElement('template');
|
|
t.innerHTML = html;
|
|
parent.appendChild(t.content.cloneNode(true));
|
|
}
|
|
|
|
// Tab navigation
|
|
document.querySelectorAll('.tab').forEach(function(tab) {
|
|
tab.addEventListener('click', function() {
|
|
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
|
|
tab.classList.add('active');
|
|
document.querySelectorAll('[id^="tab-"]').forEach(function(p) { p.classList.add('hidden'); });
|
|
el('tab-' + tab.dataset.tab).classList.remove('hidden');
|
|
if (tab.dataset.tab === 'hype') loadHypeCycle();
|
|
if (tab.dataset.tab === 'transceivers') searchTransceivers();
|
|
if (tab.dataset.tab === 'news') loadNews();
|
|
if (tab.dataset.tab === 'blog') loadBlogDrafts();
|
|
});
|
|
});
|
|
|
|
// Overview
|
|
async function loadOverview() {
|
|
try {
|
|
var health = await api('/api/health');
|
|
el('stat-transceivers').textContent = health.database.stats.transceiver_count;
|
|
el('stat-vendors').textContent = health.database.stats.vendor_count;
|
|
el('stat-standards').textContent = health.database.stats.standard_count;
|
|
el('stat-news').textContent = health.database.stats.news_count;
|
|
el('version-label').textContent = 'v' + health.version;
|
|
|
|
el('api-status').className = 'status-dot ' + (health.success ? 'green' : 'red');
|
|
el('db-status').className = 'status-dot ' + (health.database.connected ? 'green' : 'red');
|
|
} catch(e) {
|
|
el('api-status').className = 'status-dot red';
|
|
}
|
|
|
|
try {
|
|
var stats = await api('/api/search/stats');
|
|
buildDOM(el('collections-list'), stats.collections.map(function(c) {
|
|
return '<div style="display:flex;justify-content:space-between;padding:0.5rem 0;border-bottom:1px solid var(--border)">'
|
|
+ '<span style="font-family:monospace;font-size:0.85rem">' + esc(c.collection) + '</span>'
|
|
+ '<span class="badge ' + (c.pointsCount > 0 ? 'badge-green' : 'badge-yellow') + '">' + esc(c.pointsCount) + ' points</span>'
|
|
+ '</div>';
|
|
}).join(''));
|
|
el('qdrant-status').className = 'status-dot green';
|
|
} catch(e) {
|
|
el('qdrant-status').className = 'status-dot red';
|
|
}
|
|
|
|
try {
|
|
var root = await api('/');
|
|
buildDOM(el('endpoints-list'), (root.endpoints || []).map(function(e) {
|
|
return '<div>' + esc(e) + '</div>';
|
|
}).join(''));
|
|
} catch(e) {}
|
|
|
|
try {
|
|
var news = await api('/api/search?q=transceiver+optics+data+center&collection=news_embeddings&limit=5');
|
|
buildDOM(el('recent-news'), (news.results || []).map(function(n) {
|
|
return '<div style="padding:0.5rem 0;border-bottom:1px solid var(--border)">'
|
|
+ '<div style="font-weight:500;font-size:0.9rem">' + esc(n.title) + '</div>'
|
|
+ '<div style="font-size:0.8rem;color:var(--text-dim)">' + esc(n.source) + ' · ' + (n.published_at ? new Date(n.published_at).toLocaleDateString() : '') + '</div>'
|
|
+ '</div>';
|
|
}).join('') || '<div class="loading">No news yet</div>');
|
|
} catch(e) {}
|
|
}
|
|
|
|
// Semantic Search
|
|
function doSearch() {
|
|
var q = el('search-input').value;
|
|
var col = el('search-collection').value;
|
|
if (!q) return;
|
|
|
|
el('search-results').textContent = 'Searching...';
|
|
api('/api/search?q=' + encodeURIComponent(q) + '&collection=' + col + '&limit=10').then(function(data) {
|
|
buildDOM(el('search-results'), (data.results || []).map(function(r) {
|
|
var title = r.standard_name || r.title || r.question || r.symptom || (r.text ? r.text.slice(0,80) : 'Result');
|
|
var body = r.answer || r.solution || r.summary || (r.text ? r.text.slice(0,300) : '');
|
|
return '<div class="result-item">'
|
|
+ '<div style="display:flex;justify-content:space-between">'
|
|
+ '<h4>' + esc(title) + '</h4>'
|
|
+ '<span class="result-score">' + (r.score * 100).toFixed(1) + '%</span>'
|
|
+ '</div>'
|
|
+ '<div style="font-size:0.9rem;margin-top:0.5rem;color:var(--text-dim)">' + esc(body) + '</div>'
|
|
+ '<div class="result-meta">'
|
|
+ (r.form_factor ? '<span class="badge badge-blue">' + esc(r.form_factor) + '</span> ' : '')
|
|
+ (r.speed ? '<span class="badge badge-purple">' + esc(r.speed) + '</span> ' : '')
|
|
+ (r.category ? '<span class="badge badge-yellow">' + esc(r.category) + '</span> ' : '')
|
|
+ (r.severity ? '<span class="badge badge-red">' + esc(r.severity) + '</span> ' : '')
|
|
+ (r.vendor ? esc(r.vendor) : '')
|
|
+ '</div></div>';
|
|
}).join('') || '<div class="loading">No results</div>');
|
|
});
|
|
}
|
|
|
|
el('search-btn').addEventListener('click', doSearch);
|
|
el('search-input').addEventListener('keydown', function(e) { if (e.key === 'Enter') doSearch(); });
|
|
|
|
// Hype Cycle
|
|
var PHASE_COLORS = {
|
|
'Innovation Trigger': '#3b82f6',
|
|
'Peak of Inflated Expectations': '#f59e0b',
|
|
'Trough of Disillusionment': '#ef4444',
|
|
'Slope of Enlightenment': '#8b5cf6',
|
|
'Plateau of Productivity': '#10b981'
|
|
};
|
|
|
|
function hypeCurveY(x, w, h) {
|
|
// Classic Gartner hype curve shape: rise, peak, trough, slope, plateau
|
|
var t = x / w;
|
|
if (t < 0.15) return h - (t / 0.15) * h * 0.85; // rise to peak
|
|
if (t < 0.22) return h * 0.15 + ((t - 0.15) / 0.07) * h * 0.02; // peak plateau
|
|
if (t < 0.42) return h * 0.17 + ((t - 0.22) / 0.20) * h * 0.55; // drop to trough
|
|
if (t < 0.48) return h * 0.72 - ((t - 0.42) / 0.06) * h * 0.02; // trough bottom
|
|
if (t < 0.80) return h * 0.70 - ((t - 0.48) / 0.32) * h * 0.35; // slope up
|
|
return h * 0.35 - ((t - 0.80) / 0.20) * h * 0.02; // plateau
|
|
}
|
|
|
|
function positionPctToX(pct, w) {
|
|
// Map positionPct (0-100) to x on the curve
|
|
return (pct / 100) * w;
|
|
}
|
|
|
|
function renderHypeSvg(techs) {
|
|
var W = 960, H = 320, PAD = 40;
|
|
var cw = W - PAD * 2, ch = H - PAD * 2;
|
|
|
|
// Build curve path
|
|
var pts = [];
|
|
for (var i = 0; i <= cw; i += 2) {
|
|
pts.push(i + PAD + ',' + (hypeCurveY(i, cw, ch) + PAD));
|
|
}
|
|
|
|
// Phase boundary x positions on the curve
|
|
var phases = [
|
|
{ label: 'Innovation\\nTrigger', x: 0.07 },
|
|
{ label: 'Peak of Inflated\\nExpectations', x: 0.18 },
|
|
{ label: 'Trough of\\nDisillusionment', x: 0.42 },
|
|
{ label: 'Slope of\\nEnlightenment', x: 0.64 },
|
|
{ label: 'Plateau of\\nProductivity', x: 0.90 }
|
|
];
|
|
|
|
var svg = '<svg width="' + W + '" height="' + (H + 50) + '" viewBox="0 0 ' + W + ' ' + (H + 50) + '" xmlns="http://www.w3.org/2000/svg">';
|
|
|
|
// Phase region backgrounds
|
|
var regionBounds = [0, 0.15, 0.28, 0.50, 0.78, 1.0];
|
|
var regionColors = ['#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#10b981'];
|
|
for (var r = 0; r < 5; r++) {
|
|
var rx1 = regionBounds[r] * cw + PAD;
|
|
var rx2 = regionBounds[r + 1] * cw + PAD;
|
|
svg += '<rect x="' + rx1 + '" y="' + PAD + '" width="' + (rx2 - rx1) + '" height="' + ch + '" fill="' + regionColors[r] + '" opacity="0.04" />';
|
|
}
|
|
|
|
// Phase labels at bottom
|
|
for (var p = 0; p < phases.length; p++) {
|
|
var px = phases[p].x * cw + PAD;
|
|
var lines = phases[p].label.split('\\n');
|
|
for (var li = 0; li < lines.length; li++) {
|
|
svg += '<text x="' + px + '" y="' + (H + 15 + li * 13) + '" class="hype-phase-label">' + esc(lines[li]) + '</text>';
|
|
}
|
|
}
|
|
|
|
// Curve line
|
|
svg += '<polyline points="' + pts.join(' ') + '" fill="none" stroke="#4b5563" stroke-width="2.5" stroke-linecap="round" />';
|
|
|
|
// Technology dots + labels
|
|
var placed = [];
|
|
for (var ti = 0; ti < techs.length; ti++) {
|
|
var t = techs[ti];
|
|
var color = PHASE_COLORS[t.phase] || '#6b7280';
|
|
var tx = positionPctToX(t.positionPct, cw);
|
|
var ty = hypeCurveY(tx, cw, ch);
|
|
tx += PAD;
|
|
ty += PAD;
|
|
|
|
// Nudge overlapping labels
|
|
var labelY = ty - 14;
|
|
var labelX = tx + 8;
|
|
var isRight = true;
|
|
for (var pi = 0; pi < placed.length; pi++) {
|
|
if (Math.abs(placed[pi].x - tx) < 50 && Math.abs(placed[pi].y - labelY) < 16) {
|
|
labelY = placed[pi].y + 16;
|
|
}
|
|
}
|
|
if (tx > W - 160) { labelX = tx - 8; isRight = false; }
|
|
placed.push({ x: tx, y: labelY });
|
|
|
|
var dotR = 7;
|
|
svg += '<circle cx="' + tx + '" cy="' + ty + '" r="' + dotR + '" fill="' + color + '" class="hype-dot" data-tech="' + esc(t.technology) + '" />';
|
|
svg += '<text x="' + labelX + '" y="' + (labelY + 4) + '" class="hype-label" text-anchor="' + (isRight ? 'start' : 'end') + '" font-weight="600">' + esc(t.technology) + '</text>';
|
|
}
|
|
|
|
svg += '</svg>';
|
|
return svg;
|
|
}
|
|
|
|
async function openHypeDetail(techName) {
|
|
var panel = el('hype-detail');
|
|
var content = el('hype-detail-content');
|
|
content.innerHTML = '';
|
|
content.textContent = 'Loading...';
|
|
panel.classList.add('open');
|
|
|
|
try {
|
|
var data = await api('/api/hype-cycle/' + encodeURIComponent(techName));
|
|
var color = PHASE_COLORS[data.phaseLabel] || '#6b7280';
|
|
|
|
var html = '<div class="hype-detail-title">' + esc(data.technology) + '</div>';
|
|
html += '<div class="hype-detail-phase"><span class="badge" style="background:' + color + '22;color:' + color + '">' + esc(data.phaseLabel) + '</span></div>';
|
|
|
|
html += '<div class="hype-stat-grid">';
|
|
html += '<div class="hype-stat"><div class="hype-stat-label">Adoption</div><div class="hype-stat-value" style="color:' + color + '">' + (data.adoptionPct != null ? (data.adoptionPct * 100).toFixed(0) + '%' : '—') + '</div></div>';
|
|
html += '<div class="hype-stat"><div class="hype-stat-label">Position</div><div class="hype-stat-value">' + (data.positionPct || 0) + '%</div></div>';
|
|
html += '<div class="hype-stat"><div class="hype-stat-label">Peak Year</div><div class="hype-stat-value">' + esc(data.forecast && data.forecast.peakShipmentYear ? data.forecast.peakShipmentYear : '—') + '</div></div>';
|
|
html += '<div class="hype-stat"><div class="hype-stat-label">Years to Plateau</div><div class="hype-stat-value">' + (data.forecast && data.forecast.yearsToPlateauFromNow != null ? data.forecast.yearsToPlateauFromNow + 'y' : '—') + '</div></div>';
|
|
html += '</div>';
|
|
|
|
// Forecast projection bars
|
|
if (data.forecast && data.forecast.fiveYearProjection && data.forecast.fiveYearProjection.length > 0) {
|
|
html += '<div class="hype-forecast-title">5-Year Adoption Forecast</div>';
|
|
for (var fi = 0; fi < data.forecast.fiveYearProjection.length; fi++) {
|
|
var f = data.forecast.fiveYearProjection[fi];
|
|
var fColor = PHASE_COLORS[{
|
|
'INNOVATION_TRIGGER': 'Innovation Trigger',
|
|
'PEAK_OF_INFLATED_EXPECTATIONS': 'Peak of Inflated Expectations',
|
|
'TROUGH_OF_DISILLUSIONMENT': 'Trough of Disillusionment',
|
|
'SLOPE_OF_ENLIGHTENMENT': 'Slope of Enlightenment',
|
|
'PLATEAU_OF_PRODUCTIVITY': 'Plateau of Productivity'
|
|
}[f.phase] || ''] || '#6b7280';
|
|
html += '<div class="hype-forecast-bar">';
|
|
html += '<span class="year">' + f.year + '</span>';
|
|
html += '<div class="bar-track"><div class="bar-fill" style="width:' + (f.adoptionPct || 0) + '%;background:' + fColor + '"></div></div>';
|
|
html += '<span class="pct">' + (f.adoptionPct || 0) + '%</span>';
|
|
html += '</div>';
|
|
}
|
|
}
|
|
|
|
// Composite score
|
|
html += '<div style="margin-top:1.5rem;padding:1rem;background:var(--surface2);border-radius:6px">';
|
|
html += '<div style="font-size:0.75rem;text-transform:uppercase;color:var(--text-dim);letter-spacing:0.05em">Composite Score</div>';
|
|
html += '<div style="font-size:2rem;font-weight:700;color:' + color + '">' + (data.compositeScore || 0) + '<span style="font-size:0.9rem;color:var(--text-dim)"> / 100</span></div>';
|
|
html += '</div>';
|
|
|
|
buildDOM(content, html);
|
|
} catch (err) {
|
|
content.textContent = 'Failed to load: ' + err.message;
|
|
}
|
|
}
|
|
|
|
el('hype-detail-close').addEventListener('click', function() {
|
|
el('hype-detail').classList.remove('open');
|
|
});
|
|
|
|
async function loadHypeCycle() {
|
|
var data = await api('/api/hype-cycle');
|
|
var techs = data.technologies || [];
|
|
|
|
el('hype-year').textContent = data.year;
|
|
|
|
// Render SVG
|
|
var container = el('hype-svg-container');
|
|
buildDOM(container, renderHypeSvg(techs));
|
|
|
|
// Attach click handlers to dots
|
|
var dots = container.querySelectorAll('.hype-dot');
|
|
for (var di = 0; di < dots.length; di++) {
|
|
dots[di].addEventListener('click', function() {
|
|
openHypeDetail(this.getAttribute('data-tech'));
|
|
});
|
|
}
|
|
|
|
// Also render table (rows are clickable too)
|
|
buildDOM(el('hype-table'), techs.map(function(t) {
|
|
var color = PHASE_COLORS[t.phase] || '#374151';
|
|
return '<tr style="cursor:pointer" data-tech="' + esc(t.technology) + '">'
|
|
+ '<td style="font-weight:600">' + esc(t.technology) + '</td>'
|
|
+ '<td><span class="badge" style="background:' + color + '22;color:' + color + '">' + esc(t.phase) + '</span></td>'
|
|
+ '<td><div class="hype-bar"><div class="hype-fill" style="width:' + esc(t.positionPct) + '%;background:' + color + '"></div></div></td>'
|
|
+ '<td>' + (t.adoptionPct * 100).toFixed(1) + '%</td>'
|
|
+ '<td>' + esc(t.peakYear || '—') + '</td>'
|
|
+ '<td>' + (t.yearsToPlateauFromNow != null ? t.yearsToPlateauFromNow + 'y' : '—') + '</td>'
|
|
+ '</tr>';
|
|
}).join(''));
|
|
|
|
// Click handlers on table rows
|
|
var rows = el('hype-table').querySelectorAll('tr[data-tech]');
|
|
for (var ri = 0; ri < rows.length; ri++) {
|
|
rows[ri].addEventListener('click', function() {
|
|
openHypeDetail(this.getAttribute('data-tech'));
|
|
});
|
|
}
|
|
}
|
|
|
|
// Transceivers
|
|
function searchTransceivers() {
|
|
var q = el('tx-search').value;
|
|
var url = q ? '/api/transceivers?q=' + encodeURIComponent(q) + '&limit=50' : '/api/transceivers?limit=50';
|
|
api(url).then(function(data) {
|
|
buildDOM(el('tx-table'), (data.transceivers || []).map(function(t) {
|
|
return '<tr>'
|
|
+ '<td style="font-weight:500">' + esc(t.standard_name || t.slug) + '</td>'
|
|
+ '<td><span class="badge badge-blue">' + esc(t.form_factor) + '</span></td>'
|
|
+ '<td>' + esc(t.speed) + '</td>'
|
|
+ '<td>' + esc(t.reach_label) + '</td>'
|
|
+ '<td>' + esc(t.fiber_type) + '</td>'
|
|
+ '<td>' + esc(t.connector) + '</td>'
|
|
+ '<td>' + esc(t.wdm_type) + '</td>'
|
|
+ '</tr>';
|
|
}).join(''));
|
|
});
|
|
}
|
|
el('tx-search-btn').addEventListener('click', searchTransceivers);
|
|
el('tx-search').addEventListener('keydown', function(e) { if (e.key === 'Enter') searchTransceivers(); });
|
|
|
|
// News
|
|
async function loadNews() {
|
|
var data = await api('/api/search?q=optical+transceiver+data+center+networking&collection=news_embeddings&limit=20');
|
|
buildDOM(el('news-list'), (data.results || []).map(function(n) {
|
|
var urlSafe = (n.url && /^https?:\/\//.test(n.url)) ? n.url : '#';
|
|
return '<div class="result-item">'
|
|
+ '<h4>' + esc(n.title) + '</h4>'
|
|
+ '<div style="font-size:0.9rem;margin:0.5rem 0;color:var(--text-dim)">' + esc(n.summary) + '</div>'
|
|
+ '<div class="result-meta">'
|
|
+ '<span class="badge badge-blue">' + esc(n.source) + '</span> '
|
|
+ (n.published_at ? new Date(n.published_at).toLocaleDateString() : '')
|
|
+ (urlSafe !== '#' ? ' · <a href="' + esc(urlSafe) + '" target="_blank" rel="noopener noreferrer" style="color:var(--accent)">Read →</a>' : '')
|
|
+ '</div></div>';
|
|
}).join('') || '<div class="loading">No news articles</div>');
|
|
}
|
|
|
|
// Blog
|
|
function generateBlog(topic, speed) {
|
|
el('blog-list').textContent = 'Generating blog draft...';
|
|
var body = { topic: topic };
|
|
if (speed) body.speed = speed;
|
|
|
|
fetch(API + '/api/blog/generate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
if (data.success) {
|
|
showToast('Draft generated', data.draft.title + ' — ' + data.draft.word_count + ' words');
|
|
} else {
|
|
showToast('Generation failed', data.error || 'Unknown error', true);
|
|
}
|
|
loadBlogDrafts();
|
|
}).catch(function(err) {
|
|
showToast('Network error', err.message, true);
|
|
});
|
|
}
|
|
|
|
el('gen-hype').addEventListener('click', function() { generateBlog('hype_cycle', '800G'); });
|
|
el('gen-comparison').addEventListener('click', function() { generateBlog('comparison', '400G'); });
|
|
el('gen-tutorial').addEventListener('click', function() { generateBlog('tutorial'); });
|
|
|
|
async function loadBlogDrafts() {
|
|
var data = await api('/api/blog');
|
|
buildDOM(el('blog-list'), (data.drafts || []).map(function(d) {
|
|
var statusClass = d.status === 'published' ? 'badge-green' : d.status === 'review' ? 'badge-yellow' : 'badge-blue';
|
|
var keywords = (d.seo_keywords || []).map(function(k) { return '#' + esc(k); }).join(' ');
|
|
return '<div class="result-item">'
|
|
+ '<div style="display:flex;justify-content:space-between;align-items:center">'
|
|
+ '<h4>' + esc(d.title) + '</h4>'
|
|
+ '<span class="badge ' + statusClass + '">' + esc(d.status) + '</span>'
|
|
+ '</div>'
|
|
+ '<div class="result-meta">'
|
|
+ '<span class="badge badge-purple">' + esc(d.topic) + '</span> '
|
|
+ '<span class="badge badge-blue">' + esc(d.target_audience) + '</span> '
|
|
+ esc(d.word_count) + ' words · ' + new Date(d.created_at).toLocaleDateString()
|
|
+ (keywords ? ' · <span style="color:var(--text-dim)">' + keywords + '</span>' : '')
|
|
+ '</div></div>';
|
|
}).join('') || '<div class="loading">No drafts yet. Click a template above to generate.</div>');
|
|
}
|
|
|
|
// Init
|
|
loadOverview();
|
|
</script>
|
|
</body>
|
|
</html>
|