New scrapers: - GBICS.com (BigCommerce, GBP prices, 10 categories, 78 products) - Juniper HCT (Next.js SSR parser, 475 transceivers with specs/EOL) - SFPcables.com (Magento store, 16 categories, 78 products) - Fluxlight (BigCommerce, 6 pages, 118 products) - Champion ONE (compatible vendor scraper) Scraper fixes: - 10Gtek: rewritten to parse HTML spec tables (152 products) - Flexoptix: fix price extraction from Magento Hyva HTML - Register all scrapers in CLI (--gbics, --juniper, --sfpcables, etc.) Hype Cycle Engine enhancements: - Data-driven enrichment from scraped vendor/price data - Revenue lifecycle prediction (peak year, decline, revenue index) - Regional adoption model (NA, China, APAC, Europe, RoW with lag coefficients) - New API endpoints: /enriched, /lifecycle, /regional/:tech DB growth: 89 → 1,168 transceivers, 0 → 416 prices, 6 vendors Qdrant: 1,162 products embedded with nomic-embed-text Research: Norton-Bass model, standards-to-market timelines, hype signals
901 lines
40 KiB
HTML
901 lines
40 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>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bg: #0c0f16;
|
|
--surface: #13161f;
|
|
--surface2: #1a1e2a;
|
|
--surface3: #222736;
|
|
--border: #2a3040;
|
|
--border-hover: #3b4560;
|
|
--text: #d4d8e3;
|
|
--text-bright: #f0f2f5;
|
|
--text-dim: #7a829a;
|
|
--accent: #4f8ff7;
|
|
--accent-dim: rgba(79,143,247,0.12);
|
|
--green: #22c55e;
|
|
--green-dim: rgba(34,197,94,0.12);
|
|
--yellow: #eab308;
|
|
--yellow-dim: rgba(234,179,8,0.12);
|
|
--red: #ef4444;
|
|
--red-dim: rgba(239,68,68,0.12);
|
|
--purple: #a78bfa;
|
|
--purple-dim: rgba(167,139,250,0.12);
|
|
--orange: #f97316;
|
|
--orange-dim: rgba(249,115,22,0.12);
|
|
--mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', monospace;
|
|
}
|
|
* { 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;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* HEADER — clean, no gradient nonsense */
|
|
.header {
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 0 1.5rem;
|
|
height: 48px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
.header-left { display: flex; align-items: center; gap: 1.5rem; }
|
|
.header h1 {
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
color: var(--text-bright);
|
|
letter-spacing: -0.01em;
|
|
}
|
|
.header-stats {
|
|
display: flex;
|
|
gap: 1rem;
|
|
font-size: 0.75rem;
|
|
font-family: var(--mono);
|
|
color: var(--text-dim);
|
|
}
|
|
.header-stats .val { color: var(--text); font-weight: 500; }
|
|
.header .status {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
align-items: center;
|
|
font-size: 0.75rem;
|
|
color: var(--text-dim);
|
|
}
|
|
.dot {
|
|
width: 6px; height: 6px; border-radius: 50%;
|
|
display: inline-block;
|
|
margin-right: 3px;
|
|
}
|
|
.dot-ok { background: var(--green); }
|
|
.dot-err { background: var(--red); }
|
|
|
|
/* TABS */
|
|
.tabs {
|
|
display: flex;
|
|
gap: 0;
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 0 1.5rem;
|
|
background: var(--surface);
|
|
}
|
|
.tab {
|
|
padding: 0.6rem 1rem;
|
|
cursor: pointer;
|
|
border-bottom: 2px solid transparent;
|
|
color: var(--text-dim);
|
|
font-size: 0.8rem;
|
|
font-weight: 500;
|
|
transition: color 0.15s;
|
|
}
|
|
.tab:hover { color: var(--text); }
|
|
.tab.active { color: var(--text-bright); border-bottom-color: var(--accent); }
|
|
|
|
/* LAYOUT */
|
|
.main { padding: 1.25rem 1.5rem; max-width: 1600px; margin: 0 auto; }
|
|
.grid { display: grid; gap: 0.75rem; }
|
|
.g4 { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
|
|
.g3 { grid-template-columns: repeat(3, 1fr); }
|
|
.g2 { grid-template-columns: 1fr 1fr; }
|
|
.g2-1 { grid-template-columns: 2fr 1fr; }
|
|
|
|
/* CARDS */
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 1rem;
|
|
}
|
|
.card-sm { padding: 0.75rem; }
|
|
.card-label {
|
|
font-size: 0.7rem;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--text-dim);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
.card-num {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
font-family: var(--mono);
|
|
color: var(--text-bright);
|
|
letter-spacing: -0.02em;
|
|
}
|
|
.card-num small {
|
|
font-size: 0.7rem;
|
|
font-weight: 400;
|
|
color: var(--text-dim);
|
|
margin-left: 4px;
|
|
}
|
|
|
|
/* BADGES */
|
|
.b { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 0.7rem; font-weight: 600; font-family: var(--mono); }
|
|
.b-blue { background: var(--accent-dim); color: var(--accent); }
|
|
.b-green { background: var(--green-dim); color: var(--green); }
|
|
.b-yellow { background: var(--yellow-dim); color: var(--yellow); }
|
|
.b-red { background: var(--red-dim); color: var(--red); }
|
|
.b-purple { background: var(--purple-dim); color: var(--purple); }
|
|
.b-orange { background: var(--orange-dim); color: var(--orange); }
|
|
.b-neutral { background: var(--surface3); color: var(--text-dim); }
|
|
|
|
/* TABLES */
|
|
table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
|
|
th {
|
|
text-align: left; padding: 0.5rem 0.6rem;
|
|
border-bottom: 1px solid var(--border);
|
|
color: var(--text-dim); font-weight: 500;
|
|
font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;
|
|
position: sticky; top: 0; background: var(--surface); z-index: 1;
|
|
}
|
|
td { padding: 0.45rem 0.6rem; border-bottom: 1px solid rgba(42,48,64,0.5); }
|
|
tr.clickable { cursor: pointer; }
|
|
tr.clickable:hover td { background: var(--accent-dim); }
|
|
.table-wrap { max-height: 70vh; overflow-y: auto; }
|
|
|
|
/* SEARCH */
|
|
.search-row {
|
|
display: flex; gap: 0.5rem; margin-bottom: 1rem;
|
|
}
|
|
.search-row input, .search-row select {
|
|
background: var(--surface2); border: 1px solid var(--border);
|
|
border-radius: 4px; padding: 0.5rem 0.75rem;
|
|
color: var(--text); font-size: 0.85rem;
|
|
}
|
|
.search-row input { flex: 1; }
|
|
.search-row input:focus { outline: none; border-color: var(--accent); }
|
|
.search-row select { min-width: 120px; }
|
|
.btn {
|
|
background: var(--accent); color: #fff; border: none;
|
|
border-radius: 4px; padding: 0.5rem 1rem;
|
|
font-weight: 600; font-size: 0.8rem; cursor: pointer;
|
|
}
|
|
.btn:hover { opacity: 0.9; }
|
|
.btn-ghost {
|
|
background: transparent; border: 1px solid var(--border);
|
|
color: var(--text); cursor: pointer; border-radius: 4px;
|
|
padding: 0.4rem 0.75rem; font-size: 0.75rem; font-weight: 500;
|
|
}
|
|
.btn-ghost:hover { border-color: var(--border-hover); background: var(--surface2); }
|
|
|
|
/* RESULT ITEMS */
|
|
.ri {
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 0.75rem 0;
|
|
cursor: pointer;
|
|
}
|
|
.ri:last-child { border-bottom: none; }
|
|
.ri:hover { background: var(--accent-dim); margin: 0 -1rem; padding: 0.75rem 1rem; }
|
|
.ri-title { font-weight: 500; font-size: 0.85rem; color: var(--text-bright); }
|
|
.ri-body { font-size: 0.8rem; color: var(--text-dim); margin-top: 0.25rem; }
|
|
.ri-meta { font-size: 0.7rem; color: var(--text-dim); margin-top: 0.25rem; display: flex; gap: 0.5rem; align-items: center; }
|
|
|
|
/* HYPE CYCLE */
|
|
.hype-svg-wrap {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 1rem 1.5rem;
|
|
overflow-x: auto;
|
|
}
|
|
.hype-svg-wrap svg { display: block; margin: 0 auto; }
|
|
.hype-dot { cursor: pointer; transition: filter 0.15s; }
|
|
.hype-dot:hover { filter: brightness(1.4); }
|
|
.hype-label { font-size: 10px; fill: var(--text); pointer-events: none; font-family: 'Inter', sans-serif; }
|
|
.hype-phase-label { font-size: 9px; fill: var(--text-dim); text-anchor: middle; font-family: 'Inter', sans-serif; }
|
|
|
|
.hype-bar { height: 6px; background: var(--surface3); border-radius: 3px; overflow: hidden; width: 100%; }
|
|
.hype-fill { height: 100%; border-radius: 3px; transition: width 0.5s; }
|
|
|
|
/* TOOLTIPS */
|
|
.tip { position: relative; cursor: help; }
|
|
.tip::after {
|
|
content: attr(data-tip);
|
|
position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
|
|
background: #1a1a2e; color: #e0e0e0; border: 1px solid var(--border);
|
|
padding: 0.5rem 0.75rem; border-radius: 6px;
|
|
font-size: 0.72rem; line-height: 1.4; font-weight: 400; white-space: normal; width: max-content; max-width: 280px;
|
|
opacity: 0; pointer-events: none; transition: opacity 0.2s;
|
|
z-index: 500; box-shadow: 0 4px 16px rgba(0,0,0,0.5);
|
|
}
|
|
.tip::before {
|
|
content: ''; position: absolute; bottom: calc(100% + 3px); left: 50%; transform: translateX(-50%);
|
|
border: 5px solid transparent; border-top-color: var(--border);
|
|
opacity: 0; pointer-events: none; transition: opacity 0.2s; z-index: 501;
|
|
}
|
|
.tip:hover::after, .tip:hover::before { opacity: 1; }
|
|
th.tip::after { left: 0; transform: none; }
|
|
|
|
/* DETAIL PANEL (shared) */
|
|
.panel {
|
|
position: fixed; top: 0; right: 0;
|
|
width: 480px; height: 100vh;
|
|
background: var(--surface); border-left: 1px solid var(--border);
|
|
box-shadow: -4px 0 24px rgba(0,0,0,0.4);
|
|
z-index: 1000; overflow-y: auto;
|
|
transform: translateX(100%); transition: transform 0.25s ease;
|
|
padding: 1.25rem;
|
|
}
|
|
.panel.open { transform: translateX(0); }
|
|
.panel-close {
|
|
position: absolute; top: 0.75rem; right: 0.75rem;
|
|
background: var(--surface2); border: 1px solid var(--border);
|
|
color: var(--text-dim); width: 28px; height: 28px; border-radius: 4px;
|
|
cursor: pointer; font-size: 1rem;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.panel-close:hover { background: var(--border); color: var(--text); }
|
|
.panel-title { font-size: 1.1rem; font-weight: 700; color: var(--text-bright); margin-bottom: 0.15rem; }
|
|
.panel-sub { font-size: 0.8rem; color: var(--text-dim); margin-bottom: 1rem; }
|
|
.panel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 1rem; }
|
|
.panel-stat {
|
|
background: var(--surface2); border-radius: 4px; padding: 0.6rem;
|
|
}
|
|
.panel-stat-label { font-size: 0.65rem; text-transform: uppercase; color: var(--text-dim); letter-spacing: 0.04em; }
|
|
.panel-stat-val { font-size: 1.2rem; font-weight: 700; font-family: var(--mono); margin-top: 0.15rem; color: var(--text-bright); }
|
|
.panel-section { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); margin: 1rem 0 0.5rem; }
|
|
.panel-row {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
padding: 0.35rem 0; border-bottom: 1px solid rgba(42,48,64,0.5);
|
|
font-size: 0.8rem;
|
|
}
|
|
.panel-row:last-child { border-bottom: none; }
|
|
.panel-row-label { color: var(--text-dim); }
|
|
.panel-row-val { font-weight: 500; font-family: var(--mono); color: var(--text); }
|
|
|
|
.forecast-bar {
|
|
display: flex; align-items: center; gap: 0.5rem;
|
|
margin-bottom: 0.3rem; font-size: 0.8rem;
|
|
}
|
|
.forecast-bar .yr { width: 36px; color: var(--text-dim); font-family: var(--mono); font-size: 0.75rem; }
|
|
.forecast-bar .track { flex: 1; height: 5px; background: var(--surface3); border-radius: 3px; overflow: hidden; }
|
|
.forecast-bar .fill { height: 100%; border-radius: 3px; }
|
|
.forecast-bar .pct { width: 40px; text-align: right; font-family: var(--mono); font-size: 0.75rem; color: var(--text-dim); }
|
|
|
|
/* BLOG */
|
|
.gen-card {
|
|
background: var(--surface2); border: 1px solid var(--border);
|
|
border-radius: 6px; padding: 0.75rem; cursor: pointer;
|
|
transition: border-color 0.15s;
|
|
}
|
|
.gen-card:hover { border-color: var(--accent); }
|
|
.gen-card-title { font-weight: 600; font-size: 0.85rem; color: var(--text-bright); }
|
|
.gen-card-sub { font-size: 0.75rem; color: var(--text-dim); margin-top: 0.2rem; }
|
|
|
|
/* UTILITIES */
|
|
.hidden { display: none !important; }
|
|
.loading { text-align: center; padding: 2rem; color: var(--text-dim); font-size: 0.85rem; }
|
|
.mono { font-family: var(--mono); }
|
|
.dim { color: var(--text-dim); }
|
|
.mt { margin-top: 0.75rem; }
|
|
.mb { margin-bottom: 0.75rem; }
|
|
|
|
/* TOAST */
|
|
.toast {
|
|
position: fixed; top: 1rem; right: 1rem; z-index: 9999;
|
|
background: var(--surface); border: 1px solid var(--green);
|
|
border-radius: 6px; padding: 0.75rem 1.25rem; max-width: 380px;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
|
|
transform: translateX(120%); transition: transform 0.25s ease;
|
|
}
|
|
.toast.show { transform: translateX(0); }
|
|
.toast.error { border-color: var(--red); }
|
|
.toast-title { font-weight: 600; font-size: 0.8rem; color: var(--text-bright); }
|
|
.toast-body { font-size: 0.75rem; color: var(--text-dim); margin-top: 0.1rem; }
|
|
|
|
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.5} }
|
|
.pulse { animation: pulse 2s infinite; }
|
|
|
|
@media (max-width: 900px) {
|
|
.g2, .g2-1, .g3 { grid-template-columns: 1fr; }
|
|
.panel { width: 100%; }
|
|
.header-stats { display: none; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="toast" class="toast"><div class="toast-title"></div><div class="toast-body"></div></div>
|
|
|
|
<!-- HEADER -->
|
|
<div class="header">
|
|
<div class="header-left">
|
|
<h1>TIP</h1>
|
|
<div class="header-stats">
|
|
<span><span class="val" id="stat-transceivers">—</span> transceivers</span>
|
|
<span><span class="val" id="stat-vendors">—</span> vendors</span>
|
|
<span><span class="val" id="stat-standards">—</span> standards</span>
|
|
<span><span class="val" id="stat-news">—</span> news</span>
|
|
</div>
|
|
</div>
|
|
<div class="status">
|
|
<span><span class="dot dot-ok" id="api-status"></span>API</span>
|
|
<span><span class="dot dot-ok" id="db-status"></span>DB</span>
|
|
<span><span class="dot dot-ok" id="qdrant-status"></span>Qdrant</span>
|
|
<span class="mono dim" id="version-label"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TABS -->
|
|
<div class="tabs">
|
|
<div class="tab active" data-tab="overview">Overview</div>
|
|
<div class="tab" data-tab="search">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 -->
|
|
<div id="tab-overview">
|
|
<div class="grid g2 mb">
|
|
<div class="card">
|
|
<div class="card-label">Vector Collections</div>
|
|
<div id="collections-list" class="mt" style="font-size:0.8rem"></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-label">Recent News</div>
|
|
<div id="recent-news" class="mt"></div>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-label">API Endpoints</div>
|
|
<div id="endpoints-list" class="mt" style="font-family:var(--mono);font-size:0.75rem;color:var(--text-dim);line-height:1.7;columns:2"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SEARCH -->
|
|
<div id="tab-search" class="hidden">
|
|
<div class="search-row">
|
|
<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 class="btn" id="search-btn">Search</button>
|
|
</div>
|
|
<div class="card"><div id="search-results"></div></div>
|
|
</div>
|
|
|
|
<!-- HYPE CYCLE -->
|
|
<div id="tab-hype" class="hidden">
|
|
<div class="hype-svg-wrap">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem">
|
|
<div>
|
|
<span style="font-weight:600;font-size:0.9rem;color:var(--text-bright)">Optical Transceiver Hype Cycle</span>
|
|
<span class="mono dim" style="margin-left:0.5rem" id="hype-year">2026</span>
|
|
<div style="font-size:0.7rem;color:var(--text-dim);margin-top:0.15rem">Norton-Bass Model — click any technology for details</div>
|
|
</div>
|
|
<div style="display:flex;gap:0.75rem;font-size:0.65rem;color:var(--text-dim)">
|
|
<span><span class="dot dot-ok" style="background:#4f8ff7"></span>Innovation</span>
|
|
<span><span class="dot" style="background:#eab308"></span>Peak</span>
|
|
<span><span class="dot" style="background:#ef4444"></span>Trough</span>
|
|
<span><span class="dot" style="background:#a78bfa"></span>Slope</span>
|
|
<span><span class="dot" style="background:#22c55e"></span>Plateau</span>
|
|
</div>
|
|
</div>
|
|
<div id="hype-svg-container"></div>
|
|
</div>
|
|
<div class="card mt">
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead><tr><th>Technology</th><th class="tip" data-tip="Current phase in the Gartner-style technology adoption lifecycle. Technologies move from initial hype through disillusionment to mature productivity.">Phase</th><th class="tip" data-tip="Position on the hype curve (0-100%). 0% = just emerging, 50% = mid-cycle, 100% = fully mature.">Position</th><th class="tip" data-tip="Market adoption rate as percentage of total addressable market. Based on Norton-Bass diffusion model forecast.">Adoption</th><th class="tip" data-tip="Estimated year when this technology reaches peak hype / maximum media attention before reality sets in.">Peak</th><th class="tip" data-tip="Estimated years until this technology reaches the Plateau of Productivity — stable, mainstream deployment.">To Plateau</th></tr></thead>
|
|
<tbody id="hype-table"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TRANSCEIVERS -->
|
|
<div id="tab-transceivers" class="hidden">
|
|
<div class="search-row">
|
|
<input type="text" id="tx-search" placeholder="Filter: 100G LR4, QSFP28, coherent, SMF...">
|
|
<select id="tx-ff-filter">
|
|
<option value="">All Form Factors</option>
|
|
<option value="SFP">SFP</option>
|
|
<option value="SFP+">SFP+</option>
|
|
<option value="SFP28">SFP28</option>
|
|
<option value="QSFP+">QSFP+</option>
|
|
<option value="QSFP28">QSFP28</option>
|
|
<option value="QSFP-DD">QSFP-DD</option>
|
|
<option value="OSFP">OSFP</option>
|
|
<option value="CFP2">CFP2</option>
|
|
</select>
|
|
<button class="btn" id="tx-search-btn">Search</button>
|
|
</div>
|
|
<div class="card">
|
|
<div class="table-wrap">
|
|
<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><th>Category</th></tr></thead>
|
|
<tbody id="tx-table"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- NEWS -->
|
|
<div id="tab-news" class="hidden">
|
|
<div class="card">
|
|
<div id="news-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- BLOG -->
|
|
<div id="tab-blog" class="hidden">
|
|
<div class="grid g3 mb">
|
|
<div class="gen-card" id="gen-hype">
|
|
<div class="gen-card-title">Hype Cycle Analysis</div>
|
|
<div class="gen-card-sub">800G technology position article</div>
|
|
</div>
|
|
<div class="gen-card" id="gen-comparison">
|
|
<div class="gen-card-title">Product Comparison</div>
|
|
<div class="gen-card-sub">400G transceiver comparison</div>
|
|
</div>
|
|
<div class="gen-card" id="gen-tutorial">
|
|
<div class="gen-card-title">Tutorial</div>
|
|
<div class="gen-card-sub">Transceiver troubleshooting guide</div>
|
|
</div>
|
|
</div>
|
|
<div class="card"><div id="blog-list"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DETAIL PANEL (shared for hype cycle + transceivers) -->
|
|
<div id="detail-panel" class="panel">
|
|
<button class="panel-close" id="panel-close">×</button>
|
|
<div id="panel-content"></div>
|
|
</div>
|
|
|
|
<script>
|
|
var API = window.location.hostname === 'localhost' ? 'http://localhost:3201' : window.location.origin;
|
|
|
|
function esc(str) {
|
|
if (str == null) return '';
|
|
var d = document.createElement('div');
|
|
d.textContent = String(str);
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function el(id) { return document.getElementById(id); }
|
|
|
|
function api(path) { return fetch(API + path).then(function(r) { return r.json(); }); }
|
|
|
|
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) {
|
|
parent.textContent = '';
|
|
var t = document.createElement('template');
|
|
t.innerHTML = html;
|
|
parent.appendChild(t.content.cloneNode(true));
|
|
}
|
|
|
|
function openPanel(html) {
|
|
var p = el('detail-panel');
|
|
buildDOM(el('panel-content'), html);
|
|
p.classList.add('open');
|
|
}
|
|
function closePanel() { el('detail-panel').classList.remove('open'); }
|
|
el('panel-close').addEventListener('click', closePanel);
|
|
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closePanel(); });
|
|
|
|
// TABS
|
|
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 h = await api('/api/health');
|
|
el('stat-transceivers').textContent = h.database.stats.transceiver_count;
|
|
el('stat-vendors').textContent = h.database.stats.vendor_count;
|
|
el('stat-standards').textContent = h.database.stats.standard_count;
|
|
el('stat-news').textContent = h.database.stats.news_count;
|
|
el('version-label').textContent = 'v' + h.version;
|
|
el('api-status').className = 'dot ' + (h.success ? 'dot-ok' : 'dot-err');
|
|
el('db-status').className = 'dot ' + (h.database.connected ? 'dot-ok' : 'dot-err');
|
|
} catch(e) { el('api-status').className = 'dot dot-err'; }
|
|
|
|
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.35rem 0;border-bottom:1px solid var(--border)">'
|
|
+ '<span class="mono" style="font-size:0.75rem">' + esc(c.collection) + '</span>'
|
|
+ '<span class="b ' + (c.pointsCount > 0 ? 'b-green' : 'b-yellow') + '">' + esc(c.pointsCount) + '</span>'
|
|
+ '</div>';
|
|
}).join(''));
|
|
el('qdrant-status').className = 'dot dot-ok';
|
|
} catch(e) { el('qdrant-status').className = 'dot dot-err'; }
|
|
|
|
try {
|
|
var root = await api('/');
|
|
buildDOM(el('endpoints-list'), (root.endpoints || []).map(function(e) {
|
|
return '<div style="padding:0.1rem 0">' + 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 class="ri">'
|
|
+ '<div class="ri-title">' + esc(n.title) + '</div>'
|
|
+ '<div class="ri-meta"><span class="b b-blue">' + esc(n.source) + '</span> ' + (n.published_at ? new Date(n.published_at).toLocaleDateString() : '') + '</div>'
|
|
+ '</div>';
|
|
}).join('') || '<div class="loading">No news</div>');
|
|
} catch(e) {}
|
|
}
|
|
|
|
// 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=15').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="ri">'
|
|
+ '<div style="display:flex;justify-content:space-between;align-items:center">'
|
|
+ '<div class="ri-title">' + esc(title) + '</div>'
|
|
+ '<span class="mono dim" style="font-size:0.7rem">' + (r.score * 100).toFixed(1) + '%</span>'
|
|
+ '</div>'
|
|
+ '<div class="ri-body">' + esc(body) + '</div>'
|
|
+ '<div class="ri-meta">'
|
|
+ (r.form_factor ? '<span class="b b-blue">' + esc(r.form_factor) + '</span>' : '')
|
|
+ (r.speed ? '<span class="b b-purple">' + esc(r.speed) + '</span>' : '')
|
|
+ (r.category ? '<span class="b b-yellow">' + esc(r.category) + '</span>' : '')
|
|
+ (r.severity ? '<span class="b b-red">' + esc(r.severity) + '</span>' : '')
|
|
+ (r.vendor ? '<span class="dim">' + esc(r.vendor) + '</span>' : '')
|
|
+ '</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 PC = {
|
|
'Innovation Trigger': '#4f8ff7',
|
|
'Peak of Inflated Expectations': '#eab308',
|
|
'Trough of Disillusionment': '#ef4444',
|
|
'Slope of Enlightenment': '#a78bfa',
|
|
'Plateau of Productivity': '#22c55e'
|
|
};
|
|
var PHASE_MAP = {
|
|
'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'
|
|
};
|
|
var PHASE_DESC = {
|
|
'Innovation Trigger': 'Early-stage technology breakthrough. First proof-of-concept demos, limited vendor support. High risk, high potential. Expect interop issues and premium pricing.',
|
|
'Peak of Inflated Expectations': 'Maximum hype and media attention. Vendors announce products, but real-world deployments are rare. Expectations exceed what the technology can deliver today.',
|
|
'Trough of Disillusionment': 'Reality check. Early deployments reveal limitations — interop failures, cost overruns, performance gaps. Interest wanes. Only committed adopters remain.',
|
|
'Slope of Enlightenment': 'Practical benefits become clear. Second and third-generation products fix early issues. Multi-vendor support grows. Best practices emerge from real deployments.',
|
|
'Plateau of Productivity': 'Mainstream adoption. Stable pricing, broad vendor support, proven reliability. The technology is a standard part of network infrastructure.'
|
|
};
|
|
|
|
function curveY(x, w, h) {
|
|
var t = x / w;
|
|
if (t < 0.15) return h - (t / 0.15) * h * 0.85;
|
|
if (t < 0.22) return h * 0.15 + ((t - 0.15) / 0.07) * h * 0.02;
|
|
if (t < 0.42) return h * 0.17 + ((t - 0.22) / 0.20) * h * 0.55;
|
|
if (t < 0.48) return h * 0.72 - ((t - 0.42) / 0.06) * h * 0.02;
|
|
if (t < 0.80) return h * 0.70 - ((t - 0.48) / 0.32) * h * 0.35;
|
|
return h * 0.35 - ((t - 0.80) / 0.20) * h * 0.02;
|
|
}
|
|
|
|
function renderHypeSvg(techs) {
|
|
var W = 1000, H = 300, P = 35;
|
|
var cw = W - P * 2, ch = H - P * 2;
|
|
var pts = [];
|
|
for (var i = 0; i <= cw; i += 2) pts.push((i + P) + ',' + (curveY(i, cw, ch) + P));
|
|
|
|
var svg = '<svg width="' + W + '" height="' + (H + 40) + '" viewBox="0 0 ' + W + ' ' + (H + 40) + '" xmlns="http://www.w3.org/2000/svg">';
|
|
|
|
var rb = [0, 0.15, 0.28, 0.50, 0.78, 1.0];
|
|
var rc = ['#4f8ff7', '#eab308', '#ef4444', '#a78bfa', '#22c55e'];
|
|
for (var r = 0; r < 5; r++) {
|
|
svg += '<rect x="' + (rb[r]*cw+P) + '" y="' + P + '" width="' + ((rb[r+1]-rb[r])*cw) + '" height="' + ch + '" fill="' + rc[r] + '" opacity="0.03" />';
|
|
}
|
|
|
|
var pl = [
|
|
{l:'Innovation\\nTrigger',x:0.07,k:'Innovation Trigger'},
|
|
{l:'Peak of Inflated\\nExpectations',x:0.18,k:'Peak of Inflated Expectations'},
|
|
{l:'Trough of\\nDisillusionment',x:0.42,k:'Trough of Disillusionment'},
|
|
{l:'Slope of\\nEnlightenment',x:0.64,k:'Slope of Enlightenment'},
|
|
{l:'Plateau of\\nProductivity',x:0.90,k:'Plateau of Productivity'}
|
|
];
|
|
for (var p = 0; p < pl.length; p++) {
|
|
var px = pl[p].x * cw + P;
|
|
var ll = pl[p].l.split('\\n');
|
|
var phaseW = (rb[p+1]-rb[p])*cw;
|
|
svg += '<g style="cursor:help"><title>' + esc(PHASE_DESC[pl[p].k] || '') + '</title>';
|
|
svg += '<rect x="' + (rb[p]*cw+P) + '" y="' + (H+2) + '" width="' + phaseW + '" height="28" fill="transparent" />';
|
|
for (var li = 0; li < ll.length; li++) {
|
|
svg += '<text x="' + px + '" y="' + (H + 12 + li * 12) + '" class="hype-phase-label">' + esc(ll[li]) + '</text>';
|
|
}
|
|
svg += '</g>';
|
|
}
|
|
|
|
svg += '<polyline points="' + pts.join(' ') + '" fill="none" stroke="#333b4d" stroke-width="2" />';
|
|
|
|
var placed = [];
|
|
for (var ti = 0; ti < techs.length; ti++) {
|
|
var t = techs[ti], color = PC[t.phase] || '#6b7280';
|
|
var tx = (t.positionPct / 100) * cw, ty = curveY(tx, cw, ch);
|
|
tx += P; ty += P;
|
|
var ly = ty - 12, lx = tx + 7, anc = 'start';
|
|
for (var pi = 0; pi < placed.length; pi++) {
|
|
if (Math.abs(placed[pi].x - tx) < 45 && Math.abs(placed[pi].y - ly) < 14) ly = placed[pi].y + 14;
|
|
}
|
|
if (tx > W - 150) { lx = tx - 7; anc = 'end'; }
|
|
placed.push({x:tx, y:ly});
|
|
|
|
svg += '<circle cx="' + tx + '" cy="' + ty + '" r="6" fill="' + color + '" class="hype-dot" data-tech="' + esc(t.technology) + '" />';
|
|
svg += '<text x="' + lx + '" y="' + (ly + 3) + '" class="hype-label" text-anchor="' + anc + '" font-weight="600">' + esc(t.technology) + '</text>';
|
|
}
|
|
svg += '</svg>';
|
|
return svg;
|
|
}
|
|
|
|
async function openHypeDetail(name) {
|
|
openPanel('<div class="loading pulse">Loading...</div>');
|
|
try {
|
|
var d = await api('/api/hype-cycle/' + encodeURIComponent(name));
|
|
var c = PC[d.phaseLabel] || '#6b7280';
|
|
var h = '<div class="panel-title">' + esc(d.technology) + '</div>';
|
|
h += '<div class="panel-sub"><span class="b" style="background:' + c + '22;color:' + c + '">' + esc(d.phaseLabel) + '</span></div>';
|
|
h += '<div class="panel-grid">';
|
|
h += '<div class="panel-stat"><div class="panel-stat-label">Adoption</div><div class="panel-stat-val" style="color:' + c + '">' + (d.adoptionPct != null ? (d.adoptionPct*100).toFixed(0) + '%' : '—') + '</div></div>';
|
|
h += '<div class="panel-stat"><div class="panel-stat-label">Position</div><div class="panel-stat-val">' + (d.positionPct||0) + '%</div></div>';
|
|
h += '<div class="panel-stat"><div class="panel-stat-label">Peak Year</div><div class="panel-stat-val">' + esc(d.forecast && d.forecast.peakShipmentYear || '—') + '</div></div>';
|
|
h += '<div class="panel-stat"><div class="panel-stat-label">To Plateau</div><div class="panel-stat-val">' + (d.forecast && d.forecast.yearsToPlateauFromNow != null ? d.forecast.yearsToPlateauFromNow + 'y' : '—') + '</div></div>';
|
|
h += '</div>';
|
|
|
|
if (d.forecast && d.forecast.fiveYearProjection && d.forecast.fiveYearProjection.length) {
|
|
h += '<div class="panel-section">5-Year Forecast</div>';
|
|
for (var i = 0; i < d.forecast.fiveYearProjection.length; i++) {
|
|
var f = d.forecast.fiveYearProjection[i];
|
|
var fc = PC[PHASE_MAP[f.phase] || ''] || '#6b7280';
|
|
h += '<div class="forecast-bar"><span class="yr">' + f.year + '</span><div class="track"><div class="fill" style="width:' + (f.adoptionPct||0) + '%;background:' + fc + '"></div></div><span class="pct">' + (f.adoptionPct||0) + '%</span></div>';
|
|
}
|
|
}
|
|
|
|
h += '<div class="panel-section">Composite Score</div>';
|
|
h += '<div style="padding:0.6rem;background:var(--surface2);border-radius:4px">';
|
|
h += '<span class="mono" style="font-size:1.5rem;font-weight:700;color:' + c + '">' + (d.compositeScore||0) + '</span><span class="dim" style="font-size:0.8rem"> / 100</span>';
|
|
h += '</div>';
|
|
|
|
buildDOM(el('panel-content'), h);
|
|
} catch(e) { el('panel-content').textContent = 'Error: ' + e.message; }
|
|
}
|
|
|
|
async function loadHypeCycle() {
|
|
var data = await api('/api/hype-cycle');
|
|
var techs = data.technologies || [];
|
|
el('hype-year').textContent = data.year;
|
|
|
|
var c = el('hype-svg-container');
|
|
buildDOM(c, renderHypeSvg(techs));
|
|
c.querySelectorAll('.hype-dot').forEach(function(dot) {
|
|
dot.addEventListener('click', function() { openHypeDetail(this.getAttribute('data-tech')); });
|
|
});
|
|
|
|
buildDOM(el('hype-table'), techs.map(function(t) {
|
|
var color = PC[t.phase] || '#374151';
|
|
return '<tr class="clickable" data-tech="' + esc(t.technology) + '">'
|
|
+ '<td style="font-weight:500">' + esc(t.technology) + '</td>'
|
|
+ '<td><span class="b tip" data-tip="' + esc(PHASE_DESC[t.phase] || '') + '" style="background:' + color + '22;color:' + color + '">' + esc(t.phase) + '</span></td>'
|
|
+ '<td><div class="hype-bar"><div class="hype-fill" style="width:' + t.positionPct + '%;background:' + color + '"></div></div></td>'
|
|
+ '<td class="mono tip" data-tip="' + esc(t.technology) + ' has reached ' + (t.adoptionPct * 100).toFixed(0) + '% of its total addressable market based on Norton-Bass diffusion modeling.">' + (t.adoptionPct * 100).toFixed(0) + '%</td>'
|
|
+ '<td class="mono tip" data-tip="' + (t.peakYear ? 'Peak hype expected around ' + t.peakYear + '. After this, expect a reality correction before stable adoption.' : 'Peak year not yet determined.') + '">' + esc(t.peakYear || '—') + '</td>'
|
|
+ '<td class="mono tip" data-tip="' + (t.yearsToPlateauFromNow != null ? 'Estimated ' + t.yearsToPlateauFromNow + ' years until ' + esc(t.technology) + ' reaches mainstream, stable deployment.' : 'Already at plateau or timeline unknown.') + '">' + (t.yearsToPlateauFromNow != null ? t.yearsToPlateauFromNow + 'y' : '—') + '</td>'
|
|
+ '</tr>';
|
|
}).join(''));
|
|
|
|
el('hype-table').querySelectorAll('tr.clickable').forEach(function(row) {
|
|
row.addEventListener('click', function() { openHypeDetail(this.getAttribute('data-tech')); });
|
|
});
|
|
}
|
|
|
|
// TRANSCEIVERS — with click-through detail
|
|
function searchTransceivers() {
|
|
var q = el('tx-search').value;
|
|
var ff = el('tx-ff-filter').value;
|
|
var params = [];
|
|
if (q) params.push('q=' + encodeURIComponent(q));
|
|
if (ff) params.push('form_factor=' + encodeURIComponent(ff));
|
|
params.push('limit=100');
|
|
var url = '/api/transceivers?' + params.join('&');
|
|
|
|
api(url).then(function(data) {
|
|
buildDOM(el('tx-table'), (data.transceivers || []).map(function(t) {
|
|
return '<tr class="clickable" data-txid="' + esc(t.id) + '">'
|
|
+ '<td style="font-weight:500;color:var(--text-bright)">' + esc(t.standard_name || t.slug) + '</td>'
|
|
+ '<td><span class="b b-blue">' + esc(t.form_factor) + '</span></td>'
|
|
+ '<td class="mono">' + 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>'
|
|
+ '<td>' + (t.category ? '<span class="b b-neutral">' + esc(t.category) + '</span>' : '') + '</td>'
|
|
+ '</tr>';
|
|
}).join(''));
|
|
|
|
el('tx-table').querySelectorAll('tr.clickable').forEach(function(row) {
|
|
row.addEventListener('click', function() { openTxDetail(this.getAttribute('data-txid')); });
|
|
});
|
|
});
|
|
}
|
|
|
|
async function openTxDetail(id) {
|
|
openPanel('<div class="loading pulse">Loading...</div>');
|
|
try {
|
|
var data = await api('/api/transceivers/' + id);
|
|
var t = data.transceiver || data;
|
|
var h = '<div class="panel-title">' + esc(t.standard_name || t.slug) + '</div>';
|
|
h += '<div class="panel-sub">' + esc(t.description || '') + '</div>';
|
|
|
|
h += '<div class="panel-section">Specifications</div>';
|
|
var specs = [
|
|
['Form Factor', t.form_factor],
|
|
['Speed', t.speed],
|
|
['Speed (Gbps)', t.speed_gbps],
|
|
['Reach', t.reach_label],
|
|
['Reach (km)', t.reach_km],
|
|
['Fiber Type', t.fiber_type],
|
|
['Connector', t.connector],
|
|
['WDM Type', t.wdm_type],
|
|
['Wavelength', t.wavelength_nm ? t.wavelength_nm + ' nm' : null],
|
|
['Lanes', t.lanes],
|
|
['Category', t.category],
|
|
['Coherent', t.is_coherent ? 'Yes' : 'No'],
|
|
['Bidirectional', t.is_bidirectional ? 'Yes' : 'No'],
|
|
['Temperature', t.temperature_range],
|
|
['Power (W)', t.power_max_w],
|
|
['Standard', t.standard_name || t.standard_id],
|
|
];
|
|
for (var s = 0; s < specs.length; s++) {
|
|
if (specs[s][1] != null && specs[s][1] !== '' && specs[s][1] !== false) {
|
|
h += '<div class="panel-row"><span class="panel-row-label">' + esc(specs[s][0]) + '</span><span class="panel-row-val">' + esc(specs[s][1]) + '</span></div>';
|
|
}
|
|
}
|
|
|
|
buildDOM(el('panel-content'), h);
|
|
} catch(e) { el('panel-content').textContent = 'Error: ' + e.message; }
|
|
}
|
|
|
|
el('tx-search-btn').addEventListener('click', searchTransceivers);
|
|
el('tx-search').addEventListener('keydown', function(e) { if (e.key === 'Enter') searchTransceivers(); });
|
|
el('tx-ff-filter').addEventListener('change', searchTransceivers);
|
|
|
|
// NEWS
|
|
async function loadNews() {
|
|
var data = await api('/api/search?q=optical+transceiver+data+center+networking&collection=news_embeddings&limit=25');
|
|
buildDOM(el('news-list'), (data.results || []).map(function(n) {
|
|
var urlSafe = (n.url && /^https?:\/\//.test(n.url)) ? n.url : '#';
|
|
return '<div class="ri">'
|
|
+ '<div class="ri-title">' + esc(n.title) + '</div>'
|
|
+ '<div class="ri-body">' + esc(n.summary) + '</div>'
|
|
+ '<div class="ri-meta">'
|
|
+ '<span class="b b-blue">' + esc(n.source) + '</span>'
|
|
+ (n.published_at ? '<span>' + new Date(n.published_at).toLocaleDateString() + '</span>' : '')
|
|
+ (urlSafe !== '#' ? '<a href="' + esc(urlSafe) + '" target="_blank" rel="noopener noreferrer" style="color:var(--accent);text-decoration:none;font-size:0.7rem">Read →</a>' : '')
|
|
+ '</div></div>';
|
|
}).join('') || '<div class="loading">No news</div>');
|
|
}
|
|
|
|
// BLOG
|
|
function generateBlog(topic, speed) {
|
|
el('blog-list').textContent = 'Generating...';
|
|
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('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 sc = d.status === 'published' ? 'b-green' : d.status === 'review' ? 'b-yellow' : 'b-blue';
|
|
return '<div class="ri" onclick="openBlogDetail(\'' + esc(d.id) + '\')">'
|
|
+ '<div style="display:flex;justify-content:space-between;align-items:center">'
|
|
+ '<div class="ri-title">' + esc(d.title) + '</div>'
|
|
+ '<span class="b ' + sc + '">' + esc(d.status) + '</span>'
|
|
+ '</div>'
|
|
+ '<div class="ri-meta">'
|
|
+ '<span class="b b-purple">' + esc(d.topic) + '</span>'
|
|
+ '<span class="b b-neutral">' + esc(d.target_audience) + '</span>'
|
|
+ '<span class="mono">' + esc(d.word_count) + ' words</span>'
|
|
+ '<span>' + new Date(d.created_at).toLocaleDateString() + '</span>'
|
|
+ '</div></div>';
|
|
}).join('') || '<div class="loading">No drafts yet</div>');
|
|
}
|
|
|
|
async function openBlogDetail(id) {
|
|
openPanel('<div class="loading pulse">Loading...</div>');
|
|
try {
|
|
var data = await api('/api/blog/' + id);
|
|
var d = data.draft;
|
|
var h = '<div class="panel-title">' + esc(d.title) + '</div>';
|
|
h += '<div class="panel-sub"><span class="b b-purple">' + esc(d.topic) + '</span> <span class="b b-neutral">' + esc(d.target_audience) + '</span> <span class="mono dim">' + esc(d.word_count) + ' words</span></div>';
|
|
h += '<div class="panel-section">Content</div>';
|
|
h += '<div style="font-size:0.8rem;color:var(--text);white-space:pre-wrap;line-height:1.6;max-height:60vh;overflow-y:auto;background:var(--surface2);padding:0.75rem;border-radius:4px">' + esc(d.draft_content) + '</div>';
|
|
h += '<div class="panel-section">SEO Keywords</div>';
|
|
h += '<div style="display:flex;gap:0.25rem;flex-wrap:wrap">' + (d.seo_keywords || []).map(function(k) { return '<span class="b b-neutral">' + esc(k) + '</span>'; }).join('') + '</div>';
|
|
buildDOM(el('panel-content'), h);
|
|
} catch(e) { el('panel-content').textContent = 'Error: ' + e.message; }
|
|
}
|
|
|
|
// INIT
|
|
loadOverview();
|
|
</script>
|
|
</body>
|
|
</html>
|