Files
ietf-draft-analyzer/src/webui/templates/comparison.html
Christian Nennemann a46a01bd8c Add auto-heal pipeline command and fix multi-source draft processing
- 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>
2026-03-08 18:41:42 +01:00

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">&harr;</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 %}