Files
ietf-draft-analyzer/src/webui/templates/ask.html
Christian Nennemann e7527ad68e 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>
2026-03-08 12:47:47 +01:00

213 lines
11 KiB
HTML

{% extends "base.html" %}
{% set active_page = "ask" %}
{% block title %}Ask — IETF Draft Analyzer{% endblock %}
{% block extra_head %}
<style>
.ask-input {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.8), rgba(30, 41, 59, 0.4));
backdrop-filter: blur(10px);
}
.answer-card {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.8), rgba(30, 41, 59, 0.4));
backdrop-filter: blur(10px);
}
.source-row { transition: all 0.15s ease; }
.source-row:hover { background: rgba(59, 130, 246, 0.05); }
.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-8 text-center">
<h1 class="text-3xl font-bold text-white">Ask the Draft Corpus</h1>
<p class="text-slate-400 text-sm mt-2">Search across {{ "{:,}".format(stats.total if stats is defined and stats else 434) }} drafts using keyword + semantic similarity. AI synthesis is optional.</p>
</div>
<!-- Search Bar -->
<div class="max-w-3xl mx-auto mb-8">
<form method="get" action="/ask" id="askForm">
<div class="ask-input rounded-xl border border-slate-700 p-2 flex items-center gap-2">
<svg class="w-5 h-5 text-slate-500 ml-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<input type="text" name="q" value="{{ question }}" placeholder="Which drafts address agent authentication? What approaches exist for agent delegation?"
class="flex-1 bg-transparent border-0 px-3 py-3 text-base text-slate-200 placeholder-slate-500 focus:outline-none"
autofocus>
<button type="submit" class="px-6 py-3 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-500 transition-colors flex-shrink-0">
Search
</button>
</div>
<div class="flex items-center gap-4 mt-3 px-2">
<label class="text-xs text-slate-500 flex items-center gap-1.5">
<span>Sources:</span>
<select name="top" class="bg-slate-800/60 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300 focus:outline-none">
<option value="3" {% if request.args.get('top', '5') == '3' %}selected{% endif %}>3</option>
<option value="5" {% if request.args.get('top', '5') == '5' %}selected{% endif %}>5</option>
<option value="10" {% if request.args.get('top', '5') == '10' %}selected{% endif %}>10</option>
</select>
</label>
<div class="text-xs text-slate-600">Combines keyword search + semantic similarity (free, no API calls)</div>
</div>
</form>
</div>
<!-- Example questions (show when no query) -->
{% if not question %}
<div class="max-w-3xl mx-auto">
<div class="text-xs text-slate-500 uppercase tracking-wide mb-3 font-medium">Example questions</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
{% set examples = [
"Which drafts address agent authentication and identity?",
"What are the competing approaches to agent-to-agent communication?",
"How do safety mechanisms work across different proposals?",
"What protocols exist for AI model serving and inference?",
"Which drafts propose agent discovery or registration systems?",
"What are the main gaps in autonomous network operations?",
] %}
{% for q in examples %}
<a href="/ask?q={{ q | urlencode }}" class="ask-input rounded-lg border border-slate-800 px-4 py-3 text-sm text-slate-400 hover:text-blue-400 hover:border-slate-700 transition">
{{ q }}
</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Results -->
{% if result %}
<div class="max-w-3xl mx-auto">
<!-- AI Synthesized Answer (shown if cached or after user clicks synthesize) -->
<div id="answerSection">
{% if result.answer %}
<div class="answer-card rounded-xl border border-slate-800 p-6 mb-6">
<div class="flex items-center gap-2 mb-4">
<svg class="w-5 h-5 text-blue-400" 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>
<h2 class="text-lg font-semibold text-white">AI Answer</h2>
<span class="text-xs px-2 py-0.5 rounded-full bg-green-900/30 text-green-400 border border-green-800/30">cached</span>
</div>
<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">
<div>
<div class="text-sm text-slate-300 font-medium">Want an AI-synthesized answer?</div>
<div class="text-xs text-slate-500 mt-0.5">Uses Claude API (Haiku, ~$0.001). Result is cached permanently for all future visitors.</div>
</div>
<button id="synthesizeBtn" onclick="synthesizeAnswer()"
class="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm font-medium hover:bg-purple-500 transition-colors flex items-center gap-2 flex-shrink-0">
<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="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>
Synthesize
</button>
</div>
</div>
{% endif %}
{% endif %}
</div>
<!-- Source drafts (always shown — free) -->
{% if result.sources %}
<div class="answer-card rounded-xl border border-slate-800 overflow-hidden">
<div class="px-6 py-4 border-b border-slate-800">
<h3 class="text-sm font-semibold text-slate-300">Matching Drafts ({{ result.sources|length }})</h3>
</div>
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800/50 bg-slate-900/40">
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-500 uppercase w-8">#</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-500 uppercase">Draft</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-500 uppercase w-20">Match</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-500 uppercase w-16">Score</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/30">
{% for src in result.sources %}
<tr class="source-row">
<td class="px-4 py-3 text-slate-600">{{ loop.index }}</td>
<td class="px-4 py-3">
<a href="/drafts/{{ src.name }}" class="text-blue-400 hover:text-blue-300 font-medium transition">
{{ src.title }}
</a>
<div class="text-xs text-slate-600 mt-0.5 font-mono">{{ src.name }}</div>
</td>
<td class="px-4 py-3">
{% if src.match_type == 'both' %}
<span class="inline-block px-2 py-0.5 rounded text-xs font-medium bg-green-900/30 text-green-400 border border-green-800/30">both</span>
{% elif src.match_type == 'semantic' %}
<span class="inline-block px-2 py-0.5 rounded text-xs font-medium bg-purple-900/30 text-purple-400 border border-purple-800/30">semantic</span>
{% else %}
<span class="inline-block px-2 py-0.5 rounded text-xs font-medium bg-slate-800/50 text-slate-400 border border-slate-700/30">keyword</span>
{% endif %}
</td>
<td class="px-4 py-3 text-right font-mono text-xs text-slate-400">
{{ "%.3f"|format(src.similarity) if src.similarity else "-" }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
<script>
function synthesizeAnswer() {
const btn = document.getElementById('synthesizeBtn');
const section = document.getElementById('answerSection');
// Show loading state
btn.disabled = true;
btn.innerHTML = '<span class="loading-spinner"></span> Synthesizing...';
fetch('/api/ask/synthesize', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
question: {{ question | tojson }},
top_k: {{ request.args.get('top', '5') | int }}
})
})
.then(r => r.json())
.then(data => {
if (data.answer) {
section.innerHTML = `
<div class="answer-card rounded-xl border border-slate-800 p-6 mb-6">
<div class="flex items-center gap-2 mb-4">
<svg class="w-5 h-5 text-blue-400" 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>
<h2 class="text-lg font-semibold text-white">AI Answer</h2>
<span class="text-xs px-2 py-0.5 rounded-full bg-blue-900/30 text-blue-400 border border-blue-800/30">just generated</span>
</div>
<div class="text-slate-300 text-sm leading-relaxed whitespace-pre-line">${data.answer}</div>
</div>`;
}
})
.catch(err => {
btn.disabled = false;
btn.innerHTML = 'Synthesize (retry)';
section.querySelector('.text-xs.text-slate-500').textContent = 'Error: ' + err.message;
});
}
</script>
{% endif %}
{% endblock %}