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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user