feat(learn): voice mode goes, as a spoken conversation isn't wanted (ADR 0021, D68 withdrawn)

The developer's review of 4 Nov 2026: "an actual conversation with the AI
chatbot is not desirable and necessary". The field's voice-mode button,
the screen it opened, reading answers aloud and its texts go, and with
them flutter_tts, its patched copy in third_party/ (one override fewer),
the TTS_SERVICE query in the manifest and the plugin's registrants. The
microphone stays and dictates into the question. ImageApproval is the
conversation sheet's own again, as only voice mode needed it.

Whats-New: Voice mode is gone; the microphone dictates into your question.
Whats-New-De: Der Sprachmodus ist weg; das Mikrofon diktiert in deine Frage.

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 20:38:11 +01:00
commit 00212bf49c
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 18 additions and 5716 deletions

View file

@ -35,7 +35,6 @@ import '../../support/fake_conversation_gateway.dart';
import '../../support/fake_grammar_gateway.dart';
import '../../support/fake_photo_picker.dart';
import '../../support/fake_settings_gateway.dart';
import '../../support/fake_speech_output.dart';
import '../../support/fake_studyset_gateway.dart';
import '../../support/memory_asset_file_cache.dart';
import '../../support/memory_device_settings_store.dart';
@ -121,7 +120,6 @@ void main() {
late FakeFilePicker picker;
late FakePhotoPicker photos;
late FakeSpeechInput speech;
late FakeSpeechOutput voice;
late MemoryDeviceSettingsStore store;
late List<Uri> searched;
late FakeAssetGateway assetGateway;
@ -150,7 +148,6 @@ void main() {
picker = FakeFilePicker();
photos = FakePhotoPicker();
speech = FakeSpeechInput();
voice = FakeSpeechOutput();
searched = [];
assetGateway = FakeAssetGateway();
assetFiles = MemoryAssetFileCache();
@ -214,7 +211,6 @@ void main() {
files: picker,
photos: photos,
speech: speech,
speechOutput: voice,
),
location: location,
);
@ -368,10 +364,8 @@ void main() {
);
final history = at('Conversations');
final speak = at('Speak your question');
final talk = at('Voice mode');
final field = tester.getRect(find.byType(AssistantField));
expect(field.right - talk.right, lessThan(16), reason: 'at the right');
expect(speak.right, lessThan(talk.left), reason: 'the microphone before');
expect(field.right - speak.right, lessThan(16), reason: 'at the right');
await openPicker(tester);
await tester.tap(find.text('Claude Opus, thinking longer').last);
@ -379,7 +373,6 @@ void main() {
expect(find.text('Ask Claude Opus, thinking longer…'), findsOneWidget);
expect(at('Conversations'), history);
expect(at('Speak your question'), speak);
expect(at('Voice mode'), talk);
for (final icon in [
LucideIcons.plus,
@ -1559,129 +1552,4 @@ void main() {
expect(find.text('Was not shown an image'), findsOneWidget);
});
});
group("voice mode, as Claude's (D68)", () {
/// Opens voice mode from the field.
Future<void> talk(WidgetTester tester) async {
await tester.tap(
find.descendant(
of: find.byType(AssistantField),
matching: find.byTooltip('Voice mode'),
),
);
await tester.pumpAndSettle();
}
/// Says [words] and stops, as the recogniser does after a pause, and
/// lets the answer come.
Future<void> say(WidgetTester tester, String words) async {
speech.say(words);
await tester.pump();
speech.finish();
await tester.pump(const Duration(seconds: 1));
await tester.pumpAndSettle();
}
testWidgets('the field opens it over the whole screen, no bar beneath; '
'it listens, asks what was said, reads the answer aloud without its '
'marks, and listens again; ending it keeps the conversation', (
tester,
) async {
await showApp(tester);
await talk(tester);
expect(find.text("Let's talk"), findsOneWidget);
expect(find.text('Home'), findsNothing, reason: 'no navigation bar');
expect(speech.listening, isTrue);
expect(speech.languages.single, 'en-US');
assistants.answers.add(_says('Because it **joins** clauses.'));
speech.say('Why て-form?');
await tester.pump();
expect(find.text('Why て-form?'), findsOneWidget, reason: 'as heard');
speech.finish();
await tester.pump(const Duration(seconds: 1));
await tester.pumpAndSettle();
expect(_asked(assistants.asked.single.$2), startsWith('Why て-form?'));
expect(voice.spoken.single, ('Because it joins clauses.', 'en-US'));
expect(
find.text('Because it **joins** clauses.', findRichText: true),
findsNothing,
);
expect(speech.listening, isTrue, reason: 'listening again');
await tester.tap(find.byTooltip('End voice mode'));
await tester.pumpAndSettle();
expect(find.text("Let's talk"), findsNothing);
expect(speech.listening, isFalse);
expect(kept.written, isNotEmpty, reason: 'kept, as any conversation');
});
testWidgets('a second question goes on the same conversation', (
tester,
) async {
await showApp(tester);
await talk(tester);
assistants.answers
..add(_says('It joins.'))
..add(_says('As in 食べて.'));
await say(tester, 'Why て-form?');
await say(tester, 'An example?');
final second = assistants.asked.last.$2;
expect(_asked(second), startsWith('An example?'));
expect(second.messages, hasLength(3), reason: 'the first turn before');
});
testWidgets('with the speaker off, answers are shown, not read, and it '
'listens again at once', (tester) async {
await showApp(tester);
await talk(tester);
await tester.tap(find.byTooltip("Don't read answers aloud"));
await tester.pumpAndSettle();
assistants.answers.add(_says('Because it joins clauses.'));
await say(tester, 'Why て-form?');
expect(voice.spoken, isEmpty);
expect(
find.text('Because it joins clauses.', findRichText: true),
findsOneWidget,
);
expect(speech.listening, isTrue);
expect(find.byTooltip('Read answers aloud'), findsOneWidget);
});
testWidgets('the microphone stops the reading and listens at once; '
'nothing heard, it waits for the microphone', (tester) async {
voice.holds = true;
await showApp(tester);
await talk(tester);
assistants.answers.add(_says('A long answer.'));
await say(tester, 'Why?');
expect(voice.speaking, isTrue);
expect(speech.listening, isFalse);
await tester.tap(find.byTooltip('Talk'));
await tester.pumpAndSettle();
expect(voice.speaking, isFalse);
expect(speech.listening, isTrue);
speech.finish();
await tester.pumpAndSettle();
expect(speech.listening, isFalse, reason: 'nothing heard');
expect(find.text('Tap the microphone to talk'), findsOneWidget);
expect(assistants.asked, hasLength(1));
});
testWidgets('only for an assistant, and only where the device listens', (
tester,
) async {
speech.supported = false;
await showApp(tester);
expect(find.byTooltip('Voice mode'), findsNothing);
});
});
}

View file

@ -1,30 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/data/speech_output.dart';
void main() {
test('an answer is read without its marks, a link as its words, a '
"list's items as lines (ADR 0021, D68)", () {
expect(
speakableText(
'## Why て-form?\n\n'
'It **joins** clauses, as `食べて` does.\n\n'
'- First, *this*.\n'
'1. Then [that one](https://example.org).\n'
'> A quote.',
),
'Why て-form?\n\n'
'It joins clauses, as 食べて does.\n\n'
'First, this.\n'
'Then that one.\n'
'A quote.',
);
});
test('a table reads as its cells, a code block as its code', () {
expect(
speakableText('| a | b |\n|---|---|\n| 1 | 2 |'),
contains('1, 2'),
);
expect(speakableText('```dart\nprint(1);\n```'), 'print(1);');
});
}