feat(learn): the assistant's conversation a dialog on the desktop and in expanded windows (Todo's ADR 0033, D11)

- halkyon_ui's sheetShowsAsDialog says whether showAdaptiveSheet shows
  a dialog here, for a sheet of its own that can't go through it.
- Learn's conversation, a half-height sheet dragged up, asks it: where
  sheets are dialogs it is a dialog up to 720 wide and as tall as the
  window, with no handle to drag.

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-01 18:20:58 +01:00
commit 4898d0a04a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -25,7 +25,9 @@ import 'package:material_ui/material_ui.dart';
/// Opens a conversation with [choice] in [scope] for [task] in a sheet over
/// the screen it is asked on (ADR 0021, D47): at half height, dragged up to
/// the full, with follow-up questions at its foot. Asks [question] with
/// the full, with follow-up questions at its foot; where halkyon_ui's
/// sheets are dialogs (from expanded windows on, and on the desktop), a
/// dialog as tall as the window allows. Asks [question] with
/// [chosen] at once where given, a new conversation called [title] where
/// given, or continues the kept conversation [conversationId]. [choice]
/// null shows a kept conversation whose assistant has gone, which can't go
@ -66,15 +68,65 @@ Future<void> showConversation(
await cubit.ask(question, chosen);
}
}());
// Closing the sheet closes the cubit, which stops an answer on its way.
Widget provided(Widget child) =>
BlocProvider(create: (context) => cubit, child: child);
// A dialog where halkyon_ui's sheets are (Todo's ADR 0033, D11): from
// expanded windows on, and on the desktop at every width.
if (sheetShowsAsDialog(context)) {
return showDialog<void>(
context: context,
builder: (context) => provided(
_ConversationDialog(name: name, scope: scope, choice: choice),
),
);
}
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (context) => BlocProvider(
// Closing the sheet closes the cubit, which stops an answer on its
// way.
create: (context) => cubit,
child: _ConversationSheet(name: name, scope: scope, choice: choice),
builder: (context) =>
provided(_ConversationSheet(name: name, scope: scope, choice: choice)),
);
}
/// The conversation as a dialog, as tall as the window lets it, up to a
/// reading width: nothing to drag.
class const _ConversationDialog({
required final String name,
required final AssistantScope scope,
required final AssistantChoice? choice,
}) extends StatefulWidget {
@override
State<_ConversationDialog> createState() => _ConversationDialogState();
}
class _ConversationDialogState() extends State<_ConversationDialog> {
final _controller = ScrollController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Dialog(
insetPadding: context.windowSize.isAtLeastMedium
? const EdgeInsets.symmetric(horizontal: 40, vertical: 32)
: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
clipBehavior: Clip.antiAlias,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: SizedBox.expand(
child: _ConversationView(
controller: _controller,
name: widget.name,
scope: widget.scope,
choice: widget.choice,
handle: false,
),
),
),
);
}
@ -99,6 +151,7 @@ class const _ConversationSheet({
name: name,
scope: scope,
choice: choice,
handle: true,
),
),
);
@ -112,6 +165,9 @@ class const _ConversationView({
/// Who answers; null for an assistant no longer offered, which can't go
/// on.
required final AssistantChoice? choice,
/// Whether a handle to drag it by sits on top: in a bottom sheet.
required final bool handle,
}) extends StatelessWidget {
/// How close to its end the conversation counts as followed: closer, it
/// follows the answer as it grows.
@ -141,7 +197,7 @@ class const _ConversationView({
controller: controller,
slivers: [
PinnedHeaderSliver(
child: _Header(name: name, scope: scope),
child: _Header(name: name, scope: scope, handle: handle),
),
if (!state.keeps)
SliverToBoxAdapter(
@ -202,11 +258,12 @@ class const _ConversationView({
}
}
/// The handle to drag the sheet by, who answers and how far it looks, and
/// Close.
/// The handle to drag the sheet by (where [handle]), who answers and how
/// far it looks, and Close.
class const _Header({
required final String name,
required final AssistantScope scope,
required final bool handle,
}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
@ -217,19 +274,20 @@ class const _Header({
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 12),
child: Container(
width: 32,
height: 4,
decoration: BoxDecoration(
color: theme.colorScheme.onSurfaceVariant.withValues(
alpha: 0.4,
if (handle)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Container(
width: 32,
height: 4,
decoration: BoxDecoration(
color: theme.colorScheme.onSurfaceVariant.withValues(
alpha: 0.4,
),
borderRadius: BorderRadius.circular(2),
),
borderRadius: BorderRadius.circular(2),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 4, 4),
child: Row(