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>
213 lines
11 KiB
HTML
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 %}
|