feat: hype cycle hover tooltips + phase legend, fix switch-docs missing column
Dashboard: Added hover tooltips on hype cycle dots showing phase, adoption %, peak year, score. Added color-coded phase legend with technology counts. MCP: Fixed docs_portal_url column reference in switch-docs tool.
This commit is contained in:
parent
5a0cbed5a2
commit
9d9d9ed8ae
@ -328,6 +328,29 @@
|
||||
@keyframes hype-pulse-anim { 0%,100% { opacity: 0.25; r: 16; } 50% { opacity: 0.08; r: 24; } }
|
||||
.hype-connector { stroke: rgba(255,255,255,0.12); stroke-width: 1; stroke-dasharray: 2,3; }
|
||||
|
||||
/* Hype Cycle Hover Tooltip */
|
||||
.hype-tooltip {
|
||||
position: fixed; z-index: 1000; pointer-events: none;
|
||||
background: rgba(10,11,16,0.95); border: 1px solid rgba(255,129,0,0.3);
|
||||
border-radius: 8px; padding: 10px 14px; min-width: 180px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.5); backdrop-filter: blur(8px);
|
||||
font-size: 12px; color: var(--text); opacity: 0; transition: opacity 0.15s;
|
||||
}
|
||||
.hype-tooltip.visible { opacity: 1; }
|
||||
.hype-tooltip .tt-tech { font-weight: 700; font-size: 13px; margin-bottom: 4px; }
|
||||
.hype-tooltip .tt-phase { font-size: 11px; opacity: 0.7; margin-bottom: 6px; }
|
||||
.hype-tooltip .tt-row { display: flex; justify-content: space-between; gap: 12px; margin: 2px 0; }
|
||||
.hype-tooltip .tt-label { color: var(--text-dim); }
|
||||
.hype-tooltip .tt-val { font-family: 'JetBrains Mono', monospace; font-weight: 600; }
|
||||
|
||||
/* Phase Legend */
|
||||
.hype-legend {
|
||||
display: flex; flex-wrap: wrap; gap: 12px; justify-content: center;
|
||||
margin-top: 12px; padding: 8px 0;
|
||||
}
|
||||
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-dim); }
|
||||
.legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
|
||||
.hype-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 1.5rem; position: relative; z-index: 1;
|
||||
@ -1422,13 +1445,31 @@ function renderHypeSvg(techs) {
|
||||
// Outer ring
|
||||
svg += '<circle cx="' + dotX + '" cy="' + dotY + '" r="10" fill="none" stroke="' + color + '" stroke-width="0.8" opacity="0.25" />';
|
||||
|
||||
// Main dot
|
||||
svg += '<circle cx="' + dotX + '" cy="' + dotY + '" r="5.5" fill="' + color + '" class="hype-dot" data-tech="' + esc(t.technology) + '" filter="url(#glow)" stroke="rgba(255,255,255,0.25)" stroke-width="0.8" />';
|
||||
// Main dot (with hover data attributes)
|
||||
var adoptPct = t.adoptionPct != null ? Math.round(t.adoptionPct) : 0;
|
||||
var peakYr = t.peakYear || '—';
|
||||
svg += '<circle cx="' + dotX + '" cy="' + dotY + '" r="5.5" fill="' + color + '" class="hype-dot" data-tech="' + esc(t.technology) + '" data-phase="' + esc(t.phase) + '" data-adoption="' + adoptPct + '" data-peak="' + peakYr + '" data-score="' + (t.compositeScore||0) + '" filter="url(#glow)" stroke="rgba(255,255,255,0.25)" stroke-width="0.8" style="cursor:pointer" />';
|
||||
|
||||
// Inner highlight
|
||||
svg += '<circle cx="' + (dotX-1.2) + '" cy="' + (dotY-1.2) + '" r="1.5" fill="#fff" opacity="0.5" pointer-events="none" />';
|
||||
}
|
||||
svg += '</svg>';
|
||||
|
||||
// Phase legend
|
||||
svg += '<div class="hype-legend">';
|
||||
var phases = [
|
||||
{k:'Innovation Trigger',c:PC['Innovation Trigger']||'#4deaff'},
|
||||
{k:'Peak of Inflated Expectations',c:PC['Peak of Inflated Expectations']||'#fbbf24'},
|
||||
{k:'Trough of Disillusionment',c:PC['Trough of Disillusionment']||'#f87171'},
|
||||
{k:'Slope of Enlightenment',c:PC['Slope of Enlightenment']||'#a78bfa'},
|
||||
{k:'Plateau of Productivity',c:PC['Plateau of Productivity']||'#34d399'},
|
||||
{k:'Legacy / Decline',c:PC['Legacy / Decline']||'#8888a4'}
|
||||
];
|
||||
for (var pi = 0; pi < phases.length; pi++) {
|
||||
var cnt = techs.filter(function(t){return t.phase===phases[pi].k}).length;
|
||||
if (cnt > 0) svg += '<span class="legend-item"><span class="legend-dot" style="background:'+phases[pi].c+'"></span>'+phases[pi].k+' ('+cnt+')</span>';
|
||||
}
|
||||
svg += '</div>';
|
||||
return svg;
|
||||
}
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ export async function registerSwitchDocTools(server: McpServer): Promise<void> {
|
||||
const switchResult = await pool.query(
|
||||
`SELECT sw.id, sw.model, sw.series, sw.image_url, sw.datasheet_url,
|
||||
sw.product_page_url, sw.manual_urls,
|
||||
v.name as vendor_name, v.docs_portal_url, v.support_portal_url
|
||||
v.name as vendor_name, v.website as vendor_website
|
||||
FROM switches sw
|
||||
LEFT JOIN vendors v ON sw.vendor_id = v.id
|
||||
WHERE sw.model ILIKE $1
|
||||
@ -60,11 +60,8 @@ export async function registerSwitchDocTools(server: McpServer): Promise<void> {
|
||||
if (sw.datasheet_url) {
|
||||
text += `**Datasheet:** ${sw.datasheet_url}\n`;
|
||||
}
|
||||
if (sw.docs_portal_url) {
|
||||
text += `**Vendor Docs Portal:** ${sw.docs_portal_url}\n`;
|
||||
}
|
||||
if (sw.support_portal_url) {
|
||||
text += `**Support Portal:** ${sw.support_portal_url}\n`;
|
||||
if (sw.vendor_website) {
|
||||
text += `**Vendor Website:** ${sw.vendor_website}\n`;
|
||||
}
|
||||
|
||||
if (docsResult.rows.length > 0) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user