feat(learn): + opens a sheet as Claude's, Camera, Photos and Files, and what goes along in rounded rows
Following the developer's review of 3 Nov 2026 (ADR 0021, D67): - + below the assistant's field, and below a conversation's follow-up, opens "Add to your question" as Claude's "Add to session": three big tiles side by side, Camera (where the device takes photos) and Photos for an assistant reading images, and Files; below them, in the field, a row leading to what goes along, showing how far the assistant looks and how many parts are chosen. - Camera and Photos through image_picker (flutter.dev's own): the camera's own app and the system's photo picker, no permission asked on Android, a file dialog on the desktop, which has no Camera; photos made at most 2048 pixels a side, read on the device like any file attached. - What goes along is chosen in rounded rows, as Claude's Permission, in place of radio buttons and chips: how far it looks with a tick, private studysets with a switch, the parts each with a tick, a card's image with its thumbnail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
a7904bac05
commit
7be31dcbfc
3 changed files with 639 additions and 94 deletions
|
|
@ -33,6 +33,7 @@ import '../../support/fake_asset_server_gateway.dart';
|
|||
import '../../support/fake_card_gateway.dart';
|
||||
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_studyset_gateway.dart';
|
||||
import '../../support/memory_asset_file_cache.dart';
|
||||
|
|
@ -117,6 +118,7 @@ void main() {
|
|||
late FakeSettingsGateway settings;
|
||||
late FakeLlmClient llm;
|
||||
late FakeFilePicker picker;
|
||||
late FakePhotoPicker photos;
|
||||
late MemoryDeviceSettingsStore store;
|
||||
late List<Uri> searched;
|
||||
late FakeAssetGateway assetGateway;
|
||||
|
|
@ -143,6 +145,7 @@ void main() {
|
|||
settings = FakeSettingsGateway();
|
||||
llm = FakeLlmClient();
|
||||
picker = FakeFilePicker();
|
||||
photos = FakePhotoPicker();
|
||||
searched = [];
|
||||
assetGateway = FakeAssetGateway();
|
||||
assetFiles = MemoryAssetFileCache();
|
||||
|
|
@ -204,6 +207,7 @@ void main() {
|
|||
grammarGateway: grammar,
|
||||
grammarLanguages: grammarLanguages,
|
||||
files: picker,
|
||||
photos: photos,
|
||||
),
|
||||
location: location,
|
||||
);
|
||||
|
|
@ -270,6 +274,22 @@ void main() {
|
|||
await closeContext(tester);
|
||||
}
|
||||
|
||||
/// Opens + (with [tooltip]) and chooses [tile] in its sheet.
|
||||
Future<void> add(
|
||||
WidgetTester tester,
|
||||
String tile, {
|
||||
String tooltip = 'Attach a photo or file',
|
||||
Finder? within,
|
||||
}) async {
|
||||
final plus = find.byTooltip(tooltip);
|
||||
await tester.tap(
|
||||
within == null ? plus : find.descendant(of: within, matching: plus),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(tile));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Opens the menu of who answers, below the field.
|
||||
Future<void> openPicker(WidgetTester tester) async {
|
||||
await tester.tap(find.byTooltip('Who answers'));
|
||||
|
|
@ -1207,6 +1227,17 @@ void main() {
|
|||
|
||||
await tester.tap(find.byTooltip('Attach a photo or file'));
|
||||
await tester.pumpAndSettle();
|
||||
// As Claude's sheet has them (the developer's review of 3 Nov 2026).
|
||||
final tiles = [
|
||||
for (final tile in ['Camera', 'Photos', 'Files'])
|
||||
tester.getRect(find.text(tile)),
|
||||
];
|
||||
expect(tiles[0].center.dy, tiles[1].center.dy);
|
||||
expect(tiles[1].center.dy, tiles[2].center.dy);
|
||||
expect(tiles[0].left, lessThan(tiles[1].left));
|
||||
await tester.tap(find.text('Files'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Camera'), findsNothing, reason: 'the sheet closed');
|
||||
|
||||
expect(picker.asked.single, containsAll(['png', 'pdf', 'md']));
|
||||
expect(find.text('notes.md'), findsOneWidget);
|
||||
|
|
@ -1225,8 +1256,7 @@ void main() {
|
|||
await tester.pump(const Duration(seconds: 6));
|
||||
await tester.pumpAndSettle();
|
||||
picker.nextMany = [(name: 'extra.txt', bytes: bytesOf('Not this.'))];
|
||||
await tester.tap(find.byTooltip('Attach a photo or file'));
|
||||
await tester.pumpAndSettle();
|
||||
await add(tester, 'Files');
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.widgetWithText(InputChip, 'extra.txt'),
|
||||
|
|
@ -1259,6 +1289,44 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWidgets("+'s Camera takes a photo and Photos picks some, each "
|
||||
'shown above the question; a desktop has no Camera; what goes '
|
||||
"along is chosen from +'s sheet too", (tester) async {
|
||||
assistants.offer = AssistantsOffer(assistants: [_seeing]);
|
||||
await showApp(tester);
|
||||
photos
|
||||
..taken = (name: 'taken.jpg', bytes: bytesOf('a board'))
|
||||
..picked = [
|
||||
(name: 'one.jpg', bytes: bytesOf('one')),
|
||||
(name: 'two.png', bytes: bytesOf('two')),
|
||||
];
|
||||
|
||||
await add(tester, 'Camera');
|
||||
await add(tester, 'Photos');
|
||||
|
||||
expect(photos.asked, ['camera', 'photos']);
|
||||
for (final name in ['taken.jpg', 'one.jpg', 'two.png']) {
|
||||
expect(find.widgetWithText(InputChip, name), findsOneWidget);
|
||||
}
|
||||
|
||||
await tester.tap(find.byTooltip('Attach a photo or file'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('What goes along').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('How far it looks'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a desktop offers no Camera', (tester) async {
|
||||
assistants.offer = AssistantsOffer(assistants: [_seeing]);
|
||||
photos.hasCamera = false;
|
||||
await showApp(tester);
|
||||
await tester.tap(find.byTooltip('Attach a photo or file'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Camera'), findsNothing);
|
||||
expect(find.text('Photos'), findsOneWidget);
|
||||
expect(find.text('Files'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('an assistant reading no images is offered files alone, '
|
||||
'and says why a picture picked anyway stays behind', (tester) async {
|
||||
await showApp(tester);
|
||||
|
|
@ -1266,6 +1334,10 @@ void main() {
|
|||
|
||||
await tester.tap(find.byTooltip('Attach a file'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Camera'), findsNothing);
|
||||
expect(find.text('Photos'), findsNothing);
|
||||
await tester.tap(find.text('Files'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(picker.asked.single, isNot(contains('png')));
|
||||
expect(
|
||||
|
|
@ -1284,13 +1356,12 @@ void main() {
|
|||
await ask(tester, 'Why て-form?');
|
||||
|
||||
picker.next = (name: 'lines.txt', bytes: bytesOf('One.\nTwo.'));
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(BottomSheet),
|
||||
matching: find.byTooltip('Attach a file'),
|
||||
),
|
||||
await add(
|
||||
tester,
|
||||
'Files',
|
||||
tooltip: 'Attach a file',
|
||||
within: find.byType(BottomSheet),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.widgetWithText(InputChip, 'lines.txt'), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/data/photo_picker.dart';
|
||||
|
||||
/// A [PhotoPicker] that takes [taken] and picks [picked], noting each ask.
|
||||
class FakePhotoPicker() implements PhotoPicker {
|
||||
@override
|
||||
bool hasCamera = true;
|
||||
|
||||
/// What the camera takes next; null as if the person took none.
|
||||
PickedFile? taken;
|
||||
|
||||
/// What the photos picked next are.
|
||||
List<PickedFile> picked = const [];
|
||||
|
||||
/// How often the camera and the photos were asked, in order.
|
||||
final asked = <String>[];
|
||||
|
||||
@override
|
||||
Future<PickedFile?> take() async {
|
||||
asked.add('camera');
|
||||
return taken;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<PickedFile>> pick() async {
|
||||
asked.add('photos');
|
||||
return picked;
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,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/photo_picker.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';
|
||||
|
|
@ -95,6 +96,7 @@ import 'fake_note_gateway.dart';
|
|||
import 'fake_path_answer_gateway.dart';
|
||||
import 'fake_pdf_engine.dart';
|
||||
import 'fake_pdf_source_gateway.dart';
|
||||
import 'fake_photo_picker.dart';
|
||||
import 'fake_pomodoro_platform.dart';
|
||||
import 'fake_publish_gateway.dart';
|
||||
import 'fake_review_gateway.dart';
|
||||
|
|
@ -172,6 +174,7 @@ Dependencies testDependencies({
|
|||
OpenPerson? openPerson,
|
||||
WebSearch? webSearch,
|
||||
SpeechInput? speech,
|
||||
PhotoPicker? photos,
|
||||
JellyfinGateway? jellyfinGateway,
|
||||
JellyfinStore? jellyfinStore,
|
||||
JellyfinSuggestion? jellyfinSuggestion,
|
||||
|
|
@ -264,6 +267,7 @@ Dependencies testDependencies({
|
|||
deviceSearch: (query) async => false,
|
||||
),
|
||||
speech: speech ?? FakeSpeechInput(),
|
||||
photos: photos ?? FakePhotoPicker(),
|
||||
jellyfin: JellyfinRepository(
|
||||
(device) => jellyfinGateway ?? FakeJellyfinGateway(),
|
||||
jellyfinStore ?? MemoryJellyfinStore(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue