Add link editing, AI re-description, and footer
Links can now be edited manually (title, summary, category, manufacturer, tags) via an inline edit form, with a "KI neu beschreiben lassen" action that re-fetches the URL and lets the AI regenerate all fields. Editing recomputes the search embedding. Also adds an app footer showing author, version, and hostname. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
132
app/main.py
132
app/main.py
@@ -19,6 +19,11 @@ app = FastAPI(title="LinkVault")
|
||||
app.add_middleware(SessionMiddleware, secret_key=config.SECRET_KEY, max_age=60 * 60 * 24 * 14)
|
||||
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates"))
|
||||
templates.env.globals.update(
|
||||
app_author=config.APP_AUTHOR,
|
||||
app_version=config.APP_VERSION,
|
||||
app_host=config.APP_HOST,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -281,3 +286,130 @@ def delete_link(
|
||||
db.delete(link)
|
||||
db.commit()
|
||||
return Response(status_code=200)
|
||||
|
||||
|
||||
def _get_owned_link(db: Session, user: User, link_id: int) -> Link | None:
|
||||
link = db.get(Link, link_id)
|
||||
if link and link.user_id == user.id:
|
||||
return link
|
||||
return None
|
||||
|
||||
|
||||
def _parse_tags(raw: str) -> list[str]:
|
||||
return [t.strip() for t in raw.split(",") if t.strip()][:8]
|
||||
|
||||
|
||||
@app.get("/links/{link_id}/edit", response_class=HTMLResponse)
|
||||
def edit_link_form(
|
||||
link_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
user = current_user(request, db)
|
||||
if not user:
|
||||
return Response(status_code=401, headers={"HX-Redirect": "/login"})
|
||||
link = _get_owned_link(db, user, link_id)
|
||||
if not link:
|
||||
return Response(status_code=404)
|
||||
return templates.TemplateResponse(
|
||||
"_link_card_edit.html",
|
||||
{"request": request, "link": link, "ai_enabled": config.AI_ENABLED},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/links/{link_id}/view", response_class=HTMLResponse)
|
||||
def view_link_card(
|
||||
link_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
user = current_user(request, db)
|
||||
if not user:
|
||||
return Response(status_code=401, headers={"HX-Redirect": "/login"})
|
||||
link = _get_owned_link(db, user, link_id)
|
||||
if not link:
|
||||
return Response(status_code=404)
|
||||
return templates.TemplateResponse(
|
||||
"_link_card.html", {"request": request, "link": link}
|
||||
)
|
||||
|
||||
|
||||
@app.put("/links/{link_id}", response_class=HTMLResponse)
|
||||
def update_link(
|
||||
link_id: int,
|
||||
request: Request,
|
||||
title: str = Form(""),
|
||||
summary: str = Form(""),
|
||||
category: str = Form(""),
|
||||
manufacturer: str = Form(""),
|
||||
tags: str = Form(""),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
user = current_user(request, db)
|
||||
if not user:
|
||||
return Response(status_code=401, headers={"HX-Redirect": "/login"})
|
||||
link = _get_owned_link(db, user, link_id)
|
||||
if not link:
|
||||
return Response(status_code=404)
|
||||
|
||||
link.title = title.strip()[:300] or link.url
|
||||
link.summary = summary.strip()[:1000]
|
||||
link.category = category.strip()[:120]
|
||||
link.manufacturer = manufacturer.strip()[:120]
|
||||
link.tags = json.dumps(_parse_tags(tags), ensure_ascii=False)
|
||||
|
||||
embed_source = " ".join([link.title, link.summary, tags])
|
||||
embedding = ai.embed(embed_source)
|
||||
if embedding:
|
||||
link.embedding = json.dumps(embedding)
|
||||
|
||||
db.commit()
|
||||
db.refresh(link)
|
||||
return templates.TemplateResponse(
|
||||
"_link_card.html", {"request": request, "link": link}
|
||||
)
|
||||
|
||||
|
||||
@app.post("/links/{link_id}/reanalyze", response_class=HTMLResponse)
|
||||
def reanalyze_link(
|
||||
link_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
user = current_user(request, db)
|
||||
if not user:
|
||||
return Response(status_code=401, headers={"HX-Redirect": "/login"})
|
||||
link = _get_owned_link(db, user, link_id)
|
||||
if not link:
|
||||
return Response(status_code=404)
|
||||
|
||||
title, text = scraper.fetch(link.url)
|
||||
existing = facets(db, user)
|
||||
meta = ai.categorize(
|
||||
link.url,
|
||||
title,
|
||||
text,
|
||||
[name for name, _ in existing["categories"] if name != link.category],
|
||||
[name for name, _ in existing["manufacturers"] if name != link.manufacturer],
|
||||
)
|
||||
|
||||
embed_source = " ".join(
|
||||
[meta["title"], meta["summary"], " ".join(meta["tags"]), text[:1000]]
|
||||
)
|
||||
embedding = ai.embed(embed_source)
|
||||
|
||||
link.title = meta["title"]
|
||||
link.summary = meta["summary"]
|
||||
link.category = meta["category"]
|
||||
link.manufacturer = meta["manufacturer"]
|
||||
link.tags = json.dumps(meta["tags"], ensure_ascii=False)
|
||||
if embedding:
|
||||
link.embedding = json.dumps(embedding)
|
||||
link.status = "ok" if text else "no_content"
|
||||
|
||||
db.commit()
|
||||
db.refresh(link)
|
||||
return templates.TemplateResponse(
|
||||
"_link_card_edit.html",
|
||||
{"request": request, "link": link, "ai_enabled": config.AI_ENABLED},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user