Fix remaining critical, high, and medium issues from 4-perspective review
Critical fixes:
- Fix rating clamp range 1-10 → 1-5 (actual scale)
- Add `ietf ideas convergence` command (SequenceMatcher at 0.75 threshold)
- Fix "628 cross-org ideas" → 130 (verified from current DB) across 8 files
Security fixes:
- Sanitize FTS5 query input (strip special chars + boolean operators)
- Add rate limiting (10 req/min/IP) on Claude-calling endpoints
- Change <path:name> → <string:name> on draft routes
Codebase fixes:
- Add Database context manager (__enter__/__exit__)
- Wire false_positive filtering into queries (exclude by default in web UI)
- Fix Post 3 arithmetic ("~300" → "~409" distinct proposals)
Content & licensing:
- Add MIT LICENSE file
- Add IPR/FRAND notes (BCP 79, RFC 8179) to Posts 03 and 07
- Qualify "4:1 safety ratio" with monthly variation in 6 remaining files
- Add "Data as of March 2026" freeze-date headers to all 10 blog posts
- Hedge causal language in Post 04
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,72 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-3">Data Collection Methodology</h2>
|
||||
<p class="text-sm text-slate-400 leading-relaxed mb-4">
|
||||
Drafts are discovered by searching the
|
||||
<a href="https://datatracker.ietf.org" class="text-blue-400 hover:text-blue-300 transition">IETF Datatracker API</a>
|
||||
for documents whose abstract contains any of the following keywords.
|
||||
Only drafts submitted since <span class="text-slate-200 font-medium">{{ fetch_since }}</span> are included.
|
||||
</p>
|
||||
|
||||
<h3 class="text-sm font-semibold text-slate-300 mb-2">Search Keywords</h3>
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
{% for kw in search_keywords %}
|
||||
<span class="px-2.5 py-1 bg-blue-500/10 text-blue-400 border border-blue-500/20 rounded-md text-xs font-mono">{{ kw }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h3 class="text-sm font-semibold text-slate-300 mb-2">Analysis Pipeline</h3>
|
||||
<div class="text-sm text-slate-400 leading-relaxed space-y-2">
|
||||
<p><span class="text-slate-200 font-medium">1. Fetch</span> — Query Datatracker API for each keyword, deduplicate by draft name, download full text.</p>
|
||||
<p><span class="text-slate-200 font-medium">2. Rate</span> — Claude rates each draft on 5 dimensions (novelty, maturity, overlap, momentum, relevance) from 1–5, with per-dimension explanations.</p>
|
||||
<p><span class="text-slate-200 font-medium">3. Categorize</span> — Claude assigns one or more topic categories (e.g., "A2A protocols", "Agent identity/auth").</p>
|
||||
<p><span class="text-slate-200 font-medium">4. Extract Ideas</span> — Claude extracts distinct technical ideas from each draft, with novelty scores.</p>
|
||||
<p><span class="text-slate-200 font-medium">5. Embed</span> — Ollama generates vector embeddings for similarity analysis and clustering.</p>
|
||||
<p><span class="text-slate-200 font-medium">6. Author Network</span> — Author and affiliation data fetched from Datatracker to build collaboration graphs.</p>
|
||||
<p><span class="text-slate-200 font-medium">7. Gap Analysis</span> — Claude identifies areas where no existing draft adequately addresses a need.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-3 bg-slate-800/50 rounded-lg border border-slate-700/50">
|
||||
<p class="text-xs text-slate-500">
|
||||
<span class="text-amber-400/70 font-semibold">Note on keyword selection:</span>
|
||||
Keywords determine which drafts are included. Broad terms like "agent" and "autonomous" cast a wide net
|
||||
(catching some tangentially related drafts), while specific terms like "ai-agent" and "agentic" target
|
||||
the core AI agent space. The false-positive flag in ratings helps filter out irrelevant matches.
|
||||
Suggestions for additional keywords are welcome.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-3">Scoring Methodology</h2>
|
||||
<div class="text-sm text-slate-400 leading-relaxed space-y-3">
|
||||
<p>Each draft is rated by Claude AI on five dimensions, scored from 1 (lowest) to 5 (highest):</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-slate-700 text-left text-xs text-slate-500">
|
||||
<th class="py-2 pr-4 font-medium">Dimension</th>
|
||||
<th class="py-2 font-medium">What it measures</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800/50">
|
||||
<tr><td class="py-2 pr-4 text-slate-300 font-medium">Novelty</td><td class="py-2">Originality of contribution. Does it introduce genuinely new ideas?</td></tr>
|
||||
<tr><td class="py-2 pr-4 text-slate-300 font-medium">Maturity</td><td class="py-2">Completeness of the specification. Ready for implementation?</td></tr>
|
||||
<tr><td class="py-2 pr-4 text-slate-300 font-medium">Overlap</td><td class="py-2">Duplication with other drafts. High = redundant. <em>Inverted in composite score.</em></td></tr>
|
||||
<tr><td class="py-2 pr-4 text-slate-300 font-medium">Momentum</td><td class="py-2">Activity level. Revisions, WG adoption, multi-org authorship.</td></tr>
|
||||
<tr><td class="py-2 pr-4 text-slate-300 font-medium">Relevance</td><td class="py-2">How directly related to AI agent infrastructure.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="mt-2">
|
||||
<span class="text-slate-200 font-medium">Composite score</span> = (novelty + maturity + (5 - overlap) + momentum + relevance) / 5.
|
||||
Overlap is inverted so lower overlap contributes positively.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-3">Tech Stack</h2>
|
||||
<ul class="text-sm text-slate-400 space-y-2">
|
||||
|
||||
205
src/webui/templates/analytics.html
Normal file
205
src/webui/templates/analytics.html
Normal file
@@ -0,0 +1,205 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "analytics" %}
|
||||
|
||||
{% block title %}Analytics — IETF Draft Analyzer{% endblock %}
|
||||
|
||||
{% block extra_head %}<script src="/static/js/plotly.min.js"></script>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-white">Site Analytics</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">GDPR-compliant traffic overview. No cookies, no personal data stored. Visitor uniqueness estimated via daily-rotating salted hashes (cannot be correlated across days).</p>
|
||||
</div>
|
||||
|
||||
<!-- Stat cards -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4 mb-8">
|
||||
<div class="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-2xl font-bold text-blue-400">{{ data.stats.total_views }}</div>
|
||||
<div class="text-xs text-slate-400 mt-1 uppercase tracking-wider">Total Views</div>
|
||||
</div>
|
||||
<div class="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-2xl font-bold text-emerald-400">{{ data.stats.total_visitors }}</div>
|
||||
<div class="text-xs text-slate-400 mt-1 uppercase tracking-wider">Total Visits</div>
|
||||
</div>
|
||||
<div class="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-2xl font-bold text-purple-400">{{ data.stats.total_downloads }}</div>
|
||||
<div class="text-xs text-slate-400 mt-1 uppercase tracking-wider">Downloads</div>
|
||||
</div>
|
||||
<div class="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-2xl font-bold text-amber-400">{{ data.stats.today_views }}</div>
|
||||
<div class="text-xs text-slate-400 mt-1 uppercase tracking-wider">Today Views</div>
|
||||
</div>
|
||||
<div class="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-cyan-500 to-cyan-400"></div>
|
||||
<div class="text-2xl font-bold text-cyan-400">{{ data.stats.today_visitors }}</div>
|
||||
<div class="text-xs text-slate-400 mt-1 uppercase tracking-wider">Today Visitors</div>
|
||||
</div>
|
||||
<div class="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-rose-500 to-rose-400"></div>
|
||||
<div class="text-2xl font-bold text-rose-400">{{ data.stats.week_views }}</div>
|
||||
<div class="text-xs text-slate-400 mt-1 uppercase tracking-wider">7-Day Views</div>
|
||||
</div>
|
||||
<div class="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-indigo-500 to-indigo-400"></div>
|
||||
<div class="text-2xl font-bold text-indigo-400">{{ data.stats.month_views }}</div>
|
||||
<div class="text-xs text-slate-400 mt-1 uppercase tracking-wider">30-Day Views</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts row 1: Daily traffic + Hourly pattern -->
|
||||
<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">Daily Traffic (Last 30 Days)</h2>
|
||||
<div id="dailyChart" 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-3">Hourly Pattern (Last 7 Days)</h2>
|
||||
<div id="hourlyChart" style="height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts row 2: Top pages + Top referrers -->
|
||||
<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">Top Pages (Last 30 Days)</h2>
|
||||
{% if data.top_pages %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-slate-700 text-slate-400 text-xs uppercase">
|
||||
<th class="text-left py-2 pr-4">Path</th>
|
||||
<th class="text-right py-2 px-2">Views</th>
|
||||
<th class="text-right py-2 pl-2">Visitors</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for page in data.top_pages %}
|
||||
<tr class="border-b border-slate-800/50 hover:bg-slate-800/30">
|
||||
<td class="py-1.5 pr-4 text-slate-300 font-mono text-xs truncate max-w-xs">{{ page.path }}</td>
|
||||
<td class="py-1.5 px-2 text-right text-blue-400">{{ page.views }}</td>
|
||||
<td class="py-1.5 pl-2 text-right text-emerald-400">{{ page.visitors }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-slate-500 text-sm text-center mt-10">No page view data yet</p>
|
||||
{% endif %}
|
||||
</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">Top Referrers (Last 30 Days)</h2>
|
||||
{% if data.top_referrers %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-slate-700 text-slate-400 text-xs uppercase">
|
||||
<th class="text-left py-2 pr-4">Source</th>
|
||||
<th class="text-right py-2">Visits</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ref in data.top_referrers %}
|
||||
<tr class="border-b border-slate-800/50 hover:bg-slate-800/30">
|
||||
<td class="py-1.5 pr-4 text-slate-300 text-xs">{{ ref.referrer }}</td>
|
||||
<td class="py-1.5 text-right text-amber-400">{{ ref.count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-slate-500 text-sm text-center mt-10">No referrer data yet</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Downloads over time -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
|
||||
<h2 class="text-sm font-semibold text-slate-300 mb-3">Downloads Over Time</h2>
|
||||
<div id="downloadsChart" style="height: 250px;"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
const PLOTLY_LAYOUT = {
|
||||
paper_bgcolor: 'transparent',
|
||||
plot_bgcolor: 'transparent',
|
||||
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 PLOTLY_CONFIG = { responsive: true, displayModeBar: false };
|
||||
|
||||
// --- Daily traffic ---
|
||||
const daily = {{ data.daily | tojson }};
|
||||
if (daily.dates && daily.dates.length > 0) {
|
||||
Plotly.newPlot('dailyChart', [
|
||||
{
|
||||
x: daily.dates, y: daily.views,
|
||||
type: 'scatter', mode: 'lines+markers',
|
||||
name: 'Views',
|
||||
line: { color: '#3b82f6', width: 2 },
|
||||
marker: { size: 4 },
|
||||
hovertemplate: '%{x}<br>Views: %{y}<extra></extra>',
|
||||
},
|
||||
{
|
||||
x: daily.dates, y: daily.visitors,
|
||||
type: 'scatter', mode: 'lines+markers',
|
||||
name: 'Visitors',
|
||||
line: { color: '#22c55e', width: 2 },
|
||||
marker: { size: 4 },
|
||||
hovertemplate: '%{x}<br>Visitors: %{y}<extra></extra>',
|
||||
},
|
||||
], {
|
||||
...PLOTLY_LAYOUT,
|
||||
legend: { font: { size: 10, color: '#94a3b8' }, orientation: 'h', y: 1.1 },
|
||||
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: { text: 'Date', font: { size: 11 } } },
|
||||
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: { text: 'Count', font: { size: 11 } } },
|
||||
}, PLOTLY_CONFIG);
|
||||
} else {
|
||||
document.getElementById('dailyChart').innerHTML = '<p class="text-slate-500 text-sm text-center mt-20">No traffic data yet — check back after some visits</p>';
|
||||
}
|
||||
|
||||
// --- Hourly pattern ---
|
||||
const hourly = {{ data.hourly | tojson }};
|
||||
if (hourly.hours) {
|
||||
const hourLabels = hourly.hours.map(h => h.toString().padStart(2, '0') + ':00');
|
||||
Plotly.newPlot('hourlyChart', [{
|
||||
x: hourLabels, y: hourly.views,
|
||||
type: 'bar',
|
||||
marker: { color: 'rgba(168, 85, 247, 0.7)', line: { color: '#a855f7', width: 1 } },
|
||||
hovertemplate: '%{x}<br>Views: %{y}<extra></extra>',
|
||||
}], {
|
||||
...PLOTLY_LAYOUT,
|
||||
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: { text: 'Hour (UTC)', font: { size: 11 } } },
|
||||
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: { text: 'Views', font: { size: 11 } } },
|
||||
}, PLOTLY_CONFIG);
|
||||
} else {
|
||||
document.getElementById('hourlyChart').innerHTML = '<p class="text-slate-500 text-sm text-center mt-20">No hourly data yet</p>';
|
||||
}
|
||||
|
||||
// --- Downloads ---
|
||||
const downloads = {{ data.downloads_daily | tojson }};
|
||||
if (downloads.dates && downloads.dates.length > 0) {
|
||||
Plotly.newPlot('downloadsChart', [{
|
||||
x: downloads.dates, y: downloads.counts,
|
||||
type: 'bar',
|
||||
marker: { color: 'rgba(245, 158, 11, 0.7)', line: { color: '#f59e0b', width: 1 } },
|
||||
hovertemplate: '%{x}<br>Downloads: %{y}<extra></extra>',
|
||||
}], {
|
||||
...PLOTLY_LAYOUT,
|
||||
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: { text: 'Date', font: { size: 11 } } },
|
||||
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: { text: 'Downloads', font: { size: 11 } } },
|
||||
}, PLOTLY_CONFIG);
|
||||
} else {
|
||||
document.getElementById('downloadsChart').innerHTML = '<p class="text-slate-500 text-sm text-center mt-20">No downloads yet</p>';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -102,6 +102,7 @@
|
||||
<div class="text-slate-300 text-sm leading-relaxed whitespace-pre-line">{{ result.answer }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if is_admin %}
|
||||
<!-- Synthesize button (costs tokens, result is cached permanently) -->
|
||||
<div class="answer-card rounded-xl border border-slate-800 p-5 mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -118,6 +119,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{% block title %}Author Network — IETF Draft Analyzer{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="/static/js/plotly.min.js"></script>
|
||||
<script src="/static/js/d3.v7.min.js"></script>
|
||||
<style>
|
||||
#networkSvg {
|
||||
@@ -38,7 +39,7 @@
|
||||
{% block content %}
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-white">Author Network</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">Interactive collaboration graph of {{ network.nodes | length }} authors across {{ orgs | length }} organizations</p>
|
||||
<p class="text-slate-400 text-sm mt-1">Interactive collaboration graph of {{ network.nodes | length }} authors across {{ orgs | length }} organizations. Authors are connected when they co-authored 2+ drafts together. Node size reflects number of drafts authored; color represents organization. Clusters are detected via connected-component analysis (BFS) — authors in the same cluster share direct or indirect co-authorship links.</p>
|
||||
</div>
|
||||
|
||||
<!-- Summary stats -->
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}IETF Draft Analyzer{% endblock %}</title>
|
||||
<script src="/static/js/tailwind.js"></script>
|
||||
<script src="/static/js/plotly.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/fonts.css">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
@@ -118,10 +117,12 @@
|
||||
<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>
|
||||
{% 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>
|
||||
Gap Explorer
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/timeline" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'timeline' }}">
|
||||
<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
|
||||
@@ -142,10 +143,16 @@
|
||||
<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
|
||||
</a>
|
||||
{% if is_admin %}
|
||||
<a href="/monitor" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'monitor' }}">
|
||||
<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="M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M13 12a1 1 0 11-2 0 1 1 0 012 0z"/></svg>
|
||||
Monitor
|
||||
</a>
|
||||
<a href="/admin/analytics" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'analytics' }}">
|
||||
<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>
|
||||
Analytics
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="border-t border-slate-800 mt-4 pt-4">
|
||||
<a href="/about" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'about' }}">
|
||||
<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 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
@@ -159,6 +166,13 @@
|
||||
<a href="/impressum" class="hover:text-slate-400 transition">Impressum</a>
|
||||
<a href="/datenschutz" class="hover:text-slate-400 transition">Datenschutz</a>
|
||||
</div>
|
||||
{% if is_admin %}
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class="inline-block w-1.5 h-1.5 rounded-full bg-green-500"></span>
|
||||
<span class="text-green-500">Admin</span>
|
||||
<a href="/admin/logout" class="text-slate-600 hover:text-red-400 transition ml-auto">Logout</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
{% 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</p>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Summary stats -->
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
<svg class="w-4 h-4 text-slate-500" 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>
|
||||
AI Rating Analysis
|
||||
</h2>
|
||||
<p class="text-xs text-slate-500 mb-3">Rated by Claude AI on five dimensions (1–5 scale). The composite score is a weighted average. Ratings are generated from the draft's abstract and full text.</p>
|
||||
{% if draft.rating.summary %}
|
||||
<p class="text-sm text-slate-400 mb-5 leading-relaxed">{{ draft.rating.summary }}</p>
|
||||
{% endif %}
|
||||
@@ -140,6 +141,7 @@
|
||||
<svg class="w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
|
||||
Extracted Ideas <span class="text-slate-600 font-normal">({{ draft.ideas|length }})</span>
|
||||
</h2>
|
||||
<p class="text-xs text-slate-500 mb-3">Technical ideas extracted by Claude AI from the draft text. Each idea is classified by type (protocol, mechanism, framework, architecture) and rated for novelty (N:1–5).</p>
|
||||
<div class="space-y-3">
|
||||
{% for idea in draft.ideas %}
|
||||
<div class="bg-slate-800/30 rounded-lg p-4 border border-slate-800/50">
|
||||
@@ -174,7 +176,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Annotation (notes & tags) -->
|
||||
<!-- Annotation (notes & tags) — admin only -->
|
||||
{% if is_admin %}
|
||||
<div class="detail-card rounded-xl border border-slate-800 p-6" id="annotationSection">
|
||||
<h2 class="text-sm font-semibold text-slate-300 mb-3 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
||||
@@ -207,6 +210,7 @@
|
||||
</button>
|
||||
<div id="saveStatus" class="text-xs text-center mt-2 text-slate-600"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Right column: Sidebar -->
|
||||
@@ -242,6 +246,7 @@
|
||||
<svg class="w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>
|
||||
Standards Readiness
|
||||
</h2>
|
||||
<p class="text-xs text-slate-500 mb-2">Estimates how close a draft is to becoming a standard, based on six factors: working group adoption, revision count, reference density, citation count, author track record, and momentum signals. Score 0–100.</p>
|
||||
<!-- Gauge -->
|
||||
<div class="relative w-full h-6 bg-slate-800 rounded-full overflow-hidden mb-2">
|
||||
<div class="h-full rounded-full transition-all duration-700
|
||||
|
||||
@@ -53,6 +53,11 @@
|
||||
color: #4ade80;
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
.source-generated {
|
||||
background: rgba(168, 85, 247, 0.15);
|
||||
color: #c084fc;
|
||||
border: 1px solid rgba(168, 85, 247, 0.3);
|
||||
}
|
||||
.cat-pill {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
@@ -122,7 +127,7 @@
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-white">Draft Explorer</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">Browse, search, and filter {{ result.total }} rated Internet-Drafts on AI and agent topics.</p>
|
||||
<p class="text-slate-400 text-sm mt-1">Browse, search, and filter {{ result.total }} rated Internet-Drafts on AI and agent topics. Each draft is scored 1–5 across five dimensions: <b>Nov</b>(elty) = originality, <b>Mat</b>(urity) = specification completeness, <b>Rel</b>(evance) = importance to AI agents, <b>Mom</b>(entum) = adoption traction, <b>Ovl</b> (overlap) = uniqueness vs other drafts. <b>Rdy</b> = standards readiness (0–100). Search works across draft names, titles, summaries, and author names.</p>
|
||||
</div>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
@@ -157,6 +162,7 @@
|
||||
<option value="">All sources</option>
|
||||
<option value="ietf" {% if current_source == 'ietf' %}selected{% endif %}>IETF</option>
|
||||
<option value="w3c" {% if current_source == 'w3c' %}selected{% endif %}>W3C</option>
|
||||
<option value="generated" {% if current_source == 'generated' %}selected{% endif %}>Generated</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Sort -->
|
||||
@@ -224,6 +230,64 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Category Summary -->
|
||||
{% if cat_summary and current_cat %}
|
||||
<div class="bg-slate-900/80 rounded-xl border border-slate-800 p-5 mb-6">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-1">
|
||||
<h2 class="text-lg font-semibold text-white mb-2">{{ current_cat }}</h2>
|
||||
<p class="text-sm text-slate-300 leading-relaxed">{{ cat_summary.text }}</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0 text-right">
|
||||
<div class="text-3xl font-bold text-blue-400">{{ cat_summary.avg_score }}<span class="text-base text-slate-500">/5</span></div>
|
||||
<div class="text-[10px] text-slate-500 uppercase tracking-wider">Avg Score</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4 pt-4 border-t border-slate-800/60">
|
||||
<!-- Dimension profile -->
|
||||
<div>
|
||||
<h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Rating Profile</h3>
|
||||
{% for dim, val in cat_summary.dimensions.items() %}
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-xs text-slate-500 w-20">{{ dim }}</span>
|
||||
<div class="flex-1 bg-slate-800 rounded-full h-1.5 overflow-hidden">
|
||||
<div class="h-full rounded-full {% if val >= 3.5 %}bg-emerald-500{% elif val >= 2.5 %}bg-amber-500{% else %}bg-red-500{% endif %}"
|
||||
style="width: {{ (val / 5 * 100)|round }}%"></div>
|
||||
</div>
|
||||
<span class="text-xs font-mono {% if val >= 3.5 %}text-emerald-400{% elif val >= 2.5 %}text-amber-400{% else %}text-red-400{% endif %} w-6 text-right">{{ val }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Top drafts -->
|
||||
<div>
|
||||
<h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Top Drafts</h3>
|
||||
{% for name, title, score in cat_summary.top_drafts %}
|
||||
<div class="mb-1.5">
|
||||
<a href="/drafts/{{ name }}" class="text-xs text-blue-400 hover:text-blue-300 transition">{{ title|truncate(50) }}</a>
|
||||
<span class="text-xs text-slate-600 ml-1">{{ score }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Top authors + orgs -->
|
||||
<div>
|
||||
<h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Key Contributors</h3>
|
||||
{% for author, count in cat_summary.top_authors %}
|
||||
<div class="text-xs text-slate-300 mb-0.5">{{ author }} <span class="text-slate-600">({{ count }})</span></div>
|
||||
{% endfor %}
|
||||
{% if cat_summary.top_orgs %}
|
||||
<h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mt-3 mb-1">Organizations</h3>
|
||||
{% for org, count in cat_summary.top_orgs[:3] %}
|
||||
<div class="text-xs text-slate-300 mb-0.5">{{ org }} <span class="text-slate-600">({{ count }})</span></div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Results count + Compare button -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<p class="text-sm text-slate-500">
|
||||
@@ -234,12 +298,14 @@
|
||||
{% if min_score > 0 %} with score >= <span class="text-blue-400">{{ min_score }}</span>{% endif %}
|
||||
</p>
|
||||
<div class="flex items-center gap-3">
|
||||
{% if is_admin %}
|
||||
<span id="compareCount" class="text-xs text-slate-600 hidden"><span id="compareNum">0</span> selected</span>
|
||||
<button onclick="goCompare()" id="compareBtn"
|
||||
class="px-4 py-1.5 bg-slate-800 text-slate-500 rounded-lg text-xs font-medium border border-slate-700 cursor-not-allowed transition-colors hidden"
|
||||
disabled>
|
||||
Compare Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if result.pages > 1 %}
|
||||
<p class="text-xs text-slate-600">Page {{ result.page }} of {{ result.pages }}</p>
|
||||
{% endif %}
|
||||
@@ -268,9 +334,11 @@
|
||||
</a>
|
||||
</th>
|
||||
{% endmacro %}
|
||||
{% if is_admin %}
|
||||
<th class="px-2 py-3 w-8">
|
||||
<span class="text-xs text-slate-600" title="Select drafts to compare">Cmp</span>
|
||||
</th>
|
||||
{% endif %}
|
||||
{{ sort_header("score", "Score", "w-20") }}
|
||||
{{ sort_header("name", "Draft") }}
|
||||
{{ sort_header("date", "Date", "w-24 hidden md:table-cell") }}
|
||||
@@ -286,11 +354,13 @@
|
||||
<tbody class="divide-y divide-slate-800/30">
|
||||
{% for d in result.drafts %}
|
||||
<tr class="draft-row">
|
||||
{% if is_admin %}
|
||||
<!-- Compare checkbox -->
|
||||
<td class="px-2 py-3 text-center">
|
||||
<input type="checkbox" class="compare-check rounded border-slate-600 bg-slate-800 text-blue-500 focus:ring-blue-500/30 focus:ring-offset-0 w-3.5 h-3.5 cursor-pointer"
|
||||
data-name="{{ d.name }}" onchange="updateCompare()">
|
||||
</td>
|
||||
{% endif %}
|
||||
<!-- Score badge -->
|
||||
<td class="px-4 py-3">
|
||||
<span class="score-badge {% if d.score >= 3.5 %}score-high{% elif d.score >= 2.5 %}score-mid{% else %}score-low{% endif %}">
|
||||
@@ -370,7 +440,7 @@
|
||||
{% endfor %}
|
||||
{% if not result.drafts %}
|
||||
<tr>
|
||||
<td colspan="11" class="px-4 py-12 text-center text-slate-500">
|
||||
<td colspan="{{ 11 if is_admin else 10 }}" class="px-4 py-12 text-center text-slate-500">
|
||||
<svg class="w-12 h-12 mx-auto mb-3 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% block content %}
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-white">Gap Explorer</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">{{ gaps | length }} identified gaps in AI/agent standards coverage — click any gap to explore details or generate a draft</p>
|
||||
<p class="text-slate-400 text-sm mt-1">{{ gaps | length }} identified gaps in AI/agent standards coverage. Gaps are identified by Claude AI analyzing the full corpus of drafts to find areas where important problems lack adequate proposals. Severity reflects urgency: <span class="text-red-400">critical</span> = blocking issue with no draft addressing it, <span class="text-orange-400">high</span> = partially addressed but incomplete, <span class="text-yellow-400">medium</span> = some coverage exists but more work needed, <span class="text-green-400">low</span> = minor gap or niche concern.</p>
|
||||
</div>
|
||||
|
||||
<!-- Action bar -->
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
{% block title %}Idea Clusters — 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 Clusters</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">Extracted ideas grouped by semantic similarity — enriched with WG and category data</p>
|
||||
<p class="text-slate-400 text-sm mt-1">Extracted ideas grouped by semantic similarity — enriched with WG and category data. Ideas are embedded using Ollama (nomic-embed-text), then clustered via DBSCAN so that semantically related ideas from different drafts are grouped together. "Cross-WG" clusters indicate ideas that span multiple IETF working groups — potential coordination points.</p>
|
||||
</div>
|
||||
|
||||
<div id="emptyState" class="hidden">
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
{% block title %}Ideas — 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">Extracted Ideas</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">{{ data.total }} technical ideas extracted from rated drafts</p>
|
||||
<p class="text-slate-400 text-sm mt-1">{{ data.total }} technical ideas extracted from rated drafts. Claude AI reads each draft and identifies distinct technical contributions — protocols, mechanisms, frameworks, and architectures. Each idea receives a novelty score (N:1–5) indicating how original it is compared to existing work.</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats header -->
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
{% block title %}Landscape — 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 Landscape</h1>
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
{% block title %}Monitor — 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">Live Monitor</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">Track automated monitoring runs and pipeline status</p>
|
||||
<p class="text-slate-400 text-sm mt-1">Track automated monitoring runs and pipeline status. The pipeline fetches new drafts from the IETF Datatracker, rates them with Claude AI, generates embeddings with Ollama, and extracts ideas. Unprocessed counts show drafts waiting at each stage.</p>
|
||||
</div>
|
||||
|
||||
<div id="monitor-app"></div>
|
||||
|
||||
@@ -3,10 +3,20 @@
|
||||
|
||||
{% block title %}Overview — IETF Draft Analyzer{% endblock %}
|
||||
|
||||
{% block extra_head %}<script src="/static/js/plotly.min.js"></script>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-white">Dashboard Overview</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">IETF AI/Agent Internet-Drafts at a glance</p>
|
||||
<div class="mb-8 flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Dashboard Overview</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">IETF AI/Agent Internet-Drafts at a glance. Drafts are fetched from the IETF Datatracker, then analyzed by Claude AI across five dimensions (novelty, maturity, overlap, momentum, relevance) to produce a composite score from 1.0 to 5.0.</p>
|
||||
</div>
|
||||
<a href="/export/obsidian"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-purple-600/80 hover:bg-purple-500 text-white text-sm font-medium rounded-lg transition-colors flex-shrink-0"
|
||||
title="Download all research data as an Obsidian vault with interlinked notes, Mermaid charts, and YAML frontmatter">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||
Download for Obsidian
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Stat cards -->
|
||||
@@ -41,11 +51,13 @@
|
||||
<!-- Charts row 1: Score distribution + Category donut -->
|
||||
<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">Composite Score Distribution</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-300 mb-1">Composite Score Distribution</h2>
|
||||
<p class="text-xs text-slate-500 mb-3">Weighted average of five AI-rated dimensions (novelty 20%, maturity 20%, uniqueness 20%, momentum 20%, relevance 20%). Higher scores indicate drafts that are novel, mature, unique, gaining traction, and highly relevant to AI agent infrastructure.</p>
|
||||
<div id="scoreHist" 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-3">Drafts by Category</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-300 mb-1">Drafts by Category</h2>
|
||||
<p class="text-xs text-slate-500 mb-3">Categories are assigned by Claude during analysis. A draft can belong to multiple categories (e.g., both "A2A protocols" and "AI safety/alignment").</p>
|
||||
<div id="categoryPie" style="height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,22 +3,26 @@
|
||||
|
||||
{% block title %}Ratings — 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">Rating Analytics</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">Distribution and analysis of AI-generated ratings</p>
|
||||
<p class="text-slate-400 text-sm mt-1">Distribution and analysis of AI-generated ratings across five dimensions. Each draft is rated 1–5 on novelty, maturity, overlap, momentum, and relevance by Claude AI, then combined into a weighted composite score.</p>
|
||||
</div>
|
||||
|
||||
<!-- Score 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-3">Composite Score Distribution</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-300 mb-1">Composite Score Distribution</h2>
|
||||
<p class="text-xs text-slate-500 mb-3">The composite score is a weighted average of all five dimensions (each 20%). Scores range from 1.0 (low) to 5.0 (high). Most drafts cluster in the 2.0–3.5 range.</p>
|
||||
<div id="scoreHist" style="height: 300px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<!-- Dimension Box Plots -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
|
||||
<h2 class="text-sm font-semibold text-slate-300 mb-3">Score Distributions by Dimension</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-300 mb-1">Score Distributions by Dimension</h2>
|
||||
<p class="text-xs text-slate-500 mb-3"><b>Novelty</b>: originality of ideas. <b>Maturity</b>: completeness and specification detail. <b>Overlap</b>: redundancy with other drafts (high = more unique). <b>Momentum</b>: adoption likelihood and community traction. <b>Relevance</b>: importance to AI agent infrastructure.</p>
|
||||
<div id="dimDist" style="height: 350px;"></div>
|
||||
</div>
|
||||
<!-- Category Radar -->
|
||||
@@ -30,7 +34,8 @@
|
||||
|
||||
<!-- 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-3">Novelty vs Maturity (bubble = relevance)</h2>
|
||||
<h2 class="text-sm font-semibold text-slate-300 mb-1">Novelty vs Maturity (bubble = relevance)</h2>
|
||||
<p class="text-xs text-slate-500 mb-3">Each dot is a rated draft. Drafts in the top-right corner are both novel and mature — prime candidates for standardization. Bubble size reflects relevance. Click a point to view the draft.</p>
|
||||
<div id="scatter" style="height: 450px;"></div>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +43,7 @@
|
||||
<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 Drafts by Composite Score</h2>
|
||||
<p class="text-xs text-slate-500 mt-1">Highest-rated drafts across all dimensions. Green (4+) = strong, amber (3) = moderate, grey (<3) = needs improvement.</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
@@ -166,6 +172,7 @@ document.getElementById('scatter').on('plotly_click', function(data) {
|
||||
momentum: dist.momentum[i],
|
||||
overlap: dist.overlap[i],
|
||||
category: dist.categories[i],
|
||||
source: (dist.sources || [])[i] || 'ietf',
|
||||
}));
|
||||
drafts.sort((a, b) => b.score - a.score);
|
||||
|
||||
@@ -185,12 +192,13 @@ document.getElementById('scatter').on('plotly_click', function(data) {
|
||||
|
||||
top20.forEach((d, i) => {
|
||||
const shortName = d.name.replace('draft-', '').substring(0, 40);
|
||||
const sourceBadge = d.source !== 'ietf' ? ` <span style="display:inline-block;padding:1px 5px;border-radius:4px;font-size:0.55rem;font-weight:600;background:rgba(168,85,247,0.15);color:#c084fc;border:1px solid rgba(168,85,247,0.3);vertical-align:middle">${d.source.toUpperCase()}</span>` : '';
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-slate-800/50 transition';
|
||||
row.innerHTML = `
|
||||
<td class="px-4 py-3 text-slate-500 font-mono text-xs">${i + 1}</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">${shortName}</a>
|
||||
<a href="/drafts/${d.name}" class="text-blue-400 hover:text-blue-300 transition text-xs font-mono">${shortName}</a>${sourceBadge}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<span class="score-badge ${scoreClass(d.score)}">${d.score.toFixed(2)}</span>
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
{% block title %}Similarity — 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 Similarity Graph</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">Force-directed graph of draft-to-draft semantic similarity based on embeddings</p>
|
||||
<p class="text-slate-400 text-sm mt-1">Force-directed graph of draft-to-draft semantic similarity. Each draft is embedded using Ollama (nomic-embed-text) and cosine similarity is computed between all pairs. Edges are drawn between drafts exceeding the similarity threshold — tightly connected clusters indicate drafts covering similar topics. Use the slider to adjust the threshold: higher values show only the strongest relationships.</p>
|
||||
</div>
|
||||
|
||||
<!-- Summary stats -->
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
{% block title %}Timeline — 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">Timeline Animation</h1>
|
||||
@@ -53,6 +55,22 @@ const points = animData.points;
|
||||
const months = animData.months;
|
||||
const catMonthly = animData.category_monthly;
|
||||
|
||||
const MONTH_NAMES = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||
function fmtMonth(ym) {
|
||||
if (!ym) return ym;
|
||||
let y, m;
|
||||
if (ym.includes('-')) {
|
||||
[y, m] = ym.split('-');
|
||||
} else if (ym.length >= 6) {
|
||||
y = ym.slice(0, 4);
|
||||
m = ym.slice(4, 6);
|
||||
} else {
|
||||
return ym;
|
||||
}
|
||||
const mi = parseInt(m, 10) - 1;
|
||||
return (MONTH_NAMES[mi] || m) + ' ' + y;
|
||||
}
|
||||
|
||||
if (points.length > 0 && months.length > 0) {
|
||||
|
||||
// --- Stat cards ---
|
||||
@@ -64,7 +82,7 @@ if (points.length > 0 && months.length > 0) {
|
||||
<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-3xl font-bold text-blue-400">${months.length}</div>
|
||||
<div class="text-xs text-slate-400 mt-1 uppercase tracking-wider">Months Span</div>
|
||||
<div class="text-xs text-slate-500 mt-0.5">${firstMonth} to ${lastMonth}</div>
|
||||
<div class="text-xs text-slate-500 mt-0.5">${fmtMonth(firstMonth)} – ${fmtMonth(lastMonth)}</div>
|
||||
</div>
|
||||
<div class="stat-card rounded-xl border border-slate-800 p-5 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>
|
||||
@@ -130,7 +148,7 @@ if (points.length > 0 && months.length > 0) {
|
||||
// Slider steps
|
||||
const sliderSteps = months.map(month => ({
|
||||
method: 'animate',
|
||||
label: month,
|
||||
label: fmtMonth(month),
|
||||
args: [[month], { frame: { duration: 500, redraw: true }, transition: { duration: 300 }, mode: 'immediate' }],
|
||||
}));
|
||||
|
||||
@@ -182,12 +200,12 @@ if (points.length > 0 && months.length > 0) {
|
||||
|
||||
// Update badge on animation frame
|
||||
const badge = document.querySelector('#monthBadge span');
|
||||
badge.textContent = `Month: ${months[0]} (${firstCount} drafts)`;
|
||||
badge.textContent = `${fmtMonth(months[0])} — ${firstCount} drafts`;
|
||||
|
||||
document.getElementById('tsneAnim').on('plotly_animatingframe', function(ev) {
|
||||
const month = ev.name;
|
||||
const cumCount = points.filter(p => p.month <= month).length;
|
||||
badge.textContent = `Month: ${month} (${cumCount} drafts)`;
|
||||
badge.textContent = `${fmtMonth(month)} — ${cumCount} drafts`;
|
||||
});
|
||||
|
||||
// Click to navigate
|
||||
@@ -211,8 +229,9 @@ if (points.length > 0 && months.length > 0) {
|
||||
return totalB - totalA;
|
||||
});
|
||||
|
||||
const monthLabels = months.map(fmtMonth);
|
||||
const areaTraces = areaCatList.map((cat, i) => ({
|
||||
x: months,
|
||||
x: monthLabels,
|
||||
y: months.map(m => (catMonthly[m] || {})[cat] || 0),
|
||||
name: cat,
|
||||
type: 'scatter',
|
||||
|
||||
Reference in New Issue
Block a user