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:
330
src/webui/templates/idea_analysis.html
Normal file
330
src/webui/templates/idea_analysis.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user