- Add `ietf auto` command: fetches, analyzes, embeds, extracts ideas, and refreshes gaps across all sources with cost-based auto-approval - Fix SourceDocument→Draft conversion in auto fetch step - Fix gap_analysis method name in auto command - Process all 270 unrated ETSI/ISO/ITU/NIST drafts (761 total, all rated) - Update web UI templates and data layer for multi-source support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
227 lines
10 KiB
HTML
227 lines
10 KiB
HTML
{% extends "base.html" %}
|
|
{% set active_page = "drafts" %}
|
|
|
|
{% block title %}Compare Drafts — IETF Draft Analyzer{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
.compare-card {
|
|
background: linear-gradient(135deg, rgba(30, 41, 59, 0.8), rgba(30, 41, 59, 0.4));
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
.idea-shared { background: rgba(34, 197, 94, 0.1); border-color: rgba(34, 197, 94, 0.2); }
|
|
.idea-unique { background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.2); }
|
|
.ref-pill {
|
|
display: inline-block;
|
|
padding: 1px 8px;
|
|
border-radius: 9999px;
|
|
font-size: 0.65rem;
|
|
font-weight: 500;
|
|
background: rgba(51, 65, 85, 0.5);
|
|
color: #94a3b8;
|
|
border: 1px solid rgba(71, 85, 105, 0.4);
|
|
}
|
|
.loading-spinner {
|
|
border: 3px solid rgba(59, 130, 246, 0.2);
|
|
border-top-color: #3b82f6;
|
|
border-radius: 50%;
|
|
width: 20px;
|
|
height: 20px;
|
|
animation: spin 0.8s linear infinite;
|
|
display: inline-block;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<h1 class="text-2xl font-bold text-white">Compare Drafts</h1>
|
|
<p class="text-slate-400 text-sm mt-1">Side-by-side analysis of selected drafts: shared ideas, references, and AI-generated comparison.</p>
|
|
</div>
|
|
|
|
{% if not data %}
|
|
<!-- No data yet — show instructions -->
|
|
<div class="compare-card rounded-xl border border-slate-800 p-8 text-center max-w-xl mx-auto">
|
|
<svg class="w-12 h-12 mx-auto mb-4 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
|
|
</svg>
|
|
{% if names and names|length < 2 %}
|
|
<p class="text-slate-400 text-sm mb-4">Need at least 2 valid draft names to compare.</p>
|
|
{% else %}
|
|
<p class="text-slate-400 text-sm mb-4">Select drafts to compare from the <a href="/drafts" class="text-blue-400 hover:text-blue-300">Draft Explorer</a>, or enter draft names below.</p>
|
|
{% endif %}
|
|
<form method="get" action="/compare" class="mt-4">
|
|
<input type="text" name="drafts" placeholder="draft-name-1, draft-name-2, ..."
|
|
value="{{ names | join(', ') if names else '' }}"
|
|
class="w-full bg-slate-800/60 border border-slate-700 rounded-lg px-4 py-2.5 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-blue-500 mb-3">
|
|
<button type="submit" class="px-6 py-2.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-500 transition-colors">
|
|
Compare
|
|
</button>
|
|
</form>
|
|
</div>
|
|
{% else %}
|
|
|
|
<!-- Draft cards side by side -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-{{ data.drafts|length }} gap-4 mb-6">
|
|
{% for draft in data.drafts %}
|
|
<div class="compare-card rounded-xl border border-slate-800 p-5">
|
|
<a href="/drafts/{{ draft.name }}" class="text-blue-400 hover:text-blue-300 font-semibold text-sm transition">
|
|
{{ draft.title }}
|
|
</a>
|
|
<div class="text-xs text-slate-600 font-mono mt-1">{{ draft.name }}</div>
|
|
<div class="text-xs text-slate-500 mt-2 line-clamp-3">{{ (draft.abstract | striptags)[:200] }}</div>
|
|
|
|
{% if draft.rating %}
|
|
<!-- Rating radar -->
|
|
<div class="mt-3 grid grid-cols-5 gap-1 text-center">
|
|
{% for dim, label in [('novelty', 'Nov'), ('maturity', 'Mat'), ('relevance', 'Rel'), ('momentum', 'Mom'), ('overlap', 'Ovl')] %}
|
|
<div>
|
|
<div class="text-xs text-slate-500">{{ label }}</div>
|
|
{% if dim == 'overlap' %}
|
|
<div class="text-sm font-semibold {% if draft.rating[dim] <= 2 %}text-green-400{% elif draft.rating[dim] <= 3 %}text-yellow-400{% else %}text-red-400{% endif %}">
|
|
{{ draft.rating[dim] }}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-sm font-semibold {% if draft.rating[dim] >= 4 %}text-green-400{% elif draft.rating[dim] >= 3 %}text-yellow-400{% else %}text-red-400{% endif %}">
|
|
{{ draft.rating[dim] }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="text-center mt-2">
|
|
<span class="score-badge {% if draft.rating.score >= 3.5 %}score-high{% elif draft.rating.score >= 2.5 %}score-mid{% else %}score-low{% endif %}">
|
|
{{ draft.rating.score }}
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Pairwise similarities -->
|
|
{% if data.similarities %}
|
|
<div class="compare-card rounded-xl border border-slate-800 p-5 mb-6">
|
|
<h3 class="text-sm font-semibold text-slate-300 mb-3">Pairwise Embedding Similarity</h3>
|
|
<div class="space-y-2">
|
|
{% for sim in data.similarities %}
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-xs font-mono text-slate-400 w-1/3 truncate">{{ sim.a.split('-')[-1][:20] }}</span>
|
|
<span class="text-xs text-slate-600">↔</span>
|
|
<span class="text-xs font-mono text-slate-400 w-1/3 truncate">{{ sim.b.split('-')[-1][:20] }}</span>
|
|
<div class="flex-1 h-2 bg-slate-800 rounded overflow-hidden">
|
|
<div class="h-full rounded {% if sim.similarity >= 0.85 %}bg-green-500{% elif sim.similarity >= 0.7 %}bg-yellow-500{% else %}bg-blue-500{% endif %}"
|
|
style="width: {{ (sim.similarity * 100)|int }}%"></div>
|
|
</div>
|
|
<span class="text-xs font-mono font-semibold w-12 text-right {% if sim.similarity >= 0.85 %}text-green-400{% elif sim.similarity >= 0.7 %}text-yellow-400{% else %}text-blue-400{% endif %}">
|
|
{{ "%.3f"|format(sim.similarity) }}
|
|
</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Shared Ideas -->
|
|
{% if data.shared_ideas %}
|
|
<div class="compare-card rounded-xl border border-slate-800 p-5 mb-6">
|
|
<h3 class="text-sm font-semibold text-green-400 mb-3">Shared Ideas ({{ data.shared_ideas|length }})</h3>
|
|
<div class="space-y-2">
|
|
{% for idea in data.shared_ideas %}
|
|
<div class="idea-shared rounded-lg border p-3">
|
|
<div class="text-sm text-slate-200 font-medium">{{ idea.title }}</div>
|
|
<div class="text-xs text-slate-500 mt-1">Found in: {{ idea.drafts | join(', ') }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Unique Ideas per draft -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-{{ data.drafts|length }} gap-4 mb-6">
|
|
{% for draft in data.drafts %}
|
|
<div class="compare-card rounded-xl border border-slate-800 p-5">
|
|
<h3 class="text-sm font-semibold text-blue-400 mb-3">
|
|
Unique Ideas: {{ draft.name.split('-')[-1][:20] }}
|
|
<span class="text-slate-600 font-normal">({{ data.unique_ideas.get(draft.name, [])|length }})</span>
|
|
</h3>
|
|
<div class="space-y-2">
|
|
{% for idea in data.unique_ideas.get(draft.name, [])[:10] %}
|
|
<div class="idea-unique rounded-lg border p-2.5">
|
|
<div class="text-xs text-slate-300 font-medium">{{ idea.title }}</div>
|
|
{% if idea.description %}
|
|
<div class="text-xs text-slate-500 mt-0.5 line-clamp-2">{{ idea.description }}</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
{% if data.unique_ideas.get(draft.name, [])|length == 0 %}
|
|
<div class="text-xs text-slate-600 italic">No unique ideas extracted</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Shared References -->
|
|
{% if data.shared_refs %}
|
|
<div class="compare-card rounded-xl border border-slate-800 p-5 mb-6">
|
|
<h3 class="text-sm font-semibold text-slate-300 mb-3">Shared References ({{ data.shared_refs|length }})</h3>
|
|
<div class="flex flex-wrap gap-1.5">
|
|
{% for ref in data.shared_refs %}
|
|
<span class="ref-pill">{{ ref.type|upper }} {{ ref.id }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Claude Comparison (lazy-loaded) -->
|
|
<div class="compare-card rounded-xl border border-slate-800 p-5 mb-6" id="comparisonSection">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h3 class="text-sm font-semibold text-slate-300">AI Comparison Summary</h3>
|
|
<button onclick="runComparison()" id="compareBtn"
|
|
class="px-4 py-1.5 bg-blue-600 text-white rounded-lg text-xs font-medium hover:bg-blue-500 transition-colors">
|
|
Generate Comparison
|
|
</button>
|
|
</div>
|
|
<div id="comparisonResult" class="text-sm text-slate-400">
|
|
Click "Generate Comparison" to get a Claude-powered analysis of these drafts.
|
|
</div>
|
|
</div>
|
|
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
{% if data %}
|
|
<script>
|
|
async function runComparison() {
|
|
const btn = document.getElementById('compareBtn');
|
|
const result = document.getElementById('comparisonResult');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="loading-spinner"></span> Analyzing...';
|
|
result.innerHTML = '<div class="flex items-center gap-2"><span class="loading-spinner"></span> <span class="text-slate-500">Generating comparison...</span></div>';
|
|
|
|
try {
|
|
const resp = await fetch('/api/compare', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({drafts: {{ data.drafts | map(attribute='name') | list | tojson }}})
|
|
});
|
|
const data = await resp.json();
|
|
if (data.error) {
|
|
result.innerHTML = '<div class="text-red-400">Error: ' + data.error + '</div>';
|
|
} else {
|
|
result.innerHTML = '<div class="text-slate-300 whitespace-pre-line leading-relaxed">' + data.text + '</div>';
|
|
}
|
|
} catch (e) {
|
|
result.innerHTML = '<div class="text-red-400">Error: ' + e.message + '</div>';
|
|
}
|
|
btn.disabled = false;
|
|
btn.textContent = 'Regenerate';
|
|
}
|
|
</script>
|
|
{% endif %}
|
|
{% endblock %}
|