Skip to content

AI: Powered Document (4 modes: article / from list / from article / research URL) #15

Description

@Adron

feature: "powered_document" drafts a full markdown document. Four modes, set in context.mode:

context.mode Also requires Source
article (default) — drafts from the topic in input alone
from_list context.listId loads your list's schema + up to 50 rows server-side
from_article context.documentId loads your document's title + body server-side
research_url context.url fetches the page (http/https, SSRF-guarded), extracts title/description/text, cites the URL

You pass a reference, not source text. Derived modes are IDOR-guarded — a missing or non-owned reference returns 422 invalid_input. Server-fetched context is truncated before it reaches the model.

  • /suggest → kind: "document"; /generate → {documentId}.

Acceptance criteria

  • "Powered Document" action on Views/DocumentsView.xaml, gated on IsAiAvailable.
  • Mode picker; from_list / from_article pick their source from the user's own lists/documents; research_url takes a URL with http/https validation.
  • Rendered markdown preview, editable before confirming.
  • Confirm calls /api/ai/generate and opens the created document from a fresh GET.
  • 422 invalid_input on a missing/non-owned reference is reported against the source field, not as a generic failure.

Activity

  1. added
    parityWeb/API feature-parity work
    P1Table stakes
    on Sep 15, 2026
  2. Adron commented on Sep 16, 2026

    @Adron
    MemberAuthor

    Live-verified corrections from the service layer (#9 / PR #137) — read before building this

    Five things contradict what the AI epic and these issues assume.

    1. quota is not the same object on both endpoints.
    GET /api/ai/status → {usedToday, dailyLimit, remaining}. POST /api/ai/suggest → {usedToday, dailyLimit} with no remaining (verified twice). So AiQuota.Remaining is int?, and a post-suggest quota chip cannot read .Remaining — bind RemainingOrComputed instead. #10's "remaining quota is visible wherever an AI action is offered" has to account for that.

    2. There is no provider picker to build.
    AI is Anthropic-only, app-wide; per-user provider keys were removed server-side 2026-09-05. The provider field on /generate is audit-ledger-only and does not select anything, and the openai/gemini entries in defaultModels are dormant and unreachable. Don't surface a provider choice.

    3. crossPost has six fields, not two — crossPostToBluesky, selectedMastodonIds, crossPostToLinkedIn, selectedLinkedInTargets, crossPostToTwitter, linkedInLinkAsFirstComment. Both crossPost and scheduleImmediately are message_series-only (relevant to #12).

    4. A sixth 422 code exists that the epic didn't list: refused. Treat it like invalid_ai_output. Modelled and marked retryable.

    5. Quota accounting is asymmetric, and it shapes the UI.
    Input-validation 422s cost nothing (usedToday stayed 0 across two), but any call that reaches the model costs a unit — including failures. So client-side pre-flight is load-bearing, not politeness: enforce the word caps, the 10-word series gate (#12/#13), a missing powered_document source ref (#15), and the non-persistable-artifact rule (#11) before calling. And no auto-retry — a retry burns another unit.

    Also: context.spacingMinutes is accepted but ignored (series rows always land 4 minutes apart), so it's deliberately not exposed.

    Note on the exception type

    The code is surfaced on a new AiApiException, not on InterlinedApiException — the latter is sealed and was being rewritten in #130. Folding them together is flagged as a follow-up in the code. Catch AiApiException in these UI issues.

    Unverified

    POST /api/ai/generate was never called (it persists content to a shared test account). Its envelope is contract-transcribed, flagged unverified in doc comments, and keeps the raw created element for read-after-write. Whichever of #12–#15 lands first should verify it against a real account and tighten the model.

  3. Adron commented on Sep 16, 2026

    @Adron
    MemberAuthor

    Implemented in #177 — work continues in the PR from here.

    PoweredDocumentPanel covers all four context.mode variants behind #10's gate, hosted in DocumentsView.xaml with a single line and no code-behind edit (#139 is in that file too). All four reference-rejection paths were verified live and cost nothing, so their exact server wording — "List not found.", "A list must be selected.", "Document not found.", "Only http(s) URLs are supported." — is what the user sees, rendered against the source picker rather than in the generic notice slot. Attribution branches on mode plus error code, never on prose: since every client-knowable input problem is rejected locally first, a surviving invalid_input in a derived mode can only be the reference being missing or not theirs.

    Two live findings shaped this. An unrecognized context.mode is not cheap-rejected — it reaches the model and bills a unit — so mode stays a four-value enum end to end. And the returned draft contained a GFM pipe table, so the hand-rolled FlowDocument renderer needed table support; without it those rows would have collapsed into a paragraph of pipes and dashes. The renderer's line-classification predicates were split into a WPF-free file so they could be tested against the real markdown off-Windows — 25 assertions, all passing.

    POST /api/ai/generate was not called (shared account), so the confirm path re-fetches with GET /api/documents/{id}; the {documentId} field name and a real research_url fetch are the two things still unverified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Table stakesarea:aiArea: aiparityWeb/API feature-parity work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions