Files
lukasandClaude Opus 5.5 8a1b9979e9 Productivity features, UI translations, local email translation
- Snooze (with a Snoozed folder), keyboard shortcuts with a Settings
  section, mail templates (Compose > More > Templates), a Subscriptions
  folder with bulk unsubscribe/delete, and tracker blocking with a
  Privacy toggle (on by default)
- Own dialogs instead of browser popups; emoji picker in compose; compose
  windows survive a visit to Settings
- Sentence completion learned from the user's sent mail (no AI)
- Interface in English and Polish (one file per language, English as the
  fallback), picked from the browser on first run, then kept
- Email translation on the server via LibreTranslate: Translate button
  and "Translate to...", messages in several languages translated part
  by part, names/addresses/emoji/file names/phone numbers kept, an
  animated line while translating
- Antispam "Mark as spam" loads with the message, not when the menu opens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:00:31 +02:00

45 lines
1.6 KiB
Python

"""Sentence completion for the compose window (app/completion.py)."""
from __future__ import annotations
import datetime as dt
from typing import Annotated
from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app import completion
from app.db import get_session
from app.identity.deps import CurrentUser, require_csrf
from app.models import CompletionIndexState
from app.queue import enqueue_job
router = APIRouter(prefix="/completion", tags=["compose"], dependencies=[Depends(require_csrf)])
DbSession = Annotated[AsyncSession, Depends(get_session)]
class CompletionIn(BaseModel):
# The text before the cursor (its last line is all that matters). A POST,
# not a query string: what the user is typing stays out of access logs.
before: str = Field(max_length=2000)
class CompletionOut(BaseModel):
suggestion: str | None
@router.post("", response_model=CompletionOut)
async def suggest(body: CompletionIn, user: CurrentUser, db: DbSession) -> CompletionOut:
before = body.before[-300:]
built = await db.scalar(
select(CompletionIndexState.built_at).where(CompletionIndexState.user_id == user.id)
)
if built is None or built < dt.datetime.now(dt.UTC) - completion.REBUILD_AFTER:
# In the background; until it is built, the starter phrases still answer.
await enqueue_job("build_index_job", str(user.id), _job_id=f"complete:{user.id}")
personal = await completion.personal_phrases(db, user.id, completion.keys_for(before))
return CompletionOut(suggestion=completion.complete(personal, before))