Phase 5: Add Timeline View feature

- Add backend API endpoints for timeline data:
  - GET /api/v1/projects/{id}/timeline
  - GET /api/v1/projects/{id}/timeline/summary
  - GET /api/v1/entities/{id}/timeline

- Add database methods for timeline queries in db_manager.py

- Add frontend timeline view:
  - New sidebar button for timeline view
  - Timeline panel with date-grouped events
  - Visual distinction between mentions and relations
  - Entity filter dropdown
  - Statistics cards
  - Interactive event cards

- Update STATUS.md with Phase 5 progress
- Add view switching functions (switchView, switchKBTab)
- Add knowledge base loading functions
This commit is contained in:
OpenClaw Bot
2026-02-19 12:06:14 +08:00
parent 1f4fe5a33e
commit bc07aab4bb
5 changed files with 819 additions and 11 deletions

View File

@@ -598,6 +598,163 @@ class DatabaseManager:
'recent_transcripts': [dict(t) for t in recent_transcripts],
'top_entities': [dict(e) for e in top_entities]
}
# Phase 5: Timeline operations
def get_project_timeline(self, project_id: str, entity_id: str = None, start_date: str = None, end_date: str = None) -> List[dict]:
"""获取项目时间线数据 - 按时间顺序的实体提及和事件"""
conn = self.get_conn()
# 构建查询条件
conditions = ["t.project_id = ?"]
params = [project_id]
if entity_id:
conditions.append("m.entity_id = ?")
params.append(entity_id)
if start_date:
conditions.append("t.created_at >= ?")
params.append(start_date)
if end_date:
conditions.append("t.created_at <= ?")
params.append(end_date)
where_clause = " AND ".join(conditions)
# 获取实体提及时间线
mentions = conn.execute(
f"""SELECT m.*, e.name as entity_name, e.type as entity_type, e.definition,
t.filename, t.created_at as event_date, t.type as source_type
FROM entity_mentions m
JOIN entities e ON m.entity_id = e.id
JOIN transcripts t ON m.transcript_id = t.id
WHERE {where_clause}
ORDER BY t.created_at, m.start_pos""",
params
).fetchall()
# 获取关系创建时间线
relation_conditions = ["r.project_id = ?"]
relation_params = [project_id]
if start_date:
relation_conditions.append("r.created_at >= ?")
relation_params.append(start_date)
if end_date:
relation_conditions.append("r.created_at <= ?")
relation_params.append(end_date)
relation_where = " AND ".join(relation_conditions)
relations = conn.execute(
f"""SELECT r.*,
s.name as source_name, t.name as target_name,
tr.filename, r.created_at as event_date
FROM entity_relations r
JOIN entities s ON r.source_entity_id = s.id
JOIN entities t ON r.target_entity_id = t.id
LEFT JOIN transcripts tr ON r.transcript_id = tr.id
WHERE {relation_where}
ORDER BY r.created_at""",
relation_params
).fetchall()
conn.close()
# 合并并格式化时间线事件
timeline_events = []
for m in mentions:
timeline_events.append({
'id': m['id'],
'type': 'mention',
'event_date': m['event_date'],
'entity_id': m['entity_id'],
'entity_name': m['entity_name'],
'entity_type': m['entity_type'],
'entity_definition': m['definition'],
'text_snippet': m['text_snippet'],
'confidence': m['confidence'],
'source': {
'transcript_id': m['transcript_id'],
'filename': m['filename'],
'type': m['source_type']
}
})
for r in relations:
timeline_events.append({
'id': r['id'],
'type': 'relation',
'event_date': r['event_date'],
'relation_type': r['relation_type'],
'source_entity': r['source_name'],
'target_entity': r['target_name'],
'evidence': r['evidence'],
'source': {
'transcript_id': r.get('transcript_id'),
'filename': r['filename']
}
})
# 按时间排序
timeline_events.sort(key=lambda x: x['event_date'])
return timeline_events
def get_entity_timeline_summary(self, project_id: str) -> dict:
"""获取项目实体时间线摘要统计"""
conn = self.get_conn()
# 按日期统计提及数量
daily_stats = conn.execute(
"""SELECT DATE(t.created_at) as date, COUNT(*) as count
FROM entity_mentions m
JOIN transcripts t ON m.transcript_id = t.id
WHERE t.project_id = ?
GROUP BY DATE(t.created_at)
ORDER BY date""",
(project_id,)
).fetchall()
# 按实体统计提及数量
entity_stats = conn.execute(
"""SELECT e.name, e.type, COUNT(m.id) as mention_count,
MIN(t.created_at) as first_mentioned,
MAX(t.created_at) as last_mentioned
FROM entities e
LEFT JOIN entity_mentions m ON e.id = m.entity_id
LEFT JOIN transcripts t ON m.transcript_id = t.id
WHERE e.project_id = ?
GROUP BY e.id
ORDER BY mention_count DESC
LIMIT 20""",
(project_id,)
).fetchall()
# 获取活跃时间段
active_periods = conn.execute(
"""SELECT
DATE(t.created_at) as date,
COUNT(DISTINCT m.entity_id) as active_entities,
COUNT(m.id) as total_mentions
FROM transcripts t
LEFT JOIN entity_mentions m ON t.id = m.transcript_id
WHERE t.project_id = ?
GROUP BY DATE(t.created_at)
ORDER BY date""",
(project_id,)
).fetchall()
conn.close()
return {
'daily_activity': [dict(d) for d in daily_stats],
'top_entities': [dict(e) for e in entity_stats],
'active_periods': [dict(a) for a in active_periods]
}
def get_transcript_context(self, transcript_id: str, position: int, context_chars: int = 200) -> str:
"""获取转录文本的上下文"""

View File

@@ -1265,3 +1265,72 @@ async def search_entities(project_id: str, q: str):
db = get_db_manager()
entities = db.search_entities(project_id, q)
return [{"id": e.id, "name": e.name, "type": e.type, "definition": e.definition} for e in entities]
# ==================== Phase 5: 时间线视图 API ====================
@app.get("/api/v1/projects/{project_id}/timeline")
async def get_project_timeline(
project_id: str,
entity_id: str = None,
start_date: str = None,
end_date: str = None
):
"""获取项目时间线 - 按时间顺序的实体提及和关系事件"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
db = get_db_manager()
project = db.get_project(project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
timeline = db.get_project_timeline(project_id, entity_id, start_date, end_date)
return {
"project_id": project_id,
"events": timeline,
"total_count": len(timeline)
}
@app.get("/api/v1/projects/{project_id}/timeline/summary")
async def get_timeline_summary(project_id: str):
"""获取项目时间线摘要统计"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
db = get_db_manager()
project = db.get_project(project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
summary = db.get_entity_timeline_summary(project_id)
return {
"project_id": project_id,
"project_name": project.name,
**summary
}
@app.get("/api/v1/entities/{entity_id}/timeline")
async def get_entity_timeline(entity_id: str):
"""获取单个实体的时间线"""
if not DB_AVAILABLE:
raise HTTPException(status_code=500, detail="Database not available")
db = get_db_manager()
entity = db.get_entity(entity_id)
if not entity:
raise HTTPException(status_code=404, detail="Entity not found")
timeline = db.get_project_timeline(entity.project_id, entity_id)
return {
"entity_id": entity_id,
"entity_name": entity.name,
"entity_type": entity.type,
"events": timeline,
"total_count": len(timeline)
}