Architecture designer, author cluster names, FP filtering, new pages

- Add /architecture page: system-of-systems view with 8 layers, component
  cards, gap markers, source coverage chart, and clickable detail sidebar
- Give author clusters meaningful names from orgs + draft topic keywords
- Filter false positives (73 drafts, 54 ideas) from idea clusters,
  architecture, ideas listing, and search results
- Add NIST source fetcher with curated catalog of 11 AI publications
- New pages: trends, complexity, sources, false positives, idea analysis
- Clickable gap cards with full details (evidence, priority, nearby work)
- Component detail panel with linked drafts and top ideas

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 19:58:40 +01:00
parent a46a01bd8c
commit 8515e46d5d
19 changed files with 5672 additions and 202 deletions

View File

@@ -55,6 +55,14 @@ from webui.data import (
get_ask_synthesize,
get_category_summary,
global_search,
get_architecture,
get_source_comparison,
get_false_positive_profile,
get_citation_influence,
get_bcp_analysis,
get_trends_data,
get_complexity_data,
get_idea_analysis,
)
app = Flask(
@@ -306,6 +314,17 @@ def idea_clusters():
return render_template("idea_clusters.html", clusters=data)
@app.route("/architecture")
def architecture():
data = get_architecture(db())
return render_template("architecture.html", arch=data)
@app.route("/api/architecture")
def api_architecture():
return jsonify(get_architecture(db()))
@app.route("/similarity")
def similarity():
network = get_similarity_graph(db())
@@ -331,7 +350,9 @@ def authors():
@app.route("/citations")
def citations():
graph = get_citation_graph(db())
return render_template("citations.html", graph=graph)
influence = get_citation_influence(db())
bcp = get_bcp_analysis(db())
return render_template("citations.html", graph=graph, influence=influence, bcp=bcp)
@app.route("/monitor")
@@ -674,6 +695,88 @@ def create_app(dev: bool = False) -> Flask:
return app
# ── Sources & False Positives ────────────────────────────────────────────
@app.route("/sources")
def sources_page():
data = get_source_comparison(db())
return render_template("sources.html", data=data)
@app.route("/false-positives")
def false_positives_page():
data = get_false_positive_profile(db())
return render_template("false_positives.html", data=data)
@app.route("/api/sources")
def api_sources():
data = get_source_comparison(db())
return jsonify(data)
@app.route("/api/false-positives")
def api_false_positives():
data = get_false_positive_profile(db())
return jsonify(data)
# ── Citation Influence & BCP ─────────────────────────────────────────────
@app.route("/api/citations/influence")
def api_citation_influence():
return jsonify(get_citation_influence(db()))
@app.route("/api/citations/bcp")
def api_bcp_analysis():
return jsonify(get_bcp_analysis(db()))
# ── Trends & Complexity ──────────────────────────────────────────────────
@app.route("/trends")
def trends():
data = get_trends_data(db())
return render_template("trends_analysis.html", data=data)
@app.route("/complexity")
def complexity():
data = get_complexity_data(db())
return render_template("complexity.html", data=data)
@app.route("/api/trends")
def api_trends():
data = get_trends_data(db())
return jsonify(data)
@app.route("/api/complexity")
def api_complexity():
data = get_complexity_data(db())
return jsonify(data)
# ── Idea Analysis ────────────────────────────────────────────────────────
@app.route("/idea-analysis")
def idea_analysis():
data = get_idea_analysis(db())
return render_template("idea_analysis.html", data=data)
@app.route("/api/idea-analysis")
def api_idea_analysis():
data = get_idea_analysis(db())
return jsonify(data)
if __name__ == "__main__":
import argparse

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,465 @@
{% extends "base.html" %}
{% set active_page = "architecture" %}
{% block title %}Architecture — IETF Draft Analyzer{% endblock %}
{% block extra_head %}
<style>
.arch-layer {
border-left: 3px solid;
transition: all 0.2s;
}
.arch-layer:hover { background: rgba(255,255,255,0.02); }
.layer-transport { border-color: #6366f1; }
.layer-identity { border-color: #f59e0b; }
.layer-discovery { border-color: #10b981; }
.layer-communication { border-color: #3b82f6; }
.layer-coordination { border-color: #8b5cf6; }
.layer-intelligence { border-color: #ec4899; }
.layer-safety { border-color: #ef4444; }
.layer-application { border-color: #06b6d4; }
.comp-node {
background: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(10px);
cursor: pointer;
transition: all 0.2s;
}
.comp-node:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.comp-node.selected {
box-shadow: 0 0 0 2px #3b82f6;
}
.gap-marker.selected {
box-shadow: 0 0 0 2px #ef4444;
}
.gap-marker {
background: rgba(239, 68, 68, 0.1);
border: 1px dashed rgba(239, 68, 68, 0.4);
animation: pulse-border 2s ease-in-out infinite;
}
@keyframes pulse-border {
0%, 100% { border-color: rgba(239, 68, 68, 0.4); }
50% { border-color: rgba(239, 68, 68, 0.8); }
}
.severity-critical { color: #ef4444; }
.severity-high { color: #f59e0b; }
.severity-medium { color: #3b82f6; }
.severity-low { color: #6b7280; }
.source-dot {
width: 8px; height: 8px; border-radius: 50%;
display: inline-block;
}
.source-ietf { background: #3b82f6; }
.source-w3c { background: #f59e0b; }
.source-etsi { background: #f97316; }
.source-itu { background: #ec4899; }
.source-iso { background: #a855f7; }
.source-nist { background: #06b6d4; }
.detail-panel {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.95), rgba(15, 23, 42, 0.95));
backdrop-filter: blur(20px);
}
.maturity-bar {
height: 4px;
border-radius: 2px;
background: rgba(51, 65, 85, 0.5);
overflow: hidden;
}
.maturity-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s;
}
</style>
{% endblock %}
{% block content %}
<!-- Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold text-white">System-of-Systems Architecture</h1>
<p class="text-slate-400 text-sm mt-1">
Holistic view of the AI agent standards landscape — {{ arch.stats.total_components }} components across
{{ arch.layers|length }} architectural layers, with {{ arch.stats.total_gaps }} identified gaps.
Built from {{ arch.stats.total_dependencies }} cross-component relationships.
</p>
</div>
<!-- Stats row -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
{% set severity_colors = {"critical": "red", "high": "amber", "medium": "blue", "low": "slate"} %}
<div class="bg-slate-800/50 rounded-lg border border-slate-700/50 p-4 text-center">
<div class="text-2xl font-bold text-white">{{ arch.stats.total_components }}</div>
<div class="text-xs text-slate-500">Components</div>
</div>
<div class="bg-slate-800/50 rounded-lg border border-slate-700/50 p-4 text-center">
<div class="text-2xl font-bold text-white">{{ arch.layers|length }}</div>
<div class="text-xs text-slate-500">Layers</div>
</div>
<div class="bg-slate-800/50 rounded-lg border border-slate-700/50 p-4 text-center">
<div class="text-2xl font-bold text-white">{{ arch.stats.total_dependencies }}</div>
<div class="text-xs text-slate-500">Dependencies</div>
</div>
<div class="bg-slate-800/50 rounded-lg border border-slate-700/50 p-4 text-center">
<div class="text-2xl font-bold text-red-400">{{ arch.stats.total_gaps }}</div>
<div class="text-xs text-slate-500">Gaps</div>
</div>
</div>
<!-- Layer Coverage Overview -->
<div class="bg-slate-800/30 rounded-xl border border-slate-700/50 p-4 mb-6">
<div class="flex items-center justify-between mb-3">
<h2 class="text-sm font-semibold text-slate-300">Coverage by Standards Body</h2>
<div class="flex items-center gap-3">
{% for src in ['ietf', 'w3c', 'etsi', 'itu', 'iso', 'nist'] %}
<div class="flex items-center gap-1">
<span class="source-dot source-{{ src }}"></span>
<span class="text-xs text-slate-500">{{ src|upper }}</span>
</div>
{% endfor %}
<div class="flex items-center gap-1 ml-2">
<svg class="w-3 h-3 text-red-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2L2 19h20L12 2z"/></svg>
<span class="text-xs text-slate-500">Gaps</span>
</div>
</div>
</div>
<div id="coverageChart"></div>
</div>
<!-- Layered Stack View + Detail Panel side-by-side -->
<div id="stackView" class="flex gap-4">
<div class="flex-1 min-w-0">
{% for layer in arch.layers | sort(attribute='order', reverse=True) %}
<div class="arch-layer layer-{{ layer.id }} rounded-r-lg mb-3 p-4 bg-slate-800/30">
<div class="flex items-center justify-between mb-3">
<div>
<h3 class="text-sm font-semibold text-white">{{ layer.label }}</h3>
<div class="flex items-center gap-3 mt-1">
<span class="text-xs text-slate-500">{{ layer.component_count }} components</span>
<span class="text-xs text-slate-500">{{ layer.idea_count }} ideas</span>
<span class="text-xs text-slate-500">{{ layer.total_drafts }} drafts</span>
{% if layer.gap_count > 0 %}
<span class="text-xs text-red-400">{{ layer.gap_count }} gap{{ 's' if layer.gap_count > 1 }}</span>
{% endif %}
</div>
</div>
<!-- Source coverage dots -->
<div class="flex items-center gap-1.5">
{% for src, count in layer.coverage.items() %}
<div class="flex items-center gap-1" title="{{ src|upper }}: {{ count }} drafts">
<span class="source-dot source-{{ src }}"></span>
<span class="text-xs text-slate-500">{{ count }}</span>
</div>
{% endfor %}
</div>
</div>
<!-- Components in this layer -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
{% for comp in arch.components if comp.layer == layer.id %}
<div class="comp-node rounded-lg border border-slate-700/50 p-3" data-comp-id="{{ comp.id }}" onclick="selectComponent({{ comp.id }})">
<div class="flex items-center justify-between mb-1.5">
<span class="text-xs font-semibold text-slate-200 truncate" title="{{ comp.name }}">{{ comp.name }}</span>
<span class="text-xs text-slate-500" title="{{ comp.size }} ideas, {{ comp.draft_count }} drafts">{{ comp.size }}i / {{ comp.draft_count }}d</span>
</div>
<!-- Maturity bar -->
<div class="maturity-bar mb-2">
<div class="maturity-fill {% if comp.maturity >= 4 %}bg-green-500{% elif comp.maturity >= 3 %}bg-yellow-500{% else %}bg-red-500{% endif %}"
style="width: {{ (comp.maturity / 5 * 100)|int }}%"></div>
</div>
<!-- Source dots -->
<div class="flex items-center gap-1">
{% for src, cnt in comp.sources.items() %}
<span class="source-dot source-{{ src }}" title="{{ src|upper }}: {{ cnt }}"></span>
{% endfor %}
{% if comp.type_breakdown %}
<span class="text-xs text-slate-600 ml-auto">
{{ comp.type_breakdown.keys() | list | first }}
</span>
{% endif %}
</div>
</div>
{% endfor %}
<!-- Gaps in this layer -->
{% for gap in arch.gaps if gap.layer == layer.id %}
<div class="gap-marker rounded-lg p-3 cursor-pointer" data-gap-id="{{ gap.id }}" onclick="selectGap({{ gap.id }})">
<div class="flex items-center gap-1.5 mb-1">
<svg class="w-3.5 h-3.5 severity-{{ gap.severity }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
<span class="text-xs font-semibold severity-{{ gap.severity }}">GAP</span>
<span class="text-xs text-slate-600">{{ gap.severity }}</span>
</div>
<div class="text-xs text-slate-300">{{ gap.topic }}</div>
<div class="text-xs text-slate-500 mt-1 line-clamp-2">{{ gap.description[:120] }}</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<!-- Detail Panel (sticky sidebar, shown on component click) -->
<div id="detailPanel" class="detail-panel rounded-xl border border-slate-700/50 p-5 hidden w-[420px] shrink-0 sticky top-4 self-start max-h-[calc(100vh-2rem)] overflow-y-auto">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-white" id="detailTitle"></h3>
<button onclick="closeDetail()" class="text-slate-500 hover:text-slate-300 text-xs">Close</button>
</div>
<div id="detailContent"></div>
</div>
</div><!-- end stackView flex -->
{% endblock %}
{% block extra_scripts %}
<script src="/static/js/plotly.min.js"></script>
<script>
const COMPONENTS = {{ arch.components | tojson }};
const DEPENDENCIES = {{ arch.dependencies | tojson }};
const LAYERS = {{ arch.layers | tojson }};
const GAPS = {{ arch.gaps | tojson }};
const LAYER_COLORS = {
transport: '#6366f1', identity: '#f59e0b', discovery: '#10b981',
communication: '#3b82f6', coordination: '#8b5cf6', intelligence: '#ec4899',
safety: '#ef4444', application: '#06b6d4'
};
const SOURCE_COLORS = {
ietf: '#3b82f6', w3c: '#f59e0b', etsi: '#f97316',
itu: '#ec4899', iso: '#a855f7', nist: '#06b6d4'
};
function renderCoverageChart() {
const sources = ['ietf', 'w3c', 'etsi', 'itu', 'iso', 'nist'];
const sourceLabels = { ietf: 'IETF', w3c: 'W3C', etsi: 'ETSI', itu: 'ITU-T', iso: 'ISO/IEC', nist: 'NIST' };
// Sort layers top-down (highest order first)
const sortedLayers = [...LAYERS].sort((a, b) => b.order - a.order);
const labels = sortedLayers.map(l => l.label);
const traces = sources.map(src => ({
y: labels,
x: sortedLayers.map(l => (l.coverage[src] || 0)),
name: sourceLabels[src],
type: 'bar',
orientation: 'h',
marker: { color: SOURCE_COLORS[src], opacity: 0.85 },
hovertemplate: `%{y}<br>${sourceLabels[src]}: %{x} drafts<extra></extra>`,
}));
// Gap markers as annotations
const gapAnnotations = [];
sortedLayers.forEach(l => {
const layerGaps = GAPS.filter(g => g.layer === l.id);
if (layerGaps.length > 0) {
const total = sources.reduce((s, src) => s + (l.coverage[src] || 0), 0);
gapAnnotations.push({
x: total + 8,
y: l.label,
text: `${layerGaps.length} gap${layerGaps.length > 1 ? 's' : ''}`,
showarrow: false,
font: { size: 10, color: '#f87171' },
xanchor: 'left',
});
}
});
Plotly.newPlot('coverageChart', traces, {
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
barmode: 'stack',
margin: { l: 180, r: 60, t: 10, b: 30 },
xaxis: {
title: { text: 'Documents', font: { size: 11, color: '#64748b' } },
gridcolor: 'rgba(51,65,85,0.3)',
tickfont: { size: 10, color: '#94a3b8' },
},
yaxis: {
tickfont: { size: 11, color: '#cbd5e1' },
automargin: true,
},
annotations: gapAnnotations,
height: 320,
showlegend: false,
}, { responsive: true, displayModeBar: false });
}
function selectComponent(compId) {
const comp = COMPONENTS[compId];
if (!comp) return;
document.getElementById('detailPanel').classList.remove('hidden');
document.getElementById('detailTitle').textContent = comp.name;
// Find connected components
const connected = DEPENDENCIES.filter(d => d.source === compId || d.target === compId)
.map(d => {
const otherId = d.source === compId ? d.target : d.source;
return { ...COMPONENTS[otherId], sim: d.similarity, idea_a: d.idea_a, idea_b: d.idea_b };
})
.sort((a, b) => b.sim - a.sim);
// Find gaps in same layer
const layerGaps = GAPS.filter(g => g.layer === comp.layer);
let html = `
<div>
<div class="text-xs text-slate-500 mb-2">Layer: <span class="text-slate-300">${comp.layer}</span> &middot; Ideas: <span class="text-slate-300">${comp.size}</span> &middot; Maturity: <span class="text-slate-300">${comp.maturity}/5</span></div>
<div class="text-xs font-semibold text-slate-400 mb-1.5 mt-3">Source Coverage</div>
<div class="flex flex-wrap gap-2 mb-3">
${Object.entries(comp.sources).map(([s, n]) =>
`<span class="text-xs px-2 py-0.5 rounded-full" style="background:${SOURCE_COLORS[s]}22; color:${SOURCE_COLORS[s]}">${s.toUpperCase()}: ${n}</span>`
).join('')}
</div>
<div class="text-xs font-semibold text-slate-400 mb-1.5">Idea Types</div>
<div class="flex flex-wrap gap-1.5 mb-3">
${Object.entries(comp.type_breakdown).map(([t, n]) =>
`<span class="text-xs px-2 py-0.5 rounded-full bg-slate-700 text-slate-300">${t} (${n})</span>`
).join('')}
</div>
<div class="text-xs font-semibold text-slate-400 mb-1.5">Top Ideas</div>
<div class="space-y-1 mb-3">
${comp.top_ideas.map(i =>
`<div class="text-xs">&bull; ${i.draft_name ? `<a href="/drafts/${i.draft_name}" class="text-blue-400 hover:text-blue-300">${i.title}</a>` : `<span class="text-slate-300">${i.title}</span>`} <span class="text-slate-600">(${i.type})</span></div>`
).join('')}
</div>
<div class="text-xs font-semibold text-slate-400 mb-1.5">Drafts (${comp.draft_count})</div>
<div class="space-y-1 mb-3 max-h-36 overflow-y-auto">
${(comp.drafts || []).map(d =>
`<div class="text-xs"><a href="/drafts/${d.name}" class="text-blue-400 hover:text-blue-300">${d.title}</a> <span class="source-dot source-${d.source} ml-1" title="${d.source.toUpperCase()}"></span></div>`
).join('')}
${comp.draft_count > 20 ? `<div class="text-xs text-slate-600 italic">+ ${comp.draft_count - 20} more</div>` : ''}
</div>
<div class="text-xs font-semibold text-slate-400 mb-1.5">Connected Components (${connected.length})</div>
<div class="space-y-1.5 max-h-48 overflow-y-auto mb-3">
${connected.length > 0 ? connected.map(c =>
`<div class="text-xs p-2 rounded bg-slate-800/60 border border-slate-700/30">
<span class="text-slate-200">${c.name}</span>
<span class="text-slate-500 ml-1">(${c.layer})</span>
<span class="text-blue-400 ml-1 font-mono">${c.sim.toFixed(3)}</span>
<div class="text-slate-600 mt-0.5">${c.idea_a}${c.idea_b}</div>
</div>`
).join('') : '<div class="text-xs text-slate-600 italic">No connections</div>'}
</div>
${layerGaps.length > 0 ? `
<div class="text-xs font-semibold text-red-400 mb-1.5">Gaps in Layer (${layerGaps.length})</div>
<div class="space-y-1.5">
${layerGaps.map(g =>
`<div class="text-xs p-2 rounded gap-marker">
<span class="severity-${g.severity} font-semibold">${g.severity.toUpperCase()}</span>
<span class="text-slate-300 ml-1">${g.topic}</span>
</div>`
).join('')}
</div>` : ''}
</div>
`;
document.getElementById('detailContent').innerHTML = html;
// Highlight in stack view
document.querySelectorAll('.comp-node').forEach(n => n.classList.remove('selected'));
const node = document.querySelector(`[data-comp-id="${compId}"]`);
if (node) { node.classList.add('selected'); node.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }
}
function selectGap(gapId) {
const gap = GAPS.find(g => g.id === gapId);
if (!gap) return;
document.getElementById('detailPanel').classList.remove('hidden');
document.getElementById('detailTitle').innerHTML =
`<span class="severity-${gap.severity}">GAP:</span> ${gap.topic}`;
// Components in same layer (what exists nearby)
const nearby = COMPONENTS.filter(c => c.layer === gap.layer);
// Components in adjacent layers
const layerOrder = {};
LAYERS.forEach(l => { layerOrder[l.id] = l.order; });
const gapOrder = layerOrder[gap.layer] || 0;
const adjacent = COMPONENTS.filter(c => {
const co = layerOrder[c.layer] || 0;
return Math.abs(co - gapOrder) === 1;
});
const sevLabel = { critical: 'Immediate standardization needed', high: 'Should be addressed soon',
medium: 'Worth exploring', low: 'Nice to have' };
let html = `
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs px-2 py-0.5 rounded-full severity-${gap.severity}"
style="background: ${gap.severity === 'critical' ? 'rgba(239,68,68,0.15)' : gap.severity === 'high' ? 'rgba(245,158,11,0.15)' : 'rgba(59,130,246,0.15)'}">
${gap.severity.toUpperCase()}
</span>
<span class="text-xs text-slate-500">${gap.category}</span>
<span class="text-xs text-slate-600">Layer: ${gap.layer}</span>
</div>
<div class="text-xs font-semibold text-slate-400 mb-1">What's Missing</div>
<div class="text-xs text-slate-300 leading-relaxed mb-3">${gap.description}</div>
<div class="text-xs font-semibold text-slate-400 mb-1">Evidence</div>
<div class="text-xs text-slate-400 leading-relaxed mb-3 p-2 rounded bg-slate-800/60 border border-slate-700/30">${gap.evidence || 'No evidence recorded'}</div>
<div class="text-xs font-semibold text-slate-400 mb-1">Priority</div>
<div class="text-xs text-slate-300 mb-3">${sevLabel[gap.severity] || gap.severity}</div>
${nearby.length > 0 ? `
<div class="text-xs font-semibold text-slate-400 mb-1">Existing Work in Same Layer (${nearby.length})</div>
<div class="space-y-1 mb-3 max-h-32 overflow-y-auto">
${nearby.map(c =>
`<div class="text-xs p-2 rounded bg-slate-800/60 border border-slate-700/30 cursor-pointer hover:border-slate-600" onclick="selectComponent(${c.id})">
<span class="text-slate-200">${c.name}</span>
<span class="text-slate-600 ml-1">${c.size} ideas</span>
</div>`
).join('')}
</div>` : `
<div class="text-xs font-semibold text-red-400 mb-1">No Existing Components in Layer</div>
<div class="text-xs text-slate-500 mb-3">This layer has no standardization components yet — a greenfield gap.</div>`}
${adjacent.length > 0 ? `
<div class="text-xs font-semibold text-slate-400 mb-1">Adjacent Layer Components (${adjacent.length})</div>
<div class="space-y-1 max-h-32 overflow-y-auto">
${adjacent.map(c =>
`<div class="text-xs p-2 rounded bg-slate-800/60 border border-slate-700/30 cursor-pointer hover:border-slate-600" onclick="selectComponent(${c.id})">
<span class="text-slate-200">${c.name}</span>
<span class="text-slate-500 ml-1">(${c.layer})</span>
<span class="text-slate-600 ml-1">${c.size} ideas</span>
</div>`
).join('')}
</div>` : ''}
</div>
`;
document.getElementById('detailContent').innerHTML = html;
// Highlight gap card
document.querySelectorAll('.comp-node').forEach(n => n.classList.remove('selected'));
document.querySelectorAll('.gap-marker').forEach(n => n.classList.remove('selected'));
const card = document.querySelector(`[data-gap-id="${gapId}"]`);
if (card) { card.classList.add('selected'); }
}
function closeDetail() {
document.getElementById('detailPanel').classList.add('hidden');
document.querySelectorAll('.comp-node').forEach(n => n.classList.remove('selected'));
document.querySelectorAll('.gap-marker').forEach(n => n.classList.remove('selected'));
}
document.addEventListener('DOMContentLoaded', () => {
renderCoverageChart();
});
</script>
{% endblock %}

View File

@@ -119,7 +119,7 @@
<div class="cluster-card bg-slate-800/50 rounded-lg border border-slate-700/50 p-4 cursor-pointer" data-cluster-id="{{ c.id }}">
<!-- Header — click to highlight in graph -->
<div class="flex items-center justify-between mb-2" onclick="highlightCluster({{ c.id }})">
<span class="text-sm font-semibold text-white">Cluster #{{ c.id + 1 }}</span>
<span class="text-sm font-semibold text-white">{{ c.name }}</span>
<div class="flex gap-1.5 items-center">
<span class="text-xs px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-400">{{ c.size }} authors</span>
<span class="text-xs px-2 py-0.5 rounded-full bg-emerald-500/20 text-emerald-400">{{ c.draft_count }} drafts</span>

View File

@@ -117,6 +117,14 @@
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2z"/><circle cx="19" cy="19" r="3" stroke="currentColor" stroke-width="2" fill="none"/></svg>
Idea Clusters
</a>
<a href="/idea-analysis" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'idea_analysis' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
Idea Analysis
</a>
<a href="/architecture" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'architecture' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
Architecture
</a>
{% if is_admin %}
<a href="/gaps" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'gaps' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
@@ -127,6 +135,14 @@
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Timeline
</a>
<a href="/trends" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'trends' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
Trends
</a>
<a href="/complexity" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'complexity' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/></svg>
Complexity
</a>
<a href="/landscape" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'landscape' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg>
Landscape
@@ -139,6 +155,14 @@
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
Citations
</a>
<a href="/sources" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'sources' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
Sources
</a>
<a href="/false-positives" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'false_positives' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/></svg>
False Positives
</a>
<a href="/authors" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'authors' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
Authors

View File

@@ -1,10 +1,11 @@
{% extends "base.html" %}
{% set active_page = "citations" %}
{% block title %}Citation Graph — IETF Draft Analyzer{% endblock %}
{% block title %}Citations & Influence — IETF Draft Analyzer{% endblock %}
{% block extra_head %}
<script src="/static/js/d3.v7.min.js"></script>
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
<style>
#citationSvg {
width: 100%;
@@ -26,95 +27,368 @@
max-width: 320px; opacity: 0; transition: opacity 0.15s;
}
.tooltip-card.visible { opacity: 1; }
.legend-swatch { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }
.filter-btn { transition: all 0.15s; }
.filter-btn:hover { background: rgba(59, 130, 246, 0.2); }
.filter-btn.active { background: rgba(59, 130, 246, 0.3); border-color: #3b82f6; color: #60a5fa; }
.tab-btn {
transition: all 0.15s;
border-bottom: 2px solid transparent;
}
.tab-btn:hover { color: #e2e8f0; }
.tab-btn.active {
color: #60a5fa;
border-bottom-color: #3b82f6;
}
.tab-panel { display: none; }
.tab-panel.active { display: block; }
</style>
{% endblock %}
{% block content %}
<div class="mb-6">
<h1 class="text-2xl font-bold text-white">Citation Graph</h1>
<p class="text-slate-400 text-sm mt-1">Cross-reference network: {{ graph.stats.draft_count }} drafts referencing {{ graph.stats.rfc_count }} RFCs. References are extracted from each draft's text (RFC mentions, draft citations, BCP references). Node size reflects influence — how many other documents cite it. Highly-cited RFCs represent foundational standards that AI/agent drafts build upon.</p>
<h1 class="text-2xl font-bold text-white">Citations & Influence</h1>
<p class="text-slate-400 text-sm mt-1">Cross-reference network, citation influence metrics, and BCP dependency analysis across {{ influence.stats.drafts_with_refs }} drafts and {{ influence.stats.total_citations }} total citations.</p>
</div>
<!-- Summary stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 to-blue-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Drafts</div>
<div class="text-2xl font-bold text-white mt-1">{{ graph.stats.draft_count }}</div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Total Citations</div>
<div class="text-2xl font-bold text-white mt-1">{{ influence.stats.total_citations }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-orange-500 to-orange-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Referenced RFCs</div>
<div class="text-2xl font-bold text-white mt-1">{{ graph.stats.rfc_count }}</div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Unique RFCs Cited</div>
<div class="text-2xl font-bold text-white mt-1">{{ influence.stats.unique_rfcs }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-emerald-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Total Nodes</div>
<div class="text-2xl font-bold text-white mt-1">{{ graph.stats.node_count }}</div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Drafts with Refs</div>
<div class="text-2xl font-bold text-white mt-1">{{ influence.stats.drafts_with_refs }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-purple-500 to-purple-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Citation Links</div>
<div class="text-2xl font-bold text-white mt-1">{{ graph.stats.edge_count }}</div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Avg Refs/Draft</div>
<div class="text-2xl font-bold text-white mt-1">{{ influence.stats.avg_refs_per_draft }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-amber-500 to-amber-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">BCP Coverage</div>
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.coverage_pct }}%</div>
</div>
</div>
<!-- D3 Force-directed Citation Graph -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6 relative">
<div class="flex flex-wrap items-center justify-between gap-3 mb-3">
<div>
<h2 class="text-sm font-semibold text-slate-300">Cross-Reference Network</h2>
<p class="text-xs text-slate-500 mt-0.5">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-blue-500 align-middle mr-1"></span>Drafts
<span class="inline-block w-2.5 h-2.5 rounded-full bg-orange-500 align-middle mr-1 ml-3"></span>RFCs
— Node size = influence (in-degree). Drag to rearrange. Scroll to zoom.
</p>
<!-- Tabs -->
<div class="border-b border-slate-800 mb-6">
<nav class="flex gap-6">
<button class="tab-btn active px-1 pb-3 text-sm font-medium text-slate-400" data-tab="graph">Citation Graph</button>
<button class="tab-btn px-1 pb-3 text-sm font-medium text-slate-400" data-tab="influence">Influence Analysis</button>
<button class="tab-btn px-1 pb-3 text-sm font-medium text-slate-400" data-tab="bcp">BCP Dependencies</button>
</nav>
</div>
<!-- ==================== TAB 1: Citation Graph ==================== -->
<div id="tab-graph" class="tab-panel active">
<!-- D3 Force-directed Citation Graph -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6 relative">
<div class="flex flex-wrap items-center justify-between gap-3 mb-3">
<div>
<h2 class="text-sm font-semibold text-slate-300">Cross-Reference Network</h2>
<p class="text-xs text-slate-500 mt-0.5">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-blue-500 align-middle mr-1"></span>Drafts
<span class="inline-block w-2.5 h-2.5 rounded-full bg-orange-500 align-middle mr-1 ml-3"></span>RFCs
— Node size = influence. Drag to rearrange. Scroll to zoom.
</p>
</div>
<div class="flex gap-2 items-center">
<button id="resetZoom" class="text-xs px-3 py-1.5 rounded-lg border border-slate-700 text-slate-400 hover:text-white hover:border-slate-500 transition">Reset View</button>
<select id="filterCategory" class="text-xs px-3 py-1.5 rounded-lg border border-slate-700 bg-slate-800 text-slate-300 focus:outline-none focus:border-blue-500">
<option value="">All Categories</option>
</select>
<label class="text-xs text-slate-500 ml-2">Min refs:</label>
<input type="range" id="minRefsSlider" min="1" max="15" value="2" class="w-20" style="accent-color: #3b82f6;">
<span id="minRefsVal" class="text-xs text-blue-400 font-mono w-4">2</span>
</div>
</div>
<div class="flex gap-2 items-center">
<button id="resetZoom" class="text-xs px-3 py-1.5 rounded-lg border border-slate-700 text-slate-400 hover:text-white hover:border-slate-500 transition">Reset View</button>
<select id="filterCategory" class="text-xs px-3 py-1.5 rounded-lg border border-slate-700 bg-slate-800 text-slate-300 focus:outline-none focus:border-blue-500">
<option value="">All Categories</option>
</select>
<label class="text-xs text-slate-500 ml-2">Min refs:</label>
<input type="range" id="minRefsSlider" min="1" max="15" value="2" class="w-20" style="accent-color: #3b82f6;">
<span id="minRefsVal" class="text-xs text-blue-400 font-mono w-4">2</span>
<div class="relative">
<svg id="citationSvg"></svg>
<div id="tooltip" class="tooltip-card"></div>
</div>
</div>
<div class="relative">
<svg id="citationSvg"></svg>
<div id="tooltip" class="tooltip-card"></div>
<!-- Top Referenced RFCs Table -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Most Referenced RFCs</h2>
<p class="text-xs text-slate-500 mt-0.5">RFCs cited by the most drafts in the corpus</p>
</div>
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
<table class="w-full text-sm" id="rfcTable">
<thead class="sticky top-0 z-10">
<tr class="border-b border-slate-800 bg-slate-900">
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">RFC</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Cited By</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50" id="rfcBody">
</tbody>
</table>
</div>
</div>
</div>
<!-- Top Referenced RFCs Table -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Most Referenced RFCs</h2>
<p class="text-xs text-slate-500 mt-0.5">RFCs cited by the most drafts in the corpus</p>
<!-- ==================== TAB 2: Influence Analysis ==================== -->
<div id="tab-influence" class="tab-panel">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Top Cited RFCs with details -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Top 20 Most-Cited RFCs</h2>
<p class="text-xs text-slate-500 mt-0.5">Foundational standards the AI/agent ecosystem builds upon</p>
</div>
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
<table class="w-full text-sm">
<thead class="sticky top-0 z-10">
<tr class="border-b border-slate-800 bg-slate-900">
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">RFC</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Name</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Cited By</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for rfc in influence.top_cited_rfcs %}
<tr class="hover:bg-slate-800/50 transition group">
<td class="px-4 py-2.5 text-slate-500 text-xs">{{ loop.index }}</td>
<td class="px-4 py-2.5">
<a href="https://www.rfc-editor.org/rfc/rfc{{ rfc.rfc_id }}" target="_blank" rel="noopener"
class="text-orange-400 hover:text-orange-300 transition font-medium text-sm">RFC {{ rfc.rfc_id }}</a>
</td>
<td class="px-4 py-2.5 text-slate-300 text-xs">{{ rfc.name }}</td>
<td class="px-4 py-2.5 text-right">
<span class="px-2 py-0.5 rounded-full text-xs font-medium
{% if rfc.count >= 20 %}bg-orange-500/20 text-orange-400
{% elif rfc.count >= 10 %}bg-blue-500/20 text-blue-400
{% else %}bg-slate-700/50 text-slate-400{% endif %}">
{{ rfc.count }} drafts
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Top Citing Drafts -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Top 20 Most-Citing Drafts</h2>
<p class="text-xs text-slate-500 mt-0.5">Drafts with the highest outgoing reference count</p>
</div>
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
<table class="w-full text-sm">
<thead class="sticky top-0 z-10">
<tr class="border-b border-slate-800 bg-slate-900">
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Draft</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Category</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Refs</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for d in influence.top_citing_drafts %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-2.5 text-slate-500 text-xs">{{ loop.index }}</td>
<td class="px-4 py-2.5">
<a href="/drafts/{{ d.name }}" class="text-blue-400 hover:text-blue-300 transition text-sm">
{{ d.title[:60] }}{% if d.title|length > 60 %}...{% endif %}
</a>
</td>
<td class="px-4 py-2.5">
<span class="px-2 py-0.5 rounded-full text-xs bg-slate-700/50 text-slate-300">{{ d.category }}</span>
</td>
<td class="px-4 py-2.5 text-right">
<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-blue-500/20 text-blue-400">{{ d.count }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
<table class="w-full text-sm" id="rfcTable">
<thead class="sticky top-0 z-10">
<tr class="border-b border-slate-800 bg-slate-900">
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">RFC</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Cited By</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50" id="rfcBody">
</tbody>
</table>
<!-- PageRank-style influence -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden mb-6">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Influence Score (PageRank-style)</h2>
<p class="text-xs text-slate-500 mt-0.5">Drafts ranked by weighted sum of how often their cited RFCs are themselves cited — higher score means citing more foundational standards</p>
</div>
<div class="overflow-x-auto max-h-[400px] overflow-y-auto">
<table class="w-full text-sm">
<thead class="sticky top-0 z-10">
<tr class="border-b border-slate-800 bg-slate-900">
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Draft</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Category</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Out-Degree</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Influence Score</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for d in influence.top_pagerank %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-2.5 text-slate-500 text-xs">{{ loop.index }}</td>
<td class="px-4 py-2.5">
<a href="/drafts/{{ d.name }}" class="text-blue-400 hover:text-blue-300 transition text-sm">
{{ d.title[:60] }}{% if d.title|length > 60 %}...{% endif %}
</a>
</td>
<td class="px-4 py-2.5">
<span class="px-2 py-0.5 rounded-full text-xs bg-slate-700/50 text-slate-300">{{ d.category }}</span>
</td>
<td class="px-4 py-2.5 text-right text-slate-400">{{ d.out_degree }}</td>
<td class="px-4 py-2.5 text-right">
<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-purple-500/20 text-purple-400">{{ d.score }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Citation density by category chart -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Average Citations per Category</h2>
<p class="text-xs text-slate-500 mb-3">Which categories reference the most external standards</p>
<div id="categoryChart" style="height: 400px;"></div>
</div>
<!-- Draft-to-Draft network -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Draft-to-Draft Citation Network</h2>
<p class="text-xs text-slate-500 mb-3">{{ influence.draft_network|length }} cross-citations between drafts in the corpus</p>
<div id="draftNetworkChart" style="height: 500px;"></div>
</div>
</div>
<!-- ==================== TAB 3: BCP Dependencies ==================== -->
<div id="tab-bcp" class="tab-panel">
<!-- BCP Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-amber-500 to-amber-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Unique BCPs</div>
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.unique_bcps }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-emerald-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Total BCP Refs</div>
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.total_bcp_refs }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 to-blue-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Drafts with BCPs</div>
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.drafts_with_bcp }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-purple-500 to-purple-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">BCP Coverage</div>
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.coverage_pct }}%</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- BCP Citation Table -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">All BCPs by Citation Count</h2>
<p class="text-xs text-slate-500 mt-0.5">{{ bcp.coverage.unique_bcps }} unique BCPs cited across the corpus</p>
</div>
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
<table class="w-full text-sm">
<thead class="sticky top-0 z-10">
<tr class="border-b border-slate-800 bg-slate-900">
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">BCP</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Cited By</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Example Drafts</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for b in bcp.bcps %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-2.5 text-slate-500 text-xs">{{ loop.index }}</td>
<td class="px-4 py-2.5 font-medium text-amber-400">BCP {{ b.bcp_id }}</td>
<td class="px-4 py-2.5 text-right">
<span class="px-2 py-0.5 rounded-full text-xs font-medium
{% if b.count >= 50 %}bg-amber-500/20 text-amber-400
{% elif b.count >= 10 %}bg-blue-500/20 text-blue-400
{% else %}bg-slate-700/50 text-slate-400{% endif %}">
{{ b.count }}
</span>
</td>
<td class="px-4 py-2.5 text-xs text-slate-500 max-w-[200px] truncate">
{{ b.drafts[:3]|join(', ') }}{% if b.total_drafts > 3 %} +{{ b.total_drafts - 3 }} more{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- BCP by Category -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">BCP Usage by Category</h2>
<p class="text-xs text-slate-500 mt-0.5">Which categories rely most heavily on BCPs</p>
</div>
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
<table class="w-full text-sm">
<thead class="sticky top-0 z-10">
<tr class="border-b border-slate-800 bg-slate-900">
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Category</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">BCP Refs</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Unique BCPs</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Top BCPs</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for cat in bcp.by_category %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-2.5 text-slate-300 text-sm font-medium">{{ cat.category }}</td>
<td class="px-4 py-2.5 text-right text-slate-300">{{ cat.total_bcp_refs }}</td>
<td class="px-4 py-2.5 text-right text-slate-400">{{ cat.unique_bcps }}</td>
<td class="px-4 py-2.5 text-xs text-slate-500">
{% for tb in cat.top_bcps[:3] %}
<span class="inline-block px-1.5 py-0.5 rounded bg-slate-700/50 text-amber-400 mr-1 mb-0.5">BCP{{ tb.bcp_id }}({{ tb.count }})</span>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- BCP Co-citation Heatmap -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">BCP Co-Citation Heatmap</h2>
<p class="text-xs text-slate-500 mb-3">How often pairs of BCPs are cited together in the same draft. Darker = more co-citations.</p>
<div id="bcpHeatmap" style="height: 500px;"></div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
const graph = {{ graph | tojson }};
const influence = {{ influence | tojson }};
const bcp = {{ bcp | tojson }};
const PALETTE = [
'#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b',
@@ -122,7 +396,22 @@ const PALETTE = [
];
// ===========================================================
// D3.js Force-Directed Citation Network
// Tab switching
// ===========================================================
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
this.classList.add('active');
document.getElementById('tab-' + this.dataset.tab).classList.add('active');
// Trigger Plotly resize for charts that might not have rendered
window.dispatchEvent(new Event('resize'));
});
});
// ===========================================================
// D3.js Force-Directed Citation Network (Tab 1)
// ===========================================================
(function() {
if (graph.nodes.length === 0) {
@@ -184,27 +473,20 @@ const PALETTE = [
const maxInfluence = d3.max(nodes, n => n.influence) || 1;
const rScale = d3.scaleSqrt().domain([0, maxInfluence]).range([3, 24]);
// Color: drafts = blue, rfcs = orange, others = amber
function nodeColor(n) {
if (n.type === 'rfc') return '#f59e0b';
if (n.type === 'bcp') return '#eab308';
return '#3b82f6';
}
// Force simulation
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links)
.id(d => d.id)
.distance(60)
.strength(0.15)
)
.force('link', d3.forceLink(links).id(d => d.id).distance(60).strength(0.15))
.force('charge', d3.forceManyBody().strength(-80).distanceMax(350))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(d => rScale(d.influence) + 2))
.force('x', d3.forceX(width / 2).strength(0.04))
.force('y', d3.forceY(height / 2).strength(0.04));
// Zoom behavior
const g = svg.append('g');
const zoom = d3.zoom()
.scaleExtent([0.15, 5])
@@ -215,33 +497,22 @@ const PALETTE = [
svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity);
});
// Draw edges
const linkGroup = g.append('g').attr('class', 'links');
const link = linkGroup.selectAll('line')
.data(links)
.join('line')
.attr('class', 'link')
.attr('stroke', '#475569')
.attr('stroke-width', 0.8);
.data(links).join('line')
.attr('class', 'link').attr('stroke', '#475569').attr('stroke-width', 0.8);
// Draw nodes
const nodeGroup = g.append('g').attr('class', 'nodes');
const node = nodeGroup.selectAll('g')
.data(nodes)
.join('g')
.attr('class', 'node')
.data(nodes).join('g').attr('class', 'node')
.call(d3.drag()
.on('start', dragStarted)
.on('drag', dragged)
.on('end', dragEnded)
);
.on('start', dragStarted).on('drag', dragged).on('end', dragEnded));
node.append('circle')
.attr('r', d => rScale(d.influence))
.attr('fill', d => nodeColor(d))
.attr('opacity', 0.85);
// Labels for high-influence nodes
node.filter(d => d.influence >= 5)
.append('text')
.text(d => {
@@ -251,11 +522,9 @@ const PALETTE = [
})
.attr('dy', d => -(rScale(d.influence) + 4))
.attr('text-anchor', 'middle')
.attr('fill', '#94a3b8')
.attr('font-size', '8px')
.attr('fill', '#94a3b8').attr('font-size', '8px')
.attr('font-family', 'Inter, system-ui, sans-serif');
// Tooltip
const tooltip = document.getElementById('tooltip');
node.on('mouseover', function(event, d) {
@@ -270,8 +539,6 @@ const PALETTE = [
</div>
`;
tooltip.classList.add('visible');
// Highlight connected nodes
const connected = new Set();
links.forEach(l => {
const sid = typeof l.source === 'object' ? l.source.id : l.source;
@@ -280,17 +547,13 @@ const PALETTE = [
if (tid === d.id) connected.add(sid);
});
connected.add(d.id);
node.select('circle')
.attr('opacity', n => connected.has(n.id) ? 1 : 0.1);
node.selectAll('text')
.attr('opacity', n => connected.has(n.id) ? 1 : 0.1);
link
.attr('stroke-opacity', l => {
const sid = typeof l.source === 'object' ? l.source.id : l.source;
const tid = typeof l.target === 'object' ? l.target.id : l.target;
return (sid === d.id || tid === d.id) ? 0.6 : 0.02;
});
node.select('circle').attr('opacity', n => connected.has(n.id) ? 1 : 0.1);
node.selectAll('text').attr('opacity', n => connected.has(n.id) ? 1 : 0.1);
link.attr('stroke-opacity', l => {
const sid = typeof l.source === 'object' ? l.source.id : l.source;
const tid = typeof l.target === 'object' ? l.target.id : l.target;
return (sid === d.id || tid === d.id) ? 0.6 : 0.02;
});
})
.on('mousemove', function(event) {
const rect = container.getBoundingClientRect();
@@ -311,30 +574,22 @@ const PALETTE = [
}
});
// Tick handler
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
node.attr('transform', d => `translate(${d.x},${d.y})`);
});
// Drag handlers
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x; d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x; d.fy = event.y;
}
function dragged(event, d) { d.fx = event.x; d.fy = event.y; }
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null; d.fy = null;
}
// Category filter
catSelect.addEventListener('change', function() {
const cat = this.value;
if (!cat) {
@@ -344,43 +599,33 @@ const PALETTE = [
return;
}
const inCat = new Set();
nodes.forEach(n => {
if (n.type === 'draft' && n.category === cat) inCat.add(n.id);
});
// Also include RFCs referenced by those drafts
nodes.forEach(n => { if (n.type === 'draft' && n.category === cat) inCat.add(n.id); });
links.forEach(l => {
const sid = typeof l.source === 'object' ? l.source.id : l.source;
const tid = typeof l.target === 'object' ? l.target.id : l.target;
if (inCat.has(sid)) inCat.add(tid);
});
node.select('circle')
.attr('opacity', n => inCat.has(n.id) ? 1 : 0.05);
node.selectAll('text')
.attr('opacity', n => inCat.has(n.id) ? 1 : 0.05);
node.select('circle').attr('opacity', n => inCat.has(n.id) ? 1 : 0.05);
node.selectAll('text').attr('opacity', n => inCat.has(n.id) ? 1 : 0.05);
link.attr('stroke-opacity', l => {
const sid = typeof l.source === 'object' ? l.source.id : l.source;
return inCat.has(sid) ? 0.5 : 0.01;
});
});
// Min refs slider (client-side filter)
const slider = document.getElementById('minRefsSlider');
const sliderVal = document.getElementById('minRefsVal');
slider.addEventListener('input', function() {
sliderVal.textContent = this.value;
const minR = parseInt(this.value);
// Show/hide RFC nodes by influence
node.select('circle')
.attr('opacity', n => {
if (n.type === 'draft') return 0.85;
return n.influence >= minR ? 0.85 : 0.05;
});
node.selectAll('text')
.attr('opacity', n => {
if (n.type === 'draft') return 1;
return n.influence >= minR ? 1 : 0.05;
});
// Filter edges
node.select('circle').attr('opacity', n => {
if (n.type === 'draft') return 0.85;
return n.influence >= minR ? 0.85 : 0.05;
});
node.selectAll('text').attr('opacity', n => {
if (n.type === 'draft') return 1;
return n.influence >= minR ? 1 : 0.05;
});
const visibleRfcs = new Set(nodes.filter(n => n.type !== 'draft' && n.influence >= minR).map(n => n.id));
link.attr('stroke-opacity', l => {
const tid = typeof l.target === 'object' ? l.target.id : l.target;
@@ -388,5 +633,155 @@ const PALETTE = [
});
});
})();
// ===========================================================
// Plotly: Citation density by category (Tab 2)
// ===========================================================
(function() {
const cats = influence.citations_by_category;
if (!cats || cats.length === 0) return;
Plotly.newPlot('categoryChart', [{
type: 'bar',
x: cats.map(c => c.category),
y: cats.map(c => c.avg_citations),
text: cats.map(c => `${c.avg_citations} avg (${c.total_citations} total, ${c.draft_count} drafts)`),
hovertemplate: '%{x}<br>Avg: %{y:.1f} refs/draft<br>%{text}<extra></extra>',
marker: {
color: cats.map((_, i) => PALETTE[i % PALETTE.length]),
opacity: 0.85,
},
}], {
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 11 },
margin: { t: 20, r: 20, b: 80, l: 50 },
xaxis: {
tickangle: -35,
gridcolor: 'rgba(71,85,105,0.2)',
},
yaxis: {
title: 'Avg Citations per Draft',
gridcolor: 'rgba(71,85,105,0.2)',
},
}, { responsive: true, displayModeBar: false });
})();
// ===========================================================
// Plotly: Draft-to-Draft Network (Tab 2)
// ===========================================================
(function() {
const edges = influence.draft_network;
if (!edges || edges.length === 0) {
document.getElementById('draftNetworkChart').innerHTML =
'<p class="text-slate-500 text-sm text-center py-20">No draft-to-draft citations found</p>';
return;
}
// Build a simple force-layout-like visualization using Plotly scatter
// We'll use a circular layout for nodes involved in draft-to-draft citations
const nodeSet = new Set();
edges.forEach(e => { nodeSet.add(e.source); nodeSet.add(e.target); });
const nodeList = [...nodeSet];
const nodeIdx = Object.fromEntries(nodeList.map((n, i) => [n, i]));
// Circular layout
const n = nodeList.length;
const cx = 0.5, cy = 0.5, radius = 0.4;
const nodeX = nodeList.map((_, i) => cx + radius * Math.cos(2 * Math.PI * i / n));
const nodeY = nodeList.map((_, i) => cy + radius * Math.sin(2 * Math.PI * i / n));
// Edge traces
const edgeX = [], edgeY = [];
edges.forEach(e => {
const si = nodeIdx[e.source], ti = nodeIdx[e.target];
if (si !== undefined && ti !== undefined) {
edgeX.push(nodeX[si], nodeX[ti], null);
edgeY.push(nodeY[si], nodeY[ti], null);
}
});
// Short names for display
const shortNames = nodeList.map(name => {
const s = name.replace(/^draft-/, '');
return s.length > 25 ? s.slice(0, 23) + '..' : s;
});
const data = [
{
type: 'scatter', mode: 'lines',
x: edgeX, y: edgeY,
line: { color: 'rgba(71,85,105,0.3)', width: 1 },
hoverinfo: 'none',
},
{
type: 'scatter', mode: 'markers+text',
x: nodeX, y: nodeY,
text: shortNames,
textposition: 'top center',
textfont: { size: 8, color: '#94a3b8' },
marker: { size: 8, color: '#3b82f6', opacity: 0.85 },
hovertext: nodeList.map(name => {
const outCount = edges.filter(e => e.source === name).length;
const inCount = edges.filter(e => e.target === name).length;
return `${name}\nOut: ${outCount} | In: ${inCount}`;
}),
hoverinfo: 'text',
},
];
Plotly.newPlot('draftNetworkChart', data, {
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif' },
margin: { t: 10, r: 10, b: 10, l: 10 },
xaxis: { visible: false },
yaxis: { visible: false },
showlegend: false,
}, { responsive: true, displayModeBar: false });
})();
// ===========================================================
// Plotly: BCP Co-citation Heatmap (Tab 3)
// ===========================================================
(function() {
const labels = bcp.heatmap_labels;
const matrix = bcp.heatmap_matrix;
if (!labels || labels.length === 0) {
document.getElementById('bcpHeatmap').innerHTML =
'<p class="text-slate-500 text-sm text-center py-20">No BCP co-citation data</p>';
return;
}
const displayLabels = labels.map(l => 'BCP ' + l);
Plotly.newPlot('bcpHeatmap', [{
type: 'heatmap',
x: displayLabels,
y: displayLabels,
z: matrix,
colorscale: [
[0, '#0f172a'],
[0.1, '#1e3a5f'],
[0.3, '#1d4ed8'],
[0.5, '#3b82f6'],
[0.7, '#60a5fa'],
[1, '#f59e0b'],
],
hovertemplate: '%{x} + %{y}<br>Co-cited in %{z} drafts<extra></extra>',
showscale: true,
colorbar: {
title: { text: 'Co-citations', font: { color: '#94a3b8', size: 10 } },
tickfont: { color: '#94a3b8', size: 9 },
},
}], {
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 10 },
margin: { t: 20, r: 60, b: 80, l: 80 },
xaxis: { tickangle: -45, side: 'bottom' },
yaxis: { autorange: 'reversed' },
}, { responsive: true, displayModeBar: false });
})();
</script>
{% endblock %}

View File

@@ -0,0 +1,332 @@
{% extends "base.html" %}
{% set active_page = "complexity" %}
{% block title %}Complexity — IETF Draft Analyzer{% endblock %}
{% block extra_head %}<script src="/static/js/plotly.min.js"></script>{% endblock %}
{% block content %}
<div class="mb-6">
<h1 class="text-2xl font-bold text-white">Draft Complexity Matrix</h1>
<p class="text-slate-400 text-sm mt-1">Correlating structural complexity (pages, authors, citations, ideas) with quality ratings. Does more complexity mean better drafts?</p>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Avg Pages</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.stats.avg_pages }}</div>
<div class="text-xs text-slate-500 mt-1">{{ data.stats.pages_coverage_pct }}% have page data</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Avg Authors</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.stats.avg_authors }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Avg Citations</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.stats.avg_citations }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Drafts Analyzed</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.stats.total_drafts }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Metrics</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.metrics | length }} x {{ data.dimensions | length }}</div>
<div class="text-xs text-slate-500 mt-1">complexity x rating</div>
</div>
</div>
<!-- Scatter Plot Matrix -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Complexity vs Rating Scatter Plots</h2>
<p class="text-xs text-slate-500 mb-3">How structural complexity metrics relate to rating dimensions. Each dot is a rated draft. Click to navigate.</p>
<div id="scatterMatrix" style="height: 500px;"></div>
</div>
<!-- Correlation Table -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden mb-6">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Correlation Matrix</h2>
<p class="text-xs text-slate-500 mt-1">Pearson correlation between complexity metrics (rows) and rating dimensions (columns). Green = positive, red = negative. Values range from -1 to +1.</p>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-3 font-medium">Metric</th>
{% for dim in data.dimensions %}
<th class="px-4 py-3 font-medium text-center">{{ dim | capitalize }}</th>
{% endfor %}
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50" id="corrTable">
</tbody>
</table>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Top 10 Most Complex -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Top 10 Most Complex Drafts</h2>
<p class="text-xs text-slate-500 mt-1">Ranked by composite complexity (pages + authors + citations + ideas, normalized).</p>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-2 font-medium">#</th>
<th class="px-4 py-2 font-medium">Draft</th>
<th class="px-4 py-2 font-medium text-right">Pages</th>
<th class="px-4 py-2 font-medium text-right">Authors</th>
<th class="px-4 py-2 font-medium text-right">Cites</th>
<th class="px-4 py-2 font-medium text-right">Score</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50" id="complexTable">
</tbody>
</table>
</div>
</div>
<!-- Top 10 Most Efficient -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Top 10 Most Efficient Drafts</h2>
<p class="text-xs text-slate-500 mt-1">High ratings relative to low structural complexity. Efficiency = score / complexity.</p>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-2 font-medium">#</th>
<th class="px-4 py-2 font-medium">Draft</th>
<th class="px-4 py-2 font-medium text-right">Pages</th>
<th class="px-4 py-2 font-medium text-right">Authors</th>
<th class="px-4 py-2 font-medium text-right">Score</th>
<th class="px-4 py-2 font-medium text-right">Efficiency</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50" id="efficientTable">
</tbody>
</table>
</div>
</div>
</div>
<!-- Category Complexity -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Complexity by Category</h2>
<p class="text-xs text-slate-500 mb-3">Average complexity metrics per category. Wider bars = more complex category.</p>
<div id="catComplexity" style="height: 400px;"></div>
</div>
<!-- Source Complexity -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Complexity by Source</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-3 font-medium">Source</th>
<th class="px-4 py-3 font-medium text-right">Count</th>
<th class="px-4 py-3 font-medium text-right">Avg Pages</th>
<th class="px-4 py-3 font-medium text-right">Avg Authors</th>
<th class="px-4 py-3 font-medium text-right">Avg Citations</th>
<th class="px-4 py-3 font-medium text-right">Avg Score</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for s in data.source_complexity %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-2 text-slate-300 font-medium">{{ s.source | upper }}</td>
<td class="px-4 py-2 text-right text-slate-400">{{ s.count }}</td>
<td class="px-4 py-2 text-right text-slate-300">{{ s.avg_pages }}</td>
<td class="px-4 py-2 text-right text-slate-300">{{ s.avg_authors }}</td>
<td class="px-4 py-2 text-right text-slate-300">{{ s.avg_citations }}</td>
<td class="px-4 py-2 text-right">
{% if s.avg_score >= 3.0 %}
<span class="score-badge score-high">{{ s.avg_score }}</span>
{% elif s.avg_score >= 2.0 %}
<span class="score-badge score-mid">{{ s.avg_score }}</span>
{% else %}
<span class="score-badge score-low">{{ s.avg_score }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
const PLOTLY_LAYOUT = {
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 12 },
margin: { t: 30, r: 20, b: 40, l: 50 },
xaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
yaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
};
const CFG = { responsive: true, displayModeBar: false };
const cdata = {{ data | tojson }};
// Scatter plot matrix (2x3 subplots)
(function() {
const pairs = [
{ x: 'pages', y: 'novelty', xLabel: 'Pages', yLabel: 'Novelty' },
{ x: 'author_count', y: 'maturity', xLabel: 'Authors', yLabel: 'Maturity' },
{ x: 'citation_count', y: 'relevance', xLabel: 'Citations', yLabel: 'Relevance' },
{ x: 'idea_count', y: 'momentum', xLabel: 'Ideas', yLabel: 'Momentum' },
{ x: 'pages', y: 'maturity', xLabel: 'Pages', yLabel: 'Maturity' },
{ x: 'citation_count', y: 'novelty', xLabel: 'Citations', yLabel: 'Novelty' },
];
const colors = ['#3b82f6', '#22c55e', '#a855f7', '#f59e0b', '#06b6d4', '#ef4444'];
const traces = [];
const annotations = [];
pairs.forEach((p, i) => {
const row = i < 3 ? 1 : 2;
const col = (i % 3) + 1;
const xaxis = i === 0 ? 'x' : `x${i + 1}`;
const yaxis = i === 0 ? 'y' : `y${i + 1}`;
const filteredDrafts = p.x === 'pages'
? cdata.drafts.filter(d => d.pages !== null)
: cdata.drafts;
traces.push({
x: filteredDrafts.map(d => d[p.x]),
y: filteredDrafts.map(d => d[p.y]),
text: filteredDrafts.map(d => d.name),
type: 'scatter',
mode: 'markers',
marker: { color: colors[i], size: 5, opacity: 0.6 },
xaxis: xaxis,
yaxis: yaxis,
hovertemplate: `<b>%{text}</b><br>${p.xLabel}: %{x}<br>${p.yLabel}: %{y}<extra></extra>`,
showlegend: false,
});
});
const layout = {
...PLOTLY_LAYOUT,
grid: { rows: 2, columns: 3, pattern: 'independent' },
margin: { t: 30, r: 20, b: 40, l: 50 },
};
// Set axis labels
pairs.forEach((p, i) => {
const xKey = i === 0 ? 'xaxis' : `xaxis${i + 1}`;
const yKey = i === 0 ? 'yaxis' : `yaxis${i + 1}`;
layout[xKey] = { ...PLOTLY_LAYOUT.xaxis, title: { text: p.xLabel, font: { size: 10 } } };
layout[yKey] = { ...PLOTLY_LAYOUT.yaxis, title: { text: p.yLabel, font: { size: 10 } } };
});
Plotly.newPlot('scatterMatrix', traces, layout, CFG);
// Click to navigate
document.getElementById('scatterMatrix').on('plotly_click', function(ev) {
const pt = ev.points[0];
if (pt.text) window.location.href = '/drafts/' + pt.text;
});
})();
// Correlation table
(function() {
const tbody = document.getElementById('corrTable');
const metricLabels = { pages: 'Pages', author_count: 'Authors', citation_count: 'Citations', idea_count: 'Ideas', category_count: 'Categories' };
cdata.metrics.forEach(metric => {
const row = document.createElement('tr');
row.className = 'hover:bg-slate-800/50 transition';
let cells = `<td class="px-4 py-2 text-slate-300 font-medium">${metricLabels[metric] || metric}</td>`;
cdata.dimensions.forEach(dim => {
const val = cdata.correlations[metric][dim];
let bgClass, textColor;
const absVal = Math.abs(val);
if (val > 0.2) { bgClass = 'bg-green-900/30'; textColor = 'text-green-400'; }
else if (val < -0.2) { bgClass = 'bg-red-900/30'; textColor = 'text-red-400'; }
else if (absVal > 0.1) { bgClass = 'bg-yellow-900/20'; textColor = 'text-yellow-400'; }
else { bgClass = ''; textColor = 'text-slate-500'; }
cells += `<td class="px-4 py-2 text-center ${bgClass}"><span class="${textColor} font-mono text-xs">${val.toFixed(3)}</span></td>`;
});
row.innerHTML = cells;
tbody.appendChild(row);
});
})();
// Top 10 Complex table
(function() {
const tbody = document.getElementById('complexTable');
cdata.top_complex.forEach((d, i) => {
const row = document.createElement('tr');
row.className = 'hover:bg-slate-800/50 transition';
const shortName = d.name.replace('draft-', '').substring(0, 35);
const scoreClass = d.score >= 3.0 ? 'score-high' : d.score >= 2.0 ? 'score-mid' : 'score-low';
row.innerHTML = `
<td class="px-4 py-2 text-slate-500 font-mono text-xs">${i + 1}</td>
<td class="px-4 py-2"><a href="/drafts/${d.name}" class="text-blue-400 hover:text-blue-300 text-xs font-mono">${shortName}</a></td>
<td class="px-4 py-2 text-right text-slate-300">${d.pages !== null ? d.pages : '-'}</td>
<td class="px-4 py-2 text-right text-slate-300">${d.author_count}</td>
<td class="px-4 py-2 text-right text-slate-300">${d.citation_count}</td>
<td class="px-4 py-2 text-right"><span class="score-badge ${scoreClass}">${d.score.toFixed(2)}</span></td>
`;
tbody.appendChild(row);
});
})();
// Top 10 Efficient table
(function() {
const tbody = document.getElementById('efficientTable');
cdata.top_efficient.forEach((d, i) => {
const row = document.createElement('tr');
row.className = 'hover:bg-slate-800/50 transition';
const shortName = d.name.replace('draft-', '').substring(0, 35);
const scoreClass = d.score >= 3.0 ? 'score-high' : d.score >= 2.0 ? 'score-mid' : 'score-low';
row.innerHTML = `
<td class="px-4 py-2 text-slate-500 font-mono text-xs">${i + 1}</td>
<td class="px-4 py-2"><a href="/drafts/${d.name}" class="text-blue-400 hover:text-blue-300 text-xs font-mono">${shortName}</a></td>
<td class="px-4 py-2 text-right text-slate-300">${d.pages !== null ? d.pages : '-'}</td>
<td class="px-4 py-2 text-right text-slate-300">${d.author_count}</td>
<td class="px-4 py-2 text-right"><span class="score-badge ${scoreClass}">${d.score.toFixed(2)}</span></td>
<td class="px-4 py-2 text-right text-green-400 font-mono text-xs">${d.efficiency.toFixed(1)}</td>
`;
tbody.appendChild(row);
});
})();
// Category complexity bar chart
(function() {
const cats = cdata.category_complexity.slice(0, 12);
const catNames = cats.map(c => c.category);
Plotly.newPlot('catComplexity', [
{
y: catNames, x: cats.map(c => c.avg_pages),
name: 'Avg Pages', type: 'bar', orientation: 'h',
marker: { color: '#3b82f6', opacity: 0.8 },
},
{
y: catNames, x: cats.map(c => c.avg_citations),
name: 'Avg Citations', type: 'bar', orientation: 'h',
marker: { color: '#22c55e', opacity: 0.8 },
},
{
y: catNames, x: cats.map(c => c.avg_authors * 5),
name: 'Avg Authors (x5)', type: 'bar', orientation: 'h',
marker: { color: '#a855f7', opacity: 0.8 },
},
], {
...PLOTLY_LAYOUT,
barmode: 'group',
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Value' },
yaxis: { ...PLOTLY_LAYOUT.yaxis, autorange: 'reversed' },
legend: { font: { size: 10, color: '#94a3b8' }, orientation: 'h', y: -0.15 },
margin: { t: 10, r: 20, b: 60, l: 150 },
}, CFG);
})();
</script>
{% endblock %}

View File

@@ -0,0 +1,215 @@
{% extends "base.html" %}
{% set active_page = "false_positives" %}
{% block title %}False Positive Profile — IETF Draft Analyzer{% endblock %}
{% block extra_head %}<script src="/static/js/plotly.min.js"></script>{% endblock %}
{% block content %}
<div class="mb-6">
<h1 class="text-2xl font-bold text-white">False Positive Profile</h1>
<p class="text-slate-400 text-sm mt-1">Analysis of {{ data.count }} drafts flagged as false positives — documents that matched AI/agent search keywords but were determined not to be genuinely about AI agent infrastructure.</p>
</div>
<!-- Stats Panel -->
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-red-500 to-red-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">False Positives</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.count }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-yellow-500 to-yellow-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">% of All Drafts</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.pct_of_total }}%</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 to-blue-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">% of Rated</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.pct_of_rated }}%</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-green-500 to-green-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Total Rated</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.total_rated }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-purple-500 to-purple-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Total Drafts</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.total_drafts }}</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Box Plots: FP vs Non-FP -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Rating Distributions: FP vs Non-FP</h2>
<p class="text-xs text-slate-500 mb-3">Box plots comparing each rating dimension between false positives (red) and genuine AI/agent drafts (blue). Shows what rating patterns distinguish false positives.</p>
<div id="boxPlots" style="height: 400px;"></div>
</div>
<!-- Source distribution -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">False Positives by Source</h2>
<p class="text-xs text-slate-500 mb-3">Which standards bodies produce the most false positives in our search results.</p>
<div id="sourcePie" style="height: 400px;"></div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Category distribution -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Categories Assigned to False Positives</h2>
<p class="text-xs text-slate-500 mb-3">Categories that the classifier assigned to false positive drafts before they were flagged. Shows which categories are most prone to false matches.</p>
<div id="catBar" style="height: 400px;"></div>
</div>
<!-- Top Terms -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Top Terms in FP Abstracts</h2>
<p class="text-xs text-slate-500 mb-3">Most frequent words in false positive titles and abstracts (stop words excluded). These terms trigger AI/agent keyword matches but appear in unrelated contexts.</p>
<div id="termBar" style="height: 400px;"></div>
</div>
</div>
<!-- FP Table -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">All False Positives</h2>
<p class="text-xs text-slate-500 mt-1">Complete list of flagged drafts. Click a name to view details.</p>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-3 font-medium">#</th>
<th class="px-4 py-3 font-medium">Draft</th>
<th class="px-4 py-3 font-medium">Title</th>
<th class="px-4 py-3 font-medium text-center">Source</th>
<th class="px-4 py-3 font-medium text-center">Relevance</th>
<th class="px-4 py-3 font-medium">Categories</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for fp in data.fp_list %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-3 text-slate-500 font-mono text-xs">{{ loop.index }}</td>
<td class="px-4 py-3">
<a href="/drafts/{{ fp.name }}" class="text-blue-400 hover:text-blue-300 transition text-xs font-mono">{{ fp.name | replace('draft-', '') | truncate(40) }}</a>
</td>
<td class="px-4 py-3 text-slate-300 text-xs max-w-xs truncate">{{ fp.title }}</td>
<td class="px-4 py-3 text-center">
<span class="px-1.5 py-0.5 rounded text-[10px] font-semibold uppercase bg-slate-800 text-slate-400">{{ fp.source }}</span>
</td>
<td class="px-4 py-3 text-center">
<span class="{% if fp.relevance >= 3 %}text-yellow-400{% else %}text-slate-500{% endif %}">{{ fp.relevance }}</span>
</td>
<td class="px-4 py-3">
{% for cat in fp.categories[:2] %}
<span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 text-slate-400">{{ cat }}</span>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
const PLOTLY_LAYOUT = {
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 12 },
margin: { t: 20, r: 20, b: 40, l: 50 },
xaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
yaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
};
const CFG = { responsive: true, displayModeBar: false };
const fpData = {{ data | tojson }};
// Box plots: FP vs Non-FP
const dims = ['novelty', 'maturity', 'overlap', 'momentum', 'relevance'];
const dimLabels = ['Novelty', 'Maturity', 'Overlap', 'Momentum', 'Relevance'];
const boxTraces = [];
dims.forEach((d, i) => {
boxTraces.push({
y: fpData.nonfp_dims[d], name: dimLabels[i] + ' (non-FP)',
type: 'box', marker: { color: '#3b82f6' }, boxmean: true,
legendgroup: dimLabels[i], showlegend: i === 0,
});
boxTraces.push({
y: fpData.fp_dims[d], name: dimLabels[i] + ' (FP)',
type: 'box', marker: { color: '#ef4444' }, boxmean: true,
legendgroup: dimLabels[i], showlegend: i === 0,
});
});
Plotly.newPlot('boxPlots', boxTraces, {
...PLOTLY_LAYOUT,
boxmode: 'group',
showlegend: true,
legend: { font: { size: 10, color: '#94a3b8' }, orientation: 'h', y: 1.1 },
yaxis: { ...PLOTLY_LAYOUT.yaxis, range: [0.5, 5.5], dtick: 1, title: 'Rating' },
margin: { t: 40, r: 10, b: 30, l: 50 },
}, CFG);
// Source pie
const srcLabels = Object.keys(fpData.fp_sources);
const srcValues = Object.values(fpData.fp_sources);
const srcColors = {
'ietf': '#3b82f6', 'iso': '#22c55e', 'itu': '#eab308',
'w3c': '#a855f7', 'etsi': '#ef4444', 'nist': '#06b6d4',
};
Plotly.newPlot('sourcePie', [{
labels: srcLabels.map(s => s.toUpperCase()),
values: srcValues,
type: 'pie',
marker: { colors: srcLabels.map(s => srcColors[s] || '#64748b') },
textinfo: 'label+value',
textfont: { color: '#e2e8f0', size: 12 },
hovertemplate: '%{label}: %{value} drafts (%{percent})<extra></extra>',
hole: 0.4,
}], {
...PLOTLY_LAYOUT,
showlegend: false,
margin: { t: 10, r: 10, b: 10, l: 10 },
}, CFG);
// Category bar
const catLabels = Object.keys(fpData.fp_categories);
const catValues = Object.values(fpData.fp_categories);
Plotly.newPlot('catBar', [{
x: catValues,
y: catLabels,
type: 'bar',
orientation: 'h',
marker: { color: '#ef4444', opacity: 0.7 },
hovertemplate: '%{y}: %{x} drafts<extra></extra>',
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Draft Count' },
yaxis: { ...PLOTLY_LAYOUT.yaxis, autorange: 'reversed' },
margin: { t: 10, r: 10, b: 40, l: 180 },
}, CFG);
// Top terms bar
const terms = fpData.top_terms;
const termLabels = terms.map(t => t[0]);
const termValues = terms.map(t => t[1]);
Plotly.newPlot('termBar', [{
x: termValues,
y: termLabels,
type: 'bar',
orientation: 'h',
marker: { color: '#f59e0b', opacity: 0.7 },
hovertemplate: '%{y}: %{x} occurrences<extra></extra>',
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Occurrences' },
yaxis: { ...PLOTLY_LAYOUT.yaxis, autorange: 'reversed' },
margin: { t: 10, r: 10, b: 40, l: 120 },
}, CFG);
</script>
{% endblock %}

View File

@@ -0,0 +1,330 @@
{% extends "base.html" %}
{% set active_page = "idea_analysis" %}
{% block title %}Idea Novelty Analysis — IETF Draft Analyzer{% endblock %}
{% block extra_head %}<script src="/static/js/plotly.min.js"></script>{% endblock %}
{% block content %}
<div class="mb-6">
<h1 class="text-2xl font-bold text-white">Idea Novelty Deep Dive</h1>
<p class="text-slate-400 text-sm mt-1">Comprehensive analysis of {{ data.total }} technical ideas extracted from IETF AI/agent drafts. Explores novelty distribution, type breakdowns, cross-draft patterns, and correlations with draft ratings.</p>
</div>
<!-- Stats panel -->
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-3xl font-bold text-blue-400">{{ data.total }}</div>
<div class="text-xs text-slate-400 mt-1">Total Ideas</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-3xl font-bold text-purple-400">{{ data.type_count }}</div>
<div class="text-xs text-slate-400 mt-1">Idea Types</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-3xl font-bold text-green-400">{{ data.avg_novelty }}</div>
<div class="text-xs text-slate-400 mt-1">Avg Novelty Score</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-3xl font-bold text-amber-400">{{ data.scored }}</div>
<div class="text-xs text-slate-400 mt-1">Scored Ideas</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-3xl font-bold text-cyan-400">{{ data.embed_pct }}%</div>
<div class="text-xs text-slate-400 mt-1">Embeddings ({{ data.embed_count }}/{{ data.total }})</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-3xl font-bold text-rose-400">{{ data.shared_ideas | length }}</div>
<div class="text-xs text-slate-400 mt-1">Shared Ideas (2+ drafts)</div>
</div>
</div>
<!-- Row 1: Novelty histogram + Type bar chart -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Novelty Score Distribution</h2>
<p class="text-xs text-slate-500 mb-3">How many ideas at each novelty level (1=incremental, 5=groundbreaking). {{ data.unscored }} ideas have no novelty score yet.</p>
<div id="noveltyHist" style="height: 300px;"></div>
</div>
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Ideas by Type (avg novelty color)</h2>
<p class="text-xs text-slate-500 mb-3">Count of ideas per type. Bar color intensity reflects average novelty score — brighter = more novel.</p>
<div id="typeChart" style="height: 300px;"></div>
</div>
</div>
<!-- Row 2: Scatter + Sunburst -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Draft Avg Idea Novelty vs Relevance</h2>
<p class="text-xs text-slate-500 mb-3">Each dot is a draft. X-axis = average novelty of its ideas, Y-axis = relevance score. Bubble size = number of ideas. Click to view draft.</p>
<div id="scatterChart" style="height: 380px;"></div>
</div>
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Idea Type Breakdown (Sunburst)</h2>
<p class="text-xs text-slate-500 mb-3">Hierarchical view: outer ring shows novelty bands (High/Medium/Low) within each type.</p>
<div id="sunburstChart" style="height: 380px;"></div>
</div>
</div>
<!-- Ideas per draft distribution -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Ideas per Draft Distribution</h2>
<p class="text-xs text-slate-500 mb-3">How many ideas does each draft contribute? Most drafts have 2-4 ideas; some prolific drafts generate 8+.</p>
<div id="ipdChart" style="height: 280px;"></div>
</div>
<!-- Top 20 most novel ideas -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden mb-6">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Top 20 Most Novel Ideas</h2>
<p class="text-xs text-slate-500 mt-1">Ideas with novelty score of 4 or 5, sorted by novelty then draft composite score.</p>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-3 font-medium">#</th>
<th class="px-4 py-3 font-medium">Idea</th>
<th class="px-4 py-3 font-medium text-center">Novelty</th>
<th class="px-4 py-3 font-medium">Type</th>
<th class="px-4 py-3 font-medium">Draft</th>
<th class="px-4 py-3 font-medium">Description</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for idea in data.top_novel %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-3 text-slate-500 font-mono text-xs">{{ loop.index }}</td>
<td class="px-4 py-3 text-slate-200 text-xs font-medium max-w-[200px] truncate" title="{{ idea.title }}">{{ idea.title }}</td>
<td class="px-4 py-3 text-center">
<span class="px-2 py-0.5 rounded text-xs font-mono {% if idea.novelty_score == 5 %}bg-green-500/20 text-green-400{% else %}bg-emerald-500/20 text-emerald-400{% endif %}">{{ idea.novelty_score }}</span>
</td>
<td class="px-4 py-3">
<span class="px-2 py-0.5 rounded text-[10px] bg-blue-500/20 text-blue-400">{{ idea.type }}</span>
</td>
<td class="px-4 py-3">
<a href="/drafts/{{ idea.draft_name }}" class="text-blue-400 hover:text-blue-300 transition text-xs font-mono">{{ idea.draft_name | replace('draft-', '') | truncate(35) }}</a>
</td>
<td class="px-4 py-3 text-xs text-slate-500 max-w-[300px] truncate" title="{{ idea.description }}">{{ idea.description | truncate(120) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Shared ideas across drafts -->
{% if data.shared_ideas %}
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden mb-6">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Ideas Shared Across Multiple Drafts</h2>
<p class="text-xs text-slate-500 mt-1">{{ data.shared_ideas | length }} ideas appear in 2 or more drafts, indicating convergent thinking or common building blocks.</p>
</div>
<div class="divide-y divide-slate-800/50 max-h-[500px] overflow-y-auto">
{% for idea in data.shared_ideas[:30] %}
<div class="px-4 py-3 hover:bg-slate-800/50 transition">
<div class="flex items-center gap-2 mb-1 flex-wrap">
<span class="text-sm font-medium text-slate-200">{{ idea.title }}</span>
<span class="px-2 py-0.5 rounded text-[10px] font-mono bg-amber-500/20 text-amber-400">{{ idea.appearances }}x</span>
{% for t in idea.types %}
<span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-700 text-slate-400">{{ t }}</span>
{% endfor %}
</div>
<div class="flex flex-wrap gap-1 mt-1">
{% for d in idea.drafts %}
<a href="/drafts/{{ d }}" class="text-[10px] text-slate-600 hover:text-blue-400 transition font-mono">{{ d | replace('draft-', '') | truncate(30) }}</a>
{% if not loop.last %}<span class="text-slate-700 text-[10px]">|</span>{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Top idea-producing drafts -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden mb-6">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Most Prolific Drafts (by idea count)</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-3 font-medium">#</th>
<th class="px-4 py-3 font-medium">Draft</th>
<th class="px-4 py-3 font-medium text-center">Ideas</th>
<th class="px-4 py-3 font-medium text-center">Score</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for d in data.top_idea_drafts %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-3 text-slate-500 font-mono text-xs">{{ loop.index }}</td>
<td class="px-4 py-3">
<a href="/drafts/{{ d.name }}" class="text-blue-400 hover:text-blue-300 transition text-xs font-mono">{{ d.name | replace('draft-', '') | truncate(45) }}</a>
{% if d.draft_title %}
<div class="text-[10px] text-slate-600 mt-0.5">{{ d.draft_title | truncate(60) }}</div>
{% endif %}
</td>
<td class="px-4 py-3 text-center text-slate-300 font-mono">{{ d.idea_count }}</td>
<td class="px-4 py-3 text-center">
{% if d.score %}
<span class="score-badge {% if d.score >= 3.5 %}score-high{% elif d.score >= 2.5 %}score-mid{% else %}score-low{% endif %}">{{ d.score | round(2) }}</span>
{% else %}
<span class="text-slate-600">--</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Embedding status note -->
<div class="bg-slate-900/50 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-2">Embedding Coverage</h2>
<div class="flex items-center gap-4">
<div class="flex-1">
<div class="w-full bg-slate-800 rounded-full h-3">
<div class="bg-blue-500 h-3 rounded-full transition-all" style="width: {{ data.embed_pct }}%"></div>
</div>
</div>
<span class="text-sm text-slate-400">{{ data.embed_count }} / {{ data.total }} ({{ data.embed_pct }}%)</span>
</div>
<p class="text-xs text-slate-500 mt-2">To complete missing embeddings, run: <code class="bg-slate-800 px-2 py-0.5 rounded text-slate-300">ietf embed-ideas</code>. This requires Ollama running locally. Embeddings enable idea similarity search and clustering.</p>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
const PLOTLY_LAYOUT = {
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 12 },
margin: { t: 20, r: 20, b: 40, l: 50 },
xaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
yaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
};
const CFG = { responsive: true, displayModeBar: false };
const data = {{ data | tojson }};
// --- Novelty Histogram ---
Plotly.newPlot('noveltyHist', [{
x: data.novelty_histogram.labels,
y: data.novelty_histogram.values,
type: 'bar',
marker: {
color: ['#ef4444', '#f97316', '#eab308', '#22c55e', '#10b981'],
line: { color: '#0f172a', width: 1 },
},
text: data.novelty_histogram.values,
textposition: 'outside',
textfont: { color: '#94a3b8', size: 12 },
hovertemplate: 'Novelty %{x}: %{y} ideas<extra></extra>',
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Novelty Score', dtick: 1 },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Count' },
margin: { t: 30, r: 20, b: 50, l: 60 },
}, CFG);
// --- Type Bar Chart (colored by avg novelty) ---
const types = data.by_type.map(t => t.type).reverse();
const typeCounts = data.by_type.map(t => t.count).reverse();
const typeAvgN = data.by_type.map(t => t.avg_novelty).reverse();
const typeColors = typeAvgN.map(n => {
// Map avg novelty (1-5) to color intensity: red -> yellow -> green
if (n >= 3.5) return '#22c55e';
if (n >= 3.0) return '#84cc16';
if (n >= 2.5) return '#eab308';
if (n >= 2.0) return '#f97316';
return '#ef4444';
});
Plotly.newPlot('typeChart', [{
y: types, x: typeCounts,
type: 'bar', orientation: 'h',
marker: { color: typeColors },
text: typeAvgN.map(n => `avg N: ${n.toFixed(1)}`),
textposition: 'auto',
textfont: { color: '#e2e8f0', size: 10 },
hovertemplate: '<b>%{y}</b><br>Count: %{x}<br>Avg Novelty: %{text}<extra></extra>',
}], {
...PLOTLY_LAYOUT,
margin: { t: 10, r: 20, b: 40, l: 120 },
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Count' },
}, CFG);
// --- Scatter: draft avg idea novelty vs relevance ---
const scatter = data.scatter_data;
const sourceGroups = {};
scatter.forEach(d => {
if (!sourceGroups[d.source]) sourceGroups[d.source] = { x: [], y: [], size: [], text: [] };
sourceGroups[d.source].x.push(d.avg_idea_novelty);
sourceGroups[d.source].y.push(d.relevance);
sourceGroups[d.source].size.push(Math.max(d.idea_count * 3, 6));
sourceGroups[d.source].text.push(d.name);
});
const scatterTraces = Object.entries(sourceGroups).map(([src, d]) => ({
x: d.x, y: d.y, text: d.text, name: src,
mode: 'markers', type: 'scatter',
marker: { size: d.size, opacity: 0.7 },
hovertemplate: '<b>%{text}</b><br>Avg Idea Novelty: %{x:.2f}<br>Relevance: %{y}<br>Ideas: %{marker.size:.0f}<extra>' + src + '</extra>',
}));
Plotly.newPlot('scatterChart', scatterTraces, {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Avg Idea Novelty', range: [0.5, 5.5] },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Draft Relevance Score', range: [0.5, 5.5], dtick: 1 },
legend: { font: { size: 10, color: '#94a3b8' } },
hovermode: 'closest',
margin: { t: 20, r: 20, b: 50, l: 60 },
}, CFG);
document.getElementById('scatterChart').on('plotly_click', function(ev) {
const pt = ev.points[0];
if (pt.text) window.location.href = '/drafts/' + pt.text;
});
// --- Sunburst ---
const sb = data.sunburst;
Plotly.newPlot('sunburstChart', [{
type: 'sunburst',
labels: sb.labels,
parents: sb.parents,
values: sb.values,
branchvalues: 'total',
textinfo: 'label+value',
textfont: { size: 10, color: '#e2e8f0' },
marker: {
line: { width: 1, color: '#0f172a' },
},
insidetextorientation: 'radial',
}], {
...PLOTLY_LAYOUT,
margin: { t: 10, r: 10, b: 10, l: 10 },
}, CFG);
// --- Ideas per Draft histogram ---
const ipd = data.ideas_per_draft_hist;
Plotly.newPlot('ipdChart', [{
x: ipd.labels,
y: ipd.values,
type: 'bar',
marker: { color: '#6366f1', line: { color: '#4338ca', width: 1 } },
text: ipd.values,
textposition: 'outside',
textfont: { color: '#94a3b8', size: 10 },
hovertemplate: '%{x} ideas/draft: %{y} drafts<extra></extra>',
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Ideas per Draft', dtick: 1 },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Number of Drafts' },
margin: { t: 30, r: 20, b: 50, l: 60 },
}, CFG);
</script>
{% endblock %}

View File

@@ -0,0 +1,198 @@
{% extends "base.html" %}
{% set active_page = "sources" %}
{% block title %}Cross-Source Comparison — IETF Draft Analyzer{% endblock %}
{% block extra_head %}<script src="/static/js/plotly.min.js"></script>{% endblock %}
{% block content %}
<div class="mb-6">
<h1 class="text-2xl font-bold text-white">Cross-Source Comparison</h1>
<p class="text-slate-400 text-sm mt-1">Comparing drafts across {{ data.summary | length }} standards bodies on rating dimensions, category focus, and output volume.</p>
</div>
<!-- Summary Table -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden mb-6">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Standards Body Summary</h2>
<p class="text-xs text-slate-500 mt-1">Overview of each source's contribution to the AI/agent standards landscape.</p>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-3 font-medium">Source</th>
<th class="px-4 py-3 font-medium text-center">Drafts</th>
<th class="px-4 py-3 font-medium text-center">Rated</th>
<th class="px-4 py-3 font-medium text-center">Authors</th>
<th class="px-4 py-3 font-medium text-center">Ideas</th>
<th class="px-4 py-3 font-medium text-center">Avg Score</th>
<th class="px-4 py-3 font-medium">Top Category</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for row in data.summary | sort(attribute='drafts', reverse=True) %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-3">
<span class="inline-block px-2 py-0.5 rounded text-xs font-semibold uppercase
{% if row.source == 'ietf' %}bg-blue-500/20 text-blue-400
{% elif row.source == 'iso' %}bg-green-500/20 text-green-400
{% elif row.source == 'itu' %}bg-yellow-500/20 text-yellow-400
{% elif row.source == 'w3c' %}bg-purple-500/20 text-purple-400
{% elif row.source == 'etsi' %}bg-red-500/20 text-red-400
{% elif row.source == 'nist' %}bg-cyan-500/20 text-cyan-400
{% else %}bg-slate-500/20 text-slate-400{% endif %}">{{ row.source }}</span>
</td>
<td class="px-4 py-3 text-center text-white font-mono">{{ row.drafts }}</td>
<td class="px-4 py-3 text-center text-slate-400 font-mono">{{ row.rated }}</td>
<td class="px-4 py-3 text-center text-slate-400 font-mono">{{ row.authors }}</td>
<td class="px-4 py-3 text-center text-slate-400 font-mono">{{ row.ideas }}</td>
<td class="px-4 py-3 text-center">
{% if row.avg_score >= 3.5 %}
<span class="score-badge score-high">{{ row.avg_score }}</span>
{% elif row.avg_score >= 2.5 %}
<span class="score-badge score-mid">{{ row.avg_score }}</span>
{% else %}
<span class="score-badge score-low">{{ row.avg_score }}</span>
{% endif %}
</td>
<td class="px-4 py-3">
<span class="px-2 py-0.5 rounded text-[10px] bg-slate-800 text-slate-400">{{ row.top_category }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Radar Chart -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Rating Dimensions by Source</h2>
<p class="text-xs text-slate-500 mb-3">Average rating across five dimensions for each standards body. Larger shapes indicate higher average ratings.</p>
<div id="radarChart" style="height: 400px;"></div>
</div>
<!-- Stacked Bar Chart -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Category Distribution by Source</h2>
<p class="text-xs text-slate-500 mb-3">What topics each standards body focuses on. Stacked bars show the relative emphasis per source.</p>
<div id="stackedBar" style="height: 400px;"></div>
</div>
</div>
<!-- Heatmap -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Sources x Categories Heatmap</h2>
<p class="text-xs text-slate-500 mb-3">Draft counts per source-category pair. Darker cells = more drafts. Shows where each body concentrates its work.</p>
<div id="heatmap" style="height: 400px;"></div>
</div>
<!-- Unique/Shared Categories -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-3">Shared Categories</h2>
<p class="text-xs text-slate-500 mb-3">Categories covered by multiple standards bodies.</p>
<div class="flex flex-wrap gap-2">
{% for cat in data.shared_categories %}
<a href="/drafts?cat={{ cat }}" class="px-2 py-1 rounded text-xs bg-blue-500/15 text-blue-400 border border-blue-500/20 hover:bg-blue-500/25 transition">{{ cat }}</a>
{% endfor %}
</div>
</div>
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-3">Unique Categories by Source</h2>
<p class="text-xs text-slate-500 mb-3">Categories only covered by a single standards body.</p>
{% for src, cats in data.unique_categories.items() %}
{% if cats %}
<div class="mb-3">
<span class="text-xs font-semibold uppercase text-slate-500">{{ src }}</span>
<div class="flex flex-wrap gap-1 mt-1">
{% for cat in cats %}
<span class="px-2 py-0.5 rounded text-[10px] bg-slate-800 text-slate-400">{{ cat }}</span>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
const PLOTLY_LAYOUT = {
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 12 },
margin: { t: 20, r: 20, b: 40, l: 50 },
xaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
yaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
};
const CFG = { responsive: true, displayModeBar: false };
const data = {{ data | tojson }};
// Source colors
const sourceColors = {
'ietf': '#3b82f6', 'iso': '#22c55e', 'itu': '#eab308',
'w3c': '#a855f7', 'etsi': '#ef4444', 'nist': '#06b6d4',
};
function srcColor(src) { return sourceColors[src] || '#64748b'; }
// Radar Chart
const radarDims = ['novelty', 'maturity', 'overlap', 'momentum', 'relevance'];
const radarLabels = ['Novelty', 'Maturity', 'Overlap', 'Momentum', 'Relevance'];
const radarTraces = Object.entries(data.radar).map(([src, vals]) => ({
type: 'scatterpolar',
r: radarDims.map(d => vals[d]).concat([vals[radarDims[0]]]),
theta: radarLabels.concat([radarLabels[0]]),
fill: 'toself',
name: `${src.toUpperCase()} (${vals.count})`,
opacity: 0.45,
line: { color: srcColor(src) },
fillcolor: srcColor(src) + '33',
}));
Plotly.newPlot('radarChart', radarTraces, {
...PLOTLY_LAYOUT,
polar: {
bgcolor: 'rgba(0,0,0,0)',
radialaxis: { visible: true, range: [0, 5], gridcolor: '#1e293b', color: '#64748b' },
angularaxis: { gridcolor: '#1e293b', color: '#94a3b8' },
},
legend: { font: { size: 10, color: '#94a3b8' } },
margin: { t: 30, r: 60, b: 30, l: 60 },
}, CFG);
// Stacked Bar Chart
const heatmap = data.heatmap;
const barTraces = heatmap.categories.map((cat, ci) => ({
name: cat,
type: 'bar',
x: heatmap.sources.map(s => s.toUpperCase()),
y: heatmap.values.map(row => row[ci]),
}));
Plotly.newPlot('stackedBar', barTraces, {
...PLOTLY_LAYOUT,
barmode: 'stack',
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: '' },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Draft Count' },
legend: { font: { size: 9, color: '#94a3b8' }, orientation: 'h', y: -0.25 },
margin: { t: 10, r: 10, b: 80, l: 50 },
}, CFG);
// Heatmap
Plotly.newPlot('heatmap', [{
z: heatmap.values,
x: heatmap.categories,
y: heatmap.sources.map(s => s.toUpperCase()),
type: 'heatmap',
colorscale: [[0, '#0f172a'], [0.5, '#1e40af'], [1, '#3b82f6']],
hovertemplate: '%{y} / %{x}<br>Drafts: %{z}<extra></extra>',
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, tickangle: -45 },
yaxis: { ...PLOTLY_LAYOUT.yaxis, autorange: 'reversed' },
margin: { t: 10, r: 10, b: 120, l: 60 },
}, CFG);
</script>
{% endblock %}

View File

@@ -0,0 +1,284 @@
{% extends "base.html" %}
{% set active_page = "trends" %}
{% block title %}Trends — IETF Draft Analyzer{% endblock %}
{% block extra_head %}<script src="/static/js/plotly.min.js"></script>{% endblock %}
{% block content %}
<div class="mb-6">
<h1 class="text-2xl font-bold text-white">Temporal Evolution</h1>
<p class="text-slate-400 text-sm mt-1">How the AI standards landscape is changing over time. Submission volume, rating trends, category shifts, and the safety-vs-capability balance.</p>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Months Tracked</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.months | length }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Total Submissions</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.monthly_table | sum(attribute='total') }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Fastest Growing</div>
<div class="text-lg font-bold text-green-400 mt-1">{{ data.stats.fastest_growing or 'N/A' }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Newest Category</div>
<div class="text-lg font-bold text-blue-400 mt-1">{{ data.stats.newest_active or 'N/A' }}</div>
</div>
</div>
<!-- Monthly Submissions by Source -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Monthly Draft Submissions by Source</h2>
<p class="text-xs text-slate-500 mb-3">Stacked area chart showing submission volume over time, broken down by source (IETF, W3C, etc.).</p>
<div id="submissionChart" style="height: 350px;"></div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Monthly Average Ratings -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Monthly Average Ratings</h2>
<p class="text-xs text-slate-500 mb-3">Are drafts getting more mature? More novel? Track the five rating dimensions over time.</p>
<div id="ratingsChart" style="height: 350px;"></div>
</div>
<!-- Safety Ratio -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Safety vs Capability Ratio</h2>
<p class="text-xs text-slate-500 mb-3">Ratio of safety-related drafts (Security, Privacy, Trust, Safety, Governance) to capability drafts (Agents, Infrastructure, MCP, etc.). Higher = more safety focus.</p>
<div id="safetyChart" style="height: 350px;"></div>
</div>
</div>
<!-- Category Distribution Over Time -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Category Distribution Over Time</h2>
<p class="text-xs text-slate-500 mb-3">Stacked area showing which topics are growing or shrinking. Top 8 categories shown.</p>
<div id="categoryChart" style="height: 400px;"></div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Cumulative Ideas -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Cumulative Idea Count</h2>
<p class="text-xs text-slate-500 mb-3">Total number of unique technical ideas extracted from drafts over time.</p>
<div id="ideasChart" style="height: 300px;"></div>
</div>
<!-- New Authors -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Monthly New Authors</h2>
<p class="text-xs text-slate-500 mb-3">First-time contributors entering the AI standards space each month.</p>
<div id="authorsChart" style="height: 300px;"></div>
</div>
</div>
<!-- Monthly Breakdown Table -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Monthly Breakdown</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-3 font-medium">Month</th>
<th class="px-4 py-3 font-medium text-right">Total</th>
<th class="px-4 py-3 font-medium text-right">Avg Score</th>
<th class="px-4 py-3 font-medium">Trend</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for row in data.monthly_table %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-2 font-mono text-xs text-slate-300">{{ row.month }}</td>
<td class="px-4 py-2 text-right text-slate-300">{{ row.total }}</td>
<td class="px-4 py-2 text-right">
{% if row.avg_score >= 3.0 %}
<span class="text-green-400">{{ row.avg_score }}</span>
{% elif row.avg_score >= 2.0 %}
<span class="text-yellow-400">{{ row.avg_score }}</span>
{% else %}
<span class="text-slate-500">{{ row.avg_score }}</span>
{% endif %}
</td>
<td class="px-4 py-2">
{% if loop.index > 1 %}
{% set prev = data.monthly_table[loop.index0 - 1].total %}
{% if row.total > prev %}
<span class="text-green-400 text-xs font-mono">&#x2191; +{{ row.total - prev }}</span>
{% elif row.total < prev %}
<span class="text-red-400 text-xs font-mono">&#x2193; {{ row.total - prev }}</span>
{% else %}
<span class="text-slate-500 text-xs font-mono">&#x2192; 0</span>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
const PLOTLY_LAYOUT = {
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 12 },
margin: { t: 20, r: 20, b: 40, l: 50 },
xaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
yaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
};
const CFG = { responsive: true, displayModeBar: false };
const data = {{ data | tojson }};
// 1. Monthly submissions by source (stacked area)
(function() {
const bySource = {};
data.monthly_submissions.forEach(r => {
if (!bySource[r.source]) bySource[r.source] = {};
bySource[r.source][r.month] = r.count;
});
const months = data.months;
const colors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#a855f7', '#06b6d4'];
const traces = Object.entries(bySource).map(([src, monthly], i) => ({
x: months,
y: months.map(m => monthly[m] || 0),
name: src.toUpperCase(),
type: 'scatter',
mode: 'lines',
stackgroup: 'one',
line: { color: colors[i % colors.length], width: 0 },
fillcolor: colors[i % colors.length] + '80',
}));
Plotly.newPlot('submissionChart', traces, {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Month' },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Draft Count' },
legend: { font: { size: 10, color: '#94a3b8' } },
}, CFG);
})();
// 2. Monthly average ratings (5 lines)
(function() {
const dims = ['novelty', 'maturity', 'overlap', 'momentum', 'relevance'];
const labels = ['Novelty', 'Maturity', 'Overlap', 'Momentum', 'Relevance'];
const colors = ['#3b82f6', '#22c55e', '#ef4444', '#f59e0b', '#a855f7'];
const months = data.monthly_ratings.map(r => r.month);
const traces = dims.map((d, i) => ({
x: months,
y: data.monthly_ratings.map(r => r[d]),
name: labels[i],
type: 'scatter',
mode: 'lines+markers',
line: { color: colors[i], width: 2 },
marker: { size: 4 },
}));
Plotly.newPlot('ratingsChart', traces, {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Avg Rating (1-5)', range: [0.5, 5.5] },
legend: { font: { size: 10, color: '#94a3b8' } },
}, CFG);
})();
// 3. Safety ratio
(function() {
const months = data.safety_ratio.map(r => r.month);
Plotly.newPlot('safetyChart', [
{
x: months,
y: data.safety_ratio.map(r => r.ratio),
type: 'scatter',
mode: 'lines+markers',
line: { color: '#22c55e', width: 2 },
marker: { size: 5 },
name: 'Safety/Capability Ratio',
hovertemplate: '%{x}<br>Ratio: %{y:.2f}<br>Safety: %{customdata[0]}<br>Capability: %{customdata[1]}<extra></extra>',
customdata: data.safety_ratio.map(r => [r.safety, r.capability]),
},
{
x: months,
y: months.map(() => 1.0),
type: 'scatter',
mode: 'lines',
line: { color: '#64748b', width: 1, dash: 'dash' },
name: 'Parity line',
showlegend: false,
}
], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Ratio (safety / capability)' },
showlegend: false,
}, CFG);
})();
// 4. Category distribution (stacked area)
(function() {
const months = data.months;
const cats = data.top_categories;
const colors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#a855f7', '#06b6d4', '#f97316', '#ec4899'];
// Build per-cat monthly data
const catMonthly = {};
data.monthly_categories.forEach(r => {
if (!catMonthly[r.category]) catMonthly[r.category] = {};
catMonthly[r.category][r.month] = r.count;
});
const traces = cats.map((cat, i) => ({
x: months,
y: months.map(m => (catMonthly[cat] || {})[m] || 0),
name: cat,
type: 'scatter',
mode: 'lines',
stackgroup: 'one',
line: { color: colors[i % colors.length], width: 0 },
fillcolor: colors[i % colors.length] + '80',
}));
Plotly.newPlot('categoryChart', traces, {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Month' },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Draft Count' },
legend: { font: { size: 10, color: '#94a3b8' } },
}, CFG);
})();
// 5. Cumulative ideas
(function() {
Plotly.newPlot('ideasChart', [{
x: data.cumulative_ideas.map(r => r.month),
y: data.cumulative_ideas.map(r => r.total),
type: 'scatter',
mode: 'lines',
fill: 'tozeroy',
line: { color: '#3b82f6', width: 2 },
fillcolor: 'rgba(59,130,246,0.15)',
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Total Ideas' },
showlegend: false,
}, CFG);
})();
// 6. New authors per month
(function() {
Plotly.newPlot('authorsChart', [{
x: data.monthly_new_authors.map(r => r.month),
y: data.monthly_new_authors.map(r => r.count),
type: 'bar',
marker: { color: '#a855f7', opacity: 0.8 },
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'New Authors' },
showlegend: false,
}, CFG);
})();
</script>
{% endblock %}