Files
ietf-draft-analyzer/src/webui/templates/draft_detail.html
Christian Nennemann c755b2bbf3 Add author detail, idea detail, and gap-draft reverse link pages
- Author detail page (/authors/<person_id>): shows author info, all drafts
  with ratings, and co-authors with shared draft counts. Public route.
- Idea detail page (/ideas/<idea_id>): shows idea metadata, source draft,
  and top-5 most similar ideas via embedding cosine similarity. Admin route.
- Gap detail page: added "Related Drafts" section that finds drafts by
  extracting draft names from evidence text and searching by topic keywords.
- Updated author links across templates to use /authors/<person_id> URLs.
- Added DB methods: get_author_by_id, get_author_drafts, get_coauthors.
- Extended top_authors to include person_id (5th tuple element).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 03:45:00 +01:00

479 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% set active_page = "drafts" %}
{% block title %}{{ draft.name }} — IETF Draft Analyzer{% endblock %}
{% block extra_head %}
<style>
.detail-card {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.8), rgba(30, 41, 59, 0.4));
backdrop-filter: blur(10px);
}
.score-ring {
width: 100px;
height: 100px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
position: relative;
}
.score-ring::before {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
padding: 4px;
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask-composite: exclude;
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
}
.score-ring-high::before { background: linear-gradient(135deg, #22c55e, #4ade80); }
.score-ring-mid::before { background: linear-gradient(135deg, #eab308, #facc15); }
.score-ring-low::before { background: linear-gradient(135deg, #ef4444, #f87171); }
.dim-progress {
height: 8px;
border-radius: 4px;
background: rgba(51, 65, 85, 0.5);
overflow: hidden;
}
.dim-progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.6s ease;
}
.dim-high { background: linear-gradient(90deg, #22c55e, #4ade80); }
.dim-mid { background: linear-gradient(90deg, #eab308, #facc15); }
.dim-low { background: linear-gradient(90deg, #ef4444, #f87171); }
.idea-type-protocol { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border-color: rgba(59, 130, 246, 0.3); }
.idea-type-mechanism { background: rgba(168, 85, 247, 0.15); color: #c084fc; border-color: rgba(168, 85, 247, 0.3); }
.idea-type-framework { background: rgba(34, 197, 94, 0.15); color: #4ade80; border-color: rgba(34, 197, 94, 0.3); }
.idea-type-architecture { background: rgba(234, 179, 8, 0.15); color: #facc15; border-color: rgba(234, 179, 8, 0.3); }
.idea-type-default { background: rgba(100, 116, 139, 0.15); color: #94a3b8; border-color: rgba(100, 116, 139, 0.3); }
.ref-rfc { background: rgba(34, 197, 94, 0.15); color: #4ade80; }
.ref-draft { background: rgba(59, 130, 246, 0.15); color: #60a5fa; }
.ref-other { background: rgba(234, 179, 8, 0.15); color: #facc15; }
</style>
{% endblock %}
{% block content %}
<!-- Breadcrumb + Header -->
<div class="mb-6">
<a href="/drafts" class="inline-flex items-center gap-1.5 text-sm text-slate-500 hover:text-slate-300 transition group">
<svg class="w-4 h-4 group-hover:-translate-x-0.5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
Back to Explorer
</a>
<h1 class="text-xl font-bold text-white mt-3 leading-snug">{{ draft.title }}</h1>
<div class="flex flex-wrap items-center gap-3 mt-2">
<span class="text-sm text-slate-400 font-mono">{{ draft.name }}</span>
{% if draft.rev %}
<span class="text-xs px-2 py-0.5 rounded bg-slate-800 text-slate-500 border border-slate-700">rev {{ draft.rev }}</span>
{% endif %}
<span class="text-xs text-slate-600">{{ draft.date }}</span>
{% if draft.rating %}
<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>
{% endif %}
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Left column: Main content -->
<div class="lg:col-span-2 space-y-6">
<!-- Abstract -->
<div class="detail-card rounded-xl border border-slate-800 p-6">
<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="M4 6h16M4 12h16M4 18h7"/></svg>
Abstract
</h2>
<p class="text-sm text-slate-400 leading-relaxed">{{ (draft.abstract | striptags) or "No abstract available." }}</p>
</div>
<!-- Rating Analysis -->
{% if draft.rating %}
<div class="detail-card rounded-xl border border-slate-800 p-6">
<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="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 (15 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 %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for dim, label, icon in [
("novelty", "Novelty", "M13 10V3L4 14h7v7l9-11h-7z"),
("maturity", "Maturity", "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"),
("overlap", "Overlap", "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"),
("momentum", "Momentum", "M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"),
("relevance", "Relevance", "M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z")
] %}
{% set val = draft.rating[dim] %}
<div class="bg-slate-800/30 rounded-lg p-4 border border-slate-800/50">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<svg class="w-3.5 h-3.5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ icon }}"/></svg>
<span class="text-xs font-semibold text-slate-300 uppercase tracking-wide">{{ label }}</span>
</div>
{% if dim == "overlap" %}
<span class="text-lg font-bold {% if val <= 2 %}text-green-400{% elif val <= 3 %}text-amber-400{% else %}text-red-400{% endif %}">{{ val }}<span class="text-xs text-slate-600 font-normal">/5</span></span>
{% else %}
<span class="text-lg font-bold {% if val >= 4 %}text-green-400{% elif val >= 3 %}text-amber-400{% else %}text-red-400{% endif %}">{{ val }}<span class="text-xs text-slate-600 font-normal">/5</span></span>
{% endif %}
</div>
<div class="dim-progress mb-2">
{% if dim == "overlap" %}
<div class="dim-progress-fill {% if val <= 2 %}dim-high{% elif val <= 3 %}dim-mid{% else %}dim-low{% endif %}" style="width: {{ val * 20 }}%"></div>
{% else %}
<div class="dim-progress-fill {% if val >= 4 %}dim-high{% elif val >= 3 %}dim-mid{% else %}dim-low{% endif %}" style="width: {{ val * 20 }}%"></div>
{% endif %}
</div>
{% if draft.rating[dim + '_note'] %}
<p class="text-xs text-slate-500 leading-relaxed">{{ draft.rating[dim + '_note'] }}</p>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Ideas -->
{% if draft.ideas %}
<div class="detail-card rounded-xl border border-slate-800 p-6">
<h2 class="text-sm font-semibold text-slate-300 mb-4 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="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:15).</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">
<div class="flex items-start gap-2 mb-1">
<span class="text-sm font-medium text-slate-200 leading-snug">{{ idea.title }}</span>
{% if idea.type %}
{% set type_lower = idea.type|lower %}
<span class="flex-shrink-0 px-2 py-0.5 rounded-full text-[10px] font-medium border
{% if type_lower == 'protocol' %}idea-type-protocol
{% elif type_lower == 'mechanism' %}idea-type-mechanism
{% elif type_lower == 'framework' %}idea-type-framework
{% elif type_lower == 'architecture' %}idea-type-architecture
{% else %}idea-type-default{% endif %}">
{{ idea.type }}
</span>
{% endif %}
{% if idea.novelty_score is not none and idea.novelty_score %}
<span class="flex-shrink-0 px-1.5 py-0.5 rounded text-[10px] font-mono
{% if idea.novelty_score >= 4 %}bg-green-500/20 text-green-400
{% elif idea.novelty_score >= 3 %}bg-amber-500/20 text-amber-400
{% elif idea.novelty_score >= 2 %}bg-orange-500/20 text-orange-400
{% else %}bg-red-500/20 text-red-400{% endif %}"
title="Novelty score">N:{{ idea.novelty_score }}</span>
{% endif %}
</div>
{% if idea.description %}
<p class="text-xs text-slate-500 leading-relaxed mt-1">{{ idea.description }}</p>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- 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>
Notes & Tags
</h2>
<div class="mb-3">
<textarea id="annotNote" rows="3" placeholder="Add a private note about this draft..."
class="w-full bg-slate-800/60 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-blue-500 resize-y">{{ draft.annotation.note if draft.annotation else '' }}</textarea>
</div>
<div class="mb-3">
<div class="flex flex-wrap gap-1.5 mb-2" id="tagContainer">
{% if draft.annotation and draft.annotation.tags %}
{% for tag in draft.annotation.tags %}
<span class="tag-chip inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-blue-500/20 text-blue-400 border border-blue-500/30">
{{ tag }}
<button onclick="removeTag('{{ tag }}')" class="hover:text-red-400 transition ml-0.5">&times;</button>
</span>
{% endfor %}
{% endif %}
</div>
<div class="flex gap-2">
<input type="text" id="newTag" placeholder="Add tag..." maxlength="30"
class="flex-1 bg-slate-800/60 border border-slate-700 rounded-lg px-3 py-1.5 text-xs text-slate-200 placeholder-slate-500 focus:outline-none focus:border-blue-500"
onkeydown="if(event.key==='Enter'){event.preventDefault();addTag();}">
<button onclick="addTag()" class="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-xs font-medium hover:bg-blue-500 transition">Add</button>
</div>
</div>
<button onclick="saveAnnotation()" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 text-slate-300 rounded-lg text-xs font-medium hover:border-blue-500 hover:text-blue-400 transition" id="saveBtn">
Save Note
</button>
<div id="saveStatus" class="text-xs text-center mt-2 text-slate-600"></div>
</div>
{% endif %}
</div>
<!-- Right column: Sidebar -->
<div class="space-y-6">
<!-- Score Card -->
{% if draft.rating %}
<div class="detail-card rounded-xl border border-slate-800 p-6 text-center">
<div class="score-ring {% if draft.rating.score >= 3.5 %}score-ring-high{% elif draft.rating.score >= 2.5 %}score-ring-mid{% else %}score-ring-low{% endif %}">
<div>
<div class="text-3xl font-bold {% if draft.rating.score >= 3.5 %}text-green-400{% elif draft.rating.score >= 2.5 %}text-amber-400{% else %}text-red-400{% endif %}">
{{ draft.rating.score }}
</div>
<div class="text-[10px] text-slate-500 uppercase tracking-wider">Score</div>
</div>
</div>
<!-- Mini dimension summary -->
<div class="mt-4 grid grid-cols-5 gap-1 text-center">
{% for dim, abbr in [("novelty","N"), ("maturity","M"), ("overlap","O"), ("momentum","Mo"), ("relevance","R")] %}
{% set v = draft.rating[dim] %}
<div>
{% if dim == "overlap" %}
<div class="text-xs font-bold {% if v <= 2 %}text-green-400{% elif v <= 3 %}text-amber-400{% else %}text-red-400{% endif %}">{{ v }}</div>
{% else %}
<div class="text-xs font-bold {% if v >= 4 %}text-green-400{% elif v >= 3 %}text-amber-400{% else %}text-red-400{% endif %}">{{ v }}</div>
{% endif %}
<div class="text-[9px] text-slate-600 uppercase">{{ abbr }}</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Readiness Score -->
{% if draft.readiness and draft.readiness.score > 0 %}
<div class="detail-card rounded-xl border border-slate-800 p-5">
<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="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 0100.</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
{% if draft.readiness.score >= 60 %}bg-gradient-to-r from-green-600 to-green-400
{% elif draft.readiness.score >= 35 %}bg-gradient-to-r from-amber-600 to-amber-400
{% else %}bg-gradient-to-r from-red-600 to-red-400{% endif %}"
style="width: {{ draft.readiness.score }}%"></div>
<div class="absolute inset-0 flex items-center justify-center text-xs font-bold text-white">
{{ draft.readiness.score }}/100
</div>
</div>
<!-- Factor breakdown -->
<div class="space-y-1.5 mt-3">
{% for key, f in draft.readiness.factors.items() %}
<div class="flex items-center justify-between text-xs">
<span class="text-slate-500">{{ f.label }}</span>
<div class="flex items-center gap-2">
<span class="text-slate-600 font-mono text-[10px]">{{ f.detail }}</span>
<span class="font-mono font-medium
{% if f.value >= 0.7 %}text-green-400
{% elif f.value >= 0.4 %}text-amber-400
{% else %}text-red-400{% endif %}">+{{ f.contribution }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Metadata -->
<div class="detail-card rounded-xl border border-slate-800 p-5">
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Metadata
</h2>
<dl class="space-y-2.5 text-sm">
<div class="flex justify-between"><dt class="text-slate-500 text-xs">Date</dt><dd class="text-slate-300">{{ draft.date }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-500 text-xs">Revision</dt><dd class="text-slate-300">{{ draft.rev or 'N/A' }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-500 text-xs">Pages</dt><dd class="text-slate-300">{{ draft.pages or 'N/A' }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-500 text-xs">Words</dt><dd class="text-slate-300">{{ '{:,}'.format(draft.words) if draft.words else 'N/A' }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-500 text-xs">Working Group</dt><dd class="text-slate-300">{{ draft.group }}</dd></div>
</dl>
<div class="mt-4 space-y-2">
<a href="{{ draft.url }}" target="_blank" rel="noopener"
class="flex items-center justify-center gap-2 px-3 py-2 bg-blue-600 text-white rounded-lg text-xs font-medium hover:bg-blue-500 transition">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
View on Datatracker
</a>
{% if draft.text_url %}
<a href="{{ draft.text_url }}" target="_blank" rel="noopener"
class="flex items-center justify-center gap-2 px-3 py-2 border border-slate-700 text-slate-300 rounded-lg text-xs font-medium hover:border-slate-500 hover:text-white transition">
<svg class="w-3.5 h-3.5" 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>
Read Full Text
</a>
{% endif %}
</div>
</div>
<!-- Authors -->
{% if draft.authors %}
<div class="detail-card rounded-xl border border-slate-800 p-5">
<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="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 0z"/></svg>
Authors <span class="text-slate-600 font-normal">({{ draft.authors|length }})</span>
</h2>
<ul class="space-y-2.5">
{% for a in draft.authors %}
<li class="flex items-start gap-2">
<div class="w-7 h-7 rounded-full bg-slate-800 border border-slate-700 flex items-center justify-center flex-shrink-0 mt-0.5">
<span class="text-xs font-semibold text-slate-400">{{ a.name[0]|upper if a.name else '?' }}</span>
</div>
<div>
<a href="/authors/{{ a.person_id }}" class="text-sm text-blue-400 hover:text-blue-300 transition">{{ a.name }}</a>
{% if a.affiliation %}
<div class="text-xs text-slate-500">{{ a.affiliation }}</div>
{% endif %}
</div>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- Categories -->
{% if draft.rating and draft.rating.categories %}
<div class="detail-card rounded-xl border border-slate-800 p-5">
<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="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/></svg>
Categories
</h2>
<div class="flex flex-wrap gap-1.5">
{% for cat in draft.rating.categories %}
<a href="/drafts?cat={{ cat | urlencode }}"
class="px-2.5 py-1 rounded-full text-xs bg-slate-800/60 text-slate-400 border border-slate-700 hover:border-blue-500 hover:text-blue-400 transition">
{{ cat }}
</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- References -->
{% if draft.refs %}
<div class="detail-card rounded-xl border border-slate-800 p-5">
<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="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
References <span class="text-slate-600 font-normal">({{ draft.refs|length }})</span>
</h2>
<div class="flex flex-wrap gap-1.5 max-h-48 overflow-y-auto">
{% for ref in draft.refs %}
{% if ref.type == 'rfc' %}
<a href="https://www.rfc-editor.org/rfc/rfc{{ ref.id | int }}" target="_blank" rel="noopener"
class="px-2 py-0.5 rounded text-[10px] font-medium ref-rfc hover:opacity-80 transition">
RFC {{ ref.id | int }}
</a>
{% elif ref.type == 'draft' %}
{% if ref.id in known_drafts %}
<a href="/drafts/{{ ref.id }}"
class="px-2 py-0.5 rounded text-[10px] font-medium ref-draft hover:opacity-80 transition">
{{ ref.id }}
</a>
{% else %}
<a href="https://datatracker.ietf.org/doc/{{ ref.id }}/" target="_blank" rel="noopener"
class="px-2 py-0.5 rounded text-[10px] font-medium ref-draft hover:opacity-80 transition">
{{ ref.id }}
</a>
{% endif %}
{% elif ref.type == 'bcp' %}
<a href="https://www.rfc-editor.org/info/bcp{{ ref.id }}" target="_blank" rel="noopener"
class="px-2 py-0.5 rounded text-[10px] font-medium ref-other hover:opacity-80 transition">
BCP {{ ref.id }}
</a>
{% else %}
<span class="px-2 py-0.5 rounded text-[10px] font-medium ref-other">
{{ ref.type|upper }} {{ ref.id }}
</span>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
const draftName = {{ draft.name | tojson }};
function addTag() {
const input = document.getElementById('newTag');
const tag = input.value.trim();
if (!tag) return;
input.value = '';
fetch(`/api/drafts/${encodeURIComponent(draftName)}/annotate`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({add_tag: tag}),
})
.then(r => r.json())
.then(data => {
if (data.success) renderTags(data.annotation.tags);
});
}
function removeTag(tag) {
fetch(`/api/drafts/${encodeURIComponent(draftName)}/annotate`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({remove_tag: tag}),
})
.then(r => r.json())
.then(data => {
if (data.success) renderTags(data.annotation.tags);
});
}
function renderTags(tags) {
const container = document.getElementById('tagContainer');
container.innerHTML = tags.map(t =>
`<span class="tag-chip inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-blue-500/20 text-blue-400 border border-blue-500/30">
${t}
<button onclick="removeTag('${t}')" class="hover:text-red-400 transition ml-0.5">&times;</button>
</span>`
).join('');
}
function saveAnnotation() {
const note = document.getElementById('annotNote').value;
const btn = document.getElementById('saveBtn');
const status = document.getElementById('saveStatus');
btn.disabled = true;
btn.textContent = 'Saving...';
fetch(`/api/drafts/${encodeURIComponent(draftName)}/annotate`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({note: note}),
})
.then(r => r.json())
.then(data => {
btn.disabled = false;
btn.textContent = 'Save Note';
if (data.success) {
status.textContent = 'Saved';
status.className = 'text-xs text-center mt-2 text-green-400';
setTimeout(() => { status.textContent = ''; status.className = 'text-xs text-center mt-2 text-slate-600'; }, 2000);
}
})
.catch(() => {
btn.disabled = false;
btn.textContent = 'Save Note';
status.textContent = 'Error saving';
status.className = 'text-xs text-center mt-2 text-red-400';
});
}
</script>
{% endblock %}