feat(knowledge): two-tier pagination — ID pre-fetch + content batch loading
Backend: - GET /api/knowledge/ids: returns up to 100 note IDs cheaply (no body parsing), supports same filters as /api/knowledge, includes has_more - GET /api/knowledge/batch?ids=...: fetches full items for given IDs in order; used by frontend to load content in controlled batches Frontend (KnowledgeView): - Fetch 100 IDs upfront, load first 50 as content on mount - IntersectionObserver sentinel (root: null) triggers 24-item content batches as user scrolls - Proactive ID refill when queue drops below 48 unloaded IDs - fetchGen counter invalidates stale in-flight responses on filter reset - IDs claimed before async fetch to prevent double-loading - sentinelVisible ref drives post-load re-check when content doesn't push sentinel off screen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,63 @@ async def list_knowledge():
|
||||
})
|
||||
|
||||
|
||||
@knowledge_bp.route("/ids", methods=["GET"])
|
||||
@login_required
|
||||
async def list_knowledge_ids():
|
||||
"""Return note IDs only (cheap) for the two-tier pagination feed.
|
||||
|
||||
Same filter params as GET /api/knowledge.
|
||||
Additional params: limit (default 100, max 200), offset (default 0).
|
||||
Returns {ids, total, has_more}.
|
||||
"""
|
||||
uid = get_current_user_id()
|
||||
note_type = request.args.get("type", "").strip().lower() or None
|
||||
tags_raw = request.args.get("tags", "").strip()
|
||||
tags = [t.strip() for t in tags_raw.split(",") if t.strip()] if tags_raw else []
|
||||
sort = request.args.get("sort", "modified").strip().lower()
|
||||
q = request.args.get("q", "").strip() or None
|
||||
if sort not in _VALID_SORTS:
|
||||
sort = "modified"
|
||||
try:
|
||||
limit = min(int(request.args.get("limit", 100)), 200)
|
||||
offset = max(0, int(request.args.get("offset", 0)))
|
||||
except ValueError:
|
||||
return jsonify({"error": "Invalid limit or offset"}), 400
|
||||
|
||||
if note_type and note_type not in _VALID_TYPES:
|
||||
return jsonify({"error": "Invalid type"}), 400
|
||||
|
||||
from fabledassistant.services.knowledge import query_knowledge_ids
|
||||
ids, total = await query_knowledge_ids(
|
||||
user_id=uid, note_type=note_type, tags=tags,
|
||||
sort=sort, q=q, limit=limit, offset=offset,
|
||||
)
|
||||
return jsonify({"ids": ids, "total": total, "has_more": (offset + len(ids)) < total})
|
||||
|
||||
|
||||
@knowledge_bp.route("/batch", methods=["GET"])
|
||||
@login_required
|
||||
async def get_knowledge_batch():
|
||||
"""Fetch full items for a comma-separated list of IDs (max 100).
|
||||
|
||||
Returns {items: [...]} in the order of the requested IDs.
|
||||
"""
|
||||
uid = get_current_user_id()
|
||||
ids_raw = request.args.get("ids", "").strip()
|
||||
if not ids_raw:
|
||||
return jsonify({"items": []})
|
||||
try:
|
||||
ids = [int(x) for x in ids_raw.split(",") if x.strip()]
|
||||
except ValueError:
|
||||
return jsonify({"error": "Invalid IDs"}), 400
|
||||
if len(ids) > 100:
|
||||
return jsonify({"error": "Too many IDs (max 100)"}), 400
|
||||
|
||||
from fabledassistant.services.knowledge import get_knowledge_by_ids
|
||||
items = await get_knowledge_by_ids(uid, ids)
|
||||
return jsonify({"items": items})
|
||||
|
||||
|
||||
@knowledge_bp.route("/tags", methods=["GET"])
|
||||
@login_required
|
||||
async def list_knowledge_tags():
|
||||
|
||||
Reference in New Issue
Block a user