feat(app,learn,todo): the assistants in Settings and on the operator's screen

ADR 0021, step 2's app half. halkyon_app asks the module's assistant
endpoints (ServerpodAssistantGateway, ServerpodAssistantOperatorGateway)
and a person's own provider directly (halkyon_llm), behind
AssistantRepository: an AssistantChoice is a GrantedAssistant, asked
through the instance, which counts it, or a PersonalAssistant with the
person's own key, which the instance never sees. assistant_messages.dart
turns a conversation into the endpoint's models and back.

AssistantsCard, for an app's settings: what is granted and left of its
limits, agreed to once on a sheet saying what goes where, the person's
use by UTC month, the instance's privacy page, and their own assistants
where the operator allows own keys (kept ones stay listed, unasked,
where it doesn't). AssistantGrantsCard on InstanceOperatorScreen: grants
by handle with their limits in the pricing's unit, changed and taken
back after asking, everyone's use by month; a found account's menu
grants too. Nothing shows where nothing is offered or can be read.
package:halkyon_app/assistant_models.dart holds the models in pure Dart.

Learn keeps own assistants in the person's settings (ownAssistants,
schema 12, SettingsOwnAssistants); SettingsRepository.update refuses
(OwnAssistantRefused) to write a key where the settings would not be
sealed (SettingsGateway.seals). Learn depends on halkyon_llm directly.
Todo wires the gateways; a member offers no assistant. No feature asks
an assistant yet (step 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-25 18:59:41 +01:00
commit 36899cdc3b
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1304,4 +1304,319 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
@override
String get operatorFailed =>
'Das hat nicht geklappt. Versuch es noch einmal.';
@override
String get operatorAssistantNotSet =>
'Dieser Assistent ist hier nicht mehr eingerichtet.';
@override
String get operatorInvalidLimit =>
'Eine Grenze kann nicht unter null liegen.';
@override
String get assistantGrantsTitle => 'Assistenten';
@override
String get assistantGrantsNone => 'Noch hat niemand einen Assistenten.';
@override
String get assistantGrant => 'Assistenten freigeben';
@override
String get assistantGrantSave => 'Freigeben';
@override
String get assistantGrantChange => 'Grenzen ändern';
@override
String get assistantGrantAssistant => 'Assistent';
@override
String get assistantGrantDaily => 'Grenze pro Tag';
@override
String get assistantGrantMonthly => 'Grenze pro Monat';
@override
String get assistantGrantLimitHelper =>
'Leer für keine. Tage und Monate nach UTC.';
@override
String get assistantGrantLimitInvalid => 'Keine Zahl, die das nimmt';
@override
String get assistantGrantNoHandle => 'Kein Konto hat dieses Handle.';
@override
String get assistantGrantTokens => 'Tokens';
@override
String get assistantGrantNote =>
'Vor der ersten Frage stimmen sie zu, was wohin geht. Du siehst, wie viel sie nutzen, nie, was sie fragen.';
@override
String assistantPerDay(String amount) {
return '$amount pro Tag';
}
@override
String assistantPerMonth(String amount) {
return '$amount pro Monat';
}
@override
String get assistantNoLimits => 'Ohne Grenzen';
@override
String get assistantGrantAgreed => 'Zugestimmt';
@override
String get assistantGrantNotAgreed => 'Noch nicht zugestimmt';
@override
String get assistantGrantOutdated =>
'Die Preise haben sich geändert: Grenzen neu setzen';
@override
String get assistantGrantUnset => 'Nicht mehr eingerichtet';
@override
String get assistantRevoke => 'Zurücknehmen';
@override
String assistantRevokeTitle(String assistant) {
return '$assistant zurücknehmen?';
}
@override
String get assistantRevokeMessage =>
'Sie können ihn nicht mehr fragen. Was sie genutzt haben, bleibt gezählt.';
@override
String get assistantUseByMonth => 'Nutzung nach Monat';
@override
String get assistantUseTitle => 'Nutzung der Assistenten';
@override
String get assistantUseNone => 'In diesem Monat nichts genutzt.';
@override
String get assistantUseKept =>
'Je UTC-Tag gezählt, aufbewahrt für diesen Monat und die zwei davor. Nie, was gefragt wurde.';
@override
String assistantRequests(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count Anfragen',
one: '1 Anfrage',
);
return '$_temp0';
}
@override
String assistantTokens(String amount) {
return '$amount Tokens';
}
@override
String get assistantsTitle => 'Assistenten';
@override
String assistantModelAt(String model, String host) {
return '$model bei $host';
}
@override
String assistantLeftToday(String left, String limit) {
return '$left von $limit heute übrig';
}
@override
String assistantLeftThisMonth(String left, String limit) {
return '$left von $limit diesen Monat übrig';
}
@override
String get assistantAgree => 'Lesen und zustimmen';
@override
String get assistantWaitsForLimits =>
'Wartet darauf, dass seine Grenzen neu gesetzt werden';
@override
String get assistantYourUse => 'Deine Nutzung';
@override
String assistantPrivacyPage(String instance) {
return 'Was auf $instance wohin geht';
}
@override
String get assistantOwnTitle => 'Mit deinem eigenen Schlüssel';
@override
String get assistantOwnAdd => 'Assistenten mit eigenem Schlüssel hinzufügen';
@override
String get assistantOwnNote =>
'Die App fragt deinen Anbieter direkt: Diese Instanz sieht weder deinen Schlüssel noch deine Fragen und zählt nichts. Der Schlüssel liegt in deinen Einstellungen, auf deinen Geräten verschlüsselt.';
@override
String get assistantOwnEdit => 'Dein eigener Schlüssel';
@override
String get assistantOwnName => 'Name';
@override
String get assistantOwnProtocol => 'API';
@override
String get assistantOwnBaseUrl => 'Adresse';
@override
String get assistantOwnBaseUrlHelper =>
'Leer für die des Anbieters. Mistral, OpenRouter oder ein eigenes Modell: seine Adresse mit /v1.';
@override
String get assistantOwnBaseUrlInvalid =>
'Nur https, oder http im eigenen Netz';
@override
String get assistantOwnModel => 'Modell';
@override
String get assistantOwnKey => 'Schlüssel';
@override
String get assistantOwnKeyHelper =>
'Leer für ein eigenes Modell, das keinen braucht.';
@override
String get assistantOwnImages => 'Liest Bilder';
@override
String get assistantOwnSave => 'Speichern';
@override
String get assistantOwnRemove => 'Entfernen';
@override
String assistantOwnRemoveTitle(String name) {
return '$name entfernen?';
}
@override
String get assistantOwnRemoveMessage =>
'Sein Schlüssel verschwindet auf allen Geräten aus deinen Einstellungen.';
@override
String get assistantOwnRefused =>
'Der Schlüssel wurde nicht gespeichert: Dieses Gerät kann deine Einstellungen noch nicht verschlüsseln. Öffne zuerst hier die Schlüssel deines Kontos.';
@override
String assistantOwnNotAllowed(String instance) {
return 'Wer $instance betreibt, erlaubt hier keine eigenen Schlüssel, also fragt die App diese nicht.';
}
@override
String assistantConsentTitle(String name) {
return 'Bevor du $name fragst';
}
@override
String assistantConsentModel(String model, String host) {
return 'Es antwortet das Modell $model bei $host.';
}
@override
String get assistantConsentWhatGoes =>
'Dorthin geht nur, was du sendest: deine Frage und was du davon auswählst, worum es geht. Für den Anbieter gelten seine eigenen Bedingungen.';
@override
String get assistantConsentImages =>
'Es liest die Bilder, die du hinzufügst.';
@override
String assistantConsentInstance(String instance) {
return 'Es geht auf dem Weg durch $instance, das nichts davon aufbewahrt.';
}
@override
String assistantConsentOperator(String instance) {
return 'Wer $instance betreibt, sieht, wie viel du es nutzt (Anfragen, Tokens, Kosten), nie, was du fragst.';
}
@override
String get assistantConsentConversations =>
'Deine Unterhaltungen bleiben auf deinen Geräten, verschlüsselt.';
@override
String get assistantConsentAgree => 'Zustimmen';
@override
String assistantConsentAgreedOn(String date) {
return 'Du hast am $date zugestimmt.';
}
@override
String get assistantProblemNotGranted =>
'Dieser Assistent ist für dich nicht mehr freigegeben.';
@override
String get assistantProblemNotAgreed => 'Stimm zuerst zu, was wohin geht.';
@override
String assistantProblemLimitReached(String time) {
return 'Seine Grenze ist bis $time erreicht.';
}
@override
String get assistantProblemLimitReachedNow =>
'Seine Grenze ist vorerst erreicht.';
@override
String get assistantProblemLimitOutdated =>
'Es wartet darauf, dass seine Grenzen neu gesetzt werden.';
@override
String get assistantProblemImagesNotRead => 'Es liest keine Bilder.';
@override
String get assistantProblemMalformed =>
'Diese Frage kann nicht gesendet werden.';
@override
String get assistantProblemTooLarge =>
'Diese Frage ist zu lang oder enthält zu viele oder zu große Bilder.';
@override
String get assistantProblemTooManyAtOnce =>
'Warte zuerst auf die Antworten, die unterwegs sind.';
@override
String get assistantProblemProviderKey =>
'Der Anbieter hat den Schlüssel abgelehnt.';
@override
String get assistantProblemProviderBusy =>
'Der Anbieter ist ausgelastet. Versuch es gleich noch einmal.';
@override
String get assistantProblemProviderRefused =>
'Der Anbieter hat diese Frage abgelehnt.';
@override
String get assistantProblemProviderFailed =>
'Beim Anbieter ist etwas schiefgegangen. Versuch es noch einmal.';
@override
String get assistantProblemNotAllowed =>
'Das können nur die, für die er freigegeben ist.';
@override
String get assistantProblemFailed =>
'Das hat nicht geklappt. Versuch es noch einmal.';
}