diora-web/docs/ai.md

63 lines
4.9 KiB
Markdown
Raw Permalink Normal View History

# KI: Claude schlägt vor, der Nutzer entscheidet
Since 2026-10-24. Organisation help with Claude (Anthropic) across diora: books and magazines
first; podcasts, news/blogs and projects follow (same building blocks).
## The key: the user's own, end-to-end encrypted, only on the devices
Two providers, chosen in the settings (decided 2026-10-25): **Anthropic** (the official SDK) or
**OpenRouter** (the same Claude model, `anthropic/claude-opus-5.5`). OpenRouter's
Anthropic-compatible endpoint refuses the headers a browser call needs (`anthropic-version` …,
checked by a CORS preflight), so diora talks to OpenRouter's chat completions instead: plain
`fetch`, `response_format` json_schema (strict), `reasoning.effort`; no fallback on refusals, and
OpenRouter sees what is sent too. The vault keeps `{provider, anthropic_key, openrouter_key}`
(an old `{anthropic_key}` still reads as Anthropic); a key is checked before it is kept
(Anthropic: the model's info; OpenRouter: `GET /api/v1/key`).
Decided with the user: the API key is entered in the app ("KI-Assistent", the ✦ of a
feature or the app menu) and stored like the books — encrypted with the book key on the device,
kept by the server as a blob it can't read:
- `GET/POST /accounts/vault/<name>/` (`accounts.UserVault`, name `ai`): `{ct, iv}`; `{ct: ''}`
removes it. Its own store, not the library prefs (`/books/prefs/` is rewritten whole when
the folder order is saved).
- The devices decrypt it into memory only and call Claude **directly** from the page
(browser, the apps' WebView, the desktop). The diora server sees neither the key nor what is
sent; Anthropic sees what a feature needs (titles, authors, folder names …). Anthropic bills
the user.
- Without the book key on a device (or after a password change, which changes the book key)
the key can't be read: the settings ask for it again. Not on diora Reader (e-readers).
- Server-side jobs (in the background, without an app open) were left out on purpose: they
would need the key on the server. The project Radar has its own server key (docs/projects.md).
Podcast articles wait on the server as a wish until a device with the key is open
(docs/transcription.md).
## Building blocks (`static/js/ai.js`, an ES module)
- The official SDK, `@anthropic-ai/sdk` 0.133.0, bundled for the browser with esbuild 0.28.2:
`static/vendor/anthropic-sdk-0.133.0.mjs` (licence beside it). `dangerouslyAllowBrowser`:
the key is the user's own. To update: `npm install @anthropic-ai/sdk@<v> esbuild` in an empty
folder, `esbuild entry.mjs --bundle --format=esm --platform=browser --target=es2020 --minify`
with `export { default } from '@anthropic-ai/sdk'`, and change the import in ai.js.
- `DioraAI.ask({system, prompt, schema, effort})`: model `claude-opus-5-5`, structured output
(`output_config.format` json_schema), effort `low` by default, streamed, `fallbacks: "default"`
(beta `server-side-fallback-2026-07-01`) for declined requests; refusals and over-long
answers become plain German messages.
- `ask` also takes `onProgress(words)` (words written so far while streaming; OpenRouter is not streamed and reports once at the end)
and `signal` (an `AbortSignal`), both optional; `DioraAI.modelName()` is the model of the chosen provider. Used by the podcast articles.
- `DioraAI.review({title, intro, items, onApply})`: the suggestions with checkboxes — nothing
changes until the user ticks and presses "Übernehmen".
- `DioraAI.settings()`, `withKey(fn)`, `progress()`, `showError()`, `toast()`; the sheets close
with Android's back key; ⚙ in each sheet opens the settings.
## Features
| area | where | what Claude gets | what it suggests |
|---|---|---|---|
| Bücher, Zeitschriften | ✦ in the library's head (`static/js/app/55-library-tools.js`) | per book: id, title, author, folders, series, magazine mark; the existing folders | folders to add (existing first; new ones only for ≥ 3 books), series with volume, cleaned title/author where broken, magazine mark |
| Podcasts | ✦ in Abos (diora-android `web-native/static/native-podcast.js`, copied to `static/podcasts/`) | per subscription: title, author, folders, level, heard in 12 months, publishing interval, three newest episode titles | folders to add (≤ 2), level Stamm / Strom / Auf Abruf (only with a clear reason) |
| Nachrichten, Blogs | ✦ in the feeds' bar (`static/js/feeds.js`; the apps' section bar presses it) | per source: title, address, kind, Ressort or folders, recent titles shown | Ressort for news, folders for blogs, news ↔ blog when clearly wrong |
| Podcast-Artikel | ✦ Artikel in an episode (`static/js/podcast-article.js`, docs/transcription.md) | the episode's transcript (feed or Whisper), title, author, shownotes, chapters | an article from the speakers' standpoint with summary and sections, saved to the episode and to Blogs (no review sheet: it is new text, removable) |
Applied through `_updateBookMeta` (the encrypted meta, as when editing by hand).