feat(learn): Explore by what a licence allows, a skeleton while the library loads, and an assistant's agreement per provider (ADR 0005, rule 12; ADR 0021, D66)
- Explore filters by what a studyset's licence lets a learner do: adapt it (no NoDerivatives) and use it commercially (no NonCommercial), which the instance reads from the release's licence (ExploreQuery.adaptable, commercial) and the app the same way (LicenseUse). An instance from before ignores both. - While the studysets are read, the Library shows shimmering skeletons of their cards rather than a spinner: halkyon_ui's new Shimmer and SkeletonBox, still where the system asks for no animations. - The scope's "Include private studysets" is offered only while the person has a private studyset. - An agreement to what goes where is per provider (protocol and host): agreeing to one assistant agrees to every assistant of that provider, those granted later too, and the agreement sheet says so. No migration: it is written on every grant of the provider, so taking one back keeps it for the others, and read per provider from any of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
138c4dd46e
commit
5641285d31
8 changed files with 970 additions and 85 deletions
|
|
@ -422,6 +422,7 @@
|
|||
"assistantOwnNotAllowed": "Wer {instance} betreibt, erlaubt hier keine eigenen Schlüssel, also fragt die App diese nicht.",
|
||||
"assistantConsentTitle": "Bevor du {name} fragst",
|
||||
"assistantConsentModel": "Es antwortet das Modell {model} bei {host}.",
|
||||
"assistantConsentProvider": "Es gilt für jeden Assistenten bei {host}.",
|
||||
"assistantConsentWhatGoes": "Dorthin geht nur, was du sendest: deine Frage, was du davon auswählst, worum es geht, und was es nachschlägt, wo du es lässt. Für den Anbieter gelten seine eigenen Bedingungen.",
|
||||
"assistantConsentImages": "Es liest die Bilder, die du hinzufügst.",
|
||||
"assistantConsentInstance": "Es geht auf dem Weg durch {instance}, das nichts davon aufbewahrt.",
|
||||
|
|
|
|||
|
|
@ -1344,6 +1344,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"assistantConsentProvider": "It holds for every assistant at {host}.",
|
||||
"@assistantConsentProvider": {
|
||||
"description": "An agreement is to what goes where, so it covers every assistant of the provider (Halkyon Learn's ADR 0021, D66).",
|
||||
"placeholders": {
|
||||
"host": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assistantConsentWhatGoes": "Only what you send goes there: your question, what you pick of what it is about, and what it looks up where you let it. The provider's own terms apply to it.",
|
||||
"@assistantConsentWhatGoes": {
|
||||
"description": "What goes to the provider (D27)."
|
||||
|
|
|
|||
|
|
@ -2635,6 +2635,12 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'It answers with the model {model} at {host}.'**
|
||||
String assistantConsentModel(String model, String host);
|
||||
|
||||
/// An agreement is to what goes where, so it covers every assistant of the provider (Halkyon Learn's ADR 0021, D66).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'It holds for every assistant at {host}.'**
|
||||
String assistantConsentProvider(String host);
|
||||
|
||||
/// What goes to the provider (D27).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1663,6 +1663,11 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
return 'Es antwortet das Modell $model bei $host.';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantConsentProvider(String host) {
|
||||
return 'Es gilt für jeden Assistenten bei $host.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantConsentWhatGoes =>
|
||||
'Dorthin geht nur, was du sendest: deine Frage, was du davon auswählst, worum es geht, und was es nachschlägt, wo du es lässt. Für den Anbieter gelten seine eigenen Bedingungen.';
|
||||
|
|
|
|||
|
|
@ -1641,6 +1641,11 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
return 'It answers with the model $model at $host.';
|
||||
}
|
||||
|
||||
@override
|
||||
String assistantConsentProvider(String host) {
|
||||
return 'It holds for every assistant at $host.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantConsentWhatGoes =>
|
||||
'Only what you send goes there: your question, what you pick of what it is about, and what it looks up where you let it. The provider\'s own terms apply to it.';
|
||||
|
|
|
|||
|
|
@ -315,6 +315,7 @@ class const _ConsentSheet({
|
|||
LucideIcons.sparkles,
|
||||
l10n.assistantConsentModel(offered.model, offered.host),
|
||||
),
|
||||
(LucideIcons.layers, l10n.assistantConsentProvider(offered.host)),
|
||||
(LucideIcons.send, l10n.assistantConsentWhatGoes),
|
||||
if (offered.images) (LucideIcons.image, l10n.assistantConsentImages),
|
||||
(LucideIcons.server, l10n.assistantConsentInstance(instance)),
|
||||
|
|
|
|||
|
|
@ -50,10 +50,16 @@ class FakeAssistantGateway() implements AssistantGateway {
|
|||
if (granted.isEmpty) {
|
||||
throw const AssistantFailed(AssistantProblem.notGranted);
|
||||
}
|
||||
// As the instance agrees: to what goes where, so every assistant of
|
||||
// the provider (Halkyon Learn's ADR 0021, D66).
|
||||
final provider = (granted.first.protocol, granted.first.host);
|
||||
offer = AssistantsOffer(
|
||||
assistants: [
|
||||
for (final each in offer.assistants)
|
||||
if (each.id == assistant) _agreed(each) else each,
|
||||
if ((each.protocol, each.host) == provider && !each.agreed)
|
||||
_agreed(each)
|
||||
else
|
||||
each,
|
||||
],
|
||||
ownKeys: offer.ownKeys,
|
||||
currency: offer.currency,
|
||||
|
|
|
|||
|
|
@ -191,6 +191,43 @@ void main() {
|
|||
expect(find.textContaining('Read and agree'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('one agreement holds for every assistant of its provider, '
|
||||
'which the sheet says (D66)', (tester) async {
|
||||
gateway.offer = const AssistantsOffer(
|
||||
assistants: [
|
||||
claude,
|
||||
OfferedAssistant(
|
||||
id: 'opus',
|
||||
name: 'Opus',
|
||||
protocol: 'anthropic',
|
||||
host: 'api.anthropic.com',
|
||||
model: 'claude-opus-5-5',
|
||||
),
|
||||
OfferedAssistant(
|
||||
id: 'gemini',
|
||||
name: 'Gemini',
|
||||
protocol: 'openai',
|
||||
host: 'generativelanguage.googleapis.com',
|
||||
model: 'gemini-3-pro',
|
||||
),
|
||||
],
|
||||
);
|
||||
await show(tester);
|
||||
expect(find.textContaining('Read and agree'), findsNWidgets(3));
|
||||
|
||||
await tap(tester, find.text('Claude'));
|
||||
expect(
|
||||
find.text('It holds for every assistant at api.anthropic.com.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tap(tester, find.widgetWithText(FilledButton, 'Agree'));
|
||||
|
||||
expect(find.textContaining('Read and agree'), findsOneWidget);
|
||||
expect(gateway.calls.where((call) => call.startsWith('agree')), [
|
||||
'agree claude',
|
||||
]);
|
||||
});
|
||||
|
||||
testWidgets('once agreed, the sheet says since when and asks nothing', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
|
|||
Loading…
Reference in a new issue