#16 Claim-Match-Anzeige im Frontend (Stufe 2 Vorschau)
Backend:
- /api/podcasts/{id}/episodes/{ep}/claims liefert nun pro Claim match_counts
(belegt/widerspricht/erweitert je Anzahl) und best_match (target_podcast,
target_episode, target_idx, relation, reason, score).
Frontend (AnalysisView claims-Mode):
- Match-Badges in passender Farbe (gruen=belegt, rot=widerspricht, blau=erweitert)
zeigen die Anzahl Bestaetigungen je Relation.
- Best-Match-Link unter dem Claim mit Pfeil (-> bei same-podcast, klickbar zu
jumpAnswer; ↗ bei cross-podcast, nur als Hinweis sichtbar). Reason wird
inline gekuerzt angezeigt.
Robustheit: alles greift nur wenn die claim_matches-Tabelle befuellt ist.
Solange match_claims.py noch nicht gelaufen ist, bleibt das UI unveraendert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c5489eabaa
commit
b8c808cd87
@ -122,7 +122,7 @@ def _table_exists(db, name: str) -> bool:
|
|||||||
|
|
||||||
@app.get("/api/podcasts/{podcast_id}/episodes/{episode_id}/claims")
|
@app.get("/api/podcasts/{podcast_id}/episodes/{episode_id}/claims")
|
||||||
def get_episode_claims(podcast_id: str, episode_id: str, claim_type: Optional[str] = None):
|
def get_episode_claims(podcast_id: str, episode_id: str, claim_type: Optional[str] = None):
|
||||||
"""Claims (Behauptungen) für eine Episode."""
|
"""Claims (Behauptungen) für eine Episode, mit Match-Anzahlen je Relation (#16 Stufe 2)."""
|
||||||
db = get_db()
|
db = get_db()
|
||||||
if not _table_exists(db, "claims"):
|
if not _table_exists(db, "claims"):
|
||||||
db.close()
|
db.close()
|
||||||
@ -135,8 +135,36 @@ def get_episode_claims(podcast_id: str, episode_id: str, claim_type: Optional[st
|
|||||||
params.append(claim_type)
|
params.append(claim_type)
|
||||||
sql += " ORDER BY paragraph_idx, id"
|
sql += " ORDER BY paragraph_idx, id"
|
||||||
rows = db.execute(sql, params).fetchall()
|
rows = db.execute(sql, params).fetchall()
|
||||||
|
claims_list = [dict(r) for r in rows]
|
||||||
|
|
||||||
|
# Match-Counts und besten Match je claim_id anhaengen, falls Tabelle existiert
|
||||||
|
if claims_list and _table_exists(db, "claim_matches"):
|
||||||
|
ids = [c["id"] for c in claims_list]
|
||||||
|
placeholder = ",".join("?" * len(ids))
|
||||||
|
match_rows = db.execute(
|
||||||
|
f"SELECT claim_id, relation, COUNT(*) c FROM claim_matches "
|
||||||
|
f"WHERE claim_id IN ({placeholder}) GROUP BY claim_id, relation",
|
||||||
|
ids,
|
||||||
|
).fetchall()
|
||||||
|
counts = {}
|
||||||
|
for r in match_rows:
|
||||||
|
counts.setdefault(r["claim_id"], {})[r["relation"]] = r["c"]
|
||||||
|
# bester Match je claim (fuer Quick-Link)
|
||||||
|
best_rows = db.execute(
|
||||||
|
f"SELECT cm.claim_id, cm.relation, cm.target_podcast, cm.target_episode, "
|
||||||
|
f"cm.target_idx, cm.reason, cm.score "
|
||||||
|
f"FROM claim_matches cm "
|
||||||
|
f"WHERE cm.claim_id IN ({placeholder}) "
|
||||||
|
f"AND cm.id IN (SELECT MIN(id) FROM claim_matches WHERE claim_id IN ({placeholder}) "
|
||||||
|
f"GROUP BY claim_id) ",
|
||||||
|
ids + ids,
|
||||||
|
).fetchall()
|
||||||
|
best = {r["claim_id"]: dict(r) for r in best_rows}
|
||||||
|
for c in claims_list:
|
||||||
|
c["match_counts"] = counts.get(c["id"], {})
|
||||||
|
c["best_match"] = best.get(c["id"])
|
||||||
db.close()
|
db.close()
|
||||||
return {"available": True, "claims": [dict(r) for r in rows]}
|
return {"available": True, "claims": claims_list}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/podcasts/{podcast_id}/episodes/{episode_id}/questions")
|
@app.get("/api/podcasts/{podcast_id}/episodes/{episode_id}/questions")
|
||||||
|
|||||||
@ -822,6 +822,15 @@ const AnalysisView = {
|
|||||||
if (this.mode === 'claims' && it.verifiable) {
|
if (this.mode === 'claims' && it.verifiable) {
|
||||||
badges += `<span class="theme-tag" style="font-size:10px;margin-right:4px;opacity:0.7">verifizierbar</span>`;
|
badges += `<span class="theme-tag" style="font-size:10px;margin-right:4px;opacity:0.7">verifizierbar</span>`;
|
||||||
}
|
}
|
||||||
|
// Claim-Match-Badges (#16 Stufe 2)
|
||||||
|
if (this.mode === 'claims' && it.match_counts) {
|
||||||
|
const RC = {belegt: '#86efac', widerspricht: '#f87171', erweitert: '#60a5fa'};
|
||||||
|
for (const [rel, cnt] of Object.entries(it.match_counts)) {
|
||||||
|
if (cnt > 0 && RC[rel]) {
|
||||||
|
badges += `<span class="theme-tag" style="font-size:10px;margin-right:4px;color:${RC[rel]};border-color:${RC[rel]}44">${rel} ${cnt}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let answerLink = '';
|
let answerLink = '';
|
||||||
if (this.mode === 'questions') {
|
if (this.mode === 'questions') {
|
||||||
const a = it.answered;
|
const a = it.answered;
|
||||||
@ -844,11 +853,31 @@ const AnalysisView = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Best-Match-Link fuer Claims
|
||||||
|
let matchLink = '';
|
||||||
|
if (this.mode === 'claims' && it.best_match) {
|
||||||
|
const m = it.best_match;
|
||||||
|
const RC = {belegt: '#86efac', widerspricht: '#f87171', erweitert: '#60a5fa'};
|
||||||
|
const col = RC[m.relation] || 'var(--text-muted)';
|
||||||
|
const samePodcast = !m.target_podcast || m.target_podcast === CURRENT_PODCAST;
|
||||||
|
const arrow = samePodcast ? '→' : '↗';
|
||||||
|
const target = samePodcast ? escHtml(m.target_episode) : `${escHtml(m.target_podcast)} / ${escHtml(m.target_episode)}`;
|
||||||
|
if (samePodcast) {
|
||||||
|
matchLink = `<div style="margin-top:4px;font-size:12px;color:${col}" onclick="event.stopPropagation(); AnalysisView.jumpAnswer('${m.target_episode}', ${m.target_idx})">
|
||||||
|
${arrow} ${escHtml(m.relation)}: ${target}@p${m.target_idx}${m.reason ? ' — ' + escHtml(m.reason).slice(0, 80) : ''}
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
matchLink = `<div style="margin-top:4px;font-size:12px;color:${col};opacity:0.85">
|
||||||
|
${arrow} ${escHtml(m.relation)} in ${target}@p${m.target_idx}${m.reason ? ' — ' + escHtml(m.reason).slice(0, 80) : ''}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
html += `<div class="transcript-para" onclick="AnalysisView.jumpTo(${it.start_time || 0})">`;
|
html += `<div class="transcript-para" onclick="AnalysisView.jumpTo(${it.start_time || 0})">`;
|
||||||
html += `<span class="ts">${ts}</span>`;
|
html += `<span class="ts">${ts}</span>`;
|
||||||
html += badges;
|
html += badges;
|
||||||
html += escHtml(text);
|
html += escHtml(text);
|
||||||
html += answerLink;
|
html += answerLink;
|
||||||
|
html += matchLink;
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
});
|
});
|
||||||
panel.innerHTML = html;
|
panel.innerHTML = html;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user