- 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>
479 lines
28 KiB
HTML
479 lines
28 KiB
HTML
{% 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 (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 %}
|
||
<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: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">
|
||
<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">×</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 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
|
||
{% 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">×</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 %}
|