fix(assistant): chips show their text, and the field sits on the card
All checks were successful
CI / check (push) Successful in 14m32s
CI / deploy (push) Successful in 1m15s

The chips above the assistant's field cut their text at 24 characters
and scrolled sideways out of view, so they helped little. They now flow
onto as many lines as they need (about three before they scroll), each
showing as much of its text as its line holds, the whole of it on
hovering or a long press.

In a study session the button and its field now belong to the card: the
field unfolds across the card's width, inside its edges, where it was
anchored to the window's right edge and overhung the card on wide
windows. The send button keeps clear of the field's ring, more so in the
corner the chips' rounded box leaves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-10 15:03:08 +02:00
commit 8ab67b8a27
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 92 additions and 84 deletions

View file

@ -215,7 +215,11 @@ class _AssistantFieldState() extends State<AssistantField> {
color: theme.colorScheme.surfaceContainerHigh,
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
// The send button clear of the ring, more so in the corner the
// chips' rounded box leaves.
padding: offered.isEmpty
? const EdgeInsets.fromLTRB(4, 0, 6, 0)
: const EdgeInsets.fromLTRB(4, 0, 8, 8),
child: offered.isEmpty
? row
: Column(
@ -276,60 +280,72 @@ class const _TargetSwitch({
}
}
/// The parts that can go along, as chips in a row that scrolls sideways,
/// each with the start of its text.
/// The parts that can go along, as chips flowing onto as many lines as
/// they need, each showing as much of its text as its line holds; the whole
/// of it on hovering or a long press.
class const _Parts({
required final List<AssistantPart> parts,
required final Set<AssistantPart> chosen,
required final ValueChanged<AssistantPart> onToggle,
}) extends StatelessWidget {
/// How much of a part's text its chip shows.
static const _preview = 24;
/// How tall the chips grow before they scroll: about three lines.
static const _maxHeight = 132.0;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
String preview(String text) {
final line = text.replaceAll(RegExp(r'\s+'), ' ').trim();
return line.length <= _preview
? line
: '${line.substring(0, _preview - 1)}…';
}
String line(String text) => text.replaceAll(RegExp(r'\s+'), ' ').trim();
return Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (final part in parts) ...[
FilterChip(
visualDensity: VisualDensity.compact,
label: Text(switch (part.kind) {
AssistantPartKind.front => l10n.assistantLineFront(
preview(part.text),
padding: const EdgeInsets.fromLTRB(8, 10, 8, 2),
// At most about three lines of chips; more scroll.
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: _maxHeight),
child: SingleChildScrollView(
child: LayoutBuilder(
builder: (context, constraints) => Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final part in parts)
Tooltip(
message: part.sent,
waitDuration: const Duration(milliseconds: 400),
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth,
),
child: FilterChip(
visualDensity: VisualDensity.compact,
label: Text(
switch (part.kind) {
AssistantPartKind.front => l10n.assistantLineFront(
line(part.text),
),
AssistantPartKind.back => l10n.assistantLineBack(
line(part.text),
),
AssistantPartKind.hint => l10n.assistantLineHint(
part.number ?? 1,
line(part.text),
),
AssistantPartKind.explanation =>
l10n.assistantLineExplanation(line(part.text)),
AssistantPartKind.studyset =>
l10n.assistantChipStudyset(line(part.text)),
AssistantPartKind.description =>
l10n.assistantLineDescription(line(part.text)),
},
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
selected: chosen.contains(part),
onSelected: (_) => onToggle(part),
),
),
),
AssistantPartKind.back => l10n.assistantLineBack(
preview(part.text),
),
AssistantPartKind.hint => l10n.assistantLineHint(
part.number ?? 1,
preview(part.text),
),
AssistantPartKind.explanation =>
l10n.assistantLineExplanation(preview(part.text)),
AssistantPartKind.studyset => l10n.assistantChipStudyset(
preview(part.text),
),
AssistantPartKind.description =>
l10n.assistantLineDescription(preview(part.text)),
}),
selected: chosen.contains(part),
onSelected: (_) => onToggle(part),
),
const SizedBox(width: 8),
],
],
],
),
),
),
),
);

View file

@ -1,5 +1,3 @@
import 'dart:math' as math;
import 'package:clock/clock.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -331,8 +329,9 @@ class const _Showing(
/// The studyset's title, for questions to Claude about the card.
required final String title,
}) extends StatelessWidget {
/// The widest the field to ask Claude unfolds.
static const _fieldMaxWidth = 640.0;
/// How far inside the card's edges the assistant's button floats, and
/// its field unfolds.
static const _fieldInset = 8.0;
@override
Widget build(BuildContext context) {
@ -621,46 +620,39 @@ class const _Showing(
),
),
Expanded(
child: Stack(
children: [
Positioned.fill(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 1100,
),
child: Padding(
padding: const EdgeInsets.fromLTRB(
16,
12,
16,
0,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1100),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: LayoutBuilder(
builder: (context, constraints) => Stack(
fit: StackFit.expand,
children: [
body,
// Asking about the card, in the card's
// lower right corner, above the ratings.
// Unfolded, the field spans the card, the
// keyboard pushing it up.
Positioned(
right: _fieldInset,
bottom: _fieldInset + 16,
child: AssistantButton(
topic: AssistantTopicCard(
studysetTitle: title,
card: card.content,
),
small: true,
expandedWidth:
constraints.maxWidth -
2 * _fieldInset,
),
),
child: body,
),
],
),
),
),
// Asking Claude about the card, in the card's lower
// right corner, above the ratings. Unfolded, the field
// spans the card, the keyboard pushing it up.
Positioned(
right: 28,
bottom: 24,
child: AssistantButton(
topic: AssistantTopicCard(
studysetTitle: title,
card: card.content,
),
small: true,
// Within the card's sides.
expandedWidth: math.min(
MediaQuery.sizeOf(context).width - 56,
_fieldMaxWidth,
),
),
),
],
),
),
),
if (ratedBelow)