feat(learn): the assistant's field spans the screen, its buttons boxed and still as who answers changes
Following the developer's review of 3 Nov 2026 (ADR 0021, D67): - Unfolded in the Library, the field spans the screen between its margins, as on Home and a studyset: it fell short on the right, by the padding around the two folded buttons, which now stands only around those. - Who answers stands right before the buttons at the right, which no longer shift as its name changes (Claude Haiku, Claude Opus): it fills the space left, aligned to its end, rather than a share of it. - Every button of the field and of a conversation's follow-up is boxed, a tile a shade above the field with rounded corners, as Claude's apps have them; who answers too. - On a studyset with a licence, the unfolded field takes its usual place over the licence's line rather than squeezing in above it; folded, its row still keeps clear of the line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
55fc245859
commit
96b1a0fc64
4 changed files with 209 additions and 64 deletions
|
|
@ -317,48 +317,57 @@ class _AssistantFieldState() extends State<AssistantField> {
|
|||
const shape = RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(24)),
|
||||
);
|
||||
// Who answers stands right before the buttons at the right, which stay
|
||||
// where they are whatever its name (the developer's review of 3 Nov
|
||||
// 2026: they shifted as it changed).
|
||||
const gap = SizedBox(width: 6);
|
||||
final controls = Row(
|
||||
children: [
|
||||
if (widget.onClose case final close?)
|
||||
if (widget.onClose case final close?) ...[
|
||||
IconButton(
|
||||
tooltip: l10n.assistantClose,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(LucideIcons.x),
|
||||
style: boxedButtonStyle(context),
|
||||
icon: const Icon(LucideIcons.x, size: 20),
|
||||
onPressed: close,
|
||||
),
|
||||
gap,
|
||||
],
|
||||
if (answerer case AssistantAnswerer(:final choice))
|
||||
AttachButton(
|
||||
images: choice.images,
|
||||
assistant: choice.name,
|
||||
onAttached: (part) => setState(() => _attached.add(part)),
|
||||
),
|
||||
const Spacer(),
|
||||
Flexible(
|
||||
flex: 4,
|
||||
child: _Picker(
|
||||
answerers: answerers,
|
||||
current: answerer,
|
||||
effort: switch (answerer) {
|
||||
AssistantAnswerer(:final choice)
|
||||
when choice.efforts.isNotEmpty && choice.effort != null =>
|
||||
(
|
||||
efforts: choice.efforts,
|
||||
standard: choice.effort!,
|
||||
chosen: effortFor(choice, askEfforts),
|
||||
),
|
||||
_ => null,
|
||||
},
|
||||
onPicked: (picked) => setState(() => _picked = picked.key),
|
||||
onEffort: (effort) => context.read<DeviceSettingsBloc>().add(
|
||||
DeviceSettingsEventEffortChosen(answerer.key, effort),
|
||||
gap,
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: _Picker(
|
||||
answerers: answerers,
|
||||
current: answerer,
|
||||
effort: switch (answerer) {
|
||||
AssistantAnswerer(:final choice)
|
||||
when choice.efforts.isNotEmpty && choice.effort != null =>
|
||||
(
|
||||
efforts: choice.efforts,
|
||||
standard: choice.effort!,
|
||||
chosen: effortFor(choice, askEfforts),
|
||||
),
|
||||
_ => null,
|
||||
},
|
||||
onPicked: (picked) => setState(() => _picked = picked.key),
|
||||
onEffort: (effort) => context.read<DeviceSettingsBloc>().add(
|
||||
DeviceSettingsEventEffortChosen(answerer.key, effort),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (answerers.any((each) => each is AssistantAnswerer))
|
||||
if (answerers.any((each) => each is AssistantAnswerer)) ...[
|
||||
gap,
|
||||
IconButton(
|
||||
tooltip: l10n.assistantHistory,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(LucideIcons.history),
|
||||
style: boxedButtonStyle(context),
|
||||
icon: const Icon(LucideIcons.history, size: 20),
|
||||
onPressed: () => unawaited(
|
||||
showConversationHistory(
|
||||
context,
|
||||
|
|
@ -366,6 +375,8 @@ class _AssistantFieldState() extends State<AssistantField> {
|
|||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
gap,
|
||||
SpeakOrSend(
|
||||
controller: _question,
|
||||
canSend: () =>
|
||||
|
|
@ -389,7 +400,7 @@ class _AssistantFieldState() extends State<AssistantField> {
|
|||
color: theme.colorScheme.surfaceContainerHigh,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 4, 6, 6),
|
||||
padding: const EdgeInsets.fromLTRB(6, 4, 6, 6),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
|
@ -756,23 +767,30 @@ class const _Picker({
|
|||
],
|
||||
builder: (context, controller, _) => Tooltip(
|
||||
message: l10n.assistantChoose,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () =>
|
||||
controller.isOpen ? controller.close() : controller.open(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(child: label),
|
||||
const SizedBox(width: 2),
|
||||
Icon(
|
||||
LucideIcons.chevronDown,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
child: Material(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
controller.isOpen ? controller.close() : controller.open(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(child: label),
|
||||
const SizedBox(width: 2),
|
||||
Icon(
|
||||
LucideIcons.chevronDown,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -983,13 +1001,18 @@ class _AssistantButtonState() extends State<AssistantButton> {
|
|||
},
|
||||
);
|
||||
if (end != null) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
button,
|
||||
if (!_expanded) ...[const Spacer(), end],
|
||||
],
|
||||
);
|
||||
// Unfolded, the field alone, as wide as the screen less its margins
|
||||
// (the developer's review of 3 Nov 2026: it fell short on the
|
||||
// right); folded, the two buttons at the screen's margins.
|
||||
return _expanded
|
||||
? button
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [button, const Spacer(), end],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (beside == null || _expanded) {
|
||||
return button;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,20 @@ import 'package:material_ui/material_ui.dart';
|
|||
// What the field asking an assistant and a conversation's follow-up share
|
||||
// below their text, as Claude's apps have it (decided 2026-11-02, ADR 0021,
|
||||
// D67): + attaching files, and the microphone while nothing is written,
|
||||
// which gives way to the button that sends.
|
||||
// which gives way to the button that sends. Boxed as Claude's are, each on
|
||||
// a tile of its own (the developer's review of 3 Nov 2026).
|
||||
|
||||
/// The look of a question's buttons, as Claude's apps box theirs: a tile a
|
||||
/// shade above the field's, its corners rounded.
|
||||
ButtonStyle boxedButtonStyle(BuildContext context) => IconButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
fixedSize: const Size.square(40),
|
||||
minimumSize: const Size.square(40),
|
||||
padding: EdgeInsets.zero,
|
||||
);
|
||||
|
||||
/// The + below a question's field: picks files to go along, read on the
|
||||
/// device ([AssistantAttachments]), images only where the assistant
|
||||
|
|
@ -61,8 +74,8 @@ class const AttachButton({
|
|||
final l10n = AppLocalizations.of(context);
|
||||
return IconButton(
|
||||
tooltip: images ? l10n.assistantAttach : l10n.assistantAttachFile,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
style: boxedButtonStyle(context),
|
||||
icon: const Icon(LucideIcons.plus, size: 20),
|
||||
onPressed: () => unawaited(_attach(context)),
|
||||
);
|
||||
}
|
||||
|
|
@ -205,10 +218,11 @@ class _SpeakOrSendState() extends State<SpeakOrSend> {
|
|||
children: [
|
||||
IconButton(
|
||||
tooltip: l10n.assistantSpeak,
|
||||
icon: const Icon(LucideIcons.mic),
|
||||
style: boxedButtonStyle(context),
|
||||
icon: const Icon(LucideIcons.mic, size: 20),
|
||||
onPressed: () => unawaited(_listen()),
|
||||
),
|
||||
if (canSend) send,
|
||||
if (canSend) ...[const SizedBox(width: 6), send],
|
||||
],
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -183,12 +183,9 @@ class _LibraryState() extends State<_Library> {
|
|||
// Searching the web at the lower left, Create studyset at the lower
|
||||
// right (decided 2026-10-16, as Vaia has them).
|
||||
fab: switch ((asks, create)) {
|
||||
(true, final create?) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: AssistantButton(
|
||||
topic: const AssistantTopicLearning(),
|
||||
end: create,
|
||||
),
|
||||
(true, final create?) => AssistantButton(
|
||||
topic: const AssistantTopicLearning(),
|
||||
end: create,
|
||||
),
|
||||
(true, null) => const AssistantButton(
|
||||
topic: AssistantTopicLearning(),
|
||||
|
|
|
|||
|
|
@ -257,6 +257,13 @@ class const _StudysetView({
|
|||
}
|
||||
|
||||
class _StudysetViewState() extends State<_StudysetView> {
|
||||
/// Whether the assistant's button is unfolded into its field, which then
|
||||
/// takes its usual place over the licence's line rather than squeezing
|
||||
/// in above it (the developer's review of 3 Nov 2026).
|
||||
var _unfolded = false;
|
||||
|
||||
void _onUnfolded(bool unfolded) => setState(() => _unfolded = unfolded);
|
||||
|
||||
/// How far the floating bar rises above the licence line.
|
||||
static const _licenceClearance = 40.0;
|
||||
|
||||
|
|
@ -320,6 +327,7 @@ class _StudysetViewState() extends State<_StudysetView> {
|
|||
title: studyset.content.title,
|
||||
description: studyset.content.description,
|
||||
),
|
||||
onUnfolded: _onUnfolded,
|
||||
)
|
||||
: null)
|
||||
// Asking stands at the left of the bar's row, smaller (decided
|
||||
|
|
@ -333,6 +341,7 @@ class _StudysetViewState() extends State<_StudysetView> {
|
|||
),
|
||||
small: true,
|
||||
beside: _StudyBar(studyset: studyset, addMaterial: _addMaterial),
|
||||
onUnfolded: _onUnfolded,
|
||||
),
|
||||
builder: (context, fab) => TopGradient(
|
||||
color: color,
|
||||
|
|
@ -473,7 +482,7 @@ class _StudysetViewState() extends State<_StudysetView> {
|
|||
// Above the licence line, which it would cover otherwise.
|
||||
floatingActionButtonLocation: wide
|
||||
? FloatingActionButtonLocation.startFloat
|
||||
: LicenseFooter.shows(studyset.content)
|
||||
: !_unfolded && LicenseFooter.shows(studyset.content)
|
||||
? const _RaisedCenterFloat(_licenceClearance)
|
||||
: FloatingActionButtonLocation.centerFloat,
|
||||
body: Column(
|
||||
|
|
|
|||
Loading…
Reference in a new issue