Premium dark UI with ambient glow background, glass cards, gradient text, animated counters, glow effects on hype cycle dots, smooth transitions, and improved detail panel with regional adoption and revenue lifecycle data. Fix API data key mismatch (data vs transceivers).
1218 lines
55 KiB
HTML
1218 lines
55 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@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bg: #06080d;
|
|
--surface: rgba(14,17,27,0.85);
|
|
--surface-solid: #0e111b;
|
|
--surface2: rgba(22,27,42,0.7);
|
|
--surface3: #1a2035;
|
|
--border: rgba(56,68,100,0.35);
|
|
--border-hover: rgba(79,143,247,0.3);
|
|
--text: #c8cee0;
|
|
--text-bright: #edf0f7;
|
|
--text-dim: #5b6480;
|
|
--accent: #4f8ff7;
|
|
--accent-glow: rgba(79,143,247,0.25);
|
|
--accent2: #7c5cfc;
|
|
--green: #10b981;
|
|
--green-glow: rgba(16,185,129,0.2);
|
|
--yellow: #f59e0b;
|
|
--yellow-glow: rgba(245,158,11,0.15);
|
|
--red: #ef4444;
|
|
--red-glow: rgba(239,68,68,0.15);
|
|
--purple: #a78bfa;
|
|
--purple-glow: rgba(167,139,250,0.15);
|
|
--orange: #f97316;
|
|
--cyan: #22d3ee;
|
|
--cyan-glow: rgba(34,211,238,0.15);
|
|
--mono: 'JetBrains Mono', 'SF Mono', monospace;
|
|
--grad-accent: linear-gradient(135deg, #4f8ff7, #7c5cfc, #a78bfa);
|
|
--grad-warm: linear-gradient(135deg, #f59e0b, #ef4444);
|
|
--grad-cool: linear-gradient(135deg, #10b981, #22d3ee);
|
|
}
|
|
|
|
* { 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;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* === ANIMATED BACKGROUND === */
|
|
.bg-grid {
|
|
position: fixed; inset: 0; z-index: 0; pointer-events: none;
|
|
background-image:
|
|
linear-gradient(rgba(79,143,247,0.03) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(79,143,247,0.03) 1px, transparent 1px);
|
|
background-size: 60px 60px;
|
|
}
|
|
.bg-glow {
|
|
position: fixed; z-index: 0; pointer-events: none;
|
|
width: 600px; height: 600px; border-radius: 50%;
|
|
filter: blur(120px); opacity: 0.15;
|
|
}
|
|
.bg-glow-1 { top: -200px; left: -100px; background: #4f8ff7; animation: float1 20s ease-in-out infinite; }
|
|
.bg-glow-2 { bottom: -200px; right: -100px; background: #7c5cfc; animation: float2 25s ease-in-out infinite; }
|
|
.bg-glow-3 { top: 40%; left: 50%; background: #10b981; opacity: 0.08; animation: float3 30s ease-in-out infinite; }
|
|
|
|
@keyframes float1 { 0%,100%{transform:translate(0,0)} 50%{transform:translate(80px,60px)} }
|
|
@keyframes float2 { 0%,100%{transform:translate(0,0)} 50%{transform:translate(-60px,-80px)} }
|
|
@keyframes float3 { 0%,100%{transform:translate(-50%,-50%)} 50%{transform:translate(-40%,-40%)} }
|
|
|
|
/* === LAYOUT WRAPPER === */
|
|
.app { position: relative; z-index: 1; }
|
|
|
|
/* === HEADER === */
|
|
.header {
|
|
background: rgba(10,13,22,0.8);
|
|
backdrop-filter: blur(20px) saturate(1.5);
|
|
-webkit-backdrop-filter: blur(20px) saturate(1.5);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 0 2rem;
|
|
height: 56px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
position: sticky; top: 0; z-index: 100;
|
|
}
|
|
.header-left { display: flex; align-items: center; gap: 2rem; }
|
|
.logo {
|
|
display: flex; align-items: center; gap: 0.6rem;
|
|
}
|
|
.logo-mark {
|
|
width: 32px; height: 32px; border-radius: 8px;
|
|
background: var(--grad-accent);
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-weight: 800; font-size: 0.85rem; color: #fff;
|
|
box-shadow: 0 0 20px var(--accent-glow);
|
|
}
|
|
.logo-text {
|
|
font-size: 0.95rem; font-weight: 700; color: var(--text-bright);
|
|
letter-spacing: -0.02em;
|
|
}
|
|
.logo-text span { color: var(--text-dim); font-weight: 400; margin-left: 0.25rem; }
|
|
.header-stats {
|
|
display: flex; gap: 1.5rem; font-size: 0.75rem;
|
|
font-family: var(--mono); color: var(--text-dim);
|
|
}
|
|
.header-stats .val {
|
|
color: var(--accent); font-weight: 600;
|
|
text-shadow: 0 0 10px var(--accent-glow);
|
|
}
|
|
.header .status {
|
|
display: flex; gap: 1rem; align-items: center; font-size: 0.75rem;
|
|
}
|
|
.status-pill {
|
|
display: flex; align-items: center; gap: 0.35rem;
|
|
padding: 0.25rem 0.6rem; border-radius: 20px;
|
|
background: rgba(16,185,129,0.08);
|
|
border: 1px solid rgba(16,185,129,0.2);
|
|
color: var(--green); font-weight: 500;
|
|
font-size: 0.7rem; font-family: var(--mono);
|
|
}
|
|
.status-pill.err { background: rgba(239,68,68,0.08); border-color: rgba(239,68,68,0.2); color: var(--red); }
|
|
.dot {
|
|
width: 6px; height: 6px; border-radius: 50%; display: inline-block;
|
|
}
|
|
.dot-ok { background: var(--green); box-shadow: 0 0 8px var(--green-glow); animation: pulse-dot 3s infinite; }
|
|
.dot-err { background: var(--red); box-shadow: 0 0 8px var(--red-glow); }
|
|
@keyframes pulse-dot { 0%,100%{opacity:1;box-shadow:0 0 4px var(--green-glow)} 50%{opacity:0.6;box-shadow:0 0 12px var(--green)} }
|
|
.version-tag {
|
|
font-family: var(--mono); font-size: 0.65rem; color: var(--text-dim);
|
|
padding: 0.15rem 0.5rem; border-radius: 4px;
|
|
background: var(--surface2); border: 1px solid var(--border);
|
|
}
|
|
|
|
/* === TABS === */
|
|
.tabs {
|
|
display: flex; gap: 0;
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 0 2rem;
|
|
background: rgba(10,13,22,0.5);
|
|
backdrop-filter: blur(10px);
|
|
-webkit-backdrop-filter: blur(10px);
|
|
}
|
|
.tab {
|
|
padding: 0.75rem 1.25rem;
|
|
cursor: pointer;
|
|
border-bottom: 2px solid transparent;
|
|
color: var(--text-dim);
|
|
font-size: 0.8rem; font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
position: relative;
|
|
}
|
|
.tab:hover { color: var(--text); }
|
|
.tab.active {
|
|
color: var(--text-bright);
|
|
border-bottom-color: var(--accent);
|
|
}
|
|
.tab.active::after {
|
|
content: '';
|
|
position: absolute; bottom: -1px; left: 0; right: 0; height: 2px;
|
|
background: var(--accent);
|
|
box-shadow: 0 0 12px var(--accent-glow), 0 0 4px var(--accent);
|
|
}
|
|
|
|
/* === MAIN === */
|
|
.main { padding: 1.5rem 2rem; max-width: 1600px; margin: 0 auto; }
|
|
.grid { display: grid; gap: 1rem; }
|
|
.g4 { grid-template-columns: repeat(4, 1fr); }
|
|
.g3 { grid-template-columns: repeat(3, 1fr); }
|
|
.g2 { grid-template-columns: 1fr 1fr; }
|
|
.g2-1 { grid-template-columns: 2fr 1fr; }
|
|
|
|
/* === GLASS CARDS === */
|
|
.card {
|
|
background: var(--surface);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 1.25rem;
|
|
transition: border-color 0.3s, box-shadow 0.3s;
|
|
}
|
|
.card:hover {
|
|
border-color: var(--border-hover);
|
|
box-shadow: 0 4px 30px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.03);
|
|
}
|
|
.card-glow {
|
|
box-shadow: 0 0 30px var(--accent-glow);
|
|
border-color: rgba(79,143,247,0.25);
|
|
}
|
|
.card-label {
|
|
font-size: 0.7rem; font-weight: 500;
|
|
text-transform: uppercase; letter-spacing: 0.08em;
|
|
color: var(--text-dim); margin-bottom: 0.5rem;
|
|
}
|
|
.card-num {
|
|
font-size: 2rem; font-weight: 800;
|
|
font-family: var(--mono); letter-spacing: -0.03em;
|
|
background: var(--grad-accent);
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
.card-num.green { background: var(--grad-cool); -webkit-background-clip: text; background-clip: text; }
|
|
.card-num.warm { background: var(--grad-warm); -webkit-background-clip: text; background-clip: text; }
|
|
.card-num small {
|
|
font-size: 0.7rem; font-weight: 400;
|
|
-webkit-text-fill-color: var(--text-dim);
|
|
}
|
|
|
|
/* === STAT CARDS (overview) === */
|
|
.stat-card {
|
|
background: var(--surface);
|
|
backdrop-filter: blur(12px);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 1.25rem;
|
|
position: relative; overflow: hidden;
|
|
transition: all 0.3s;
|
|
}
|
|
.stat-card::before {
|
|
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
|
|
background: var(--grad-accent); opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
.stat-card:hover { border-color: var(--border-hover); transform: translateY(-2px); }
|
|
.stat-card:hover::before { opacity: 1; }
|
|
.stat-icon {
|
|
width: 36px; height: 36px; border-radius: 10px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 1rem; margin-bottom: 0.75rem;
|
|
}
|
|
.stat-icon.blue { background: rgba(79,143,247,0.1); color: var(--accent); }
|
|
.stat-icon.green { background: rgba(16,185,129,0.1); color: var(--green); }
|
|
.stat-icon.purple { background: rgba(167,139,250,0.1); color: var(--purple); }
|
|
.stat-icon.orange { background: rgba(249,115,22,0.1); color: var(--orange); }
|
|
.stat-icon.cyan { background: rgba(34,211,238,0.1); color: var(--cyan); }
|
|
.stat-label { font-size: 0.72rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 500; }
|
|
.stat-val {
|
|
font-size: 1.75rem; font-weight: 800; font-family: var(--mono);
|
|
color: var(--text-bright); letter-spacing: -0.03em; margin-top: 0.15rem;
|
|
}
|
|
|
|
/* === BADGES === */
|
|
.b {
|
|
display: inline-block; padding: 2px 8px; border-radius: 6px;
|
|
font-size: 0.68rem; font-weight: 600; font-family: var(--mono);
|
|
letter-spacing: 0.02em;
|
|
}
|
|
.b-blue { background: rgba(79,143,247,0.12); color: var(--accent); border: 1px solid rgba(79,143,247,0.2); }
|
|
.b-green { background: rgba(16,185,129,0.12); color: var(--green); border: 1px solid rgba(16,185,129,0.2); }
|
|
.b-yellow { background: rgba(245,158,11,0.12); color: var(--yellow); border: 1px solid rgba(245,158,11,0.2); }
|
|
.b-red { background: rgba(239,68,68,0.12); color: var(--red); border: 1px solid rgba(239,68,68,0.2); }
|
|
.b-purple { background: rgba(167,139,250,0.12); color: var(--purple); border: 1px solid rgba(167,139,250,0.2); }
|
|
.b-orange { background: rgba(249,115,22,0.12); color: var(--orange); border: 1px solid rgba(249,115,22,0.2); }
|
|
.b-cyan { background: rgba(34,211,238,0.12); color: var(--cyan); border: 1px solid rgba(34,211,238,0.2); }
|
|
.b-neutral { background: var(--surface3); color: var(--text-dim); border: 1px solid var(--border); }
|
|
|
|
/* === TABLES === */
|
|
table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
|
|
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.68rem; text-transform: uppercase; letter-spacing: 0.06em;
|
|
position: sticky; top: 0; background: var(--surface-solid); z-index: 1;
|
|
}
|
|
td { padding: 0.55rem 0.75rem; border-bottom: 1px solid rgba(56,68,100,0.15); }
|
|
tr.clickable { cursor: pointer; transition: background 0.15s; }
|
|
tr.clickable:hover td { background: rgba(79,143,247,0.06); }
|
|
.table-wrap { max-height: 70vh; overflow-y: auto; border-radius: 8px; }
|
|
.table-wrap::-webkit-scrollbar { width: 6px; }
|
|
.table-wrap::-webkit-scrollbar-track { background: transparent; }
|
|
.table-wrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
|
|
/* === SEARCH === */
|
|
.search-row { display: flex; gap: 0.5rem; margin-bottom: 1.25rem; }
|
|
.search-row input, .search-row select {
|
|
background: var(--surface2); border: 1px solid var(--border);
|
|
border-radius: 8px; padding: 0.6rem 1rem;
|
|
color: var(--text); font-size: 0.85rem;
|
|
backdrop-filter: blur(8px);
|
|
transition: border-color 0.2s, box-shadow 0.2s;
|
|
}
|
|
.search-row input { flex: 1; }
|
|
.search-row input:focus {
|
|
outline: none; border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px var(--accent-glow);
|
|
}
|
|
.search-row select { min-width: 140px; }
|
|
.btn {
|
|
background: var(--grad-accent); color: #fff; border: none;
|
|
border-radius: 8px; padding: 0.6rem 1.25rem;
|
|
font-weight: 600; font-size: 0.8rem; cursor: pointer;
|
|
transition: transform 0.15s, box-shadow 0.15s;
|
|
box-shadow: 0 2px 12px var(--accent-glow);
|
|
}
|
|
.btn:hover { transform: translateY(-1px); box-shadow: 0 4px 20px var(--accent-glow); }
|
|
.btn:active { transform: translateY(0); }
|
|
.btn-ghost {
|
|
background: transparent; border: 1px solid var(--border);
|
|
color: var(--text); cursor: pointer; border-radius: 8px;
|
|
padding: 0.5rem 0.85rem; font-size: 0.75rem; font-weight: 500;
|
|
transition: all 0.2s;
|
|
}
|
|
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); background: rgba(79,143,247,0.05); }
|
|
|
|
/* === RESULT ITEMS === */
|
|
.ri {
|
|
border-bottom: 1px solid rgba(56,68,100,0.2);
|
|
padding: 1rem 0; cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
.ri:last-child { border-bottom: none; }
|
|
.ri:hover { background: rgba(79,143,247,0.04); margin: 0 -1.25rem; padding: 1rem 1.25rem; border-radius: 8px; }
|
|
.ri-title { font-weight: 600; font-size: 0.85rem; color: var(--text-bright); }
|
|
.ri-body { font-size: 0.8rem; color: var(--text-dim); margin-top: 0.3rem; line-height: 1.6; }
|
|
.ri-meta { font-size: 0.7rem; color: var(--text-dim); margin-top: 0.4rem; display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
|
|
|
|
/* === HYPE CYCLE === */
|
|
.hype-wrap {
|
|
background: var(--surface);
|
|
backdrop-filter: blur(12px);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 1.5rem;
|
|
overflow-x: auto;
|
|
}
|
|
.hype-wrap svg { display: block; margin: 0 auto; }
|
|
.hype-dot { cursor: pointer; transition: all 0.2s; }
|
|
.hype-dot:hover { filter: brightness(1.5) drop-shadow(0 0 6px currentColor); }
|
|
.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-header {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.hype-title {
|
|
font-weight: 700; font-size: 1rem; color: var(--text-bright);
|
|
}
|
|
.hype-sub { font-size: 0.72rem; color: var(--text-dim); margin-top: 0.2rem; }
|
|
.hype-legend { display: flex; gap: 1rem; font-size: 0.68rem; color: var(--text-dim); align-items: center; }
|
|
.hype-legend-dot {
|
|
width: 8px; height: 8px; border-radius: 50%; display: inline-block;
|
|
margin-right: 0.3rem; vertical-align: middle;
|
|
}
|
|
|
|
.hype-bar { height: 6px; background: var(--surface3); border-radius: 3px; overflow: hidden; width: 100%; }
|
|
.hype-fill { height: 100%; border-radius: 3px; transition: width 0.8s cubic-bezier(0.22, 1, 0.36, 1); }
|
|
|
|
/* === TOOLTIPS === */
|
|
.tip { position: relative; cursor: help; }
|
|
.tip::after {
|
|
content: attr(data-tip);
|
|
position: absolute; bottom: calc(100% + 10px); left: 50%; transform: translateX(-50%);
|
|
background: rgba(10,13,22,0.95); color: #e0e0e0;
|
|
border: 1px solid var(--border);
|
|
padding: 0.6rem 0.85rem; border-radius: 10px;
|
|
font-size: 0.72rem; line-height: 1.5; font-weight: 400;
|
|
white-space: normal; width: max-content; max-width: 300px;
|
|
opacity: 0; pointer-events: none; transition: opacity 0.2s;
|
|
z-index: 500; box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
|
backdrop-filter: blur(20px);
|
|
}
|
|
.tip::before {
|
|
content: ''; position: absolute; bottom: calc(100% + 5px); left: 50%; transform: translateX(-50%);
|
|
border: 6px 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 === */
|
|
.panel {
|
|
position: fixed; top: 0; right: 0;
|
|
width: 520px; height: 100vh;
|
|
background: rgba(10,13,22,0.95);
|
|
backdrop-filter: blur(24px) saturate(1.5);
|
|
-webkit-backdrop-filter: blur(24px) saturate(1.5);
|
|
border-left: 1px solid var(--border);
|
|
box-shadow: -8px 0 40px rgba(0,0,0,0.5);
|
|
z-index: 1000; overflow-y: auto;
|
|
transform: translateX(100%); transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
|
padding: 1.5rem;
|
|
}
|
|
.panel.open { transform: translateX(0); }
|
|
.panel::-webkit-scrollbar { width: 4px; }
|
|
.panel::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
|
.panel-close {
|
|
position: absolute; top: 1rem; right: 1rem;
|
|
background: var(--surface2); border: 1px solid var(--border);
|
|
color: var(--text-dim); width: 32px; height: 32px; border-radius: 8px;
|
|
cursor: pointer; font-size: 1.1rem;
|
|
display: flex; align-items: center; justify-content: center;
|
|
transition: all 0.2s;
|
|
}
|
|
.panel-close:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
.panel-title {
|
|
font-size: 1.2rem; font-weight: 800; color: var(--text-bright);
|
|
margin-bottom: 0.2rem; letter-spacing: -0.02em;
|
|
}
|
|
.panel-sub { font-size: 0.8rem; color: var(--text-dim); margin-bottom: 1.25rem; }
|
|
.panel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; margin-bottom: 1.25rem; }
|
|
.panel-stat {
|
|
background: var(--surface2); border-radius: 10px; padding: 0.75rem;
|
|
border: 1px solid var(--border);
|
|
}
|
|
.panel-stat-label { font-size: 0.65rem; text-transform: uppercase; color: var(--text-dim); letter-spacing: 0.06em; font-weight: 500; }
|
|
.panel-stat-val {
|
|
font-size: 1.3rem; font-weight: 800; font-family: var(--mono);
|
|
margin-top: 0.2rem; color: var(--text-bright);
|
|
}
|
|
.panel-section {
|
|
font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
|
|
letter-spacing: 0.06em; color: var(--text-dim);
|
|
margin: 1.25rem 0 0.6rem;
|
|
display: flex; align-items: center; gap: 0.5rem;
|
|
}
|
|
.panel-section::after {
|
|
content: ''; flex: 1; height: 1px;
|
|
background: linear-gradient(90deg, var(--border), transparent);
|
|
}
|
|
.panel-row {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
padding: 0.4rem 0; border-bottom: 1px solid rgba(56,68,100,0.15);
|
|
font-size: 0.8rem;
|
|
}
|
|
.panel-row:last-child { border-bottom: none; }
|
|
.panel-row-label { color: var(--text-dim); }
|
|
.panel-row-val { font-weight: 600; font-family: var(--mono); color: var(--text); }
|
|
|
|
.forecast-bar {
|
|
display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.4rem;
|
|
}
|
|
.forecast-bar .yr { width: 40px; color: var(--text-dim); font-family: var(--mono); font-size: 0.75rem; }
|
|
.forecast-bar .track { flex: 1; height: 6px; background: var(--surface3); border-radius: 3px; overflow: hidden; }
|
|
.forecast-bar .fill { height: 100%; border-radius: 3px; transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1); }
|
|
.forecast-bar .pct { width: 44px; text-align: right; font-family: var(--mono); font-size: 0.75rem; color: var(--text-dim); }
|
|
|
|
/* === BLOG CARDS === */
|
|
.gen-card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px; padding: 1.25rem; cursor: pointer;
|
|
transition: all 0.3s; position: relative; overflow: hidden;
|
|
}
|
|
.gen-card::before {
|
|
content: ''; position: absolute; inset: 0;
|
|
background: var(--grad-accent); opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
.gen-card:hover { border-color: var(--accent); transform: translateY(-3px); box-shadow: 0 8px 30px rgba(79,143,247,0.1); }
|
|
.gen-card:hover::before { opacity: 0.03; }
|
|
.gen-card-title { font-weight: 700; font-size: 0.9rem; color: var(--text-bright); position: relative; }
|
|
.gen-card-sub { font-size: 0.75rem; color: var(--text-dim); margin-top: 0.3rem; position: relative; }
|
|
|
|
/* === COLLECTION ITEM === */
|
|
.col-item {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
padding: 0.5rem 0; border-bottom: 1px solid rgba(56,68,100,0.15);
|
|
}
|
|
.col-item:last-child { border-bottom: none; }
|
|
.col-name { font-family: var(--mono); font-size: 0.75rem; color: var(--text); }
|
|
|
|
/* === ENDPOINT LIST === */
|
|
.endpoint-grid {
|
|
display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem;
|
|
font-family: var(--mono); font-size: 0.72rem; color: var(--text-dim); line-height: 1.8;
|
|
}
|
|
.endpoint-item {
|
|
padding: 0.2rem 0.5rem; border-radius: 4px;
|
|
transition: all 0.15s;
|
|
}
|
|
.endpoint-item:hover { background: rgba(79,143,247,0.06); color: var(--accent); }
|
|
|
|
/* === UTILITIES === */
|
|
.hidden { display: none !important; }
|
|
.loading { text-align: center; padding: 2.5rem; color: var(--text-dim); font-size: 0.85rem; }
|
|
.mono { font-family: var(--mono); }
|
|
.dim { color: var(--text-dim); }
|
|
.mt { margin-top: 1rem; }
|
|
.mb { margin-bottom: 1rem; }
|
|
|
|
/* === TOAST === */
|
|
.toast {
|
|
position: fixed; top: 1.25rem; right: 1.25rem; z-index: 9999;
|
|
background: rgba(10,13,22,0.95);
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid var(--green);
|
|
border-radius: 12px; padding: 0.85rem 1.5rem; max-width: 400px;
|
|
box-shadow: 0 8px 40px rgba(0,0,0,0.5), 0 0 20px var(--green-glow);
|
|
transform: translateX(120%); transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
|
}
|
|
.toast.show { transform: translateX(0); }
|
|
.toast.error { border-color: var(--red); box-shadow: 0 8px 40px rgba(0,0,0,0.5), 0 0 20px var(--red-glow); }
|
|
.toast-title { font-weight: 700; font-size: 0.8rem; color: var(--text-bright); }
|
|
.toast-body { font-size: 0.75rem; color: var(--text-dim); margin-top: 0.15rem; }
|
|
|
|
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
|
.pulse { animation: pulse 1.5s infinite; }
|
|
|
|
@keyframes fadeInUp {
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.fade-in { animation: fadeInUp 0.4s ease-out; }
|
|
|
|
@media (max-width: 1100px) { .g4 { grid-template-columns: repeat(2, 1fr); } }
|
|
@media (max-width: 900px) {
|
|
.g2, .g2-1, .g3, .g4 { grid-template-columns: 1fr; }
|
|
.panel { width: 100%; }
|
|
.header-stats { display: none; }
|
|
.main { padding: 1rem; }
|
|
.header { padding: 0 1rem; }
|
|
.tabs { padding: 0 1rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- AMBIENT BACKGROUND -->
|
|
<div class="bg-grid"></div>
|
|
<div class="bg-glow bg-glow-1"></div>
|
|
<div class="bg-glow bg-glow-2"></div>
|
|
<div class="bg-glow bg-glow-3"></div>
|
|
|
|
<div class="app">
|
|
|
|
<div id="toast" class="toast"><div class="toast-title"></div><div class="toast-body"></div></div>
|
|
|
|
<!-- HEADER -->
|
|
<div class="header">
|
|
<div class="header-left">
|
|
<div class="logo">
|
|
<div class="logo-mark">TIP</div>
|
|
<div class="logo-text">Transceiver Intelligence<span>Platform</span></div>
|
|
</div>
|
|
<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> articles</span>
|
|
</div>
|
|
</div>
|
|
<div class="status">
|
|
<div class="status-pill" id="api-pill"><span class="dot dot-ok" id="api-status"></span>API</div>
|
|
<div class="status-pill" id="db-pill"><span class="dot dot-ok" id="db-status"></span>DB</div>
|
|
<div class="status-pill" id="qdrant-pill"><span class="dot dot-ok" id="qdrant-status"></span>Qdrant</div>
|
|
<span class="version-tag" 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" class="fade-in">
|
|
<div class="grid g4 mb">
|
|
<div class="stat-card">
|
|
<div class="stat-icon blue">⚙</div>
|
|
<div class="stat-label">Transceivers</div>
|
|
<div class="stat-val" id="ov-transceivers">—</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon green">★</div>
|
|
<div class="stat-label">Vendors</div>
|
|
<div class="stat-val" id="ov-vendors">—</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon purple">⚗</div>
|
|
<div class="stat-label">Standards</div>
|
|
<div class="stat-val" id="ov-standards">—</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon cyan">✎</div>
|
|
<div class="stat-label">News Articles</div>
|
|
<div class="stat-val" id="ov-news">—</div>
|
|
</div>
|
|
</div>
|
|
<div class="grid g2 mb">
|
|
<div class="card">
|
|
<div class="card-label">Vector Collections</div>
|
|
<div id="collections-list" class="mt"></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-label">Recent Intelligence</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="endpoint-grid mt"></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-wrap">
|
|
<div class="hype-header">
|
|
<div>
|
|
<div class="hype-title">Optical Transceiver Hype Cycle <span class="mono dim" id="hype-year">2026</span></div>
|
|
<div class="hype-sub">Norton-Bass Multigenerational Diffusion Model — click any technology for details</div>
|
|
</div>
|
|
<div class="hype-legend">
|
|
<span><span class="hype-legend-dot" style="background:#4f8ff7"></span>Innovation</span>
|
|
<span><span class="hype-legend-dot" style="background:#f59e0b"></span>Peak</span>
|
|
<span><span class="hype-legend-dot" style="background:#ef4444"></span>Trough</span>
|
|
<span><span class="hype-legend-dot" style="background:#a78bfa"></span>Slope</span>
|
|
<span><span class="hype-legend-dot" style="background:#10b981"></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 technology adoption lifecycle.">Phase</th>
|
|
<th class="tip" data-tip="Position on the hype curve (0-100%).">Position</th>
|
|
<th class="tip" data-tip="Market adoption rate based on Norton-Bass diffusion model.">Adoption</th>
|
|
<th class="tip" data-tip="Estimated year of peak hype / maximum attention.">Peak</th>
|
|
<th class="tip" data-tip="Years until mainstream, stable 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="CFP">CFP</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>
|
|
|
|
</div><!-- .app -->
|
|
|
|
<!-- DETAIL PANEL -->
|
|
<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(); });
|
|
|
|
// Animated number counter
|
|
function animateValue(el, target, duration) {
|
|
if (!el || isNaN(target)) { if (el) el.textContent = target; return; }
|
|
var start = 0, startTime = null;
|
|
target = parseInt(target);
|
|
function step(ts) {
|
|
if (!startTime) startTime = ts;
|
|
var p = Math.min((ts - startTime) / duration, 1);
|
|
p = 1 - Math.pow(1 - p, 3); // ease-out cubic
|
|
el.textContent = Math.floor(p * target).toLocaleString();
|
|
if (p < 1) requestAnimationFrame(step);
|
|
}
|
|
requestAnimationFrame(step);
|
|
}
|
|
|
|
// 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'); });
|
|
var target = el('tab-' + tab.dataset.tab);
|
|
target.classList.remove('hidden');
|
|
target.classList.add('fade-in');
|
|
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');
|
|
animateValue(el('stat-transceivers'), h.database.stats.transceiver_count, 800);
|
|
animateValue(el('stat-vendors'), h.database.stats.vendor_count, 600);
|
|
animateValue(el('stat-standards'), h.database.stats.standard_count, 500);
|
|
animateValue(el('stat-news'), h.database.stats.news_count, 700);
|
|
animateValue(el('ov-transceivers'), h.database.stats.transceiver_count, 1000);
|
|
animateValue(el('ov-vendors'), h.database.stats.vendor_count, 800);
|
|
animateValue(el('ov-standards'), h.database.stats.standard_count, 700);
|
|
animateValue(el('ov-news'), h.database.stats.news_count, 900);
|
|
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');
|
|
if (!h.success) el('api-pill').classList.add('err');
|
|
if (!h.database.connected) el('db-pill').classList.add('err');
|
|
} catch(e) {
|
|
el('api-status').className = 'dot dot-err';
|
|
el('api-pill').classList.add('err');
|
|
}
|
|
|
|
try {
|
|
var stats = await api('/api/search/stats');
|
|
buildDOM(el('collections-list'), stats.collections.map(function(c) {
|
|
return '<div class="col-item">'
|
|
+ '<span class="col-name">' + esc(c.collection) + '</span>'
|
|
+ '<span class="b ' + (c.pointsCount > 0 ? 'b-green' : 'b-yellow') + '">' + esc(c.pointsCount) + ' vectors</span>'
|
|
+ '</div>';
|
|
}).join(''));
|
|
el('qdrant-status').className = 'dot dot-ok';
|
|
} catch(e) {
|
|
el('qdrant-status').className = 'dot dot-err';
|
|
el('qdrant-pill').classList.add('err');
|
|
}
|
|
|
|
try {
|
|
var root = await api('/');
|
|
buildDOM(el('endpoints-list'), (root.endpoints || []).map(function(e) {
|
|
return '<div class="endpoint-item">' + 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 yet</div>');
|
|
} catch(e) {}
|
|
}
|
|
|
|
// SEARCH
|
|
function doSearch() {
|
|
var q = el('search-input').value;
|
|
var col = el('search-collection').value;
|
|
if (!q) return;
|
|
el('search-results').innerHTML = '<div class="loading pulse">Searching...</div>';
|
|
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) : '');
|
|
var score = (r.score * 100).toFixed(1);
|
|
var scoreColor = score > 70 ? 'var(--green)' : score > 40 ? 'var(--yellow)' : 'var(--text-dim)';
|
|
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" style="font-size:0.72rem;color:' + scoreColor + ';font-weight:600">' + score + '%</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 found</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': '#f59e0b',
|
|
'Trough of Disillusionment': '#ef4444',
|
|
'Slope of Enlightenment': '#a78bfa',
|
|
'Plateau of Productivity': '#10b981'
|
|
};
|
|
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.',
|
|
'Peak of Inflated Expectations': 'Maximum hype and media attention. Vendors announce products, but real-world deployments are rare.',
|
|
'Trough of Disillusionment': 'Reality check. Early deployments reveal limitations. Only committed adopters remain.',
|
|
'Slope of Enlightenment': 'Practical benefits become clear. Multi-vendor support grows. Best practices emerge.',
|
|
'Plateau of Productivity': 'Mainstream adoption. Stable pricing, broad vendor support, proven reliability.'
|
|
};
|
|
|
|
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 = 320, P = 40;
|
|
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 + 45) + '" viewBox="0 0 ' + W + ' ' + (H + 45) + '" xmlns="http://www.w3.org/2000/svg">';
|
|
|
|
// Defs for glow effects
|
|
svg += '<defs>';
|
|
svg += '<filter id="glow"><feGaussianBlur stdDeviation="3" result="blur"/><feComposite in="SourceGraphic" in2="blur" operator="over"/></filter>';
|
|
svg += '<linearGradient id="curveGrad" x1="0%" y1="0%" x2="100%" y2="0%">';
|
|
svg += '<stop offset="0%" stop-color="#4f8ff7" stop-opacity="0.6"/>';
|
|
svg += '<stop offset="25%" stop-color="#f59e0b" stop-opacity="0.6"/>';
|
|
svg += '<stop offset="50%" stop-color="#ef4444" stop-opacity="0.6"/>';
|
|
svg += '<stop offset="75%" stop-color="#a78bfa" stop-opacity="0.6"/>';
|
|
svg += '<stop offset="100%" stop-color="#10b981" stop-opacity="0.6"/>';
|
|
svg += '</linearGradient>';
|
|
svg += '<linearGradient id="fillGrad" x1="0%" y1="0%" x2="0%" y2="100%">';
|
|
svg += '<stop offset="0%" stop-color="url(#curveGrad)" stop-opacity="0.08"/>';
|
|
svg += '<stop offset="100%" stop-color="transparent" stop-opacity="0"/>';
|
|
svg += '</linearGradient>';
|
|
svg += '</defs>';
|
|
|
|
// Phase region fills
|
|
var rb = [0, 0.15, 0.28, 0.50, 0.78, 1.0];
|
|
var rc = ['#4f8ff7', '#f59e0b', '#ef4444', '#a78bfa', '#10b981'];
|
|
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.025" rx="2" />';
|
|
}
|
|
|
|
// Area fill under curve
|
|
var areaPoints = pts.join(' ') + ' ' + (cw + P) + ',' + (ch + P) + ' ' + P + ',' + (ch + P);
|
|
svg += '<polygon points="' + areaPoints + '" fill="url(#fillGrad)" />';
|
|
|
|
// Phase labels
|
|
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+4) + '" width="' + phaseW + '" height="32" fill="transparent" />';
|
|
for (var li = 0; li < ll.length; li++) {
|
|
svg += '<text x="' + px + '" y="' + (H + 16 + li * 13) + '" class="hype-phase-label">' + esc(ll[li]) + '</text>';
|
|
}
|
|
svg += '</g>';
|
|
}
|
|
|
|
// Curve line with gradient
|
|
svg += '<polyline points="' + pts.join(' ') + '" fill="none" stroke="url(#curveGrad)" stroke-width="2.5" stroke-linecap="round" />';
|
|
|
|
// Technology dots
|
|
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 - 14, lx = tx + 9, anc = 'start';
|
|
for (var pi = 0; pi < placed.length; pi++) {
|
|
if (Math.abs(placed[pi].x - tx) < 50 && Math.abs(placed[pi].y - ly) < 15) ly = placed[pi].y + 15;
|
|
}
|
|
if (tx > W - 160) { lx = tx - 9; anc = 'end'; }
|
|
placed.push({x:tx, y:ly});
|
|
|
|
// Glow ring
|
|
svg += '<circle cx="' + tx + '" cy="' + ty + '" r="12" fill="' + color + '" opacity="0.12" />';
|
|
svg += '<circle cx="' + tx + '" cy="' + ty + '" r="7" fill="' + color + '" class="hype-dot" data-tech="' + esc(t.technology) + '" filter="url(#glow)" />';
|
|
svg += '<circle cx="' + tx + '" cy="' + ty + '" r="3" fill="#fff" opacity="0.6" pointer-events="none" />';
|
|
svg += '<text x="' + lx + '" y="' + (ly + 4) + '" 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 ' + esc(name) + '...</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 + '18;color:' + c + ';border:1px solid ' + c + '33">' + 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 + ';box-shadow:0 0 8px ' + fc + '44"></div></div><span class="pct">' + (f.adoptionPct||0) + '%</span></div>';
|
|
}
|
|
}
|
|
|
|
// Regional adoption
|
|
if (d.regionalAdoption && d.regionalAdoption.length) {
|
|
h += '<div class="panel-section">Regional Adoption</div>';
|
|
for (var ri = 0; ri < d.regionalAdoption.length; ri++) {
|
|
var ra = d.regionalAdoption[ri];
|
|
h += '<div class="panel-row"><span class="panel-row-label">' + esc(ra.region) + '</span><span class="panel-row-val"><span class="b b-cyan" style="margin-right:4px">' + esc(ra.adoptionPhase) + '</span>' + esc(ra.estimatedPeakYear) + '</span></div>';
|
|
}
|
|
}
|
|
|
|
// Revenue lifecycle
|
|
if (d.revenueLifecycle) {
|
|
var rl = d.revenueLifecycle;
|
|
h += '<div class="panel-section">Revenue Lifecycle</div>';
|
|
var phaseColors = { growing: 'var(--green)', peaking: 'var(--yellow)', declining: 'var(--orange)', legacy: 'var(--text-dim)' };
|
|
h += '<div class="panel-grid">';
|
|
h += '<div class="panel-stat"><div class="panel-stat-label">Revenue Phase</div><div class="panel-stat-val" style="color:' + (phaseColors[rl.currentPhase]||'var(--text)') + ';font-size:1rem;text-transform:capitalize">' + esc(rl.currentPhase) + '</div></div>';
|
|
h += '<div class="panel-stat"><div class="panel-stat-label">Revenue Index</div><div class="panel-stat-val">' + esc(rl.revenueIndex) + '<small>/100</small></div></div>';
|
|
h += '</div>';
|
|
h += '<div class="panel-row"><span class="panel-row-label">Peak Revenue Year</span><span class="panel-row-val">' + esc(rl.estimatedPeakRevenueYear) + '</span></div>';
|
|
h += '<div class="panel-row"><span class="panel-row-label">Decline Start</span><span class="panel-row-val">' + esc(rl.estimatedDeclineStartYear) + '</span></div>';
|
|
h += '<div class="panel-row"><span class="panel-row-label">Half-Life</span><span class="panel-row-val">' + esc(rl.revenueHalfLifeYears) + ' years</span></div>';
|
|
}
|
|
|
|
h += '<div class="panel-section">Composite Score</div>';
|
|
h += '<div style="padding:0.75rem;background:var(--surface2);border-radius:10px;border:1px solid var(--border)">';
|
|
h += '<span class="mono" style="font-size:1.75rem;font-weight:800;background:' + c + ';-webkit-background-clip:text;-webkit-text-fill-color:transparent">' + (d.compositeScore||0) + '</span>';
|
|
h += '<span style="font-size:0.8rem;color:var(--text-dim)"> / 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:600;color:var(--text-bright)">' + esc(t.technology) + '</td>'
|
|
+ '<td><span class="b tip" data-tip="' + esc(PHASE_DESC[t.phase] || '') + '" style="background:' + color + '18;color:' + color + ';border:1px solid ' + color + '33">' + esc(t.phase) + '</span></td>'
|
|
+ '<td><div class="hype-bar"><div class="hype-fill" style="width:' + t.positionPct + '%;background:' + color + ';box-shadow:0 0 6px ' + color + '44"></div></div></td>'
|
|
+ '<td class="mono tip" data-tip="' + esc(t.technology) + ': ' + (t.adoptionPct * 100).toFixed(0) + '% market adoption (Norton-Bass model).">' + (t.adoptionPct * 100).toFixed(0) + '%</td>'
|
|
+ '<td class="mono">' + esc(t.peakYear || '—') + '</td>'
|
|
+ '<td class="mono">' + (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
|
|
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');
|
|
|
|
api('/api/transceivers?' + params.join('&')).then(function(data) {
|
|
buildDOM(el('tx-table'), (data.data || data.transceivers || []).map(function(t) {
|
|
return '<tr class="clickable" data-txid="' + esc(t.id) + '">'
|
|
+ '<td style="font-weight:600;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.data || 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.72rem;font-weight:500">Read →</a>' : '')
|
|
+ '</div></div>';
|
|
}).join('') || '<div class="loading">No news yet</div>');
|
|
}
|
|
|
|
// BLOG
|
|
function generateBlog(topic, speed) {
|
|
el('blog-list').innerHTML = '<div class="loading pulse">Generating article...</div>';
|
|
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 — click a card above to generate</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.7;max-height:60vh;overflow-y:auto;background:var(--surface2);padding:1rem;border-radius:10px;border:1px solid var(--border)">' + esc(d.draft_content) + '</div>';
|
|
h += '<div class="panel-section">SEO Keywords</div>';
|
|
h += '<div style="display:flex;gap:0.3rem;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>
|