Idea quality pipeline, web UI features, academic paper
- Tighten idea extraction prompts (1-4 ideas, no sub-features) reducing 1,907 ideas to 468 across 434 drafts (78% reduction) - Add embedding-based dedup (ietf dedup-ideas) for same-draft similarity - Add novelty scoring (ietf ideas score) and filtering (ietf ideas filter) using Claude to rate ideas 1-5, removing 49 generic building blocks - Final count: 419 high-quality ideas (avg 1.1/draft) - Web UI: gap explorer with live draft generation and pre-generated demos - Web UI: D3.js author collaboration network (498 nodes, 1142 edges, 68 clusters, org filtering, interactive zoom/pan) - Academic paper: 15-page LaTeX workshop paper analyzing the 434-draft AI agent standards landscape - Save improvement ideas backlog to data/reports/improvement-ideas.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
232
src/webui/templates/landscape.html
Normal file
232
src/webui/templates/landscape.html
Normal file
@@ -0,0 +1,232 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "landscape" %}
|
||||
|
||||
{% block title %}Landscape — IETF Draft Analyzer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-white">Draft Landscape</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">Multi-dimensional visualization of the AI/agent draft space</p>
|
||||
</div>
|
||||
|
||||
<!-- Embedding-based t-SNE map -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6" id="tsneSection">
|
||||
<h2 class="text-sm font-semibold text-slate-300 mb-1">Embedding Landscape (t-SNE)</h2>
|
||||
<p class="text-xs text-slate-500 mb-3">768-dim embeddings projected to 2D. Color = category, size = composite score. Click for draft detail.</p>
|
||||
<div id="tsneMap" style="height: 560px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main scatter: Novelty vs Maturity -->
|
||||
<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">Novelty vs Maturity</h2>
|
||||
<p class="text-xs text-slate-500 mb-3">Bubble size = composite score, color = category. Hover for details.</p>
|
||||
<div id="mainScatter" style="height: 560px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<!-- Novelty vs Overlap quadrant -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
|
||||
<h2 class="text-sm font-semibold text-slate-300 mb-1">Innovation-Uniqueness Quadrant</h2>
|
||||
<p class="text-xs text-slate-500 mb-3">Novelty vs Overlap — find the novel and unique drafts.</p>
|
||||
<div id="quadrantChart" style="height: 450px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Score distributions -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
|
||||
<h2 class="text-sm font-semibold text-slate-300 mb-1">Score Distributions</h2>
|
||||
<p class="text-xs text-slate-500 mb-3">Violin plots for each rating dimension.</p>
|
||||
<div id="violinChart" style="height: 450px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category 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">Category Distribution</h2>
|
||||
<p class="text-xs text-slate-500 mb-3">Number of rated drafts per primary category.</p>
|
||||
<div id="categoryBar" style="height: 400px;"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
const PLOTLY_LAYOUT = {
|
||||
paper_bgcolor: 'transparent', plot_bgcolor: 'rgba(15,23,42,0.5)',
|
||||
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 12 },
|
||||
margin: { t: 20, r: 20, b: 50, l: 50 },
|
||||
xaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
|
||||
yaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
|
||||
};
|
||||
const CFG = { responsive: true, displayModeBar: false };
|
||||
|
||||
const PALETTE = [
|
||||
'#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b',
|
||||
'#06b6d4', '#ec4899', '#84cc16', '#f97316', '#8b5cf6',
|
||||
'#14b8a6', '#e11d48', '#64748b', '#eab308', '#6366f1',
|
||||
];
|
||||
|
||||
const dist = {{ dist | tojson }};
|
||||
const tsneData = {{ tsne_data | tojson }};
|
||||
|
||||
// --- 0. t-SNE Embedding Map ---
|
||||
if (tsneData.length > 0) {
|
||||
const tsneCatGroups = {};
|
||||
tsneData.forEach(d => {
|
||||
if (!tsneCatGroups[d.category]) tsneCatGroups[d.category] = { x: [], y: [], size: [], text: [], names: [] };
|
||||
tsneCatGroups[d.category].x.push(d.x);
|
||||
tsneCatGroups[d.category].y.push(d.y);
|
||||
tsneCatGroups[d.category].size.push(Math.max(d.score * 4, 6));
|
||||
tsneCatGroups[d.category].text.push(d.title);
|
||||
tsneCatGroups[d.category].names.push(d.name);
|
||||
});
|
||||
|
||||
const catList = Object.keys(tsneCatGroups).sort((a, b) =>
|
||||
tsneCatGroups[b].x.length - tsneCatGroups[a].x.length
|
||||
);
|
||||
|
||||
const tsneTraces = catList.map((cat, i) => {
|
||||
const g = tsneCatGroups[cat];
|
||||
return {
|
||||
x: g.x, y: g.y, text: g.text, name: cat,
|
||||
customdata: g.names,
|
||||
mode: 'markers', type: 'scatter',
|
||||
marker: {
|
||||
size: g.size,
|
||||
color: PALETTE[i % PALETTE.length],
|
||||
opacity: 0.8,
|
||||
line: { width: 0.5, color: 'rgba(255,255,255,0.15)' },
|
||||
},
|
||||
hovertemplate: '<b>%{text}</b><extra>' + cat + '</extra>',
|
||||
};
|
||||
});
|
||||
|
||||
const tsnePlot = Plotly.newPlot('tsneMap', tsneTraces, {
|
||||
...PLOTLY_LAYOUT,
|
||||
xaxis: { visible: false, showgrid: false, zeroline: false },
|
||||
yaxis: { visible: false, showgrid: false, zeroline: false },
|
||||
legend: { font: { size: 10, color: '#94a3b8' }, bgcolor: 'transparent' },
|
||||
hovermode: 'closest',
|
||||
margin: { t: 10, r: 20, b: 10, l: 20 },
|
||||
}, CFG);
|
||||
|
||||
// Click to navigate to draft detail
|
||||
document.getElementById('tsneMap').on('plotly_click', function(data) {
|
||||
const pt = data.points[0];
|
||||
if (pt.customdata) {
|
||||
window.location.href = '/drafts/' + pt.customdata;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
document.getElementById('tsneSection').style.display = 'none';
|
||||
}
|
||||
|
||||
// --- Group by category for rating-based charts ---
|
||||
const catGroups = {};
|
||||
dist.names.forEach((name, i) => {
|
||||
const cat = dist.categories[i];
|
||||
if (!catGroups[cat]) catGroups[cat] = { x: [], y: [], nov: [], ovl: [], size: [], text: [], scores: [] };
|
||||
catGroups[cat].x.push(dist.novelty[i] + (Math.random() - 0.5) * 0.25);
|
||||
catGroups[cat].y.push(dist.maturity[i] + (Math.random() - 0.5) * 0.25);
|
||||
catGroups[cat].nov.push(dist.novelty[i]);
|
||||
catGroups[cat].ovl.push(dist.overlap[i]);
|
||||
catGroups[cat].size.push(Math.max(dist.scores[i] * 4, 5));
|
||||
catGroups[cat].text.push(name);
|
||||
catGroups[cat].scores.push(dist.scores[i]);
|
||||
});
|
||||
|
||||
// --- 1. Main Scatter: Novelty vs Maturity ---
|
||||
const mainTraces = Object.entries(catGroups).map(([cat, d]) => ({
|
||||
x: d.x, y: d.y, text: d.text, name: cat,
|
||||
customdata: d.scores.map((s, i) => [s, d.nov[i], d.ovl[i]]),
|
||||
mode: 'markers', type: 'scatter',
|
||||
marker: { size: d.size, opacity: 0.75, line: { width: 0.5, color: 'rgba(255,255,255,0.15)' } },
|
||||
hovertemplate: '<b>%{text}</b><br>Novelty: %{customdata[1]}<br>Maturity: %{y:.0f}<br>Score: %{customdata[0]:.2f}<br>Overlap: %{customdata[2]}<extra>' + cat + '</extra>',
|
||||
}));
|
||||
Plotly.newPlot('mainScatter', mainTraces, {
|
||||
...PLOTLY_LAYOUT,
|
||||
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Novelty', range: [0.3, 5.7], dtick: 1 },
|
||||
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Maturity', range: [0.3, 5.7], dtick: 1 },
|
||||
legend: { font: { size: 10, color: '#94a3b8' }, bgcolor: 'transparent' },
|
||||
}, CFG);
|
||||
|
||||
// Click to navigate to draft detail
|
||||
document.getElementById('mainScatter').on('plotly_click', function(data) {
|
||||
const pt = data.points[0];
|
||||
if (pt.text) {
|
||||
window.location.href = '/drafts/' + pt.text;
|
||||
}
|
||||
});
|
||||
|
||||
// --- 2. Novelty vs Overlap Quadrant ---
|
||||
const quadTraces = Object.entries(catGroups).map(([cat, d]) => ({
|
||||
x: d.nov.map(v => v + (Math.random() - 0.5) * 0.25),
|
||||
y: d.ovl.map(v => v + (Math.random() - 0.5) * 0.25),
|
||||
text: d.text, name: cat,
|
||||
customdata: d.scores,
|
||||
mode: 'markers', type: 'scatter',
|
||||
marker: { size: 7, opacity: 0.7 },
|
||||
hovertemplate: '<b>%{text}</b><br>Novelty: %{x:.0f}<br>Overlap: %{y:.0f}<br>Score: %{customdata:.2f}<extra>' + cat + '</extra>',
|
||||
showlegend: false,
|
||||
}));
|
||||
Plotly.newPlot('quadrantChart', quadTraces, {
|
||||
...PLOTLY_LAYOUT,
|
||||
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Novelty', range: [0.3, 5.7], dtick: 1 },
|
||||
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Overlap', range: [0.3, 5.7], dtick: 1 },
|
||||
shapes: [
|
||||
{ type: 'line', x0: 3, x1: 3, y0: 0, y1: 6, line: { color: '#334155', width: 1, dash: 'dash' } },
|
||||
{ type: 'line', x0: 0, x1: 6, y0: 3, y1: 3, line: { color: '#334155', width: 1, dash: 'dash' } },
|
||||
],
|
||||
annotations: [
|
||||
{ x: 4.5, y: 1.2, text: 'Novel & Unique', showarrow: false, font: { size: 11, color: '#4ade80' } },
|
||||
{ x: 4.5, y: 5.0, text: 'Novel & Overlapping', showarrow: false, font: { size: 11, color: '#facc15' } },
|
||||
{ x: 1.5, y: 1.2, text: 'Mature & Unique', showarrow: false, font: { size: 11, color: '#60a5fa' } },
|
||||
{ x: 1.5, y: 5.0, text: 'Crowded', showarrow: false, font: { size: 11, color: '#f87171' } },
|
||||
],
|
||||
}, CFG);
|
||||
|
||||
// --- 3. Violin / Box Plots ---
|
||||
const dims = ['novelty', 'maturity', 'overlap', 'momentum', 'relevance'];
|
||||
const dimColors = ['#3b82f6', '#22c55e', '#ef4444', '#f59e0b', '#a855f7'];
|
||||
const violinTraces = dims.map((d, i) => ({
|
||||
y: dist[d],
|
||||
name: d.charAt(0).toUpperCase() + d.slice(1),
|
||||
type: 'violin',
|
||||
box: { visible: true },
|
||||
meanline: { visible: true },
|
||||
line: { color: dimColors[i] },
|
||||
fillcolor: dimColors[i] + '30',
|
||||
opacity: 0.85,
|
||||
}));
|
||||
Plotly.newPlot('violinChart', violinTraces, {
|
||||
...PLOTLY_LAYOUT,
|
||||
showlegend: false,
|
||||
yaxis: { ...PLOTLY_LAYOUT.yaxis, range: [0.3, 5.7], dtick: 1, title: 'Score' },
|
||||
}, CFG);
|
||||
|
||||
// --- 4. Category Distribution ---
|
||||
const catCounts = {};
|
||||
dist.categories.forEach(c => { catCounts[c] = (catCounts[c] || 0) + 1; });
|
||||
const sorted = Object.entries(catCounts).sort((a, b) => b[1] - a[1]);
|
||||
const catNames = sorted.map(s => s[0]).reverse();
|
||||
const catValues = sorted.map(s => s[1]).reverse();
|
||||
|
||||
Plotly.newPlot('categoryBar', [{
|
||||
y: catNames, x: catValues,
|
||||
type: 'bar', orientation: 'h',
|
||||
marker: {
|
||||
color: catValues.map((_, i) => {
|
||||
const pct = i / Math.max(catValues.length - 1, 1);
|
||||
return `hsl(${210 + pct * 120}, 70%, 55%)`;
|
||||
}),
|
||||
},
|
||||
text: catValues.map(v => v.toString()),
|
||||
textposition: 'outside',
|
||||
textfont: { color: '#94a3b8', size: 11 },
|
||||
hovertemplate: '<b>%{y}</b><br>%{x} drafts<extra></extra>',
|
||||
}], {
|
||||
...PLOTLY_LAYOUT,
|
||||
margin: { t: 10, r: 60, b: 40, l: 220 },
|
||||
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Number of Drafts' },
|
||||
yaxis: { ...PLOTLY_LAYOUT.yaxis, automargin: true },
|
||||
}, CFG);
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user