French-Coach / notebook.py
Asma-F's picture
Deploy: French Coach app (MiniCPM4.1-8B ZeroGPU + React frontend)
4fd1234 verified
Raw
History Blame Contribute Delete
5.3 kB
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