| import json |
| from datetime import date |
| from db import get_cursor |
| import curator |
|
|
|
|
| def save_page(user_id: str, raw_text: str, annotations: dict) -> tuple[str, str]: |
| """Curates the page (title/summary/type/resources), inserts into DB. Returns (page_id, title).""" |
| import nlp as _nlp |
| curated = curator.curate_page(raw_text) |
| category = _nlp.detect_category(raw_text[:300]) |
| title = curated["title"] |
| metadata = { |
| "category": category, |
| "summary": curated["summary"], |
| "page_type": curated["page_type"], |
| "links": curated["links"], |
| "books": curated["books"], |
| } |
| with get_cursor() as cur: |
| cur.execute( |
| """INSERT INTO pages (user_id, title, date, raw_text, annotations, metadata) |
| VALUES (%s, %s, %s, %s, %s, %s) RETURNING id::text""", |
| (user_id, title, date.today(), raw_text, |
| json.dumps(annotations), json.dumps(metadata)), |
| ) |
| page_id = cur.fetchone()["id"] |
| return page_id, title |
|
|
|
|
| def list_pages(user_id: str) -> list[dict]: |
| """Sidebar list — returns [{id, title, date, category, page_type, preview}] newest-first, max 50. |
| |
| Fetches the first 300 chars of raw_text to detect category and build a hover |
| preview without pulling the full lesson text over the wire. |
| """ |
| import nlp as _nlp |
| with get_cursor() as cur: |
| cur.execute( |
| "SELECT id::text, title, date::text, " |
| "LEFT(raw_text, 300) AS snippet, " |
| "COALESCE(metadata->>'category', '') AS stored_category, " |
| "COALESCE(metadata->>'page_type', 'lesson') AS page_type " |
| "FROM pages WHERE user_id = %s ORDER BY created_at DESC LIMIT 50", |
| (user_id,), |
| ) |
| rows = [dict(r) for r in cur.fetchall()] |
| for row in rows: |
| snippet = row.pop("snippet", "") or "" |
| stored_cat = row.pop("stored_category", "") or "" |
| row["category"] = stored_cat if stored_cat else _nlp.detect_category(snippet) |
| preview = snippet[:100] |
| row["preview"] = preview + ("…" if len(snippet) > 100 else "") |
| return rows |
|
|
|
|
| def list_resources(user_id: str) -> list[dict]: |
| """Return resource-type pages: [{id, title, links, books}], newest-first.""" |
| with get_cursor() as cur: |
| cur.execute( |
| "SELECT id::text, title, " |
| "COALESCE(metadata->'links', '[]'::jsonb) AS links, " |
| "COALESCE(metadata->'books', '[]'::jsonb) AS books " |
| "FROM pages WHERE user_id = %s AND metadata->>'page_type' = 'resource' " |
| "ORDER BY created_at DESC", |
| (user_id,), |
| ) |
| return [dict(r) for r in cur.fetchall()] |
|
|
|
|
| def update_resource_links(page_id: str, user_id: str, links: list[dict]) -> bool: |
| """Overwrite metadata->'links' on a resource page. Returns True if a row was updated.""" |
| with get_cursor() as cur: |
| cur.execute( |
| "UPDATE pages SET metadata = jsonb_set(metadata, '{links}', %s::jsonb) " |
| "WHERE id = %s AND user_id = %s AND metadata->>'page_type' = 'resource' " |
| "RETURNING id", |
| (json.dumps(links), page_id, user_id), |
| ) |
| return cur.fetchone() is not None |
|
|
|
|
| def update_title(page_id: str, user_id: str, title: str) -> str: |
| """Persist a user-edited title (overrides the auto-generated one). Returns the saved title.""" |
| title = (title or "").strip()[:80] or "Untitled Lesson" |
| with get_cursor() as cur: |
| cur.execute( |
| "UPDATE pages SET title = %s WHERE id = %s AND user_id = %s", |
| (title, page_id, user_id), |
| ) |
| return title |
|
|
|
|
| def get_page(page_id: str, user_id: str) -> dict | None: |
| with get_cursor() as cur: |
| cur.execute( |
| "SELECT id::text, title, raw_text, annotations " |
| "FROM pages WHERE id = %s AND user_id = %s", |
| (page_id, user_id), |
| ) |
| row = cur.fetchone() |
| return dict(row) if row else None |
|
|
|
|
| def update_annotations(page_id: str, annotations: dict) -> None: |
| """Persist LLM-enriched annotations back to the page row.""" |
| with get_cursor() as cur: |
| cur.execute( |
| "UPDATE pages SET annotations = %s WHERE id = %s", |
| (json.dumps(annotations), page_id), |
| ) |
|
|
|
|
| def update_page(page_id: str, user_id: str, raw_text: str, annotations: dict) -> str: |
| """Update page content in-place, keep existing title. Returns the title.""" |
| with get_cursor() as cur: |
| cur.execute( |
| "SELECT title FROM pages WHERE id = %s AND user_id = %s", |
| (page_id, user_id), |
| ) |
| row = cur.fetchone() |
| title = row["title"] if row else raw_text.split("\n")[0][:60] |
| with get_cursor() as cur: |
| cur.execute( |
| "UPDATE pages SET raw_text = %s, annotations = %s WHERE id = %s AND user_id = %s", |
| (raw_text, json.dumps(annotations), page_id, user_id), |
| ) |
| return title |
|
|
|
|
| def delete_page(page_id: str, user_id: str) -> bool: |
| """Delete page row (exercises cascade). Returns True if a row was deleted.""" |
| with get_cursor() as cur: |
| cur.execute( |
| "DELETE FROM pages WHERE id = %s AND user_id = %s RETURNING id", |
| (page_id, user_id), |
| ) |
| return cur.fetchone() is not None |
|
|