feat(learn): the assistant's field laid out as Claude's, with files attached and questions spoken, and no Check (ADR 0021, D67)

The question's text stands above its buttons: + attaching files, who
answers as a chip (Effort now a submenu of it), the conversations kept,
and the microphone while nothing is written, which gives way to the
button sending. What goes along is one row above the text, opening a
sheet with how far the assistant looks and the parts to send. A
conversation's follow-up has the same + and microphone.

Files are read on the device: an image goes as itself to an assistant
that reads images, a PDF as its pages' text, a text file as its text,
each cut at 20,000 characters and labelled by its name after the
question.

Voice is the device's own speech recognition (speech_to_text: Android,
Apple, Windows; none on Linux), in the app's language, the microphone
asked for once tapped. The privacy page says that Android's recogniser
may send the sound to Google or the phone's maker.

The field's Check goes, with its LanguageTool sheet (amending ADR 0024);
the editors' underlining and Why? stay.

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-02 05:50:36 +01:00
commit 447e00c52a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 1843 additions and 1085 deletions

View file

@ -0,0 +1,51 @@
import 'package:flutter/foundation.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/data/speech_input.dart';
/// A [SpeechInput] the test speaks into: [say] hands words on, [finish]
/// ends the listen.
class FakeSpeechInput() implements SpeechInput {
@override
bool supported = true;
/// Whether the device can listen when asked: false as if the microphone
/// were refused.
bool listens = true;
/// The languages asked to listen in, in order.
final languages = <String?>[];
ValueChanged<String>? _onWords;
VoidCallback? _onDone;
/// Whether a listen is under way.
bool get listening => _onWords != null;
/// Hands on [words], as all heard so far.
void say(String words) => _onWords?.call(words);
/// Ends the listen, as the recogniser does after a pause.
void finish() {
final done = _onDone;
_onWords = null;
_onDone = null;
done?.call();
}
@override
Future<bool> listen({
required String? language,
required ValueChanged<String> onWords,
required VoidCallback onDone,
}) async {
languages.add(language);
if (!listens) {
return false;
}
_onWords = onWords;
_onDone = onDone;
return true;
}
@override
Future<void> stop() async => finish();
}

View file

@ -25,6 +25,7 @@ import 'package:halkyon_learn_flutter/src/feature/archive/model/import_archive.d
import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_files.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_lookups.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/data/conversation_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/data/speech_input.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/data/web_search.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_gateway.dart';
@ -105,6 +106,7 @@ import 'fake_publish_gateway.dart';
import 'fake_review_gateway.dart';
import 'fake_settings_gateway.dart';
import 'fake_sound_player.dart';
import 'fake_speech_input.dart';
import 'fake_studyset_gateway.dart';
import 'fake_tag_gateway.dart';
import 'fake_youtube_videos.dart';
@ -176,6 +178,7 @@ Dependencies testDependencies({
bool syncedBefore = true,
OpenPerson? openPerson,
WebSearch? webSearch,
SpeechInput? speech,
JellyfinGateway? jellyfinGateway,
JellyfinStore? jellyfinStore,
JellyfinSuggestion? jellyfinSuggestion,
@ -260,6 +263,7 @@ Dependencies testDependencies({
open: (url) async => true,
deviceSearch: (query) async => false,
),
speech: speech ?? FakeSpeechInput(),
jellyfin: JellyfinRepository(
(device) => jellyfinGateway ?? FakeJellyfinGateway(),
jellyfinStore ?? MemoryJellyfinStore(),