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>
This commit is contained in:
2026-03-09 03:45:00 +01:00
parent 4a368bde62
commit c755b2bbf3
16 changed files with 548 additions and 18 deletions

View File

@@ -103,6 +103,78 @@ def get_ideas_by_type(db: Database) -> dict:
"ideas": all_ideas,
}
def get_idea_detail(db: Database, idea_id: int) -> dict | None:
"""Return a single idea with source draft info and similar ideas."""
row = db.conn.execute("SELECT * FROM ideas WHERE id = ?", (idea_id,)).fetchone()
if not row:
return None
idea = {
"id": row["id"],
"title": row["title"],
"description": row["description"],
"type": row["idea_type"],
"draft_name": row["draft_name"],
"novelty_score": row["novelty_score"],
}
# Get source draft info
draft = db.get_draft(row["draft_name"])
if draft:
idea["draft_title"] = draft.title
idea["draft_date"] = draft.date
# Get category from ratings
rated = db.drafts_with_ratings(limit=2000)
for d, r in rated:
if d.name == row["draft_name"]:
idea["categories"] = r.categories
break
# Find similar ideas using embeddings
similar = []
emb_row = db.conn.execute(
"SELECT vector FROM idea_embeddings WHERE idea_id = ?", (idea_id,)
).fetchone()
if emb_row:
target_vec = np.frombuffer(emb_row["vector"], dtype=np.float32)
all_embs = db.all_idea_embeddings()
# Compute cosine similarities
scores = []
for other_id, other_vec in all_embs.items():
if other_id == idea_id:
continue
cos_sim = float(np.dot(target_vec, other_vec) / (
np.linalg.norm(target_vec) * np.linalg.norm(other_vec) + 1e-9))
scores.append((other_id, cos_sim))
scores.sort(key=lambda x: x[1], reverse=True)
top_5 = scores[:5]
# Fetch idea details for top 5
if top_5:
ids = [s[0] for s in top_5]
sim_map = {s[0]: s[1] for s in top_5}
placeholders = ",".join("?" * len(ids))
sim_rows = db.conn.execute(
f"SELECT id, title, idea_type, draft_name FROM ideas WHERE id IN ({placeholders})",
ids,
).fetchall()
sim_dict = {r["id"]: r for r in sim_rows}
for sid, score in top_5:
sr = sim_dict.get(sid)
if sr:
similar.append({
"id": sr["id"],
"title": sr["title"],
"type": sr["idea_type"],
"draft_name": sr["draft_name"],
"similarity": round(score, 3),
})
idea["similar"] = similar
return idea
def get_timeline_data(db: Database) -> TimelineData:
"""Return monthly counts by category for timeline chart."""
pairs = db.drafts_with_ratings(limit=1000)