Idea quality pipeline, web UI features, academic paper
- Tighten idea extraction prompts (1-4 ideas, no sub-features) reducing 1,907 ideas to 468 across 434 drafts (78% reduction) - Add embedding-based dedup (ietf dedup-ideas) for same-draft similarity - Add novelty scoring (ietf ideas score) and filtering (ietf ideas filter) using Claude to rate ideas 1-5, removing 49 generic building blocks - Final count: 419 high-quality ideas (avg 1.1/draft) - Web UI: gap explorer with live draft generation and pre-generated demos - Web UI: D3.js author collaboration network (498 nodes, 1142 edges, 68 clusters, org filtering, interactive zoom/pan) - Academic paper: 15-page LaTeX workshop paper analyzing the 434-draft AI agent standards landscape - Save improvement ideas backlog to data/reports/improvement-ideas.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
297
src/webui/app.py
Normal file
297
src/webui/app.py
Normal file
@@ -0,0 +1,297 @@
|
||||
"""IETF Draft Analyzer — Web Dashboard.
|
||||
|
||||
Run with: python src/webui/app.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure project src is on path
|
||||
_project_root = Path(__file__).resolve().parent.parent.parent
|
||||
sys.path.insert(0, str(_project_root / "src"))
|
||||
|
||||
from flask import Flask, render_template, request, jsonify, abort, g
|
||||
|
||||
from webui.data import (
|
||||
get_db,
|
||||
get_overview_stats,
|
||||
get_category_counts,
|
||||
get_drafts_page,
|
||||
get_draft_detail,
|
||||
get_rating_distributions,
|
||||
get_timeline_data,
|
||||
get_ideas_by_type,
|
||||
get_all_gaps,
|
||||
get_gap_detail,
|
||||
get_generated_drafts,
|
||||
read_generated_draft,
|
||||
get_top_authors,
|
||||
get_org_data,
|
||||
get_category_radar_data,
|
||||
get_score_histogram,
|
||||
get_coauthor_network,
|
||||
get_cross_org_data,
|
||||
get_landscape_tsne,
|
||||
get_similarity_graph,
|
||||
get_timeline_animation_data,
|
||||
get_idea_clusters,
|
||||
get_monitor_status,
|
||||
get_author_network_full,
|
||||
)
|
||||
|
||||
app = Flask(
|
||||
__name__,
|
||||
template_folder=str(Path(__file__).parent / "templates"),
|
||||
)
|
||||
app.config["SECRET_KEY"] = "ietf-dashboard-dev"
|
||||
|
||||
|
||||
# --- Database lifecycle (per-request to avoid SQLite threading issues) ---
|
||||
|
||||
|
||||
def db():
|
||||
if "db" not in g:
|
||||
g.db = get_db()
|
||||
return g.db
|
||||
|
||||
|
||||
# --- Routes ---
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def overview():
|
||||
stats = get_overview_stats(db())
|
||||
categories = get_category_counts(db())
|
||||
timeline = get_timeline_data(db())
|
||||
scores = get_score_histogram(db())
|
||||
radar = get_category_radar_data(db())
|
||||
return render_template(
|
||||
"overview.html",
|
||||
stats=stats,
|
||||
categories=categories,
|
||||
timeline=timeline,
|
||||
scores=scores,
|
||||
radar=radar,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/drafts")
|
||||
def drafts():
|
||||
page = request.args.get("page", 1, type=int)
|
||||
search = request.args.get("q", "")
|
||||
category = request.args.get("cat", "")
|
||||
min_score = request.args.get("min_score", 0.0, type=float)
|
||||
sort = request.args.get("sort", "score")
|
||||
sort_dir = request.args.get("dir", "desc")
|
||||
|
||||
result = get_drafts_page(
|
||||
db(),
|
||||
page=page,
|
||||
search=search,
|
||||
category=category,
|
||||
min_score=min_score,
|
||||
sort=sort,
|
||||
sort_dir=sort_dir,
|
||||
)
|
||||
categories = get_category_counts(db())
|
||||
return render_template(
|
||||
"drafts.html",
|
||||
result=result,
|
||||
categories=categories,
|
||||
search=search,
|
||||
current_cat=category,
|
||||
min_score=min_score,
|
||||
sort=sort,
|
||||
sort_dir=sort_dir,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/drafts/<path:name>")
|
||||
def draft_detail(name: str):
|
||||
detail = get_draft_detail(db(), name)
|
||||
if not detail:
|
||||
abort(404)
|
||||
return render_template("draft_detail.html", draft=detail)
|
||||
|
||||
|
||||
@app.route("/ideas")
|
||||
def ideas():
|
||||
data = get_ideas_by_type(db())
|
||||
return render_template("ideas.html", data=data)
|
||||
|
||||
|
||||
@app.route("/gaps")
|
||||
def gaps():
|
||||
gap_list = get_all_gaps(db())
|
||||
generated = get_generated_drafts()
|
||||
return render_template("gaps.html", gaps=gap_list, generated_drafts=generated)
|
||||
|
||||
|
||||
@app.route("/gaps/demo")
|
||||
def gaps_demo():
|
||||
"""Show a pre-generated example draft so users can see output without API calls."""
|
||||
generated = get_generated_drafts()
|
||||
# Default to the first generated draft, or allow selection via query param
|
||||
selected = request.args.get("file", "")
|
||||
draft_text = None
|
||||
draft_info = None
|
||||
if selected:
|
||||
draft_text = read_generated_draft(selected)
|
||||
for g in generated:
|
||||
if g["filename"] == selected:
|
||||
draft_info = g
|
||||
break
|
||||
elif generated:
|
||||
draft_info = generated[0]
|
||||
draft_text = read_generated_draft(draft_info["filename"])
|
||||
return render_template(
|
||||
"gap_demo.html",
|
||||
generated_drafts=generated,
|
||||
draft_text=draft_text,
|
||||
draft_info=draft_info,
|
||||
selected=selected,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/gaps/<int:gap_id>")
|
||||
def gap_detail(gap_id: int):
|
||||
gap = get_gap_detail(db(), gap_id)
|
||||
if not gap:
|
||||
abort(404)
|
||||
generated = get_generated_drafts()
|
||||
return render_template("gap_detail.html", gap=gap, generated_drafts=generated)
|
||||
|
||||
|
||||
@app.route("/gaps/<int:gap_id>/generate", methods=["POST"])
|
||||
def gap_generate(gap_id: int):
|
||||
"""Trigger draft generation for a gap. Returns JSON with the generated text."""
|
||||
gap = get_gap_detail(db(), gap_id)
|
||||
if not gap:
|
||||
return jsonify({"error": "Gap not found"}), 404
|
||||
|
||||
try:
|
||||
from ietf_analyzer.config import Config
|
||||
from ietf_analyzer.analyzer import Analyzer
|
||||
from ietf_analyzer.draftgen import DraftGenerator
|
||||
|
||||
cfg = Config.load()
|
||||
database = db()
|
||||
analyzer = Analyzer(cfg, database)
|
||||
generator = DraftGenerator(cfg, database, analyzer)
|
||||
|
||||
# Generate into a file named after the gap
|
||||
slug = gap["topic"].lower().replace(" ", "-")[:40]
|
||||
output_path = str(Path(_project_root) / "data" / "reports" / "generated-drafts" / f"draft-gap-{gap_id}-{slug}.txt")
|
||||
path = generator.generate(gap["topic"], output_path=output_path)
|
||||
draft_text = Path(path).read_text(errors="replace")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"text": draft_text,
|
||||
"filename": Path(path).name,
|
||||
"path": path,
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/ratings")
|
||||
def ratings():
|
||||
distributions = get_rating_distributions(db())
|
||||
radar = get_category_radar_data(db())
|
||||
return render_template(
|
||||
"ratings.html",
|
||||
dist=distributions,
|
||||
radar=radar,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/landscape")
|
||||
def landscape():
|
||||
distributions = get_rating_distributions(db())
|
||||
tsne_data = get_landscape_tsne(db())
|
||||
return render_template(
|
||||
"landscape.html",
|
||||
dist=distributions,
|
||||
tsne_data=tsne_data,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/timeline")
|
||||
def timeline_animation():
|
||||
data = get_timeline_animation_data(db())
|
||||
return render_template("timeline.html", animation=data)
|
||||
|
||||
|
||||
@app.route("/idea-clusters")
|
||||
def idea_clusters():
|
||||
data = get_idea_clusters(db())
|
||||
return render_template("idea_clusters.html", clusters=data)
|
||||
|
||||
|
||||
@app.route("/similarity")
|
||||
def similarity():
|
||||
network = get_similarity_graph(db())
|
||||
return render_template("similarity.html", network=network)
|
||||
|
||||
|
||||
@app.route("/authors")
|
||||
def authors():
|
||||
top = get_top_authors(db(), limit=50)
|
||||
orgs = get_org_data(db(), limit=20)
|
||||
network = get_author_network_full(db())
|
||||
cross_org = get_cross_org_data(db(), limit=20)
|
||||
return render_template(
|
||||
"authors.html",
|
||||
authors=top,
|
||||
orgs=orgs,
|
||||
orgs_data=orgs,
|
||||
network=network,
|
||||
cross_org=cross_org,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/monitor")
|
||||
def monitor_page():
|
||||
status = get_monitor_status(db())
|
||||
return render_template("monitor.html", status=status)
|
||||
|
||||
|
||||
@app.route("/about")
|
||||
def about():
|
||||
stats = get_overview_stats(db())
|
||||
return render_template("about.html", stats=stats)
|
||||
|
||||
|
||||
# --- API endpoints for AJAX (used by client-side charts) ---
|
||||
|
||||
|
||||
@app.route("/api/drafts")
|
||||
def api_drafts():
|
||||
page = request.args.get("page", 1, type=int)
|
||||
search = request.args.get("q", "")
|
||||
category = request.args.get("cat", "")
|
||||
min_score = request.args.get("min_score", 0.0, type=float)
|
||||
sort = request.args.get("sort", "score")
|
||||
sort_dir = request.args.get("dir", "desc")
|
||||
return jsonify(
|
||||
get_drafts_page(db(), page=page, search=search, category=category,
|
||||
min_score=min_score, sort=sort, sort_dir=sort_dir)
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/stats")
|
||||
def api_stats():
|
||||
return jsonify(get_overview_stats(db()))
|
||||
|
||||
|
||||
@app.route("/api/authors/network")
|
||||
def api_author_network():
|
||||
return jsonify(get_author_network_full(db()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting IETF Draft Analyzer Dashboard on http://127.0.0.1:5000")
|
||||
app.run(debug=True, host="127.0.0.1", port=5000)
|
||||
Reference in New Issue
Block a user