feat(app): the operator's overview of screens, grants to several people at once with one budget each, assistants by provider
Halkyon Learn's ADR 0021, D57 to D63, on halkyon_base's budgets and effort: - The operator's screen is an overview (Waiting, Operators, Accounts, Assistants, each with its count) whose rows open screens of their own, read 50 at a time as they scroll (OperatorListCubit, OperatorPage), so hundreds of accounts push nothing else down (D62). - A handle is picked among the accounts there are (AccountSuggestionField): appointing an operator and granting take no other (D61). - AssistantGrantsScreen replaces AssistantGrantsCard: the assistants set up by provider, with their efforts and prices; the people granted, found by their handle's start; one sheet granting several assistants, a provider's at once, to several people, each with one budget in money and tokens a day and a month, or changing one person's (D58, D59). - Assistants are grouped by provider wherever they are listed, each under its name with a dot in its colour (ProviderLabel, D60). - AssistantsCard shows what is left of the person's budget for all their assistants together; the months of the figures run oldest first (D57). - AssistantChoice says the efforts a question may ask at and the ceiling on output each asks for (D63); a person's own key gets none and keeps 4,096 tokens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
24ed63585e
commit
cc87120bc8
50 changed files with 5348 additions and 2709 deletions
|
|
@ -62,26 +62,40 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
|
|||
the person's app opens, after each change, and every 5 minutes only while
|
||||
they operate it), `InstanceOperatorCard` or `InstanceOperatorTiles` for an
|
||||
app's settings and `WaitingAccountsBadge` for its Settings cog, both
|
||||
nothing at all for anyone else, and `InstanceOperatorScreen`: the accounts
|
||||
that wait, approved once their person confirmed the words read out, or
|
||||
refused; who operates, named by handle and taken back (never the last);
|
||||
accounts found by the start of their handle, suspended or named. Only the
|
||||
register's service answers: on a member, nothing shows.
|
||||
nothing at all for anyone else, and `InstanceOperatorScreen` (D62): an
|
||||
overview of how many wait, operate and have accounts, and of the
|
||||
assistants set up, each row opening a screen of its own, every list read
|
||||
50 at a time as it is scrolled (`OperatorListCubit`, `OperatorPage`): the
|
||||
accounts that wait, approved once their person confirmed the words read
|
||||
out, or refused; who operates, named by a handle picked among those
|
||||
suggested and taken back (never the last); accounts found by the start of
|
||||
their handle, suspended, named or granted assistants. A handle is only
|
||||
ever picked among the accounts there are (`AccountSuggestionField`, D61).
|
||||
Only the register's service answers: on a member, nothing shows.
|
||||
- **Assistants** (`lib/src/assistant/`, Halkyon Learn's ADR 0021, step 2):
|
||||
`AssistantRepository` on `ServerpodAssistantGateway` (the module's
|
||||
`assistant` endpoint; refusals as `AssistantFailed`) asks an
|
||||
`AssistantChoice` in halkyon_llm's terms: a `GrantedAssistant` through the
|
||||
instance, which counts it, or a `PersonalAssistant` with the person's own
|
||||
key directly (`LlmClient`), the instance seeing neither key nor question;
|
||||
instance, which counts it, at an effort among those the operator allows
|
||||
and with that effort's ceiling on output (`efforts`, `outputCeilingAt`,
|
||||
D63), or a `PersonalAssistant` with the person's own key directly
|
||||
(`LlmClient`), without an effort and at 4,096 output tokens, the instance
|
||||
seeing neither key nor question;
|
||||
`assistant_messages.dart` turns a conversation into the endpoint's models
|
||||
and back, part by part. `AssistantsCard` is an app's settings' card: what
|
||||
is granted and left of its limits, agreed to once (the consent sheet),
|
||||
the person's use by UTC month, the instance's privacy page, and the
|
||||
is granted, under each provider's name in its colour (`ProviderLabel`,
|
||||
`providerColorIn`, D60), what is left of the person's budget for them
|
||||
together (D58), each agreed to once (the consent sheet), the person's use
|
||||
by UTC month, oldest first (D57), the instance's privacy page, and the
|
||||
person's own assistants where the operator allows them, kept where the
|
||||
app says (`OwnAssistantStore`: Learn's sealed settings). For the operator,
|
||||
`AssistantOperatorRepository` (grants by exact handle) and
|
||||
`AssistantGrantsCard` on `InstanceOperatorScreen`: grants with their
|
||||
limits, granted, changed and taken back, and everyone's use by month.
|
||||
`AssistantOperatorRepository` and `AssistantGrantsScreen`, opened from
|
||||
`InstanceOperatorScreen`: the assistants set up, by provider, with their
|
||||
efforts and whether they are priced; the people granted some, each with
|
||||
one budget in money and tokens, a day and a month (D58, D59), found by
|
||||
their handle's start; one sheet granting several assistants, a
|
||||
provider's at once, to several people at once, or changing one person's
|
||||
(`showAssistantGrantSheet`); and everyone's use by month.
|
||||
`package:halkyon_app/assistant_models.dart` holds the models alone, in
|
||||
pure Dart, for an app's own models. For the app's warnings before a
|
||||
private studyset's content goes (ADR 0021, D9): `providerTermsOf` names a
|
||||
|
|
|
|||
|
|
@ -7,5 +7,6 @@ export 'src/assistant/model/assistant_amount.dart';
|
|||
export 'src/assistant/model/assistant_failed.dart';
|
||||
export 'src/assistant/model/assistant_grant.dart';
|
||||
export 'src/assistant/model/assistant_offer.dart';
|
||||
export 'src/assistant/model/assistant_provider.dart';
|
||||
export 'src/assistant/model/own_assistant.dart';
|
||||
export 'src/assistant/model/provider_terms.dart';
|
||||
|
|
|
|||
|
|
@ -16,12 +16,14 @@
|
|||
/// - **The instance's operators**: `InstanceOperatorCard` (or
|
||||
/// `InstanceOperatorTiles`) and `WaitingAccountsBadge` for whoever
|
||||
/// operates the instance, on `OperatingCubit`, leading to
|
||||
/// `InstanceOperatorScreen`, where accounts are approved, refused and
|
||||
/// suspended and operators named (Halkyon Learn's ADR 0021, D21 to D24).
|
||||
/// `InstanceOperatorScreen`, an overview whose screens approve, refuse and
|
||||
/// suspend accounts and name operators (Halkyon Learn's ADR 0021, D21 to
|
||||
/// D24, D61, D62).
|
||||
/// - **Assistants**: `AssistantsCard` for an app's settings, what the
|
||||
/// operator grants a person and their own keys where allowed, on
|
||||
/// `AssistantRepository`; `AssistantGrantsCard` on the operator's screen,
|
||||
/// on `AssistantOperatorRepository` (Halkyon Learn's ADR 0021, step 2).
|
||||
/// `AssistantRepository`; `AssistantGrantsScreen` from the operator's
|
||||
/// screen, on `AssistantOperatorRepository` (Halkyon Learn's ADR 0021,
|
||||
/// step 2, D57 to D63).
|
||||
/// `package:halkyon_app/assistant_models.dart` holds the models alone.
|
||||
/// - **Signing in**: with SimpleLogin (ADR 0020: `SimpleLoginRepository`,
|
||||
/// its sheet and `SimpleLoginSection`), with a password and TOTP (ADR
|
||||
|
|
@ -44,7 +46,7 @@ export 'src/app/app_locale.dart';
|
|||
export 'src/app/app_scope.dart';
|
||||
export 'src/app/instance_gate.dart';
|
||||
export 'src/app/person_scope.dart';
|
||||
export 'src/assistant/bloc/assistant_grants_cubit.dart';
|
||||
export 'src/assistant/bloc/assistant_grant_cubit.dart';
|
||||
export 'src/assistant/bloc/assistant_use_cubit.dart';
|
||||
export 'src/assistant/bloc/assistants_cubit.dart';
|
||||
export 'src/assistant/data/assistant_gateway.dart';
|
||||
|
|
@ -62,9 +64,11 @@ export 'src/assistant/model/assistant_amount.dart';
|
|||
export 'src/assistant/model/assistant_failed.dart';
|
||||
export 'src/assistant/model/assistant_grant.dart';
|
||||
export 'src/assistant/model/assistant_offer.dart';
|
||||
export 'src/assistant/model/assistant_provider.dart';
|
||||
export 'src/assistant/model/own_assistant.dart';
|
||||
export 'src/assistant/model/provider_terms.dart';
|
||||
export 'src/assistant/widget/assistant_grants_card.dart';
|
||||
export 'src/assistant/widget/assistant_grants_screen.dart';
|
||||
export 'src/assistant/widget/assistant_provider_view.dart';
|
||||
export 'src/assistant/widget/assistant_use_view.dart';
|
||||
export 'src/assistant/widget/assistant_words.dart';
|
||||
export 'src/assistant/widget/assistants_card.dart';
|
||||
|
|
@ -108,8 +112,10 @@ export 'src/instance/widget/instance_location.dart';
|
|||
export 'src/instance/widget/instance_location_words.dart';
|
||||
export 'src/instance/widget/instance_step.dart';
|
||||
export 'src/instance/widget/instance_tile.dart';
|
||||
export 'src/operator/bloc/instance_operator_cubit.dart';
|
||||
export 'src/operator/bloc/account_suggestions_cubit.dart';
|
||||
export 'src/operator/bloc/operating_cubit.dart';
|
||||
export 'src/operator/bloc/operator_list_cubit.dart';
|
||||
export 'src/operator/bloc/operator_overview_cubit.dart';
|
||||
export 'src/operator/data/instance_operator_gateway.dart';
|
||||
export 'src/operator/data/instance_operator_repository.dart';
|
||||
export 'src/operator/data/serverpod_instance_operator_gateway.dart';
|
||||
|
|
|
|||
|
|
@ -314,6 +314,9 @@
|
|||
"operatorAccounts": "Konten und Admins",
|
||||
"operatorWaitingCount": "{count, plural, =0{Niemand wartet auf Freigabe} =1{1 Konto wartet auf Freigabe} other{{count} Konten warten auf Freigabe}}",
|
||||
"operatorWaitingTitle": "Wartend",
|
||||
"operatorOperatorsCount": "{count, plural, =1{1 Admin} other{{count} Admins}}",
|
||||
"operatorAccountsTitle": "Konten",
|
||||
"operatorAccountsCount": "{count, plural, =1{1 Konto} other{{count} Konten}}",
|
||||
"operatorNobodyWaits": "Niemand wartet.",
|
||||
"operatorNoHandle": "Noch kein Handle",
|
||||
"operatorNewSince": "Neu, wartet seit {date}",
|
||||
|
|
@ -331,7 +334,7 @@
|
|||
"operatorAppointMessage": "Admins geben hier Konten frei, lehnen sie ab, sperren sie und ernennen Admins, so wie du.",
|
||||
"operatorAppointConfirm": "Ernennen",
|
||||
"operatorHandle": "Handle",
|
||||
"operatorFindTitle": "Ein Konto finden",
|
||||
"operatorHandleSuggested": "Vorgeschlagen werden nur Handles, die es hier gibt.",
|
||||
"operatorFoundNone": "Kein Handle beginnt so.",
|
||||
"operatorMadeOn": "Seit {date}",
|
||||
"operatorIsOperator": "Admin",
|
||||
|
|
@ -350,27 +353,39 @@
|
|||
"operatorInvalidLimit": "Eine Grenze kann nicht unter null liegen.",
|
||||
"assistantGrantsTitle": "Assistenten",
|
||||
"assistantGrantsNone": "Noch hat niemand einen Assistenten.",
|
||||
"assistantSetCount": "{count, plural, =0{Keiner eingerichtet} =1{1 Assistent} other{{count} Assistenten}}",
|
||||
"assistantPeopleCount": "{count, plural, =0{niemandem freigegeben} =1{1 Person} other{{count} Personen}}",
|
||||
"assistantSetUpTitle": "Hier eingerichtet",
|
||||
"assistantPeopleTitle": "Personen",
|
||||
"assistantGranteesFoundNone": "Niemand mit Assistenten hat ein Handle, das so beginnt.",
|
||||
"assistantUnpriced": "Ohne Preise: zählt nur Tokens",
|
||||
"assistantEffort": "{effort, select, low{niedrig} medium{mittel} high{hoch} xhigh{sehr hoch} max{maximal} other{{effort}}}",
|
||||
"assistantEffortName": "{effort, select, low{Niedrig} medium{Mittel} high{Hoch} xhigh{Sehr hoch} max{Maximal} other{{effort}}}",
|
||||
"assistantEffortSetUp": "Denkaufwand {effort}, bis {max}",
|
||||
"assistantEffortFixed": "Denkaufwand {effort}",
|
||||
"assistantNameUnset": "{name} (nicht mehr eingerichtet)",
|
||||
"assistantNotAgreedCount": "{count, plural, =1{1 noch ohne Zustimmung} other{{count} noch ohne Zustimmung}}",
|
||||
"assistantGrant": "Assistenten freigeben",
|
||||
"assistantGrantSave": "Freigeben",
|
||||
"assistantGrantChange": "Grenzen ändern",
|
||||
"assistantGrantAssistant": "Assistent",
|
||||
"assistantGrantDaily": "Grenze pro Tag",
|
||||
"assistantGrantMonthly": "Grenze pro Monat",
|
||||
"assistantGrantChange": "Assistenten ändern",
|
||||
"assistantGrantLimitHelper": "Leer für keine. Tage und Monate nach UTC.",
|
||||
"assistantGrantBudget": "Budget, für alle zusammen",
|
||||
"assistantGrantMoneyDaily": "Geld pro Tag",
|
||||
"assistantGrantMoneyMonthly": "Geld pro Monat",
|
||||
"assistantGrantTokensDaily": "Tokens pro Tag",
|
||||
"assistantGrantTokensMonthly": "Tokens pro Monat",
|
||||
"assistantGrantMoneyNote": "Geld zählt die Assistenten mit Preisen, Tokens zählen alle. Was zuerst aufgebraucht ist, hält sie an.",
|
||||
"assistantGrantReplaces": "{count, plural, =1{Die Freigabe ersetzt das Budget dieser Person:} other{Die Freigabe ersetzt die Budgets dieser Personen:}}",
|
||||
"assistantGrantUnsetDropped": "Beim Speichern wird zurückgenommen, was nicht mehr eingerichtet ist: {names}.",
|
||||
"assistantGrantLimitInvalid": "Keine Zahl, die das nimmt",
|
||||
"assistantGrantNoHandle": "Kein Konto hat dieses Handle.",
|
||||
"assistantGrantTokens": "Tokens",
|
||||
"assistantGrantNote": "Vor der ersten Frage stimmen sie zu, was wohin geht. Du siehst, wie viel sie nutzen, nie, was sie fragen.",
|
||||
"assistantGrantNote": "Vor der ersten Frage stimmen sie zu, was wohin geht. Assistenten, die sie schon haben, bleiben. Du siehst, wie viel sie nutzen, nie, was sie fragen.",
|
||||
"assistantPerDay": "{amount} pro Tag",
|
||||
"assistantPerMonth": "{amount} pro Monat",
|
||||
"assistantNoLimits": "Ohne Grenzen",
|
||||
"assistantGrantAgreed": "Zugestimmt",
|
||||
"assistantGrantNotAgreed": "Noch nicht zugestimmt",
|
||||
"assistantGrantOutdated": "Die Preise haben sich geändert: Grenzen neu setzen",
|
||||
"assistantGrantUnset": "Nicht mehr eingerichtet",
|
||||
"assistantRevoke": "Zurücknehmen",
|
||||
"assistantRevokeTitle": "{assistant} zurücknehmen?",
|
||||
"assistantRevokeMessage": "Sie können ihn nicht mehr fragen. Was sie genutzt haben, bleibt gezählt.",
|
||||
"assistantRevokeAllTitle": "Alle Assistenten zurücknehmen?",
|
||||
"assistantRevokeMessage": "Sie können keinen mehr fragen, und ihr Budget entfällt. Was sie genutzt haben, bleibt gezählt.",
|
||||
"assistantUseByMonth": "Nutzung nach Monat",
|
||||
"assistantUseTitle": "Nutzung der Assistenten",
|
||||
"assistantUseNone": "In diesem Monat nichts genutzt.",
|
||||
|
|
@ -381,8 +396,8 @@
|
|||
"assistantModelAt": "{model} bei {host}",
|
||||
"assistantLeftToday": "{left} von {limit} heute übrig",
|
||||
"assistantLeftThisMonth": "{left} von {limit} diesen Monat übrig",
|
||||
"assistantYourBudget": "Dein Budget, für alle zusammen",
|
||||
"assistantAgree": "Lesen und zustimmen",
|
||||
"assistantWaitsForLimits": "Wartet darauf, dass seine Grenzen neu gesetzt werden",
|
||||
"assistantYourUse": "Deine Nutzung",
|
||||
"assistantPrivacyPage": "Was auf {instance} wohin geht",
|
||||
"assistantOwnTitle": "Mit deinem eigenen Schlüssel",
|
||||
|
|
@ -415,9 +430,9 @@
|
|||
"assistantConsentAgreedOn": "Du hast am {date} zugestimmt.",
|
||||
"assistantProblemNotGranted": "Dieser Assistent ist für dich nicht mehr freigegeben.",
|
||||
"assistantProblemNotAgreed": "Stimm zuerst zu, was wohin geht.",
|
||||
"assistantProblemInvalidEffort": "Mit diesem Denkaufwand antwortet er nicht.",
|
||||
"assistantProblemLimitReached": "Seine Grenze ist bis {time} erreicht.",
|
||||
"assistantProblemLimitReachedNow": "Seine Grenze ist vorerst erreicht.",
|
||||
"assistantProblemLimitOutdated": "Es wartet darauf, dass seine Grenzen neu gesetzt werden.",
|
||||
"assistantProblemImagesNotRead": "Es liest keine Bilder.",
|
||||
"assistantProblemMalformed": "Diese Frage kann nicht gesendet werden.",
|
||||
"assistantProblemTooLarge": "Diese Frage ist zu lang oder enthält zu viele oder zu große Bilder.",
|
||||
|
|
|
|||
|
|
@ -764,6 +764,28 @@
|
|||
"@operatorWaitingTitle": {
|
||||
"description": "The card of accounts that wait for approval, new or suspended."
|
||||
},
|
||||
"operatorOperatorsCount": "{count, plural, =1{1 operator} other{{count} operators}}",
|
||||
"@operatorOperatorsCount": {
|
||||
"description": "How many operate the instance, in the overview's row.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"operatorAccountsTitle": "Accounts",
|
||||
"@operatorAccountsTitle": {
|
||||
"description": "The overview's row leading to every account, and that screen's title."
|
||||
},
|
||||
"operatorAccountsCount": "{count, plural, =1{1 account} other{{count} accounts}}",
|
||||
"@operatorAccountsCount": {
|
||||
"description": "How many accounts the instance has.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"operatorNobodyWaits": "Nobody waits.",
|
||||
"@operatorNobodyWaits": {
|
||||
"description": "The waiting card, empty."
|
||||
|
|
@ -842,9 +864,9 @@
|
|||
"@operatorHandle": {
|
||||
"description": "The field for an account's handle."
|
||||
},
|
||||
"operatorFindTitle": "Find an account",
|
||||
"@operatorFindTitle": {
|
||||
"description": "The card searching accounts by the start of their handle."
|
||||
"operatorHandleSuggested": "Only handles there are here are suggested.",
|
||||
"@operatorHandleSuggested": {
|
||||
"description": "Below a field suggesting handles as they are typed."
|
||||
},
|
||||
"operatorFoundNone": "No account's handle starts like this.",
|
||||
"@operatorFoundNone": {
|
||||
|
|
@ -923,7 +945,98 @@
|
|||
"@assistantGrantsNone": {
|
||||
"description": "No assistant is granted to anyone."
|
||||
},
|
||||
"assistantGrant": "Grant an assistant",
|
||||
"assistantSetCount": "{count, plural, =0{None set up} =1{1 assistant} other{{count} assistants}}",
|
||||
"@assistantSetCount": {
|
||||
"description": "How many assistants are set up, in the overview's row.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantPeopleCount": "{count, plural, =0{nobody granted} =1{1 person} other{{count} people}}",
|
||||
"@assistantPeopleCount": {
|
||||
"description": "How many people have an assistant, after how many are set up.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantSetUpTitle": "Set up here",
|
||||
"@assistantSetUpTitle": {
|
||||
"description": "The card of the assistants set up in deploy/assistants.env."
|
||||
},
|
||||
"assistantPeopleTitle": "People",
|
||||
"@assistantPeopleTitle": {
|
||||
"description": "The card, and the sheet section, of the people granted assistants."
|
||||
},
|
||||
"assistantGranteesFoundNone": "Nobody with an assistant has a handle starting like this.",
|
||||
"@assistantGranteesFoundNone": {
|
||||
"description": "No person granted one matches the start of a handle typed."
|
||||
},
|
||||
"assistantUnpriced": "No prices: counts its tokens only",
|
||||
"@assistantUnpriced": {
|
||||
"description": "An assistant set up without prices, which a budget counts in tokens alone."
|
||||
},
|
||||
"assistantEffort": "{effort, select, low{low} medium{medium} high{high} xhigh{extra high} max{max} other{{effort}}}",
|
||||
"@assistantEffort": {
|
||||
"description": "How much a model thinks before it answers, as a word in a sentence.",
|
||||
"placeholders": {
|
||||
"effort": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantEffortName": "{effort, select, low{Low} medium{Medium} high{High} xhigh{Extra high} max{Max} other{{effort}}}",
|
||||
"@assistantEffortName": {
|
||||
"description": "How much a model thinks before it answers, as a name standing alone, as in a menu.",
|
||||
"placeholders": {
|
||||
"effort": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantEffortSetUp": "Effort {effort}, up to {max}",
|
||||
"@assistantEffortSetUp": {
|
||||
"description": "An assistant's effort unless asked otherwise, and the highest a question may ask.",
|
||||
"placeholders": {
|
||||
"effort": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantEffortFixed": "Effort {effort}",
|
||||
"@assistantEffortFixed": {
|
||||
"description": "An assistant's effort, the only one it takes.",
|
||||
"placeholders": {
|
||||
"effort": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantNameUnset": "{name} (no longer set up)",
|
||||
"@assistantNameUnset": {
|
||||
"description": "An assistant granted that is no longer set up.",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantNotAgreedCount": "{count, plural, =1{1 not agreed to yet} other{{count} not agreed to yet}}",
|
||||
"@assistantNotAgreedCount": {
|
||||
"description": "How many of a person's assistants they haven't agreed to yet.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantGrant": "Grant assistants",
|
||||
"@assistantGrant": {
|
||||
"description": "Opens the sheet granting an assistant to an account."
|
||||
},
|
||||
|
|
@ -931,39 +1044,65 @@
|
|||
"@assistantGrantSave": {
|
||||
"description": "The grant sheet's button."
|
||||
},
|
||||
"assistantGrantChange": "Change limits",
|
||||
"assistantGrantChange": "Change assistants",
|
||||
"@assistantGrantChange": {
|
||||
"description": "A grant's menu item, and the sheet's title, changing its limits."
|
||||
},
|
||||
"assistantGrantAssistant": "Assistant",
|
||||
"@assistantGrantAssistant": {
|
||||
"description": "Above the choice of assistant in the grant sheet."
|
||||
},
|
||||
"assistantGrantDaily": "Limit a day",
|
||||
"@assistantGrantDaily": {
|
||||
"description": "The daily limit field."
|
||||
},
|
||||
"assistantGrantMonthly": "Limit a month",
|
||||
"@assistantGrantMonthly": {
|
||||
"description": "The monthly limit field."
|
||||
"description": "The sheet changing a person's assistants and budget, and a person's menu item."
|
||||
},
|
||||
"assistantGrantLimitHelper": "Empty for none. Days and months are UTC's.",
|
||||
"@assistantGrantLimitHelper": {
|
||||
"description": "Under the limit fields: the instance counts by UTC days and months (D42)."
|
||||
},
|
||||
"assistantGrantBudget": "Budget, for all of them together",
|
||||
"@assistantGrantBudget": {
|
||||
"description": "The sheet's section of a person's budget."
|
||||
},
|
||||
"assistantGrantMoneyDaily": "Money a day",
|
||||
"@assistantGrantMoneyDaily": {
|
||||
"description": "A budget field."
|
||||
},
|
||||
"assistantGrantMoneyMonthly": "Money a month",
|
||||
"@assistantGrantMoneyMonthly": {
|
||||
"description": "A budget field."
|
||||
},
|
||||
"assistantGrantTokensDaily": "Tokens a day",
|
||||
"@assistantGrantTokensDaily": {
|
||||
"description": "A budget field."
|
||||
},
|
||||
"assistantGrantTokensMonthly": "Tokens a month",
|
||||
"@assistantGrantTokensMonthly": {
|
||||
"description": "A budget field."
|
||||
},
|
||||
"assistantGrantMoneyNote": "Money counts the assistants with prices, tokens count every one. Whichever runs out first stops them.",
|
||||
"@assistantGrantMoneyNote": {
|
||||
"description": "Below the budget fields."
|
||||
},
|
||||
"assistantGrantReplaces": "{count, plural, =1{Granting replaces the budget this person has:} other{Granting replaces the budgets these people have:}}",
|
||||
"@assistantGrantReplaces": {
|
||||
"description": "Before the people whose budget a grant replaces, each with theirs.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantGrantUnsetDropped": "Saving takes back what is no longer set up: {names}.",
|
||||
"@assistantGrantUnsetDropped": {
|
||||
"description": "Changing a person's assistants drops those no longer set up.",
|
||||
"placeholders": {
|
||||
"names": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantGrantLimitInvalid": "Not a number this takes",
|
||||
"@assistantGrantLimitInvalid": {
|
||||
"description": "A limit typed that is no amount or number of tokens."
|
||||
},
|
||||
"assistantGrantNoHandle": "No account has this handle.",
|
||||
"@assistantGrantNoHandle": {
|
||||
"description": "The grant sheet: the handle typed is nobody's."
|
||||
},
|
||||
"assistantGrantTokens": "tokens",
|
||||
"@assistantGrantTokens": {
|
||||
"description": "After a limit in tokens, for an assistant without prices."
|
||||
},
|
||||
"assistantGrantNote": "They agree once to what goes where before they ask it. You see how much they use, never what they ask.",
|
||||
"assistantGrantNote": "They agree once to what goes where before they ask one. Assistants they have already stay. You see how much they use, never what they ask.",
|
||||
"@assistantGrantNote": {
|
||||
"description": "In the grant sheet: consent (D40) and what the operator sees (D3)."
|
||||
},
|
||||
|
|
@ -989,36 +1128,15 @@
|
|||
"@assistantNoLimits": {
|
||||
"description": "A grant without limits."
|
||||
},
|
||||
"assistantGrantAgreed": "Agreed",
|
||||
"@assistantGrantAgreed": {
|
||||
"description": "A grant whose person agreed to what goes where."
|
||||
},
|
||||
"assistantGrantNotAgreed": "Not agreed yet",
|
||||
"@assistantGrantNotAgreed": {
|
||||
"description": "A grant whose person hasn't agreed yet."
|
||||
},
|
||||
"assistantGrantOutdated": "Its prices changed: set its limits again",
|
||||
"@assistantGrantOutdated": {
|
||||
"description": "A grant whose limits are in another unit than the pricing now; it answers nothing until set again."
|
||||
},
|
||||
"assistantGrantUnset": "No longer set up",
|
||||
"@assistantGrantUnset": {
|
||||
"description": "A grant of an assistant taken out of deploy/assistants.env."
|
||||
},
|
||||
"assistantRevoke": "Take back",
|
||||
"@assistantRevoke": {
|
||||
"description": "A grant's menu item."
|
||||
},
|
||||
"assistantRevokeTitle": "Take back {assistant}?",
|
||||
"@assistantRevokeTitle": {
|
||||
"description": "Asks before taking a grant back.",
|
||||
"placeholders": {
|
||||
"assistant": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
"assistantRevokeAllTitle": "Take back every assistant?",
|
||||
"@assistantRevokeAllTitle": {
|
||||
"description": "Asks before taking every assistant back from a person."
|
||||
},
|
||||
"assistantRevokeMessage": "They can no longer ask it. What they used stays counted.",
|
||||
"assistantRevokeMessage": "They can no longer ask any of them, and their budget goes. What they used stays counted.",
|
||||
"@assistantRevokeMessage": {
|
||||
"description": "Under the question taking a grant back."
|
||||
},
|
||||
|
|
@ -1096,14 +1214,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"assistantYourBudget": "Your budget, for all of them",
|
||||
"@assistantYourBudget": {
|
||||
"description": "The person's budget for all their assistants."
|
||||
},
|
||||
"assistantAgree": "Read and agree",
|
||||
"@assistantAgree": {
|
||||
"description": "Opens the consent sheet of an assistant not agreed to yet."
|
||||
},
|
||||
"assistantWaitsForLimits": "Waits for its limits to be set again",
|
||||
"@assistantWaitsForLimits": {
|
||||
"description": "An assistant whose limits are outdated: it answers nothing until the operator sets them again."
|
||||
},
|
||||
"assistantYourUse": "Your use",
|
||||
"@assistantYourUse": {
|
||||
"description": "Opens the person's own figures."
|
||||
|
|
@ -1276,6 +1394,10 @@
|
|||
"@assistantProblemNotAgreed": {
|
||||
"description": "notAgreed."
|
||||
},
|
||||
"assistantProblemInvalidEffort": "It doesn't think at this effort.",
|
||||
"@assistantProblemInvalidEffort": {
|
||||
"description": "A question asked an effort its assistant does not take."
|
||||
},
|
||||
"assistantProblemLimitReached": "Its limit is reached until {time}.",
|
||||
"@assistantProblemLimitReached": {
|
||||
"description": "limitReached, with when it resets.",
|
||||
|
|
@ -1289,10 +1411,6 @@
|
|||
"@assistantProblemLimitReachedNow": {
|
||||
"description": "Refused: a limit is reached, the instance not saying until when."
|
||||
},
|
||||
"assistantProblemLimitOutdated": "It waits for its limits to be set again.",
|
||||
"@assistantProblemLimitOutdated": {
|
||||
"description": "limitOutdated."
|
||||
},
|
||||
"assistantProblemImagesNotRead": "It reads no images.",
|
||||
"@assistantProblemImagesNotRead": {
|
||||
"description": "imagesNotRead."
|
||||
|
|
|
|||
|
|
@ -1987,6 +1987,24 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'Waiting'**
|
||||
String get operatorWaitingTitle;
|
||||
|
||||
/// How many operate the instance, in the overview's row.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =1{1 operator} other{{count} operators}}'**
|
||||
String operatorOperatorsCount(int count);
|
||||
|
||||
/// The overview's row leading to every account, and that screen's title.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Accounts'**
|
||||
String get operatorAccountsTitle;
|
||||
|
||||
/// How many accounts the instance has.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =1{1 account} other{{count} accounts}}'**
|
||||
String operatorAccountsCount(int count);
|
||||
|
||||
/// The waiting card, empty.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2089,11 +2107,11 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'Handle'**
|
||||
String get operatorHandle;
|
||||
|
||||
/// The card searching accounts by the start of their handle.
|
||||
/// Below a field suggesting handles as they are typed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Find an account'**
|
||||
String get operatorFindTitle;
|
||||
/// **'Only handles there are here are suggested.'**
|
||||
String get operatorHandleSuggested;
|
||||
|
||||
/// A search that found nothing.
|
||||
///
|
||||
|
|
@ -2203,10 +2221,82 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'Nobody has an assistant yet.'**
|
||||
String get assistantGrantsNone;
|
||||
|
||||
/// How many assistants are set up, in the overview's row.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =0{None set up} =1{1 assistant} other{{count} assistants}}'**
|
||||
String assistantSetCount(int count);
|
||||
|
||||
/// How many people have an assistant, after how many are set up.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =0{nobody granted} =1{1 person} other{{count} people}}'**
|
||||
String assistantPeopleCount(int count);
|
||||
|
||||
/// The card of the assistants set up in deploy/assistants.env.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set up here'**
|
||||
String get assistantSetUpTitle;
|
||||
|
||||
/// The card, and the sheet section, of the people granted assistants.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'People'**
|
||||
String get assistantPeopleTitle;
|
||||
|
||||
/// No person granted one matches the start of a handle typed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Nobody with an assistant has a handle starting like this.'**
|
||||
String get assistantGranteesFoundNone;
|
||||
|
||||
/// An assistant set up without prices, which a budget counts in tokens alone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No prices: counts its tokens only'**
|
||||
String get assistantUnpriced;
|
||||
|
||||
/// How much a model thinks before it answers, as a word in a sentence.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{effort, select, low{low} medium{medium} high{high} xhigh{extra high} max{max} other{{effort}}}'**
|
||||
String assistantEffort(String effort);
|
||||
|
||||
/// How much a model thinks before it answers, as a name standing alone, as in a menu.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{effort, select, low{Low} medium{Medium} high{High} xhigh{Extra high} max{Max} other{{effort}}}'**
|
||||
String assistantEffortName(String effort);
|
||||
|
||||
/// An assistant's effort unless asked otherwise, and the highest a question may ask.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Effort {effort}, up to {max}'**
|
||||
String assistantEffortSetUp(String effort, String max);
|
||||
|
||||
/// An assistant's effort, the only one it takes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Effort {effort}'**
|
||||
String assistantEffortFixed(String effort);
|
||||
|
||||
/// An assistant granted that is no longer set up.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{name} (no longer set up)'**
|
||||
String assistantNameUnset(String name);
|
||||
|
||||
/// How many of a person's assistants they haven't agreed to yet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =1{1 not agreed to yet} other{{count} not agreed to yet}}'**
|
||||
String assistantNotAgreedCount(int count);
|
||||
|
||||
/// Opens the sheet granting an assistant to an account.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Grant an assistant'**
|
||||
/// **'Grant assistants'**
|
||||
String get assistantGrant;
|
||||
|
||||
/// The grant sheet's button.
|
||||
|
|
@ -2215,48 +2305,72 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'Grant'**
|
||||
String get assistantGrantSave;
|
||||
|
||||
/// A grant's menu item, and the sheet's title, changing its limits.
|
||||
/// The sheet changing a person's assistants and budget, and a person's menu item.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change limits'**
|
||||
/// **'Change assistants'**
|
||||
String get assistantGrantChange;
|
||||
|
||||
/// Above the choice of assistant in the grant sheet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Assistant'**
|
||||
String get assistantGrantAssistant;
|
||||
|
||||
/// The daily limit field.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Limit a day'**
|
||||
String get assistantGrantDaily;
|
||||
|
||||
/// The monthly limit field.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Limit a month'**
|
||||
String get assistantGrantMonthly;
|
||||
|
||||
/// Under the limit fields: the instance counts by UTC days and months (D42).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Empty for none. Days and months are UTC\'s.'**
|
||||
String get assistantGrantLimitHelper;
|
||||
|
||||
/// The sheet's section of a person's budget.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Budget, for all of them together'**
|
||||
String get assistantGrantBudget;
|
||||
|
||||
/// A budget field.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Money a day'**
|
||||
String get assistantGrantMoneyDaily;
|
||||
|
||||
/// A budget field.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Money a month'**
|
||||
String get assistantGrantMoneyMonthly;
|
||||
|
||||
/// A budget field.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tokens a day'**
|
||||
String get assistantGrantTokensDaily;
|
||||
|
||||
/// A budget field.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tokens a month'**
|
||||
String get assistantGrantTokensMonthly;
|
||||
|
||||
/// Below the budget fields.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Money counts the assistants with prices, tokens count every one. Whichever runs out first stops them.'**
|
||||
String get assistantGrantMoneyNote;
|
||||
|
||||
/// Before the people whose budget a grant replaces, each with theirs.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =1{Granting replaces the budget this person has:} other{Granting replaces the budgets these people have:}}'**
|
||||
String assistantGrantReplaces(int count);
|
||||
|
||||
/// Changing a person's assistants drops those no longer set up.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Saving takes back what is no longer set up: {names}.'**
|
||||
String assistantGrantUnsetDropped(String names);
|
||||
|
||||
/// A limit typed that is no amount or number of tokens.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not a number this takes'**
|
||||
String get assistantGrantLimitInvalid;
|
||||
|
||||
/// The grant sheet: the handle typed is nobody's.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No account has this handle.'**
|
||||
String get assistantGrantNoHandle;
|
||||
|
||||
/// After a limit in tokens, for an assistant without prices.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2266,7 +2380,7 @@ abstract class HalkyonAppLocalizations {
|
|||
/// In the grant sheet: consent (D40) and what the operator sees (D3).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'They agree once to what goes where before they ask it. You see how much they use, never what they ask.'**
|
||||
/// **'They agree once to what goes where before they ask one. Assistants they have already stay. You see how much they use, never what they ask.'**
|
||||
String get assistantGrantNote;
|
||||
|
||||
/// A daily limit.
|
||||
|
|
@ -2287,46 +2401,22 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'No limits'**
|
||||
String get assistantNoLimits;
|
||||
|
||||
/// A grant whose person agreed to what goes where.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Agreed'**
|
||||
String get assistantGrantAgreed;
|
||||
|
||||
/// A grant whose person hasn't agreed yet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not agreed yet'**
|
||||
String get assistantGrantNotAgreed;
|
||||
|
||||
/// A grant whose limits are in another unit than the pricing now; it answers nothing until set again.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Its prices changed: set its limits again'**
|
||||
String get assistantGrantOutdated;
|
||||
|
||||
/// A grant of an assistant taken out of deploy/assistants.env.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No longer set up'**
|
||||
String get assistantGrantUnset;
|
||||
|
||||
/// A grant's menu item.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Take back'**
|
||||
String get assistantRevoke;
|
||||
|
||||
/// Asks before taking a grant back.
|
||||
/// Asks before taking every assistant back from a person.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Take back {assistant}?'**
|
||||
String assistantRevokeTitle(String assistant);
|
||||
/// **'Take back every assistant?'**
|
||||
String get assistantRevokeAllTitle;
|
||||
|
||||
/// Under the question taking a grant back.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'They can no longer ask it. What they used stays counted.'**
|
||||
/// **'They can no longer ask any of them, and their budget goes. What they used stays counted.'**
|
||||
String get assistantRevokeMessage;
|
||||
|
||||
/// Opens everyone's figures (D3).
|
||||
|
|
@ -2389,18 +2479,18 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'{left} of {limit} left this month'**
|
||||
String assistantLeftThisMonth(String left, String limit);
|
||||
|
||||
/// The person's budget for all their assistants.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your budget, for all of them'**
|
||||
String get assistantYourBudget;
|
||||
|
||||
/// Opens the consent sheet of an assistant not agreed to yet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Read and agree'**
|
||||
String get assistantAgree;
|
||||
|
||||
/// An assistant whose limits are outdated: it answers nothing until the operator sets them again.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Waits for its limits to be set again'**
|
||||
String get assistantWaitsForLimits;
|
||||
|
||||
/// Opens the person's own figures.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2593,6 +2683,12 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'Agree to what goes where first.'**
|
||||
String get assistantProblemNotAgreed;
|
||||
|
||||
/// A question asked an effort its assistant does not take.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'It doesn\'t think at this effort.'**
|
||||
String get assistantProblemInvalidEffort;
|
||||
|
||||
/// limitReached, with when it resets.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2605,12 +2701,6 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'Its limit is reached for now.'**
|
||||
String get assistantProblemLimitReachedNow;
|
||||
|
||||
/// limitOutdated.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'It waits for its limits to be set again.'**
|
||||
String get assistantProblemLimitOutdated;
|
||||
|
||||
/// imagesNotRead.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1194,6 +1194,31 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get operatorWaitingTitle => 'Wartend';
|
||||
|
||||
@override
|
||||
String operatorOperatorsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count Admins',
|
||||
one: '1 Admin',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get operatorAccountsTitle => 'Konten';
|
||||
|
||||
@override
|
||||
String operatorAccountsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count Konten',
|
||||
one: '1 Konto',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get operatorNobodyWaits => 'Niemand wartet.';
|
||||
|
||||
|
|
@ -1254,7 +1279,8 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
String get operatorHandle => 'Handle';
|
||||
|
||||
@override
|
||||
String get operatorFindTitle => 'Ein Konto finden';
|
||||
String get operatorHandleSuggested =>
|
||||
'Vorgeschlagen werden nur Handles, die es hier gibt.';
|
||||
|
||||
@override
|
||||
String get operatorFoundNone => 'Kein Handle beginnt so.';
|
||||
|
|
@ -1319,6 +1345,101 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get assistantGrantsNone => 'Noch hat niemand einen Assistenten.';
|
||||
|
||||
@override
|
||||
String assistantSetCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count Assistenten',
|
||||
one: '1 Assistent',
|
||||
zero: 'Keiner eingerichtet',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantPeopleCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count Personen',
|
||||
one: '1 Person',
|
||||
zero: 'niemandem freigegeben',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantSetUpTitle => 'Hier eingerichtet';
|
||||
|
||||
@override
|
||||
String get assistantPeopleTitle => 'Personen';
|
||||
|
||||
@override
|
||||
String get assistantGranteesFoundNone =>
|
||||
'Niemand mit Assistenten hat ein Handle, das so beginnt.';
|
||||
|
||||
@override
|
||||
String get assistantUnpriced => 'Ohne Preise: zählt nur Tokens';
|
||||
|
||||
@override
|
||||
String assistantEffort(String effort) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
effort,
|
||||
{
|
||||
'low': 'niedrig',
|
||||
'medium': 'mittel',
|
||||
'high': 'hoch',
|
||||
'xhigh': 'sehr hoch',
|
||||
'max': 'maximal',
|
||||
'other': '$effort',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantEffortName(String effort) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
effort,
|
||||
{
|
||||
'low': 'Niedrig',
|
||||
'medium': 'Mittel',
|
||||
'high': 'Hoch',
|
||||
'xhigh': 'Sehr hoch',
|
||||
'max': 'Maximal',
|
||||
'other': '$effort',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantEffortSetUp(String effort, String max) {
|
||||
return 'Denkaufwand $effort, bis $max';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantEffortFixed(String effort) {
|
||||
return 'Denkaufwand $effort';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantNameUnset(String name) {
|
||||
return '$name (nicht mehr eingerichtet)';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantNotAgreedCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count noch ohne Zustimmung',
|
||||
one: '1 noch ohne Zustimmung',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantGrant => 'Assistenten freigeben';
|
||||
|
||||
|
|
@ -1326,33 +1447,56 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
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';
|
||||
String get assistantGrantChange => 'Assistenten ändern';
|
||||
|
||||
@override
|
||||
String get assistantGrantLimitHelper =>
|
||||
'Leer für keine. Tage und Monate nach UTC.';
|
||||
|
||||
@override
|
||||
String get assistantGrantLimitInvalid => 'Keine Zahl, die das nimmt';
|
||||
String get assistantGrantBudget => 'Budget, für alle zusammen';
|
||||
|
||||
@override
|
||||
String get assistantGrantNoHandle => 'Kein Konto hat dieses Handle.';
|
||||
String get assistantGrantMoneyDaily => 'Geld pro Tag';
|
||||
|
||||
@override
|
||||
String get assistantGrantMoneyMonthly => 'Geld pro Monat';
|
||||
|
||||
@override
|
||||
String get assistantGrantTokensDaily => 'Tokens pro Tag';
|
||||
|
||||
@override
|
||||
String get assistantGrantTokensMonthly => 'Tokens pro Monat';
|
||||
|
||||
@override
|
||||
String get assistantGrantMoneyNote =>
|
||||
'Geld zählt die Assistenten mit Preisen, Tokens zählen alle. Was zuerst aufgebraucht ist, hält sie an.';
|
||||
|
||||
@override
|
||||
String assistantGrantReplaces(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Die Freigabe ersetzt die Budgets dieser Personen:',
|
||||
one: 'Die Freigabe ersetzt das Budget dieser Person:',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantGrantUnsetDropped(String names) {
|
||||
return 'Beim Speichern wird zurückgenommen, was nicht mehr eingerichtet ist: $names.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantGrantLimitInvalid => 'Keine Zahl, die das nimmt';
|
||||
|
||||
@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.';
|
||||
'Vor der ersten Frage stimmen sie zu, was wohin geht. Assistenten, die sie schon haben, bleiben. Du siehst, wie viel sie nutzen, nie, was sie fragen.';
|
||||
|
||||
@override
|
||||
String assistantPerDay(String amount) {
|
||||
|
|
@ -1367,30 +1511,15 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
@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?';
|
||||
}
|
||||
String get assistantRevokeAllTitle => 'Alle Assistenten zurücknehmen?';
|
||||
|
||||
@override
|
||||
String get assistantRevokeMessage =>
|
||||
'Sie können ihn nicht mehr fragen. Was sie genutzt haben, bleibt gezählt.';
|
||||
'Sie können keinen mehr fragen, und ihr Budget entfällt. Was sie genutzt haben, bleibt gezählt.';
|
||||
|
||||
@override
|
||||
String get assistantUseByMonth => 'Nutzung nach Monat';
|
||||
|
|
@ -1440,11 +1569,10 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get assistantAgree => 'Lesen und zustimmen';
|
||||
String get assistantYourBudget => 'Dein Budget, für alle zusammen';
|
||||
|
||||
@override
|
||||
String get assistantWaitsForLimits =>
|
||||
'Wartet darauf, dass seine Grenzen neu gesetzt werden';
|
||||
String get assistantAgree => 'Lesen und zustimmen';
|
||||
|
||||
@override
|
||||
String get assistantYourUse => 'Deine Nutzung';
|
||||
|
|
@ -1568,6 +1696,10 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get assistantProblemNotAgreed => 'Stimm zuerst zu, was wohin geht.';
|
||||
|
||||
@override
|
||||
String get assistantProblemInvalidEffort =>
|
||||
'Mit diesem Denkaufwand antwortet er nicht.';
|
||||
|
||||
@override
|
||||
String assistantProblemLimitReached(String time) {
|
||||
return 'Seine Grenze ist bis $time erreicht.';
|
||||
|
|
@ -1577,10 +1709,6 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
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.';
|
||||
|
||||
|
|
|
|||
|
|
@ -1173,6 +1173,31 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get operatorWaitingTitle => 'Waiting';
|
||||
|
||||
@override
|
||||
String operatorOperatorsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count operators',
|
||||
one: '1 operator',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get operatorAccountsTitle => 'Accounts';
|
||||
|
||||
@override
|
||||
String operatorAccountsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count accounts',
|
||||
one: '1 account',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get operatorNobodyWaits => 'Nobody waits.';
|
||||
|
||||
|
|
@ -1233,7 +1258,8 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
String get operatorHandle => 'Handle';
|
||||
|
||||
@override
|
||||
String get operatorFindTitle => 'Find an account';
|
||||
String get operatorHandleSuggested =>
|
||||
'Only handles there are here are suggested.';
|
||||
|
||||
@override
|
||||
String get operatorFoundNone => 'No account\'s handle starts like this.';
|
||||
|
|
@ -1298,39 +1324,157 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
String get assistantGrantsNone => 'Nobody has an assistant yet.';
|
||||
|
||||
@override
|
||||
String get assistantGrant => 'Grant an assistant';
|
||||
String assistantSetCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count assistants',
|
||||
one: '1 assistant',
|
||||
zero: 'None set up',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantPeopleCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count people',
|
||||
one: '1 person',
|
||||
zero: 'nobody granted',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantSetUpTitle => 'Set up here';
|
||||
|
||||
@override
|
||||
String get assistantPeopleTitle => 'People';
|
||||
|
||||
@override
|
||||
String get assistantGranteesFoundNone =>
|
||||
'Nobody with an assistant has a handle starting like this.';
|
||||
|
||||
@override
|
||||
String get assistantUnpriced => 'No prices: counts its tokens only';
|
||||
|
||||
@override
|
||||
String assistantEffort(String effort) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
effort,
|
||||
{
|
||||
'low': 'low',
|
||||
'medium': 'medium',
|
||||
'high': 'high',
|
||||
'xhigh': 'extra high',
|
||||
'max': 'max',
|
||||
'other': '$effort',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantEffortName(String effort) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
effort,
|
||||
{
|
||||
'low': 'Low',
|
||||
'medium': 'Medium',
|
||||
'high': 'High',
|
||||
'xhigh': 'Extra high',
|
||||
'max': 'Max',
|
||||
'other': '$effort',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantEffortSetUp(String effort, String max) {
|
||||
return 'Effort $effort, up to $max';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantEffortFixed(String effort) {
|
||||
return 'Effort $effort';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantNameUnset(String name) {
|
||||
return '$name (no longer set up)';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantNotAgreedCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count not agreed to yet',
|
||||
one: '1 not agreed to yet',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantGrant => 'Grant assistants';
|
||||
|
||||
@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';
|
||||
String get assistantGrantChange => 'Change assistants';
|
||||
|
||||
@override
|
||||
String get assistantGrantLimitHelper =>
|
||||
'Empty for none. Days and months are UTC\'s.';
|
||||
|
||||
@override
|
||||
String get assistantGrantLimitInvalid => 'Not a number this takes';
|
||||
String get assistantGrantBudget => 'Budget, for all of them together';
|
||||
|
||||
@override
|
||||
String get assistantGrantNoHandle => 'No account has this handle.';
|
||||
String get assistantGrantMoneyDaily => 'Money a day';
|
||||
|
||||
@override
|
||||
String get assistantGrantMoneyMonthly => 'Money a month';
|
||||
|
||||
@override
|
||||
String get assistantGrantTokensDaily => 'Tokens a day';
|
||||
|
||||
@override
|
||||
String get assistantGrantTokensMonthly => 'Tokens a month';
|
||||
|
||||
@override
|
||||
String get assistantGrantMoneyNote =>
|
||||
'Money counts the assistants with prices, tokens count every one. Whichever runs out first stops them.';
|
||||
|
||||
@override
|
||||
String assistantGrantReplaces(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Granting replaces the budgets these people have:',
|
||||
one: 'Granting replaces the budget this person has:',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantGrantUnsetDropped(String names) {
|
||||
return 'Saving takes back what is no longer set up: $names.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantGrantLimitInvalid => 'Not a number this takes';
|
||||
|
||||
@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.';
|
||||
'They agree once to what goes where before they ask one. Assistants they have already stay. You see how much they use, never what they ask.';
|
||||
|
||||
@override
|
||||
String assistantPerDay(String amount) {
|
||||
|
|
@ -1345,30 +1489,15 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
@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?';
|
||||
}
|
||||
String get assistantRevokeAllTitle => 'Take back every assistant?';
|
||||
|
||||
@override
|
||||
String get assistantRevokeMessage =>
|
||||
'They can no longer ask it. What they used stays counted.';
|
||||
'They can no longer ask any of them, and their budget goes. What they used stays counted.';
|
||||
|
||||
@override
|
||||
String get assistantUseByMonth => 'Use by month';
|
||||
|
|
@ -1418,10 +1547,10 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get assistantAgree => 'Read and agree';
|
||||
String get assistantYourBudget => 'Your budget, for all of them';
|
||||
|
||||
@override
|
||||
String get assistantWaitsForLimits => 'Waits for its limits to be set again';
|
||||
String get assistantAgree => 'Read and agree';
|
||||
|
||||
@override
|
||||
String get assistantYourUse => 'Your use';
|
||||
|
|
@ -1544,6 +1673,10 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get assistantProblemNotAgreed => 'Agree to what goes where first.';
|
||||
|
||||
@override
|
||||
String get assistantProblemInvalidEffort =>
|
||||
'It doesn\'t think at this effort.';
|
||||
|
||||
@override
|
||||
String assistantProblemLimitReached(String time) {
|
||||
return 'Its limit is reached until $time.';
|
||||
|
|
@ -1552,10 +1685,6 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
@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.';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,199 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/assistant/data/assistant_repository.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_grant.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_offer.dart';
|
||||
import 'package:halkyon_app/src/operator/model/operated_account.dart';
|
||||
|
||||
/// What the sheet granting assistants, or changing a person's, shows.
|
||||
final class const AssistantGrantState({
|
||||
/// The people to grant to, in the order added; none while changing.
|
||||
final List<OperatedAccount> accounts = const [],
|
||||
|
||||
/// The person whose assistants and budget are changed; null while
|
||||
/// granting.
|
||||
final AssistantGrantee? grantee,
|
||||
|
||||
/// The IDs of the assistants chosen.
|
||||
final Set<String> chosen = const {},
|
||||
|
||||
/// Those of [accounts] with an assistant or a budget already, whose
|
||||
/// budget a grant replaces.
|
||||
final List<AssistantGrantee> replaced = const [],
|
||||
|
||||
/// Saving.
|
||||
final bool busy = false,
|
||||
|
||||
/// Why the instance refused the last save; null while it didn't.
|
||||
final OperatorFailed? refused,
|
||||
}) with Equatable {
|
||||
/// Whether it can be saved: someone to grant to, and an assistant.
|
||||
bool get canSave =>
|
||||
!busy && chosen.isNotEmpty && (grantee != null || accounts.isNotEmpty);
|
||||
|
||||
AssistantGrantState copyWith({
|
||||
List<OperatedAccount>? accounts,
|
||||
Set<String>? chosen,
|
||||
List<AssistantGrantee>? replaced,
|
||||
bool? busy,
|
||||
OperatorFailed? refused,
|
||||
bool clearRefused = false,
|
||||
}) => AssistantGrantState(
|
||||
accounts: accounts ?? this.accounts,
|
||||
grantee: grantee,
|
||||
chosen: chosen ?? this.chosen,
|
||||
replaced: replaced ?? this.replaced,
|
||||
busy: busy ?? this.busy,
|
||||
refused: clearRefused ? null : refused ?? this.refused,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
accounts,
|
||||
grantee,
|
||||
chosen,
|
||||
replaced,
|
||||
busy,
|
||||
refused,
|
||||
];
|
||||
}
|
||||
|
||||
/// Granting assistants to several people at once, all with one budget, or
|
||||
/// changing one person's (Halkyon Learn's ADR 0021, D58): the people by
|
||||
/// handle, only accounts there are (D61), the assistants chosen one by one
|
||||
/// or a provider's at once; those whose budget a grant replaces named
|
||||
/// before it does.
|
||||
class AssistantGrantCubit(
|
||||
final AssistantOperatorRepository _operators, {
|
||||
|
||||
/// The assistants set, whose order the chosen go in.
|
||||
required final AssistantsSetUp setUp,
|
||||
|
||||
/// The people to grant to at first.
|
||||
List<OperatedAccount> accounts = const [],
|
||||
|
||||
/// The person to change instead; their assistants still set are chosen
|
||||
/// at first.
|
||||
AssistantGrantee? grantee,
|
||||
}) extends Cubit<AssistantGrantState> {
|
||||
this
|
||||
: super(
|
||||
AssistantGrantState(
|
||||
accounts: accounts,
|
||||
grantee: grantee,
|
||||
chosen: {
|
||||
for (final grant in grantee?.grants ?? const <AssistantGrant>[])
|
||||
if (grant.assistantSet && setUp.named(grant.assistant) != null)
|
||||
grant.assistant,
|
||||
},
|
||||
),
|
||||
) {
|
||||
unawaited(_readReplaced());
|
||||
}
|
||||
|
||||
/// Which read answered last, so an older answer arriving late is
|
||||
/// dropped.
|
||||
var _reads = 0;
|
||||
|
||||
/// Adds [account] to those granted to, once.
|
||||
void add(OperatedAccount account) {
|
||||
if (state.accounts.any((each) => each.id == account.id)) {
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(accounts: [...state.accounts, account]));
|
||||
unawaited(_readReplaced());
|
||||
}
|
||||
|
||||
/// Takes the account [id] names off those granted to.
|
||||
void remove(String id) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
accounts: [
|
||||
for (final account in state.accounts)
|
||||
if (account.id != id) account,
|
||||
],
|
||||
),
|
||||
);
|
||||
unawaited(_readReplaced());
|
||||
}
|
||||
|
||||
/// Chooses the assistant [id] names, or leaves it out.
|
||||
void choose(String id, {required bool on}) => chooseAll([id], on: on);
|
||||
|
||||
/// Chooses each of [ids] (a provider's), or leaves them out.
|
||||
void chooseAll(Iterable<String> ids, {required bool on}) => emit(
|
||||
state.copyWith(
|
||||
chosen: on
|
||||
? {...state.chosen, ...ids}
|
||||
: state.chosen.difference(ids.toSet()),
|
||||
),
|
||||
);
|
||||
|
||||
/// Grants each person each assistant chosen, or gives the person changed
|
||||
/// exactly those, with [budget]; whether it was saved, the instance's
|
||||
/// refusal the state's otherwise.
|
||||
Future<bool> save(AssistantBudget budget) async {
|
||||
if (!state.canSave) {
|
||||
return false;
|
||||
}
|
||||
emit(state.copyWith(busy: true, clearRefused: true));
|
||||
final assistants = [
|
||||
for (final assistant in setUp.assistants)
|
||||
if (state.chosen.contains(assistant.id)) assistant.id,
|
||||
];
|
||||
try {
|
||||
if (state.grantee case final grantee?) {
|
||||
await _operators.change(
|
||||
account: grantee.account,
|
||||
assistants: assistants,
|
||||
budget: budget,
|
||||
);
|
||||
} else {
|
||||
await _operators.grant(
|
||||
accounts: [for (final account in state.accounts) account.id],
|
||||
assistants: assistants,
|
||||
budget: budget,
|
||||
);
|
||||
}
|
||||
} on Object catch (error) {
|
||||
log('Granting assistants failed: ${error.runtimeType}', name: 'AI');
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
busy: false,
|
||||
refused: error is OperatorFailed
|
||||
? error
|
||||
: const OperatorFailed(OperatorProblem.failed),
|
||||
),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(busy: false));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _readReplaced() async {
|
||||
final read = ++_reads;
|
||||
List<AssistantGrantee> replaced;
|
||||
try {
|
||||
replaced = [
|
||||
for (final grantee in await _operators.granteesOf([
|
||||
for (final account in state.accounts) account.id,
|
||||
]))
|
||||
if (grantee.grants.isNotEmpty || !grantee.budget.isNone) grantee,
|
||||
];
|
||||
} on Object catch (error) {
|
||||
log('Reading budgets failed: ${error.runtimeType}', name: 'AI');
|
||||
replaced = const [];
|
||||
}
|
||||
if (!isClosed && read == _reads) {
|
||||
emit(state.copyWith(replaced: replaced));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/assistant/data/assistant_repository.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_grant.dart';
|
||||
import 'package:halkyon_app/src/operator/model/operated_account.dart';
|
||||
|
||||
/// What the operator's screen shows of the assistants.
|
||||
final class const AssistantGrantsState({
|
||||
/// Reading the assistants set and the grants.
|
||||
final bool loading = true,
|
||||
|
||||
/// Why they couldn't be read; null while they could.
|
||||
final OperatorFailed? unreadable,
|
||||
|
||||
/// The assistants set in `deploy/assistants.env`, without their keys.
|
||||
final AssistantsSetUp setUp = const AssistantsSetUp(),
|
||||
|
||||
/// Every grant, by handle.
|
||||
final List<AssistantGrant> grants = const [],
|
||||
|
||||
/// What is under way: a grant's key ([keyOf]), or `grant` for a new one;
|
||||
/// null for nothing.
|
||||
final String? busy,
|
||||
|
||||
/// Why the last action failed, nothing changed; null for none.
|
||||
final OperatorFailed? problem,
|
||||
}) with Equatable {
|
||||
/// Whether there is anything to show: an assistant set, or a grant left
|
||||
/// of one no longer set.
|
||||
bool get showsAnything => setUp.assistants.isNotEmpty || grants.isNotEmpty;
|
||||
|
||||
/// What names [grant] while it is acted on.
|
||||
static String keyOf(AssistantGrant grant) =>
|
||||
'${grant.account}/${grant.assistant}';
|
||||
|
||||
AssistantGrantsState copyWith({
|
||||
bool? loading,
|
||||
OperatorFailed? unreadable,
|
||||
bool readable = false,
|
||||
AssistantsSetUp? setUp,
|
||||
List<AssistantGrant>? grants,
|
||||
String? busy,
|
||||
bool clearBusy = false,
|
||||
OperatorFailed? problem,
|
||||
bool clearProblem = false,
|
||||
}) => AssistantGrantsState(
|
||||
loading: loading ?? this.loading,
|
||||
unreadable: readable ? null : unreadable ?? this.unreadable,
|
||||
setUp: setUp ?? this.setUp,
|
||||
grants: grants ?? this.grants,
|
||||
busy: clearBusy ? null : busy ?? this.busy,
|
||||
problem: clearProblem ? null : problem ?? this.problem,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
loading,
|
||||
unreadable,
|
||||
setUp,
|
||||
grants,
|
||||
busy,
|
||||
problem,
|
||||
];
|
||||
}
|
||||
|
||||
/// The assistants the operator grants (Halkyon Learn's ADR 0021, points 2,
|
||||
/// 6 and 8): granted by handle with their limits, the limits changed, taken
|
||||
/// back. One action at a time, the grants read anew after each.
|
||||
class AssistantGrantsCubit(final AssistantOperatorRepository _operators)
|
||||
extends Cubit<AssistantGrantsState> {
|
||||
this : super(const AssistantGrantsState());
|
||||
|
||||
/// Reads the assistants set and the grants.
|
||||
Future<void> load() async {
|
||||
try {
|
||||
final setUp = await _operators.setUp();
|
||||
final grants = await _operators.grants();
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loading: false,
|
||||
readable: true,
|
||||
setUp: setUp,
|
||||
grants: grants,
|
||||
),
|
||||
);
|
||||
}
|
||||
} on Object catch (error) {
|
||||
log('Reading the grants failed: ${error.runtimeType}', name: 'AI');
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(loading: false, unreadable: _failure(error)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Grants [assistant] to the account with [handle], with [dailyLimit] and
|
||||
/// [monthlyLimit] in its pricing's unit; why it failed, or null once
|
||||
/// granted. The sheet asking says it, so it stays out of `problem`.
|
||||
Future<OperatorFailed?> grant({
|
||||
required String handle,
|
||||
required String assistant,
|
||||
required int? dailyLimit,
|
||||
required int? monthlyLimit,
|
||||
}) => _act(
|
||||
'grant',
|
||||
() => _operators.grant(
|
||||
handle: handle,
|
||||
assistant: assistant,
|
||||
dailyLimit: dailyLimit,
|
||||
monthlyLimit: monthlyLimit,
|
||||
),
|
||||
);
|
||||
|
||||
/// Sets [grant]'s limits; why it failed, or null once set.
|
||||
Future<OperatorFailed?> changeLimits(
|
||||
AssistantGrant grant, {
|
||||
required int? dailyLimit,
|
||||
required int? monthlyLimit,
|
||||
}) => _act(
|
||||
AssistantGrantsState.keyOf(grant),
|
||||
() => _operators.changeLimits(
|
||||
account: grant.account,
|
||||
assistant: grant.assistant,
|
||||
dailyLimit: dailyLimit,
|
||||
monthlyLimit: monthlyLimit,
|
||||
),
|
||||
);
|
||||
|
||||
/// Takes [grant] back; a failure is the state's `problem`.
|
||||
Future<void> revoke(AssistantGrant grant) async {
|
||||
final failed = await _act(
|
||||
AssistantGrantsState.keyOf(grant),
|
||||
() => _operators.revoke(
|
||||
account: grant.account,
|
||||
assistant: grant.assistant,
|
||||
),
|
||||
);
|
||||
if (failed != null && !isClosed) {
|
||||
emit(state.copyWith(problem: failed));
|
||||
}
|
||||
}
|
||||
|
||||
Future<OperatorFailed?> _act(
|
||||
String what,
|
||||
Future<void> Function() action,
|
||||
) async {
|
||||
if (state.busy != null) {
|
||||
return const OperatorFailed(OperatorProblem.failed);
|
||||
}
|
||||
emit(state.copyWith(busy: what, clearProblem: true));
|
||||
OperatorFailed? failed;
|
||||
try {
|
||||
await action();
|
||||
} on Object catch (error) {
|
||||
log('An assistant grant failed: ${error.runtimeType}', name: 'AI');
|
||||
failed = _failure(error);
|
||||
}
|
||||
await load();
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(clearBusy: true));
|
||||
}
|
||||
return failed;
|
||||
}
|
||||
|
||||
static OperatorFailed _failure(Object error) => error is OperatorFailed
|
||||
? error
|
||||
: const OperatorFailed(OperatorProblem.failed);
|
||||
}
|
||||
|
|
@ -10,8 +10,9 @@ final class const AssistantUseState({
|
|||
/// The month shown, its first day in UTC.
|
||||
required final DateTime month,
|
||||
|
||||
/// The months the instance keeps, the latest first: this one and the
|
||||
/// two before (Halkyon Learn's ADR 0021, D41).
|
||||
/// The months the instance keeps, the oldest first, as a timeline runs:
|
||||
/// the two before this one, and this one (Halkyon Learn's ADR 0021, D41,
|
||||
/// D57).
|
||||
required final List<DateTime> months,
|
||||
|
||||
/// Reading it.
|
||||
|
|
@ -39,7 +40,7 @@ class AssistantUseCubit(
|
|||
return AssistantUseState(
|
||||
month: month,
|
||||
months: [
|
||||
for (var back = 0; back < 3; back++)
|
||||
for (var back = 2; back >= 0; back--)
|
||||
DateTime.utc(month.year, month.month - back),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import 'package:halkyon_llm/halkyon_llm.dart';
|
|||
/// Learn's ADR 0021, points 6 to 8), on the signed-in person's session:
|
||||
/// failures are `AssistantFailed`s. Tests replace it with a fake.
|
||||
abstract interface class AssistantGateway() {
|
||||
/// The assistants granted to the person, with what is left of their
|
||||
/// limits; none on a member of another's register (D43).
|
||||
/// The assistants granted to the person, with their budget and what is
|
||||
/// used of it; none on a member of another's register (D43).
|
||||
Future<AssistantsOffer> offered();
|
||||
|
||||
/// Records that the person agrees to what goes where with [assistant],
|
||||
|
|
@ -25,26 +25,39 @@ abstract interface class AssistantGateway() {
|
|||
}
|
||||
|
||||
/// The boundary to granting assistants (Halkyon Learn's ADR 0021, points 2,
|
||||
/// 6 and 8), on the operator's session: refusals are `OperatorFailed`s.
|
||||
/// 6 and 8, D58), on the operator's session: refusals are `OperatorFailed`s.
|
||||
abstract interface class AssistantOperatorGateway() {
|
||||
/// The assistants set in `deploy/assistants.env`, without their keys.
|
||||
/// The assistants set in `deploy/assistants.env`, without their keys, and
|
||||
/// how many people have any.
|
||||
Future<AssistantsSetUp> setUp();
|
||||
|
||||
/// Every grant, by handle.
|
||||
Future<List<AssistantGrant>> grants();
|
||||
|
||||
/// Grants [assistant] to [account] with [dailyLimit] and [monthlyLimit]
|
||||
/// (null: none) in its pricing's unit; granted already, the limits change
|
||||
/// and the agreement stays.
|
||||
Future<void> grant({
|
||||
required String account,
|
||||
required String assistant,
|
||||
required int? dailyLimit,
|
||||
required int? monthlyLimit,
|
||||
/// The people with an assistant whose handle starts with [startingWith]
|
||||
/// (every one for nothing), by handle, a page from [offset] on.
|
||||
Future<List<AssistantGrantee>> grantees({
|
||||
required String startingWith,
|
||||
required int offset,
|
||||
});
|
||||
|
||||
/// Takes [assistant] back from [account].
|
||||
Future<void> revoke({required String account, required String assistant});
|
||||
/// Those of [accounts] with an assistant or a budget.
|
||||
Future<List<AssistantGrantee>> granteesOf(List<String> accounts);
|
||||
|
||||
/// Grants each of [accounts] each of [assistants] they lack, their
|
||||
/// agreements to the others kept, and sets each one's budget to [budget].
|
||||
Future<void> grant({
|
||||
required List<String> accounts,
|
||||
required List<String> assistants,
|
||||
required AssistantBudget budget,
|
||||
});
|
||||
|
||||
/// Gives [account] exactly [assistants] and [budget].
|
||||
Future<void> change({
|
||||
required String account,
|
||||
required List<String> assistants,
|
||||
required AssistantBudget budget,
|
||||
});
|
||||
|
||||
/// Takes every assistant and the budget back from [account].
|
||||
Future<void> revoke(String account);
|
||||
|
||||
/// Everyone's use in the UTC month [month] is in, per person and
|
||||
/// assistant.
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ protocol.AssistantQuestion questionOf(String assistant, LlmRequest request) =>
|
|||
),
|
||||
],
|
||||
maxOutputTokens: request.maxOutputTokens,
|
||||
effort: switch (request.effort) {
|
||||
final effort? => protocol.AssistantEffort.fromJson(effort.key),
|
||||
null => null,
|
||||
},
|
||||
);
|
||||
|
||||
/// [message] as the module's endpoint carries it.
|
||||
|
|
@ -223,8 +227,8 @@ AssistantFailed failureOf(Object error) => switch (error) {
|
|||
protocol.AssistantRefusal.notGranted => AssistantProblem.notGranted,
|
||||
protocol.AssistantRefusal.notAgreed => AssistantProblem.notAgreed,
|
||||
protocol.AssistantRefusal.limitReached => AssistantProblem.limitReached,
|
||||
protocol.AssistantRefusal.limitOutdated =>
|
||||
AssistantProblem.limitOutdated,
|
||||
protocol.AssistantRefusal.invalidEffort =>
|
||||
AssistantProblem.invalidEffort,
|
||||
protocol.AssistantRefusal.imagesNotRead =>
|
||||
AssistantProblem.imagesNotRead,
|
||||
protocol.AssistantRefusal.malformed => AssistantProblem.malformed,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:halkyon_app/src/assistant/model/assistant_grant.dart';
|
|||
import 'package:halkyon_app/src/assistant/model/assistant_offer.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/own_assistant.dart';
|
||||
import 'package:halkyon_app/src/operator/data/instance_operator_gateway.dart';
|
||||
import 'package:halkyon_app/src/operator/data/instance_operator_repository.dart';
|
||||
import 'package:halkyon_app/src/operator/model/operated_account.dart';
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
|
||||
|
|
@ -22,6 +23,18 @@ sealed class const AssistantChoice() {
|
|||
|
||||
/// Whether it reads images.
|
||||
bool get images;
|
||||
|
||||
/// The efforts a question may ask it at, from the least (D63); empty:
|
||||
/// none to choose.
|
||||
List<LlmEffort> get efforts;
|
||||
|
||||
/// The effort a question takes unless it names one; null: the model's
|
||||
/// own.
|
||||
LlmEffort? get effort;
|
||||
|
||||
/// The ceiling on output tokens a question at [asked] asks for, its
|
||||
/// thinking within it.
|
||||
int outputCeilingAt(LlmEffort? asked);
|
||||
}
|
||||
|
||||
/// An assistant the operator granted, asked through the instance.
|
||||
|
|
@ -35,12 +48,27 @@ final class const GrantedAssistant(final OfferedAssistant assistant)
|
|||
|
||||
@override
|
||||
bool get images => assistant.images;
|
||||
|
||||
@override
|
||||
List<LlmEffort> get efforts => assistant.efforts;
|
||||
|
||||
@override
|
||||
LlmEffort? get effort => assistant.effort;
|
||||
|
||||
/// The effort's, as the instance bounds it.
|
||||
@override
|
||||
int outputCeilingAt(LlmEffort? asked) =>
|
||||
(asked ?? effort)?.outputCeiling ?? LlmEffort.defaultOutputCeiling;
|
||||
}
|
||||
|
||||
/// An assistant with the person's own key, asked directly: the instance
|
||||
/// sees neither the key nor what is asked, and counts nothing.
|
||||
final class const PersonalAssistant(final OwnAssistant assistant)
|
||||
extends AssistantChoice {
|
||||
/// The ceiling every question to one asks for: a long explanation, which
|
||||
/// any model takes, as nobody knows how far the person's model goes.
|
||||
static const outputCeiling = 4096;
|
||||
|
||||
@override
|
||||
String get name => assistant.name;
|
||||
|
||||
|
|
@ -49,6 +77,15 @@ final class const PersonalAssistant(final OwnAssistant assistant)
|
|||
|
||||
@override
|
||||
bool get images => assistant.images;
|
||||
|
||||
@override
|
||||
List<LlmEffort> get efforts => const [];
|
||||
|
||||
@override
|
||||
LlmEffort? get effort => null;
|
||||
|
||||
@override
|
||||
int outputCeilingAt(LlmEffort? asked) => outputCeiling;
|
||||
}
|
||||
|
||||
/// The assistants the signed-in person may ask (Halkyon Learn's ADR 0021):
|
||||
|
|
@ -67,8 +104,8 @@ class AssistantRepository(
|
|||
/// assistants to read them anew.
|
||||
Stream<void> get changes => _changes.stream;
|
||||
|
||||
/// The assistants granted to the person, with what is left of their
|
||||
/// limits.
|
||||
/// The assistants granted to the person, with their budget and what is
|
||||
/// used of it.
|
||||
Future<AssistantsOffer> offered() => _gateway.offered();
|
||||
|
||||
/// Records that the person agrees to what goes where with [assistant].
|
||||
|
|
@ -109,6 +146,7 @@ class AssistantRepository(
|
|||
return _llm
|
||||
.send(
|
||||
endpoint,
|
||||
// No effort: the person's own key gets none to choose from.
|
||||
LlmRequest(
|
||||
messages: request.messages,
|
||||
maxOutputTokens: request.maxOutputTokens,
|
||||
|
|
@ -133,12 +171,12 @@ class AssistantRepository(
|
|||
}
|
||||
}
|
||||
|
||||
/// Granting assistants (Halkyon Learn's ADR 0021, points 2, 6 and 8), as
|
||||
/// the operator's screen uses it. Refusals are [OperatorFailed]s.
|
||||
/// Granting assistants (Halkyon Learn's ADR 0021, points 2, 6 and 8, D58),
|
||||
/// as the operator's screens use it. Refusals are [OperatorFailed]s.
|
||||
class AssistantOperatorRepository(
|
||||
final AssistantOperatorGateway _gateway,
|
||||
|
||||
/// Where accounts are found by their handles.
|
||||
/// Where the accounts to grant to are found by their handles.
|
||||
final InstanceOperatorGateway _accounts,
|
||||
) {
|
||||
final _changes = StreamController<void>.broadcast();
|
||||
|
|
@ -146,61 +184,56 @@ class AssistantOperatorRepository(
|
|||
/// Every change made through this repository.
|
||||
Stream<void> get changes => _changes.stream;
|
||||
|
||||
/// The assistants set, without their keys.
|
||||
/// The assistants set, without their keys, and how many people have any.
|
||||
Future<AssistantsSetUp> setUp() => _gateway.setUp();
|
||||
|
||||
/// Every grant, by handle.
|
||||
Future<List<AssistantGrant>> grants() => _gateway.grants();
|
||||
/// The people with an assistant whose handle starts with [start] (an `@`
|
||||
/// before it left out; every one for nothing typed), by handle, a page
|
||||
/// from [offset] on.
|
||||
Future<List<AssistantGrantee>> grantees(String start, {int offset = 0}) =>
|
||||
_gateway.grantees(
|
||||
startingWith: start.trim().replaceFirst(RegExp('^@'), ''),
|
||||
offset: offset,
|
||||
);
|
||||
|
||||
/// Grants [assistant] to the account with [handle] (an `@` before it
|
||||
/// left out, whatever its case), with [dailyLimit] and [monthlyLimit] in
|
||||
/// its pricing's unit (null: none); refused as `notFound` for a handle
|
||||
/// nobody has.
|
||||
/// The accounts a handle typed as [start] suggests to grant to, at most
|
||||
/// [count]: approved and not suspended, whose handle starts so; none for
|
||||
/// nothing typed (D61).
|
||||
Future<List<OperatedAccount>> grantable(String start, {int count = 5}) =>
|
||||
suggestedAccounts(
|
||||
_accounts,
|
||||
start,
|
||||
eligible: (account) => !account.waits,
|
||||
count: count,
|
||||
);
|
||||
|
||||
/// Those of [accounts] with an assistant or a budget: whose budget a
|
||||
/// grant to them replaces.
|
||||
Future<List<AssistantGrantee>> granteesOf(List<String> accounts) =>
|
||||
accounts.isEmpty ? Future.value(const []) : _gateway.granteesOf(accounts);
|
||||
|
||||
/// Grants each of [accounts] each of [assistants] they lack, the
|
||||
/// agreements to the others kept, and sets each one's budget to [budget].
|
||||
Future<void> grant({
|
||||
required String handle,
|
||||
required String assistant,
|
||||
required int? dailyLimit,
|
||||
required int? monthlyLimit,
|
||||
}) async {
|
||||
final typed = handle.trim().replaceFirst(RegExp('^@'), '');
|
||||
final account = typed.isEmpty
|
||||
? null
|
||||
: (await _accounts.accountsStartingWith(typed))
|
||||
.where(
|
||||
(account) =>
|
||||
account.handle?.toLowerCase() == typed.toLowerCase(),
|
||||
)
|
||||
.firstOrNull;
|
||||
if (account == null) {
|
||||
throw const OperatorFailed(OperatorProblem.notFound);
|
||||
}
|
||||
await changeLimits(
|
||||
account: account.id,
|
||||
assistant: assistant,
|
||||
dailyLimit: dailyLimit,
|
||||
monthlyLimit: monthlyLimit,
|
||||
);
|
||||
}
|
||||
|
||||
/// Sets the limits of [assistant]'s grant to [account], granting it where
|
||||
/// it isn't; the person's agreement stays.
|
||||
Future<void> changeLimits({
|
||||
required String account,
|
||||
required String assistant,
|
||||
required int? dailyLimit,
|
||||
required int? monthlyLimit,
|
||||
required List<String> accounts,
|
||||
required List<String> assistants,
|
||||
required AssistantBudget budget,
|
||||
}) => _changing(
|
||||
_gateway.grant(
|
||||
account: account,
|
||||
assistant: assistant,
|
||||
dailyLimit: dailyLimit,
|
||||
monthlyLimit: monthlyLimit,
|
||||
),
|
||||
_gateway.grant(accounts: accounts, assistants: assistants, budget: budget),
|
||||
);
|
||||
|
||||
/// Takes [assistant] back from [account].
|
||||
Future<void> revoke({required String account, required String assistant}) =>
|
||||
_changing(_gateway.revoke(account: account, assistant: assistant));
|
||||
/// Gives [account] exactly [assistants] and [budget]; the agreements to
|
||||
/// those kept stay.
|
||||
Future<void> change({
|
||||
required String account,
|
||||
required List<String> assistants,
|
||||
required AssistantBudget budget,
|
||||
}) => _changing(
|
||||
_gateway.change(account: account, assistants: assistants, budget: budget),
|
||||
);
|
||||
|
||||
/// Takes every assistant and the budget back from [account].
|
||||
Future<void> revoke(String account) => _changing(_gateway.revoke(account));
|
||||
|
||||
/// Everyone's use in the UTC month [month] is in.
|
||||
Future<List<AssistantUse>> figures(DateTime month) =>
|
||||
|
|
|
|||
|
|
@ -25,16 +25,17 @@ class ServerpodAssistantGateway(final protocol.Caller _base)
|
|||
model: offer.model,
|
||||
images: offer.images,
|
||||
agreedAt: offer.agreedAt,
|
||||
unit: unitOf(offer.limitUnit),
|
||||
dailyLimit: offer.dailyLimit,
|
||||
monthlyLimit: offer.monthlyLimit,
|
||||
usedToday: offer.usedToday,
|
||||
usedThisMonth: offer.usedThisMonth,
|
||||
outdated: offer.outdated,
|
||||
effort: effortOf(offer.effort),
|
||||
efforts: [
|
||||
for (final effort in offer.efforts) ?effortOf(effort),
|
||||
],
|
||||
),
|
||||
],
|
||||
ownKeys: offered.ownKeys,
|
||||
currency: offered.currency,
|
||||
budget: budgetOf(offered.budget),
|
||||
usedToday: budgetUseOf(offered.usedToday),
|
||||
usedThisMonth: budgetUseOf(offered.usedThisMonth),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -73,55 +74,69 @@ class ServerpodAssistantOperatorGateway(final protocol.Caller _base)
|
|||
images: info.images,
|
||||
priceInput: info.priceInput,
|
||||
priceOutput: info.priceOutput,
|
||||
effort: effortOf(info.effort),
|
||||
maxEffort: effortOf(info.maxEffort),
|
||||
),
|
||||
],
|
||||
currency: setUp.currency,
|
||||
ownKeys: setUp.ownKeys,
|
||||
people: setUp.people,
|
||||
);
|
||||
});
|
||||
|
||||
@override
|
||||
Future<List<AssistantGrant>> grants() => operatorCall(
|
||||
Future<List<AssistantGrantee>> grantees({
|
||||
required String startingWith,
|
||||
required int offset,
|
||||
}) => operatorCall(
|
||||
() async => [
|
||||
for (final grant in await _base.assistantOperator.grants())
|
||||
AssistantGrant(
|
||||
account: grant.account.uuid,
|
||||
handle: grant.handle,
|
||||
assistant: grant.assistant,
|
||||
assistantSet: grant.assistantSet,
|
||||
grantedAt: grant.grantedAt,
|
||||
unit: unitOf(grant.limitUnit),
|
||||
dailyLimit: grant.dailyLimit,
|
||||
monthlyLimit: grant.monthlyLimit,
|
||||
agreedAt: grant.agreedAt,
|
||||
outdated: grant.outdated,
|
||||
),
|
||||
...(await _base.assistantOperator.grantees(
|
||||
startingWith: startingWith,
|
||||
offset: offset,
|
||||
)).map(_granteeOf),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<AssistantGrantee>> granteesOf(List<String> accounts) =>
|
||||
operatorCall(
|
||||
() async => [
|
||||
...(await _base.assistantOperator.granteesOf(
|
||||
accounts: [for (final account in accounts) _idOf(account)],
|
||||
)).map(_granteeOf),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> grant({
|
||||
required String account,
|
||||
required String assistant,
|
||||
required int? dailyLimit,
|
||||
required int? monthlyLimit,
|
||||
required List<String> accounts,
|
||||
required List<String> assistants,
|
||||
required AssistantBudget budget,
|
||||
}) => operatorCall(
|
||||
() => _base.assistantOperator.grant(
|
||||
account: UuidValue.withValidation(account),
|
||||
assistant: assistant,
|
||||
dailyLimit: dailyLimit,
|
||||
monthlyLimit: monthlyLimit,
|
||||
accounts: [for (final account in accounts) _idOf(account)],
|
||||
assistants: assistants,
|
||||
budget: _limitsOf(budget),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> revoke({required String account, required String assistant}) =>
|
||||
operatorCall(
|
||||
() => _base.assistantOperator.revoke(
|
||||
account: UuidValue.withValidation(account),
|
||||
assistant: assistant,
|
||||
),
|
||||
);
|
||||
Future<void> change({
|
||||
required String account,
|
||||
required List<String> assistants,
|
||||
required AssistantBudget budget,
|
||||
}) => operatorCall(
|
||||
() => _base.assistantOperator.change(
|
||||
account: _idOf(account),
|
||||
assistants: assistants,
|
||||
budget: _limitsOf(budget),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> revoke(String account) => operatorCall(
|
||||
() => _base.assistantOperator.revoke(account: _idOf(account)),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<AssistantUse>> figures(DateTime month) => operatorCall(
|
||||
|
|
@ -131,13 +146,52 @@ class ServerpodAssistantOperatorGateway(final protocol.Caller _base)
|
|||
),
|
||||
],
|
||||
);
|
||||
|
||||
static UuidValue _idOf(String account) => UuidValue.withValidation(account);
|
||||
|
||||
static protocol.AssistantLimits _limitsOf(AssistantBudget budget) =>
|
||||
protocol.AssistantLimits(
|
||||
dailyCostMicros: budget.dailyCostMicros,
|
||||
monthlyCostMicros: budget.monthlyCostMicros,
|
||||
dailyTokens: budget.dailyTokens,
|
||||
monthlyTokens: budget.monthlyTokens,
|
||||
);
|
||||
|
||||
static AssistantGrantee _granteeOf(protocol.AssistantGrantee grantee) =>
|
||||
AssistantGrantee(
|
||||
account: grantee.account.uuid,
|
||||
handle: grantee.handle,
|
||||
grants: [
|
||||
for (final grant in grantee.assistants)
|
||||
AssistantGrant(
|
||||
assistant: grant.assistant,
|
||||
assistantSet: grant.assistantSet,
|
||||
grantedAt: grant.grantedAt,
|
||||
agreedAt: grant.agreedAt,
|
||||
),
|
||||
],
|
||||
budget: budgetOf(grantee.budget),
|
||||
usedToday: budgetUseOf(grantee.usedToday),
|
||||
usedThisMonth: budgetUseOf(grantee.usedThisMonth),
|
||||
);
|
||||
}
|
||||
|
||||
/// [unit] in the app's terms.
|
||||
AssistantLimitUnit unitOf(protocol.AssistantLimitUnit unit) => switch (unit) {
|
||||
protocol.AssistantLimitUnit.costMicros => AssistantLimitUnit.money,
|
||||
protocol.AssistantLimitUnit.tokens => AssistantLimitUnit.tokens,
|
||||
};
|
||||
/// [effort] in halkyon_llm's terms; null for none, or one this app doesn't
|
||||
/// know.
|
||||
LlmEffort? effortOf(protocol.AssistantEffort? effort) =>
|
||||
effort == null ? null : LlmEffort.named(effort.name);
|
||||
|
||||
/// [limits] in the app's terms.
|
||||
AssistantBudget budgetOf(protocol.AssistantLimits limits) => AssistantBudget(
|
||||
dailyCostMicros: limits.dailyCostMicros,
|
||||
monthlyCostMicros: limits.monthlyCostMicros,
|
||||
dailyTokens: limits.dailyTokens,
|
||||
monthlyTokens: limits.monthlyTokens,
|
||||
);
|
||||
|
||||
/// [use] in the app's terms.
|
||||
BudgetUse budgetUseOf(protocol.AssistantUse use) =>
|
||||
BudgetUse(costMicros: use.costMicros, tokens: use.tokens);
|
||||
|
||||
/// A month's [figures] in the app's terms.
|
||||
AssistantUse useOf(protocol.AssistantMonthFigures figures) => AssistantUse(
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ enum AssistantProblem() {
|
|||
/// The person hasn't agreed to what goes where, as it is now.
|
||||
notAgreed,
|
||||
|
||||
/// The day's or the month's limit is used up (`AssistantFailed.resetsAt`).
|
||||
/// A day's or a month's limit of the person's budget is used up
|
||||
/// (`AssistantFailed.resetsAt`).
|
||||
limitReached,
|
||||
|
||||
/// Its limits wait for the operator to set them again.
|
||||
limitOutdated,
|
||||
/// The question named an effort the assistant doesn't offer.
|
||||
invalidEffort,
|
||||
|
||||
/// It reads no images.
|
||||
imagesNotRead,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_offer.dart';
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
|
||||
/// An assistant the operator set in `deploy/assistants.env`, as the
|
||||
/// operator's screen shows it (Halkyon Learn's ADR 0021, point 4): never
|
||||
|
|
@ -18,11 +19,17 @@ final class const SetUpAssistant({
|
|||
|
||||
/// What a million of the answer's tokens cost; null without prices.
|
||||
final double? priceOutput,
|
||||
|
||||
/// The effort a question takes unless it names one (D63); null: the
|
||||
/// model's own.
|
||||
final LlmEffort? effort,
|
||||
|
||||
/// The highest effort a question may name; null: none to choose.
|
||||
final LlmEffort? maxEffort,
|
||||
}) with Equatable {
|
||||
/// What its limits are counted in: money with prices, else tokens (D38).
|
||||
AssistantLimitUnit get unit => priceInput != null && priceOutput != null
|
||||
? AssistantLimitUnit.money
|
||||
: AssistantLimitUnit.tokens;
|
||||
/// Whether it has prices, so it counts against a budget's money as well
|
||||
/// as its tokens (D59).
|
||||
bool get priced => priceInput != null && priceOutput != null;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
|
|
@ -34,6 +41,8 @@ final class const SetUpAssistant({
|
|||
images,
|
||||
priceInput,
|
||||
priceOutput,
|
||||
effort,
|
||||
maxEffort,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -47,6 +56,9 @@ final class const AssistantsSetUp({
|
|||
|
||||
/// Whether people may add their own keys.
|
||||
final bool ownKeys = false,
|
||||
|
||||
/// How many people have an assistant granted.
|
||||
final int people = 0,
|
||||
}) with Equatable {
|
||||
/// The assistant [id] names; null for one not set (any more).
|
||||
SetUpAssistant? named(String id) {
|
||||
|
|
@ -59,17 +71,11 @@ final class const AssistantsSetUp({
|
|||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [assistants, currency, ownKeys];
|
||||
List<Object?> get props => [assistants, currency, ownKeys, people];
|
||||
}
|
||||
|
||||
/// An assistant granted to an account, as the operator sees it (D2, D4).
|
||||
/// An assistant granted to a person, as the operator sees it (D2, D4).
|
||||
final class const AssistantGrant({
|
||||
/// The account's ID.
|
||||
required final String account,
|
||||
|
||||
/// Its handle; null for one without.
|
||||
final String? handle,
|
||||
|
||||
/// The assistant's ID.
|
||||
required final String assistant,
|
||||
|
||||
|
|
@ -78,29 +84,45 @@ final class const AssistantGrant({
|
|||
final bool assistantSet = true,
|
||||
required final DateTime grantedAt,
|
||||
|
||||
/// What [dailyLimit] and [monthlyLimit] are counted in.
|
||||
final AssistantLimitUnit unit = AssistantLimitUnit.tokens,
|
||||
final int? dailyLimit,
|
||||
final int? monthlyLimit,
|
||||
|
||||
/// When the person agreed to what goes where; null until they do.
|
||||
final DateTime? agreedAt,
|
||||
|
||||
/// Whether its limits are in another unit than the pricing's now, to be
|
||||
/// set again.
|
||||
final bool outdated = false,
|
||||
}) with Equatable {
|
||||
@override
|
||||
List<Object?> get props => [assistant, assistantSet, grantedAt, agreedAt];
|
||||
}
|
||||
|
||||
/// A person granted assistants, as the operator sees them (D58): which,
|
||||
/// their budget for all of them, and what they used of it.
|
||||
final class const AssistantGrantee({
|
||||
/// The account's ID.
|
||||
required final String account,
|
||||
|
||||
/// Its handle; null for one without.
|
||||
final String? handle,
|
||||
|
||||
/// Their assistants, in the operator's order (those set no more last).
|
||||
final List<AssistantGrant> grants = const [],
|
||||
|
||||
/// Their budget; none: no limits.
|
||||
final AssistantBudget budget = AssistantBudget.none,
|
||||
|
||||
/// What this UTC day used so far.
|
||||
final BudgetUse usedToday = BudgetUse.nothing,
|
||||
|
||||
/// What this UTC month used so far.
|
||||
final BudgetUse usedThisMonth = BudgetUse.nothing,
|
||||
}) with Equatable {
|
||||
/// The budget's limits with what is left of each.
|
||||
List<BudgetLeft> get left =>
|
||||
budgetLeft(budget, today: usedToday, thisMonth: usedThisMonth);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
account,
|
||||
handle,
|
||||
assistant,
|
||||
assistantSet,
|
||||
grantedAt,
|
||||
unit,
|
||||
dailyLimit,
|
||||
monthlyLimit,
|
||||
agreedAt,
|
||||
outdated,
|
||||
grants,
|
||||
budget,
|
||||
usedToday,
|
||||
usedThisMonth,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
|
||||
/// What an assistant's limits and figures are counted in (Halkyon Learn's
|
||||
/// ADR 0021, D38): money where the operator set its prices, else tokens.
|
||||
/// What an amount of a budget or of use is counted in (Halkyon Learn's ADR
|
||||
/// 0021, D59): money, or tokens.
|
||||
enum AssistantLimitUnit() {
|
||||
/// Millionths of the instance's currency (`AssistantsOffer.currency`).
|
||||
money,
|
||||
|
|
@ -12,9 +13,92 @@ enum AssistantLimitUnit() {
|
|||
tokens,
|
||||
}
|
||||
|
||||
/// A person's budget for all their assistants together (Halkyon Learn's
|
||||
/// ADR 0021, D58 and D59): a limit per UTC day and month, in money and in
|
||||
/// tokens, each null for none. Whichever runs out first stops them: the
|
||||
/// money the assistants with prices, the tokens every one.
|
||||
final class const AssistantBudget({
|
||||
/// The most a UTC day may cost, in millionths of the currency.
|
||||
final int? dailyCostMicros,
|
||||
|
||||
/// The most a UTC month may cost, in millionths of the currency.
|
||||
final int? monthlyCostMicros,
|
||||
|
||||
/// The most tokens a UTC day may use.
|
||||
final int? dailyTokens,
|
||||
|
||||
/// The most tokens a UTC month may use.
|
||||
final int? monthlyTokens,
|
||||
}) with Equatable {
|
||||
/// No limits.
|
||||
static const none = AssistantBudget();
|
||||
|
||||
/// Whether it has no limit at all.
|
||||
bool get isNone =>
|
||||
dailyCostMicros == null &&
|
||||
monthlyCostMicros == null &&
|
||||
dailyTokens == null &&
|
||||
monthlyTokens == null;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
dailyCostMicros,
|
||||
monthlyCostMicros,
|
||||
dailyTokens,
|
||||
monthlyTokens,
|
||||
];
|
||||
}
|
||||
|
||||
/// What a person used of all their assistants together in a UTC day or
|
||||
/// month, as their budget counts it (D58).
|
||||
final class const BudgetUse({
|
||||
/// What it cost, in millionths of the currency.
|
||||
final int costMicros = 0,
|
||||
|
||||
/// The tokens, of every assistant.
|
||||
final int tokens = 0,
|
||||
}) with Equatable {
|
||||
/// Nothing used.
|
||||
static const nothing = BudgetUse();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [costMicros, tokens];
|
||||
}
|
||||
|
||||
/// One of a budget's limits with what is left of it: the day's or the
|
||||
/// month's, in money or tokens.
|
||||
typedef BudgetLeft = ({
|
||||
AssistantLimitUnit unit,
|
||||
bool daily,
|
||||
int limit,
|
||||
int left,
|
||||
});
|
||||
|
||||
/// The limits [budget] sets, the day's before the month's and money before
|
||||
/// tokens, each with what is left after [today] and [thisMonth].
|
||||
List<BudgetLeft> budgetLeft(
|
||||
AssistantBudget budget, {
|
||||
required BudgetUse today,
|
||||
required BudgetUse thisMonth,
|
||||
}) => [
|
||||
for (final (unit, daily, limit, used) in [
|
||||
(AssistantLimitUnit.money, true, budget.dailyCostMicros, today.costMicros),
|
||||
(AssistantLimitUnit.tokens, true, budget.dailyTokens, today.tokens),
|
||||
(
|
||||
AssistantLimitUnit.money,
|
||||
false,
|
||||
budget.monthlyCostMicros,
|
||||
thisMonth.costMicros,
|
||||
),
|
||||
(AssistantLimitUnit.tokens, false, budget.monthlyTokens, thisMonth.tokens),
|
||||
])
|
||||
if (limit != null)
|
||||
(unit: unit, daily: daily, limit: limit, left: max(0, limit - used)),
|
||||
];
|
||||
|
||||
/// An assistant the operator granted to the signed-in person (Halkyon
|
||||
/// Learn's ADR 0021, points 6 and 9): who answers, what is left of its
|
||||
/// limits, and whether the person agreed to what goes where.
|
||||
/// Learn's ADR 0021, points 6 and 9): who answers, how much it may think,
|
||||
/// and whether the person agreed to what goes where.
|
||||
final class const OfferedAssistant({
|
||||
/// The ID the operator gave it, which questions name.
|
||||
required final String id,
|
||||
|
|
@ -39,40 +123,17 @@ final class const OfferedAssistant({
|
|||
/// until they do, or once it moved to another provider.
|
||||
final DateTime? agreedAt,
|
||||
|
||||
/// What its limits and use are counted in.
|
||||
final AssistantLimitUnit unit = AssistantLimitUnit.tokens,
|
||||
/// The effort a question takes unless it names one (D63); null: the
|
||||
/// model's own.
|
||||
final LlmEffort? effort,
|
||||
|
||||
/// The most a UTC day may use, in [unit]; null for no daily limit.
|
||||
final int? dailyLimit,
|
||||
|
||||
/// The most a UTC month may use, in [unit]; null for no monthly limit.
|
||||
final int? monthlyLimit,
|
||||
|
||||
/// What today used, in [unit].
|
||||
final int usedToday = 0,
|
||||
|
||||
/// What this month used, in [unit].
|
||||
final int usedThisMonth = 0,
|
||||
|
||||
/// Whether its limits were set in another unit than its pricing's now:
|
||||
/// it answers nothing until the operator sets them again.
|
||||
final bool outdated = false,
|
||||
/// The efforts a question may name, from the least; empty: none to
|
||||
/// choose.
|
||||
final List<LlmEffort> efforts = const [],
|
||||
}) with Equatable {
|
||||
/// Whether the person agreed to what goes where.
|
||||
bool get agreed => agreedAt != null;
|
||||
|
||||
/// What is left of today's limit; null without one.
|
||||
int? get leftToday => switch (dailyLimit) {
|
||||
final limit? => max(0, limit - usedToday),
|
||||
null => null,
|
||||
};
|
||||
|
||||
/// What is left of this month's limit; null without one.
|
||||
int? get leftThisMonth => switch (monthlyLimit) {
|
||||
final limit? => max(0, limit - usedThisMonth),
|
||||
null => null,
|
||||
};
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
|
|
@ -82,16 +143,13 @@ final class const OfferedAssistant({
|
|||
model,
|
||||
images,
|
||||
agreedAt,
|
||||
unit,
|
||||
dailyLimit,
|
||||
monthlyLimit,
|
||||
usedToday,
|
||||
usedThisMonth,
|
||||
outdated,
|
||||
effort,
|
||||
efforts,
|
||||
];
|
||||
}
|
||||
|
||||
/// The assistants the instance offers the signed-in person.
|
||||
/// The assistants the instance offers the signed-in person, with their
|
||||
/// budget for them together and what is used of it.
|
||||
final class const AssistantsOffer({
|
||||
/// Those granted to them, in the operator's order.
|
||||
final List<OfferedAssistant> assistants = const [],
|
||||
|
|
@ -101,12 +159,32 @@ final class const AssistantsOffer({
|
|||
|
||||
/// The currency of what is counted in money (ISO 4217).
|
||||
final String currency = 'USD',
|
||||
|
||||
/// Their budget (D58); none: no limits.
|
||||
final AssistantBudget budget = AssistantBudget.none,
|
||||
|
||||
/// What this UTC day used so far.
|
||||
final BudgetUse usedToday = BudgetUse.nothing,
|
||||
|
||||
/// What this UTC month used so far.
|
||||
final BudgetUse usedThisMonth = BudgetUse.nothing,
|
||||
}) with Equatable {
|
||||
/// Nothing offered: an instance without assistants, or a member (D43).
|
||||
static const none = AssistantsOffer();
|
||||
|
||||
/// The budget's limits with what is left of each.
|
||||
List<BudgetLeft> get left =>
|
||||
budgetLeft(budget, today: usedToday, thisMonth: usedThisMonth);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [assistants, ownKeys, currency];
|
||||
List<Object?> get props => [
|
||||
assistants,
|
||||
ownKeys,
|
||||
currency,
|
||||
budget,
|
||||
usedToday,
|
||||
usedThisMonth,
|
||||
];
|
||||
}
|
||||
|
||||
/// What a person used of one assistant in one UTC month: their own, or,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Who provides an assistant, as people know them by its host (Halkyon
|
||||
/// Learn's ADR 0021, D60): Anthropic, OpenAI, Google, Mistral and
|
||||
/// OpenRouter by their APIs' hosts, any other host by itself (a model of
|
||||
/// the operator's own, a proxy).
|
||||
final class const AssistantProvider(
|
||||
/// What names it: a known provider's own word (`anthropic`), else the
|
||||
/// host.
|
||||
final String key,
|
||||
|
||||
/// Its name as people know it (`Anthropic`), else the host.
|
||||
final String name,
|
||||
) with Equatable {
|
||||
/// The provider at [host].
|
||||
factory of(String host) => switch (host.toLowerCase()) {
|
||||
'api.anthropic.com' => anthropic,
|
||||
'api.openai.com' => openai,
|
||||
'generativelanguage.googleapis.com' => google,
|
||||
'api.mistral.ai' => mistral,
|
||||
'openrouter.ai' => openrouter,
|
||||
final other => AssistantProvider(other, other),
|
||||
};
|
||||
|
||||
static const anthropic = AssistantProvider('anthropic', 'Anthropic');
|
||||
static const openai = AssistantProvider('openai', 'OpenAI');
|
||||
static const google = AssistantProvider('google', 'Google');
|
||||
static const mistral = AssistantProvider('mistral', 'Mistral');
|
||||
static const openrouter = AssistantProvider('openrouter', 'OpenRouter');
|
||||
|
||||
@override
|
||||
List<Object?> get props => [key, name];
|
||||
}
|
||||
|
||||
/// [items] by their provider, the host of each as [hostOf] gives it: the
|
||||
/// providers in the order their first item comes, each one's items in
|
||||
/// theirs.
|
||||
List<(AssistantProvider, List<T>)> byProvider<T>(
|
||||
Iterable<T> items,
|
||||
String Function(T item) hostOf,
|
||||
) {
|
||||
final groups = <AssistantProvider, List<T>>{};
|
||||
for (final item in items) {
|
||||
(groups[AssistantProvider.of(hostOf(item))] ??= []).add(item);
|
||||
}
|
||||
return [
|
||||
for (final MapEntry(key: provider, value: each) in groups.entries)
|
||||
(provider, each),
|
||||
];
|
||||
}
|
||||
|
|
@ -1,458 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/l10n/halkyon_app_localizations.dart';
|
||||
import 'package:halkyon_app/src/app/app_scope.dart';
|
||||
import 'package:halkyon_app/src/assistant/bloc/assistant_grants_cubit.dart';
|
||||
import 'package:halkyon_app/src/assistant/bloc/assistant_use_cubit.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_amount.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_grant.dart';
|
||||
import 'package:halkyon_app/src/assistant/model/assistant_offer.dart';
|
||||
import 'package:halkyon_app/src/assistant/widget/assistant_use_view.dart';
|
||||
import 'package:halkyon_app/src/assistant/widget/assistant_words.dart';
|
||||
import 'package:halkyon_app/src/operator/model/operated_account.dart';
|
||||
import 'package:halkyon_app/src/operator/widget/instance_operator_screen.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// The operator's screen's Assistants card (Halkyon Learn's ADR 0021,
|
||||
/// points 2, 6 and 8), as [AssistantGrantsCubit] above it reads them: each
|
||||
/// grant by handle, with its assistant, limits and whether its person
|
||||
/// agreed, its limits changed or taken back after asking; granting one;
|
||||
/// and everyone's use by month. Nothing where no assistant is set and none
|
||||
/// granted, or where they couldn't be read; otherwise [above] of space
|
||||
/// before it.
|
||||
class const AssistantGrantsCard({final double above = 0, super.key})
|
||||
extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = HalkyonAppLocalizations.of(context);
|
||||
return BlocConsumer<AssistantGrantsCubit, AssistantGrantsState>(
|
||||
listenWhen: (previous, current) =>
|
||||
current.problem != null && current.problem != previous.problem,
|
||||
listener: (context, state) =>
|
||||
ScaffoldMessenger.maybeOf(context)?.showSnackBar(
|
||||
SnackBar(content: Text(operatorProblemText(l10n, state.problem))),
|
||||
),
|
||||
builder: (context, state) {
|
||||
if (state.loading || !state.showsAnything) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final set = state.setUp;
|
||||
final card = SectionCard(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
OperatorCardTitle(l10n.assistantGrantsTitle),
|
||||
if (state.grants.isEmpty)
|
||||
ListTile(title: Text(l10n.assistantGrantsNone))
|
||||
else
|
||||
for (final grant in state.grants)
|
||||
_GrantTile(grant: grant, state: state),
|
||||
if (set.assistants.isNotEmpty)
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.plus),
|
||||
title: Text(l10n.assistantGrant),
|
||||
onTap: state.busy == null
|
||||
? () => unawaited(showAssistantGrantSheet(context))
|
||||
: null,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.chartColumn),
|
||||
title: Text(l10n.assistantUseByMonth),
|
||||
trailing: const Icon(LucideIcons.chevronRight),
|
||||
onTap: () => unawaited(_showUse(context, set)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: above),
|
||||
child: card,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showUse(BuildContext context, AssistantsSetUp set) {
|
||||
final operators = AppScope.of(context).assistantOperators;
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => BlocProvider(
|
||||
create: (context) => AssistantUseCubit(operators.figures)..show(),
|
||||
child: _UseScreen(set),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class const _GrantTile({
|
||||
required final AssistantGrant grant,
|
||||
required final AssistantGrantsState state,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = HalkyonAppLocalizations.of(context);
|
||||
final set = state.setUp;
|
||||
final assistant = set.named(grant.assistant);
|
||||
final busy = state.busy == AssistantGrantsState.keyOf(grant);
|
||||
return ListTile(
|
||||
leading: const Icon(LucideIcons.sparkles),
|
||||
title: Text(switch (grant.handle) {
|
||||
final handle? => '@$handle',
|
||||
null => l10n.operatorNoHandle,
|
||||
}),
|
||||
subtitle: Text(
|
||||
[
|
||||
assistant?.name ?? grant.assistant,
|
||||
if (!grant.assistantSet)
|
||||
l10n.assistantGrantUnset
|
||||
else if (grant.outdated)
|
||||
l10n.assistantGrantOutdated
|
||||
else
|
||||
assistantLimitsText(
|
||||
context,
|
||||
dailyLimit: grant.dailyLimit,
|
||||
monthlyLimit: grant.monthlyLimit,
|
||||
unit: grant.unit,
|
||||
currency: set.currency,
|
||||
),
|
||||
if (grant.agreedAt == null)
|
||||
l10n.assistantGrantNotAgreed
|
||||
else
|
||||
l10n.assistantGrantAgreed,
|
||||
].join(' · '),
|
||||
),
|
||||
trailing: busy
|
||||
? const ButtonLoadingIndicator()
|
||||
: PopupMenuButton<_GrantAction>(
|
||||
icon: const Icon(LucideIcons.ellipsisVertical),
|
||||
enabled: state.busy == null,
|
||||
onSelected: (action) => unawaited(_act(context, action)),
|
||||
itemBuilder: (context) => [
|
||||
if (grant.assistantSet)
|
||||
PopupMenuItem(
|
||||
value: _GrantAction.change,
|
||||
child: Text(l10n.assistantGrantChange),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _GrantAction.revoke,
|
||||
child: Text(l10n.assistantRevoke),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _act(BuildContext context, _GrantAction action) async {
|
||||
final l10n = HalkyonAppLocalizations.of(context);
|
||||
final cubit = context.read<AssistantGrantsCubit>();
|
||||
switch (action) {
|
||||
case _GrantAction.change:
|
||||
await showAssistantGrantSheet(context, grant: grant);
|
||||
case _GrantAction.revoke:
|
||||
final name = state.setUp.named(grant.assistant)?.name;
|
||||
if (await confirmDestructiveAction(
|
||||
context,
|
||||
title: l10n.assistantRevokeTitle(name ?? grant.assistant),
|
||||
message: l10n.assistantRevokeMessage,
|
||||
confirmLabel: l10n.assistantRevoke,
|
||||
)) {
|
||||
await cubit.revoke(grant);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _GrantAction() {
|
||||
change,
|
||||
revoke,
|
||||
}
|
||||
|
||||
/// Opens the sheet granting an assistant, [handle] filled in where given
|
||||
/// (from the operator's Find), or changing [grant]'s limits;
|
||||
/// [AssistantGrantsCubit] above [context] acts.
|
||||
Future<void> showAssistantGrantSheet(
|
||||
BuildContext context, {
|
||||
String? handle,
|
||||
AssistantGrant? grant,
|
||||
}) {
|
||||
final cubit = context.read<AssistantGrantsCubit>();
|
||||
return showAdaptiveSheet<void>(
|
||||
context,
|
||||
swipeable: false,
|
||||
builder: (context) => BlocProvider.value(
|
||||
value: cubit,
|
||||
child: _GrantSheet(handle: handle, grant: grant),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class const _GrantSheet({
|
||||
required final String? handle,
|
||||
required final AssistantGrant? grant,
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
State<_GrantSheet> createState() => _GrantSheetState();
|
||||
}
|
||||
|
||||
class _GrantSheetState() extends State<_GrantSheet> {
|
||||
late final _handle = TextEditingController(text: widget.handle ?? '');
|
||||
late final _daily = TextEditingController(text: _limitOf(true));
|
||||
late final _monthly = TextEditingController(text: _limitOf(false));
|
||||
late String? _assistant =
|
||||
widget.grant?.assistant ??
|
||||
context
|
||||
.read<AssistantGrantsCubit>()
|
||||
.state
|
||||
.setUp
|
||||
.assistants
|
||||
.firstOrNull
|
||||
?.id;
|
||||
|
||||
/// Why the instance refused; null while it didn't.
|
||||
OperatorFailed? _refused;
|
||||
|
||||
/// Whether a limit typed isn't a number, by field, once Grant was
|
||||
/// pressed.
|
||||
var _dailyInvalid = false;
|
||||
var _monthlyInvalid = false;
|
||||
var _busy = false;
|
||||
|
||||
String _limitOf(bool daily) => switch (widget.grant) {
|
||||
final grant? => limitText(
|
||||
daily ? grant.dailyLimit : grant.monthlyLimit,
|
||||
grant.unit,
|
||||
),
|
||||
null => '',
|
||||
};
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_handle.dispose();
|
||||
_daily.dispose();
|
||||
_monthly.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
int? _parse(TextEditingController field, AssistantLimitUnit unit) {
|
||||
try {
|
||||
return limitFromText(field.text, unit);
|
||||
} on FormatException {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submit(SetUpAssistant assistant) async {
|
||||
final unit = assistant.unit;
|
||||
final daily = _parse(_daily, unit);
|
||||
final monthly = _parse(_monthly, unit);
|
||||
setState(() {
|
||||
_dailyInvalid = daily == -1;
|
||||
_monthlyInvalid = monthly == -1;
|
||||
_refused = null;
|
||||
});
|
||||
if (_dailyInvalid || _monthlyInvalid) {
|
||||
return;
|
||||
}
|
||||
final cubit = context.read<AssistantGrantsCubit>();
|
||||
setState(() => _busy = true);
|
||||
final refused = switch (widget.grant) {
|
||||
final grant? => await cubit.changeLimits(
|
||||
grant,
|
||||
dailyLimit: daily,
|
||||
monthlyLimit: monthly,
|
||||
),
|
||||
null => await cubit.grant(
|
||||
handle: _handle.text,
|
||||
assistant: assistant.id,
|
||||
dailyLimit: daily,
|
||||
monthlyLimit: monthly,
|
||||
),
|
||||
};
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (refused == null) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
setState(() {
|
||||
_busy = false;
|
||||
_refused = refused;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = HalkyonAppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final set = context.watch<AssistantGrantsCubit>().state.setUp;
|
||||
final grant = widget.grant;
|
||||
final chosen = switch (_assistant) {
|
||||
final id? => set.named(id),
|
||||
null => null,
|
||||
};
|
||||
final suffix = switch (chosen?.unit) {
|
||||
AssistantLimitUnit.money => set.currency,
|
||||
AssistantLimitUnit.tokens || null => l10n.assistantGrantTokens,
|
||||
};
|
||||
final refusedHandle = _refused?.problem == OperatorProblem.notFound;
|
||||
return SheetScaffold(
|
||||
title: grant == null ? l10n.assistantGrant : l10n.assistantGrantChange,
|
||||
busy: _busy,
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (grant == null)
|
||||
TextField(
|
||||
controller: _handle,
|
||||
autofocus: widget.handle == null,
|
||||
autocorrect: false,
|
||||
enabled: !_busy,
|
||||
maxLength: 30,
|
||||
buildCounter: nearLimitCounter,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.operatorHandle,
|
||||
prefixText: '@',
|
||||
errorText: refusedHandle ? l10n.assistantGrantNoHandle : null,
|
||||
),
|
||||
)
|
||||
else
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(LucideIcons.sparkles),
|
||||
title: Text(switch (grant.handle) {
|
||||
final handle? => '@$handle',
|
||||
null => l10n.operatorNoHandle,
|
||||
}),
|
||||
subtitle: Text(chosen?.name ?? grant.assistant),
|
||||
),
|
||||
if (grant == null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.assistantGrantAssistant,
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
RadioGroup<String>(
|
||||
groupValue: _assistant,
|
||||
onChanged: (id) {
|
||||
if (!_busy) {
|
||||
setState(() => _assistant = id);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
for (final assistant in set.assistants)
|
||||
RadioListTile<String>(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: assistant.id,
|
||||
title: Text(assistant.name),
|
||||
subtitle: Text(
|
||||
l10n.assistantModelAt(assistant.model, assistant.host),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _daily,
|
||||
enabled: !_busy,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.assistantGrantDaily,
|
||||
suffixText: suffix,
|
||||
helperText: l10n.assistantGrantLimitHelper,
|
||||
errorText: _dailyInvalid ? l10n.assistantGrantLimitInvalid : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _monthly,
|
||||
enabled: !_busy,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.assistantGrantMonthly,
|
||||
suffixText: suffix,
|
||||
helperText: l10n.assistantGrantLimitHelper,
|
||||
errorText: _monthlyInvalid
|
||||
? l10n.assistantGrantLimitInvalid
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (_refused case final refused? when !refusedHandle) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
operatorProblemText(l10n, refused),
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.assistantGrantNote,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
footer: FilledButton(
|
||||
onPressed: _busy || chosen == null ? null : () => _submit(chosen),
|
||||
child: Text(
|
||||
grant == null
|
||||
? l10n.assistantGrantSave
|
||||
: MaterialLocalizations.of(context).saveButtonLabel,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Everyone's use by month, as the operator's screen opens it.
|
||||
class const _UseScreen(final AssistantsSetUp _set) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = HalkyonAppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
return TopGradient(
|
||||
color: Brand.primary,
|
||||
background: theme.colorScheme.surfaceContainerLow,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
systemOverlayStyle: TopGradient.overlayStyle(context),
|
||||
title: Text(l10n.assistantUseTitle),
|
||||
),
|
||||
body: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: readableContentMaxWidth,
|
||||
),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
children: [
|
||||
SectionCard(
|
||||
child: AssistantUseView(
|
||||
names: {
|
||||
for (final assistant in _set.assistants)
|
||||
assistant.id: assistant.name,
|
||||
},
|
||||
currency: _set.currency,
|
||||
byPerson: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue