feat(learn): Add material no longer offers flashcards from a text (ADR 0021, D56 amended)

The developer's review of 4 Nov 2026. The entry goes from a studyset's
Add material, and with it the sheet for pasting a text
(proposeCardsFromText, _TextSheet), its three texts and proposeCards'
fromStudyset, which only it set to false. A note's and a PDF page's
flashcards stay; their tests now drive the proposals sheet from a note.

Whats-New: Add material no longer offers Flashcards from a text; a note's or a PDF page's flashcards stay.
Whats-New-De: Material hinzufügen bietet keine Karteikarten aus einem Text mehr an; die aus einer Notiz oder einer PDF-Seite bleiben.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-04 21:19:30 +01:00
commit bda09a2959
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
10 changed files with 35 additions and 158 deletions

File diff suppressed because one or more lines are too long

View file

@ -1875,9 +1875,6 @@
"proposalsFromNote": "Karteikarten aus dieser Notiz",
"proposalsFromPage": "Aus dieser Seite",
"proposalsFromSelection": "Aus der Auswahl",
"proposalsFromText": "Karteikarten aus einem Text",
"proposalsTextHint": "Füg den Text ein, aus dem Karteikarten werden sollen",
"proposalsPropose": "Karteikarten vorschlagen",
"settingsSearchEngine": "Suchmaschine",
"searchEngineDevice": "Suche des Geräts",
"searchEngineCustom": "Eigene Adresse",

View file

@ -2248,10 +2248,6 @@
"@proposalsFromPage": {"description": "In a PDF's flashcards: has an assistant propose cards from the page shown (D56)."},
"proposalsFromSelection": "From the selection",
"@proposalsFromSelection": {"description": "In a PDF's flashcards: has an assistant propose cards from the text selected (D56)."},
"proposalsFromText": "Flashcards from a text",
"@proposalsFromText": {"description": "Add material: has an assistant propose cards from text pasted (D56)."},
"proposalsTextHint": "Paste the text to make flashcards from",
"proposalsPropose": "Propose flashcards",
"settingsSearchEngine": "Search engine",
"searchEngineDevice": "The device's search",
"searchEngineCustom": "Your own address",

View file

@ -11090,24 +11090,6 @@ abstract class AppLocalizations {
/// **'From the selection'**
String get proposalsFromSelection;
/// Add material: has an assistant propose cards from text pasted (D56).
///
/// In en, this message translates to:
/// **'Flashcards from a text'**
String get proposalsFromText;
/// No description provided for @proposalsTextHint.
///
/// In en, this message translates to:
/// **'Paste the text to make flashcards from'**
String get proposalsTextHint;
/// No description provided for @proposalsPropose.
///
/// In en, this message translates to:
/// **'Propose flashcards'**
String get proposalsPropose;
/// No description provided for @settingsSearchEngine.
///
/// In en, this message translates to:

View file

@ -7054,16 +7054,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get proposalsFromSelection => 'Aus der Auswahl';
@override
String get proposalsFromText => 'Karteikarten aus einem Text';
@override
String get proposalsTextHint =>
'Füg den Text ein, aus dem Karteikarten werden sollen';
@override
String get proposalsPropose => 'Karteikarten vorschlagen';
@override
String get settingsSearchEngine => 'Suchmaschine';

View file

@ -6991,15 +6991,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get proposalsFromSelection => 'From the selection';
@override
String get proposalsFromText => 'Flashcards from a text';
@override
String get proposalsTextHint => 'Paste the text to make flashcards from';
@override
String get proposalsPropose => 'Propose flashcards';
@override
String get settingsSearchEngine => 'Search engine';

View file

@ -26,13 +26,12 @@ bool proposesCards(BuildContext context) =>
/// studyset [studysetId] (ADR 0021, D55), in a sheet where the person
/// chooses them, changes them and adds them, tied to [source] where they
/// come from a PDF's page; once [readyToAsk], [text] counting as the
/// studyset's content where it is [fromStudyset] (a note, a page), so a
/// private one goes only once agreed to. Says how many were added.
/// studyset's content (a note, a page), so a private one goes only once
/// agreed to. Says how many were added.
Future<void> proposeCards(
BuildContext context, {
required String studysetId,
required String text,
bool fromStudyset = true,
PdfPageRef? source,
}) async {
final choice = nextAssistant(context);
@ -41,7 +40,7 @@ Future<void> proposeCards(
}
final scope = AssistantScope(
studysetId: studysetId,
partsFrom: fromStudyset ? {studysetId} : const {},
partsFrom: {studysetId},
);
if (!await readyToAsk(context, choice: choice, scope: scope) ||
!context.mounted) {
@ -73,27 +72,6 @@ Future<void> proposeCards(
}
}
/// Asks for a text to make flashcards of, pasted or typed, then has them
/// proposed for [studysetId] (D56): the text is the person's, not the
/// studyset's, so nothing private is said to go.
Future<void> proposeCardsFromText(
BuildContext context, {
required String studysetId,
}) async {
final text = await showAdaptiveSheet<String>(
context,
builder: (context) => const _TextSheet(),
);
if (text != null && context.mounted) {
await proposeCards(
context,
studysetId: studysetId,
text: text,
fromStudyset: false,
);
}
}
/// The cards [name] proposes, each chosen or not, changed in a sheet of
/// its own, and Add for those chosen, which closes with how many.
class const _ProposalsSheet({required final String name})
@ -276,50 +254,3 @@ class _EditSheetState() extends State<_EditSheet> {
);
}
}
/// A text to make flashcards of; closes with it.
class const _TextSheet() extends StatefulWidget {
@override
State<_TextSheet> createState() => _TextSheetState();
}
class _TextSheetState() extends State<_TextSheet> {
final _text = TextEditingController();
@override
void dispose() {
_text.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return SheetScaffold(
title: l10n.proposalsFromText,
body: TextField(
controller: _text,
autofocus: true,
// As much as goes to the assistant.
maxLength: maxProposalSource,
buildCounter: nearLimitCounter,
minLines: 6,
maxLines: 14,
decoration: InputDecoration(
hintText: l10n.proposalsTextHint,
border: const OutlineInputBorder(),
),
),
footer: ListenableBuilder(
listenable: _text,
builder: (context, _) => FilledButton.icon(
icon: const Icon(LucideIcons.sparkles),
label: Text(l10n.proposalsPropose),
onPressed: _text.text.trim().isEmpty
? null
: () => Navigator.of(context).pop(_text.text),
),
),
);
}
}

View file

@ -14,7 +14,6 @@ import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_importer.dart';
import 'package:halkyon_learn_flutter/src/feature/anki/widget/anki_import_sheet.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_topic.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/assistant_field.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/card_proposals_sheet.dart';
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_list.dart';
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_text_import_sheet.dart';
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.dart';
@ -1394,16 +1393,6 @@ class _AddMaterialState() extends State<_AddMaterial> {
onPressed: () => context.push(RoutePaths.jellyfin(studysetId)),
child: Text(l10n.studysetWatch),
),
// Only where an assistant can be asked (D56).
if (proposesCards(context))
MenuItemButton(
leadingIcon: const Icon(LucideIcons.sparkles),
onPressed: () {
context.read<ContentTabCubit>().show(StudysetTab.flashcards);
unawaited(proposeCardsFromText(context, studysetId: studysetId));
},
child: Text(l10n.proposalsFromText),
),
// Only for a language the instance has a dictionary of.
if (dictionary != null)
MenuItemButton(

View file

@ -144,27 +144,28 @@ void main() {
];
}
testWidgets('Add material proposes flashcards from a text pasted: every '
'one chosen, changed or left out, and those chosen added', (
tester,
) async {
await showApp(tester, RoutePaths.studyset('japanese'));
/// Shows a note of Japanese's holding [text], for its flashcards.
Future<void> showNote(WidgetTester tester, String text) async {
final notes = NoteRepository(noteGateway);
addTearDown(notes.dispose);
final (:noteId, revisionId: _) = await notes.create(
'japanese',
NoteContent(title: 'Verbs', body: RichContent.plain(text)),
);
await showApp(tester, RoutePaths.note('japanese', noteId));
}
testWidgets("a note's proposed flashcards: every one chosen, changed or "
'left out, and those chosen added', (tester) async {
await showNote(tester, '食べる means to eat.');
assistants.answers.add(
_proposes([('食べる', 'to eat'), ('飲む', 'to drink'), ('見る', 'to see')]),
);
await tester.tap(find.text('Add material'));
await tester.pumpAndSettle();
await tapAndSettle(tester, find.text('Flashcards from a text'));
await tester.enterText(
find.byType(TextField).last,
'食べる means to eat.',
);
await tester.pump();
await tapAndSettle(tester, find.text('Propose flashcards'));
await tapAndSettle(tester, find.byTooltip('Flashcards from this note'));
expect(find.text('Flashcards by Claude'), findsOneWidget);
expect(_sent(assistants.asked.single.$2), '食べる means to eat.');
expect(_sent(assistants.asked.single.$2), 'Verbs\n\n食べる means to eat.');
expect(find.text('Add 3 flashcards'), findsOneWidget);
await tester.tap(find.text('飲む'));
@ -196,7 +197,7 @@ void main() {
testWidgets('none proposed says so, and Try again asks once more', (
tester,
) async {
await showApp(tester, RoutePaths.studyset('japanese'));
await showNote(tester, 'Some text.');
assistants.answers
..add([
LlmFinished(
@ -206,12 +207,7 @@ void main() {
),
])
..add(_proposes([('Q', 'A')]));
await tester.tap(find.text('Add material'));
await tester.pumpAndSettle();
await tapAndSettle(tester, find.text('Flashcards from a text'));
await tester.enterText(find.byType(TextField).last, 'Some text.');
await tester.pump();
await tapAndSettle(tester, find.text('Propose flashcards'));
await tapAndSettle(tester, find.byTooltip('Flashcards from this note'));
expect(find.text('Claude proposed no flashcards.'), findsOneWidget);
await tapAndSettle(tester, find.text('Try again'));
@ -282,6 +278,16 @@ void main() {
);
});
testWidgets('Add material offers no flashcards from a text (the '
"developer's review of 4 Nov 2026): a note's, a page's are enough", (
tester,
) async {
await showApp(tester, RoutePaths.studyset('japanese'));
await tester.tap(find.text('Add material'));
await tester.pumpAndSettle();
expect(find.text('Flashcards from a text'), findsNothing);
});
testWidgets('nothing offers flashcards without an assistant', (
tester,
) async {
@ -292,13 +298,6 @@ void main() {
'japanese',
NoteContent(title: 'Kana', body: RichContent.plain('あ is a.')),
);
await showApp(tester, RoutePaths.studyset('japanese'));
await tester.tap(find.text('Add material'));
await tester.pumpAndSettle();
expect(find.text('Flashcards from a text'), findsNothing);
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
await showApp(tester, RoutePaths.note('japanese', noteId));
expect(find.byTooltip('Flashcards from this note'), findsNothing);
});

View file

@ -364,7 +364,9 @@ On 26 Oct, how the third step is built (the assistant answering in Learn):
Add material › Flashcards from a text (a paragraph pasted or typed). Amended 26 Oct, as
built: a note's text selected is left for later, as the note editor's selection can't
be reached from its app bar as the PDF reader's is; and the entry is named for what it
makes, as the other Add material entries are, not "With an assistant".
makes, as the other Add material entries are, not "With an assistant". Amended 4 Nov
2026 (the developer's review): Flashcards from a text goes from Add material, and with
it the sheet for pasting a text; a note's and a PDF page's stay.
On 26 Oct, after the first use on learn.halkyon.eu (the developer: granting three people
all models of two providers would take dozens of sheets; a list of hundreds waiting would