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:
parent
bc4953aaf8
commit
bda09a2959
10 changed files with 35 additions and 158 deletions
File diff suppressed because one or more lines are too long
|
|
@ -1875,9 +1875,6 @@
|
||||||
"proposalsFromNote": "Karteikarten aus dieser Notiz",
|
"proposalsFromNote": "Karteikarten aus dieser Notiz",
|
||||||
"proposalsFromPage": "Aus dieser Seite",
|
"proposalsFromPage": "Aus dieser Seite",
|
||||||
"proposalsFromSelection": "Aus der Auswahl",
|
"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",
|
"settingsSearchEngine": "Suchmaschine",
|
||||||
"searchEngineDevice": "Suche des Geräts",
|
"searchEngineDevice": "Suche des Geräts",
|
||||||
"searchEngineCustom": "Eigene Adresse",
|
"searchEngineCustom": "Eigene Adresse",
|
||||||
|
|
|
||||||
|
|
@ -2248,10 +2248,6 @@
|
||||||
"@proposalsFromPage": {"description": "In a PDF's flashcards: has an assistant propose cards from the page shown (D56)."},
|
"@proposalsFromPage": {"description": "In a PDF's flashcards: has an assistant propose cards from the page shown (D56)."},
|
||||||
"proposalsFromSelection": "From the selection",
|
"proposalsFromSelection": "From the selection",
|
||||||
"@proposalsFromSelection": {"description": "In a PDF's flashcards: has an assistant propose cards from the text selected (D56)."},
|
"@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",
|
"settingsSearchEngine": "Search engine",
|
||||||
"searchEngineDevice": "The device's search",
|
"searchEngineDevice": "The device's search",
|
||||||
"searchEngineCustom": "Your own address",
|
"searchEngineCustom": "Your own address",
|
||||||
|
|
|
||||||
|
|
@ -11090,24 +11090,6 @@ abstract class AppLocalizations {
|
||||||
/// **'From the selection'**
|
/// **'From the selection'**
|
||||||
String get proposalsFromSelection;
|
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.
|
/// No description provided for @settingsSearchEngine.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -7054,16 +7054,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get proposalsFromSelection => 'Aus der Auswahl';
|
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
|
@override
|
||||||
String get settingsSearchEngine => 'Suchmaschine';
|
String get settingsSearchEngine => 'Suchmaschine';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6991,15 +6991,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get proposalsFromSelection => 'From the selection';
|
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
|
@override
|
||||||
String get settingsSearchEngine => 'Search engine';
|
String get settingsSearchEngine => 'Search engine';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,12 @@ bool proposesCards(BuildContext context) =>
|
||||||
/// studyset [studysetId] (ADR 0021, D55), in a sheet where the person
|
/// studyset [studysetId] (ADR 0021, D55), in a sheet where the person
|
||||||
/// chooses them, changes them and adds them, tied to [source] where they
|
/// chooses them, changes them and adds them, tied to [source] where they
|
||||||
/// come from a PDF's page; once [readyToAsk], [text] counting as the
|
/// 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
|
/// studyset's content (a note, a page), so a private one goes only once
|
||||||
/// private one goes only once agreed to. Says how many were added.
|
/// agreed to. Says how many were added.
|
||||||
Future<void> proposeCards(
|
Future<void> proposeCards(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required String studysetId,
|
required String studysetId,
|
||||||
required String text,
|
required String text,
|
||||||
bool fromStudyset = true,
|
|
||||||
PdfPageRef? source,
|
PdfPageRef? source,
|
||||||
}) async {
|
}) async {
|
||||||
final choice = nextAssistant(context);
|
final choice = nextAssistant(context);
|
||||||
|
|
@ -41,7 +40,7 @@ Future<void> proposeCards(
|
||||||
}
|
}
|
||||||
final scope = AssistantScope(
|
final scope = AssistantScope(
|
||||||
studysetId: studysetId,
|
studysetId: studysetId,
|
||||||
partsFrom: fromStudyset ? {studysetId} : const {},
|
partsFrom: {studysetId},
|
||||||
);
|
);
|
||||||
if (!await readyToAsk(context, choice: choice, scope: scope) ||
|
if (!await readyToAsk(context, choice: choice, scope: scope) ||
|
||||||
!context.mounted) {
|
!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
|
/// 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.
|
/// its own, and Add for those chosen, which closes with how many.
|
||||||
class const _ProposalsSheet({required final String name})
|
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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/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/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/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_list.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_text_import_sheet.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';
|
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)),
|
onPressed: () => context.push(RoutePaths.jellyfin(studysetId)),
|
||||||
child: Text(l10n.studysetWatch),
|
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.
|
// Only for a language the instance has a dictionary of.
|
||||||
if (dictionary != null)
|
if (dictionary != null)
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
|
|
|
||||||
|
|
@ -144,27 +144,28 @@ void main() {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
testWidgets('Add material proposes flashcards from a text pasted: every '
|
/// Shows a note of Japanese's holding [text], for its flashcards.
|
||||||
'one chosen, changed or left out, and those chosen added', (
|
Future<void> showNote(WidgetTester tester, String text) async {
|
||||||
tester,
|
final notes = NoteRepository(noteGateway);
|
||||||
) async {
|
addTearDown(notes.dispose);
|
||||||
await showApp(tester, RoutePaths.studyset('japanese'));
|
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(
|
assistants.answers.add(
|
||||||
_proposes([('食べる', 'to eat'), ('飲む', 'to drink'), ('見る', 'to see')]),
|
_proposes([('食べる', 'to eat'), ('飲む', 'to drink'), ('見る', 'to see')]),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.tap(find.text('Add material'));
|
await tapAndSettle(tester, find.byTooltip('Flashcards from this note'));
|
||||||
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'));
|
|
||||||
|
|
||||||
expect(find.text('Flashcards by Claude'), findsOneWidget);
|
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);
|
expect(find.text('Add 3 flashcards'), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.text('飲む'));
|
await tester.tap(find.text('飲む'));
|
||||||
|
|
@ -196,7 +197,7 @@ void main() {
|
||||||
testWidgets('none proposed says so, and Try again asks once more', (
|
testWidgets('none proposed says so, and Try again asks once more', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
await showApp(tester, RoutePaths.studyset('japanese'));
|
await showNote(tester, 'Some text.');
|
||||||
assistants.answers
|
assistants.answers
|
||||||
..add([
|
..add([
|
||||||
LlmFinished(
|
LlmFinished(
|
||||||
|
|
@ -206,12 +207,7 @@ void main() {
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
..add(_proposes([('Q', 'A')]));
|
..add(_proposes([('Q', 'A')]));
|
||||||
await tester.tap(find.text('Add material'));
|
await tapAndSettle(tester, find.byTooltip('Flashcards from this note'));
|
||||||
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'));
|
|
||||||
|
|
||||||
expect(find.text('Claude proposed no flashcards.'), findsOneWidget);
|
expect(find.text('Claude proposed no flashcards.'), findsOneWidget);
|
||||||
await tapAndSettle(tester, find.text('Try again'));
|
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', (
|
testWidgets('nothing offers flashcards without an assistant', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|
@ -292,13 +298,6 @@ void main() {
|
||||||
'japanese',
|
'japanese',
|
||||||
NoteContent(title: 'Kana', body: RichContent.plain('あ is a.')),
|
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));
|
await showApp(tester, RoutePaths.note('japanese', noteId));
|
||||||
expect(find.byTooltip('Flashcards from this note'), findsNothing);
|
expect(find.byTooltip('Flashcards from this note'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
|
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
|
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
|
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
|
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
|
all models of two providers would take dozens of sheets; a list of hundreds waiting would
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue