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

@ -1283,6 +1283,314 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
@override
String get operatorFailed => 'That didn\'t work. Try again.';
@override
String get operatorAssistantNotSet =>
'That assistant isn\'t set up here any more.';
@override
String get operatorInvalidLimit => 'A limit can\'t be below zero.';
@override
String get assistantGrantsTitle => 'Assistants';
@override
String get assistantGrantsNone => 'Nobody has an assistant yet.';
@override
String get assistantGrant => 'Grant an assistant';
@override
String get assistantGrantSave => 'Grant';
@override
String get assistantGrantChange => 'Change limits';
@override
String get assistantGrantAssistant => 'Assistant';
@override
String get assistantGrantDaily => 'Limit a day';
@override
String get assistantGrantMonthly => 'Limit a month';
@override
String get assistantGrantLimitHelper =>
'Empty for none. Days and months are UTC\'s.';
@override
String get assistantGrantLimitInvalid => 'Not a number this takes';
@override
String get assistantGrantNoHandle => 'No account has this handle.';
@override
String get assistantGrantTokens => 'tokens';
@override
String get assistantGrantNote =>
'They agree once to what goes where before they ask it. You see how much they use, never what they ask.';
@override
String assistantPerDay(String amount) {
return '$amount a day';
}
@override
String assistantPerMonth(String amount) {
return '$amount a month';
}
@override
String get assistantNoLimits => 'No limits';
@override
String get assistantGrantAgreed => 'Agreed';
@override
String get assistantGrantNotAgreed => 'Not agreed yet';
@override
String get assistantGrantOutdated =>
'Its prices changed: set its limits again';
@override
String get assistantGrantUnset => 'No longer set up';
@override
String get assistantRevoke => 'Take back';
@override
String assistantRevokeTitle(String assistant) {
return 'Take back $assistant?';
}
@override
String get assistantRevokeMessage =>
'They can no longer ask it. What they used stays counted.';
@override
String get assistantUseByMonth => 'Use by month';
@override
String get assistantUseTitle => 'Use of assistants';
@override
String get assistantUseNone => 'Nothing used in this month.';
@override
String get assistantUseKept =>
'Counted per UTC day, kept for this month and the two before. Never what was asked.';
@override
String assistantRequests(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count requests',
one: '1 request',
);
return '$_temp0';
}
@override
String assistantTokens(String amount) {
return '$amount tokens';
}
@override
String get assistantsTitle => 'Assistants';
@override
String assistantModelAt(String model, String host) {
return '$model at $host';
}
@override
String assistantLeftToday(String left, String limit) {
return '$left of $limit left today';
}
@override
String assistantLeftThisMonth(String left, String limit) {
return '$left of $limit left this month';
}
@override
String get assistantAgree => 'Read and agree';
@override
String get assistantWaitsForLimits => 'Waits for its limits to be set again';
@override
String get assistantYourUse => 'Your use';
@override
String assistantPrivacyPage(String instance) {
return 'What goes where on $instance';
}
@override
String get assistantOwnTitle => 'With your own key';
@override
String get assistantOwnAdd => 'Add an assistant with your own key';
@override
String get assistantOwnNote =>
'The app asks your provider directly: this instance sees neither your key nor your questions, and counts nothing. The key is kept in your settings, encrypted on your devices.';
@override
String get assistantOwnEdit => 'Your own key';
@override
String get assistantOwnName => 'Name';
@override
String get assistantOwnProtocol => 'API';
@override
String get assistantOwnBaseUrl => 'Address';
@override
String get assistantOwnBaseUrlHelper =>
'Empty for the provider\'s own. Mistral, OpenRouter or a model of your own: its address with /v1.';
@override
String get assistantOwnBaseUrlInvalid =>
'Only https, or http on your own network';
@override
String get assistantOwnModel => 'Model';
@override
String get assistantOwnKey => 'Key';
@override
String get assistantOwnKeyHelper =>
'Empty for a model of your own that takes none.';
@override
String get assistantOwnImages => 'Reads images';
@override
String get assistantOwnSave => 'Save';
@override
String get assistantOwnRemove => 'Remove';
@override
String assistantOwnRemoveTitle(String name) {
return 'Remove $name?';
}
@override
String get assistantOwnRemoveMessage =>
'Its key goes from your settings on every device.';
@override
String get assistantOwnRefused =>
'The key wasn\'t kept: this device can\'t encrypt your settings yet. Open your account\'s keys here first.';
@override
String assistantOwnNotAllowed(String instance) {
return 'Whoever runs $instance doesn\'t allow own keys here, so the app doesn\'t ask these.';
}
@override
String assistantConsentTitle(String name) {
return 'Before you ask $name';
}
@override
String assistantConsentModel(String model, String host) {
return 'It answers with the model $model at $host.';
}
@override
String get assistantConsentWhatGoes =>
'Only what you send goes there: your question and what you pick of what it is about. The provider\'s own terms apply to it.';
@override
String get assistantConsentImages => 'It reads the images you add.';
@override
String assistantConsentInstance(String instance) {
return 'It passes through $instance on its way, which keeps none of it.';
}
@override
String assistantConsentOperator(String instance) {
return 'Whoever runs $instance sees how much you use it (requests, tokens, cost), never what you ask.';
}
@override
String get assistantConsentConversations =>
'Your conversations are kept on your devices, encrypted.';
@override
String get assistantConsentAgree => 'Agree';
@override
String assistantConsentAgreedOn(String date) {
return 'You agreed on $date.';
}
@override
String get assistantProblemNotGranted =>
'This assistant isn\'t granted to you any more.';
@override
String get assistantProblemNotAgreed => 'Agree to what goes where first.';
@override
String assistantProblemLimitReached(String time) {
return 'Its limit is reached until $time.';
}
@override
String get assistantProblemLimitReachedNow => 'Its limit is reached for now.';
@override
String get assistantProblemLimitOutdated =>
'It waits for its limits to be set again.';
@override
String get assistantProblemImagesNotRead => 'It reads no images.';
@override
String get assistantProblemMalformed => 'This question can\'t be sent.';
@override
String get assistantProblemTooLarge =>
'This question is too long, or holds too many or too large images.';
@override
String get assistantProblemTooManyAtOnce =>
'Wait for the answers on their way first.';
@override
String get assistantProblemProviderKey => 'The provider refused the key.';
@override
String get assistantProblemProviderBusy =>
'The provider is busy. Try again in a moment.';
@override
String get assistantProblemProviderRefused =>
'The provider refused this question.';
@override
String get assistantProblemProviderFailed =>
'The provider failed. Try again.';
@override
String get assistantProblemNotAllowed =>
'Only people it is granted to can do this.';
@override
String get assistantProblemFailed => 'That didn\'t work. Try again.';
}
/// The translations for English, as used in the United Kingdom (`en_GB`).