feat(learn): choosing how much an assistant thinks, and the assistants by provider in the field
Halkyon Learn's ADR 0021, D60 and D63: - A chip after the scope's offers the efforts the operator allows for the assistant asked, its own marked; the one picked is kept per assistant on the device (DeviceSettings.askEfforts) and asked at, also by a conversation continued from the history. One the operator no longer allows counts as none. - Every question asks for its effort's ceiling on output, as the thinking counts towards it (8,192 to 65,536 tokens; 16,384 at the model's own), where it asked for 4,096; the flashcards proposed, Explain and Why? ask at the assistant's own effort, a person's own key at none and 4,096. - The field's picker lists the assistants under their providers' names, each in its colour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
cc87120bc8
commit
8b5bdfed8c
15 changed files with 474 additions and 50 deletions
|
|
@ -1829,6 +1829,8 @@
|
|||
"assistantHintAsk": "{name} fragen…",
|
||||
"assistantSendAsk": "Fragen",
|
||||
"assistantScope": "Was er nachschlagen darf",
|
||||
"assistantEffortTooltip": "Denkaufwand: wie viel er nachdenkt, bevor er antwortet",
|
||||
"assistantEffortDefault": "{effort} (Standard)",
|
||||
"assistantScopeTicked": "Nur was ich auswähle",
|
||||
"assistantScopeStudyset": "Dieses Lernset",
|
||||
"assistantScopeTree": "Ganzer Baum",
|
||||
|
|
|
|||
|
|
@ -2149,6 +2149,10 @@
|
|||
"assistantSendAsk": "Ask",
|
||||
"assistantScope": "What it may look up",
|
||||
"@assistantScope": {"description": "Tooltip of the scope chip: how far an assistant may look beyond the parts chosen."},
|
||||
"assistantEffortTooltip": "Effort: how much it thinks before it answers",
|
||||
"@assistantEffortTooltip": {"description": "Tooltip of the effort chip in the field (ADR 0021, D63): more effort thinks longer and uses more of the budget."},
|
||||
"assistantEffortDefault": "{effort} (default)",
|
||||
"@assistantEffortDefault": {"description": "The effort the operator set up for an assistant, among those to choose, by its name.", "placeholders": {"effort": {"type": "String"}}},
|
||||
"assistantScopeTicked": "Only what I choose",
|
||||
"assistantScopeStudyset": "This studyset",
|
||||
"assistantScopeTree": "Its tree",
|
||||
|
|
|
|||
|
|
@ -10791,6 +10791,18 @@ abstract class AppLocalizations {
|
|||
/// **'What it may look up'**
|
||||
String get assistantScope;
|
||||
|
||||
/// Tooltip of the effort chip in the field (ADR 0021, D63): more effort thinks longer and uses more of the budget.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Effort: how much it thinks before it answers'**
|
||||
String get assistantEffortTooltip;
|
||||
|
||||
/// The effort the operator set up for an assistant, among those to choose, by its name.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{effort} (default)'**
|
||||
String assistantEffortDefault(String effort);
|
||||
|
||||
/// No description provided for @assistantScopeTicked.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -6758,6 +6758,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get assistantScope => 'Was er nachschlagen darf';
|
||||
|
||||
@override
|
||||
String get assistantEffortTooltip =>
|
||||
'Denkaufwand: wie viel er nachdenkt, bevor er antwortet';
|
||||
|
||||
@override
|
||||
String assistantEffortDefault(String effort) {
|
||||
return '$effort (Standard)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantScopeTicked => 'Nur was ich auswähle';
|
||||
|
||||
|
|
|
|||
|
|
@ -6694,6 +6694,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get assistantScope => 'What it may look up';
|
||||
|
||||
@override
|
||||
String get assistantEffortTooltip =>
|
||||
'Effort: how much it thinks before it answers';
|
||||
|
||||
@override
|
||||
String assistantEffortDefault(String effort) {
|
||||
return '$effort (default)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get assistantScopeTicked => 'Only what I choose';
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/data/device_settings_store.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
|
||||
/// The device's own settings, starting at what it kept: a choice applies at
|
||||
/// once and is kept for the next start.
|
||||
|
|
@ -39,6 +40,10 @@ class DeviceSettingsBloc(
|
|||
on<DeviceSettingsEventAskedWith>(
|
||||
(event, emit) => _apply(emit, state.withAskWith(event.key)),
|
||||
);
|
||||
on<DeviceSettingsEventEffortChosen>(
|
||||
(event, emit) =>
|
||||
_apply(emit, state.withAskEffort(event.key, event.effort)),
|
||||
);
|
||||
on<DeviceSettingsEventCheckGrammarChosen>(
|
||||
(event, emit) => _apply(emit, state.withCheckGrammar(event.on)),
|
||||
);
|
||||
|
|
@ -126,6 +131,16 @@ final class const DeviceSettingsEventAskedWith(final String key)
|
|||
List<Object?> get props => [key];
|
||||
}
|
||||
|
||||
/// The person chose to ask the assistant [key] (`DeviceSettings.askWith`)
|
||||
/// at [effort].
|
||||
final class const DeviceSettingsEventEffortChosen(
|
||||
final String key,
|
||||
final LlmEffort effort,
|
||||
) extends DeviceSettingsEvent {
|
||||
@override
|
||||
List<Object?> get props => [key, effort];
|
||||
}
|
||||
|
||||
/// The person turned checking writing as it is written [on] or off.
|
||||
final class const DeviceSettingsEventCheckGrammarChosen(final bool on)
|
||||
extends DeviceSettingsEvent {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
|||
|
||||
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
|
||||
/// Where a device keeps its own settings.
|
||||
abstract interface class DeviceSettingsStore() {
|
||||
|
|
@ -70,6 +71,11 @@ class FileDeviceSettingsStore(final File _file) implements DeviceSettingsStore {
|
|||
final String key => key,
|
||||
_ => null,
|
||||
},
|
||||
askEfforts: {
|
||||
if (json['askEfforts'] case final Map<String, Object?> efforts)
|
||||
for (final MapEntry(:key, :value) in efforts.entries)
|
||||
if (value case final String name) key: ?LlmEffort.named(name),
|
||||
},
|
||||
checkGrammar: json['checkGrammar'] != false,
|
||||
);
|
||||
} on FormatException catch (error) {
|
||||
|
|
@ -94,6 +100,10 @@ class FileDeviceSettingsStore(final File _file) implements DeviceSettingsStore {
|
|||
'customSearchUrl': ?settings.customSearchUrl,
|
||||
'pdfSidebarShare': ?settings.pdfSidebarShare,
|
||||
'askWith': ?settings.askWith,
|
||||
'askEfforts': {
|
||||
for (final MapEntry(:key, :value) in settings.askEfforts.entries)
|
||||
key: value.key,
|
||||
},
|
||||
'checkGrammar': settings.checkGrammar,
|
||||
}),
|
||||
flush: true,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
|
||||
/// How the app looks: as the device does, or always light or dark.
|
||||
enum Appearance() {
|
||||
|
|
@ -12,8 +13,8 @@ enum Appearance() {
|
|||
/// app looks, the language it speaks, the style it cites sources in,
|
||||
/// whether studying shows the Pomodoro timer, which studysets' card
|
||||
/// browsers show one card at a time, whether and how the person searches
|
||||
/// the web from the app, who answered the field last, and whether writing
|
||||
/// is checked as it is written.
|
||||
/// the web from the app, who answered the field last and at which effort,
|
||||
/// and whether writing is checked as it is written.
|
||||
/// Unlike the person's settings, they don't follow anyone to other devices,
|
||||
/// and they apply before anyone is signed in.
|
||||
final class const DeviceSettings({
|
||||
|
|
@ -58,6 +59,11 @@ final class const DeviceSettings({
|
|||
/// null before the first.
|
||||
final String? askWith,
|
||||
|
||||
/// The effort each assistant was last asked at from the field, by its
|
||||
/// key as [askWith] names it (ADR 0021, D63); one the operator no longer
|
||||
/// allows counts as none chosen.
|
||||
final Map<String, LlmEffort> askEfforts = const {},
|
||||
|
||||
/// Whether the editors underline what the instance's LanguageTool finds
|
||||
/// as the person writes (ADR 0024, Settings › Writing): on unless turned
|
||||
/// off; nothing is checked where the instance checks nothing.
|
||||
|
|
@ -103,6 +109,10 @@ final class const DeviceSettings({
|
|||
/// These settings with [key] having answered last ([askWith]).
|
||||
DeviceSettings withAskWith(String key) => _copy(askWith: key);
|
||||
|
||||
/// These settings with the assistant [key] asked at [effort].
|
||||
DeviceSettings withAskEffort(String key, LlmEffort effort) =>
|
||||
_copy(askEfforts: {...askEfforts, key: effort});
|
||||
|
||||
/// These settings with writing checked as it is written [on] or off.
|
||||
DeviceSettings withCheckGrammar(bool on) => _copy(checkGrammar: on);
|
||||
|
||||
|
|
@ -117,6 +127,7 @@ final class const DeviceSettings({
|
|||
Object? customSearchUrl = _keep,
|
||||
double? pdfSidebarShare,
|
||||
Object? askWith = _keep,
|
||||
Map<String, LlmEffort>? askEfforts,
|
||||
bool? checkGrammar,
|
||||
}) => DeviceSettings(
|
||||
appearance: appearance ?? this.appearance,
|
||||
|
|
@ -133,6 +144,7 @@ final class const DeviceSettings({
|
|||
: customSearchUrl as String?,
|
||||
pdfSidebarShare: pdfSidebarShare ?? this.pdfSidebarShare,
|
||||
askWith: identical(askWith, _keep) ? this.askWith : askWith as String?,
|
||||
askEfforts: askEfforts ?? this.askEfforts,
|
||||
checkGrammar: checkGrammar ?? this.checkGrammar,
|
||||
);
|
||||
|
||||
|
|
@ -148,6 +160,7 @@ final class const DeviceSettings({
|
|||
customSearchUrl,
|
||||
pdfSidebarShare,
|
||||
askWith,
|
||||
askEfforts,
|
||||
checkGrammar,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,10 +64,6 @@ class CardProposalsCubit({
|
|||
}) extends Cubit<CardProposalsState> {
|
||||
this : super(const CardProposalsState());
|
||||
|
||||
/// How long the answer may be, in tokens: room for every card of a
|
||||
/// proposal, as a conversation's answer has.
|
||||
static const maxOutputTokens = 4096;
|
||||
|
||||
StreamSubscription<LlmEvent>? _proposing;
|
||||
Completer<void>? _proposed;
|
||||
|
||||
|
|
@ -94,7 +90,9 @@ class CardProposalsCubit({
|
|||
messages: [
|
||||
LlmMessage.user([LlmText(text)]),
|
||||
],
|
||||
maxOutputTokens: maxOutputTokens,
|
||||
// At the assistant's own effort, with room for its thinking
|
||||
// and every card of a proposal (D63).
|
||||
maxOutputTokens: _choice.outputCeilingAt(null),
|
||||
system: proposalPrompt(),
|
||||
tools: const [proposeCardsTool],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -121,6 +121,10 @@ class ConversationCubit({
|
|||
/// The language the app speaks to the person, as a BCP 47 tag, which a
|
||||
/// check is answered in.
|
||||
final String? _answerIn,
|
||||
|
||||
/// The effort every question asks at, one [_choice] offers (D63); null
|
||||
/// for the assistant's own.
|
||||
final LlmEffort? _effort,
|
||||
}) extends Cubit<ConversationState> {
|
||||
this
|
||||
: super(
|
||||
|
|
@ -130,10 +134,6 @@ class ConversationCubit({
|
|||
),
|
||||
);
|
||||
|
||||
/// How long an answer may be, in tokens: a long explanation, not an
|
||||
/// essay, so a limit isn't used up by one question.
|
||||
static const maxOutputTokens = 4096;
|
||||
|
||||
/// How many rounds of lookups a question gets: the round after is told
|
||||
/// to answer with what it found, and lookups asked for after that end
|
||||
/// the turn.
|
||||
|
|
@ -248,9 +248,12 @@ class ConversationCubit({
|
|||
choice,
|
||||
LlmRequest(
|
||||
messages: [...history, ...turn],
|
||||
maxOutputTokens: maxOutputTokens,
|
||||
// Room for the thinking its effort allows, which counts
|
||||
// towards it, and the answer after.
|
||||
maxOutputTokens: choice.outputCeilingAt(_effort),
|
||||
system: system,
|
||||
tools: tools,
|
||||
effort: _effort,
|
||||
),
|
||||
turn,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/bloc/conversation_cubit.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_conversation.dart';
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
|
||||
/// Who answers a question asked in the field: the web search, or an
|
||||
/// assistant (ADR 0021).
|
||||
|
|
@ -37,6 +38,14 @@ AssistantChoice? assistantChoiceOf(AssistantsState state, AssistantRef ref) =>
|
|||
state,
|
||||
).where((choice) => assistantRefOf(choice) == ref).firstOrNull;
|
||||
|
||||
/// The effort [choice] is asked at by the person: the one this device last
|
||||
/// chose for it ([chosen], `DeviceSettings.askEfforts`) while the operator
|
||||
/// still allows it; null for the assistant's own (D63).
|
||||
LlmEffort? effortFor(AssistantChoice choice, Map<String, LlmEffort> chosen) {
|
||||
final effort = chosen[assistantRefOf(choice).key];
|
||||
return choice.efforts.contains(effort) ? effort : null;
|
||||
}
|
||||
|
||||
/// Who answers the field's question among [answerers]: the one [picked]
|
||||
/// here, else the one that answered last ([askWith]), else an assistant
|
||||
/// agreed to, any assistant, the web search; null for none at all.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_topi
|
|||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/answerers.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/conversation_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/private_sends.dart';
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// The assistant asked where the app asks for the person, beyond the field
|
||||
|
|
@ -53,8 +54,8 @@ Future<bool> readyToAsk(
|
|||
|
||||
/// Asks [choice] [question], with the parts [chosen], within [scope] for
|
||||
/// [task], in the conversation's sheet over the screen (D47), a new
|
||||
/// conversation called [title] where given, once [readyToAsk]. Whether it
|
||||
/// was asked.
|
||||
/// conversation called [title] where given, at [effort] (D63; the
|
||||
/// assistant's own where null), once [readyToAsk]. Whether it was asked.
|
||||
Future<bool> askAssistant(
|
||||
BuildContext context, {
|
||||
required AssistantChoice choice,
|
||||
|
|
@ -63,6 +64,7 @@ Future<bool> askAssistant(
|
|||
List<AssistantPart> chosen = const [],
|
||||
AssistantTask task = AssistantTask.ask,
|
||||
String? title,
|
||||
LlmEffort? effort,
|
||||
}) async {
|
||||
if (!await readyToAsk(context, choice: choice, scope: scope) ||
|
||||
!context.mounted) {
|
||||
|
|
@ -82,6 +84,7 @@ Future<bool> askAssistant(
|
|||
title: title,
|
||||
question: question,
|
||||
chosen: chosen,
|
||||
effort: effort,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import 'package:halkyon_learn_flutter/src/feature/assistant/widget/grammar_check
|
|||
import 'package:halkyon_learn_flutter/src/feature/settings/model/person_settings.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
|
@ -273,6 +274,10 @@ class _AssistantFieldState() extends State<AssistantField> {
|
|||
question: question,
|
||||
chosen: chosen,
|
||||
task: task,
|
||||
effort: effortFor(
|
||||
choice,
|
||||
context.read<DeviceSettingsBloc>().state.askEfforts,
|
||||
),
|
||||
) ||
|
||||
!mounted) {
|
||||
return;
|
||||
|
|
@ -298,6 +303,10 @@ class _AssistantFieldState() extends State<AssistantField> {
|
|||
return const SizedBox.shrink();
|
||||
}
|
||||
final asksAssistant = answerer is AssistantAnswerer;
|
||||
final askEfforts = context
|
||||
.select<DeviceSettingsBloc, Map<String, LlmEffort>>(
|
||||
(bloc) => bloc.state.askEfforts,
|
||||
);
|
||||
final offered = _offered(answerer);
|
||||
final chips = offered.isNotEmpty || asksAssistant;
|
||||
final shape = chips
|
||||
|
|
@ -420,25 +429,41 @@ class _AssistantFieldState() extends State<AssistantField> {
|
|||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_Parts(
|
||||
leading: asksAssistant
|
||||
? _ScopeChip(
|
||||
reaches: [
|
||||
AssistantReach.ticked,
|
||||
if (widget.topic.studysetId != null)
|
||||
AssistantReach.studyset,
|
||||
if (widget.topic.studysetId != null &&
|
||||
_nested == true)
|
||||
AssistantReach.tree,
|
||||
AssistantReach.library,
|
||||
],
|
||||
reach: _reach,
|
||||
includePrivate: _includePrivate,
|
||||
onReach: (reach) =>
|
||||
setState(() => _reach = reach),
|
||||
onIncludePrivate: (on) =>
|
||||
setState(() => _includePrivate = on),
|
||||
)
|
||||
: null,
|
||||
leading: [
|
||||
if (asksAssistant)
|
||||
_ScopeChip(
|
||||
reaches: [
|
||||
AssistantReach.ticked,
|
||||
if (widget.topic.studysetId != null)
|
||||
AssistantReach.studyset,
|
||||
if (widget.topic.studysetId != null &&
|
||||
_nested == true)
|
||||
AssistantReach.tree,
|
||||
AssistantReach.library,
|
||||
],
|
||||
reach: _reach,
|
||||
includePrivate: _includePrivate,
|
||||
onReach: (reach) =>
|
||||
setState(() => _reach = reach),
|
||||
onIncludePrivate: (on) =>
|
||||
setState(() => _includePrivate = on),
|
||||
),
|
||||
if (answerer case AssistantAnswerer(:final choice)
|
||||
when choice.efforts.isNotEmpty)
|
||||
if (choice.effort case final standard?)
|
||||
_EffortChip(
|
||||
efforts: choice.efforts,
|
||||
standard: standard,
|
||||
chosen: effortFor(choice, askEfforts),
|
||||
onChosen: (effort) =>
|
||||
context.read<DeviceSettingsBloc>().add(
|
||||
DeviceSettingsEventEffortChosen(
|
||||
answerer.key,
|
||||
effort,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
parts: offered,
|
||||
chosen: _chosen,
|
||||
onToggle: (part) => setState(
|
||||
|
|
@ -480,17 +505,31 @@ class const _Picker({
|
|||
child: sign,
|
||||
);
|
||||
}
|
||||
MenuItemButton item(Answerer answerer, {Color? color}) => MenuItemButton(
|
||||
leadingIcon: Icon(_iconOf(answerer), color: color),
|
||||
trailingIcon: answerer.key == current.key
|
||||
? const Icon(LucideIcons.check)
|
||||
: null,
|
||||
onPressed: () => onPicked(answerer),
|
||||
child: _AnswererLabel(answerer: answerer),
|
||||
);
|
||||
return MenuAnchor(
|
||||
menuChildren: [
|
||||
for (final answerer in answerers)
|
||||
MenuItemButton(
|
||||
leadingIcon: Icon(_iconOf(answerer)),
|
||||
trailingIcon: answerer.key == current.key
|
||||
? const Icon(LucideIcons.check)
|
||||
: null,
|
||||
onPressed: () => onPicked(answerer),
|
||||
child: _AnswererLabel(answerer: answerer),
|
||||
for (final answerer in answerers.whereType<WebAnswerer>())
|
||||
item(answerer),
|
||||
// The assistants under their providers' names, in their colours
|
||||
// (D60), so where a question goes shows before it goes.
|
||||
for (final (provider, group) in byProvider(
|
||||
answerers.whereType<AssistantAnswerer>(),
|
||||
(answerer) => answerer.choice.host,
|
||||
)) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 4),
|
||||
child: ProviderLabel(provider),
|
||||
),
|
||||
for (final answerer in group)
|
||||
item(answerer, color: providerColorIn(context, provider)),
|
||||
],
|
||||
],
|
||||
builder: (context, controller, _) => IconButton(
|
||||
tooltip: l10n.assistantChoose,
|
||||
|
|
@ -612,12 +651,65 @@ class const _ScopeChip({
|
|||
}
|
||||
}
|
||||
|
||||
/// How much the assistant thinks before it answers, as a chip after the
|
||||
/// scope's (D63): its menu holds the [efforts] the operator allows, the
|
||||
/// assistant's own ([standard]) marked so; [chosen] is the one this device
|
||||
/// asks it at, null for its own.
|
||||
class const _EffortChip({
|
||||
required final List<LlmEffort> efforts,
|
||||
required final LlmEffort standard,
|
||||
required final LlmEffort? chosen,
|
||||
required final ValueChanged<LlmEffort> onChosen,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final words = HalkyonAppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final shown = chosen ?? standard;
|
||||
return MenuAnchor(
|
||||
menuChildren: [
|
||||
for (final each in efforts)
|
||||
MenuItemButton(
|
||||
leadingIcon: each == shown
|
||||
? const Icon(LucideIcons.check)
|
||||
: const SizedBox.square(dimension: 24),
|
||||
onPressed: () => onChosen(each),
|
||||
child: Text(
|
||||
each == standard
|
||||
? l10n.assistantEffortDefault(
|
||||
assistantEffortName(words, each),
|
||||
)
|
||||
: assistantEffortName(words, each),
|
||||
),
|
||||
),
|
||||
],
|
||||
builder: (context, controller, _) => FilterChip(
|
||||
visualDensity: VisualDensity.compact,
|
||||
showCheckmark: false,
|
||||
selected: shown != standard,
|
||||
tooltip: l10n.assistantEffortTooltip,
|
||||
avatar: Icon(LucideIcons.brain, color: theme.colorScheme.primary),
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(assistantEffortName(words, shown)),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(LucideIcons.chevronDown, size: 14),
|
||||
],
|
||||
),
|
||||
onSelected: (_) =>
|
||||
controller.isOpen ? controller.close() : controller.open(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The parts that can go along, as chips flowing onto as many lines as
|
||||
/// they need, after [leading] where there is one, each showing as much of
|
||||
/// its text as its line holds; the whole of it on hovering or a long
|
||||
/// press.
|
||||
/// they need, after [leading], 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 Widget? leading,
|
||||
required final List<Widget> leading,
|
||||
required final List<AssistantPart> parts,
|
||||
required final Set<AssistantPart> chosen,
|
||||
required final ValueChanged<AssistantPart> onToggle,
|
||||
|
|
@ -628,7 +720,6 @@ class const _Parts({
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final leading = this.leading;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 10, 8, 2),
|
||||
// At most about three lines of chips; more scroll.
|
||||
|
|
@ -640,7 +731,7 @@ class const _Parts({
|
|||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
?leading,
|
||||
...leading,
|
||||
for (final part in parts)
|
||||
Tooltip(
|
||||
// An image's text is its asset row's id.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/bloc/device_settings_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_conversation.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/answerers.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/assistant_words.dart';
|
||||
|
|
@ -157,6 +158,7 @@ class const _ConversationTile({
|
|||
// The conversation's sheet over the screen, not over this list,
|
||||
// opened from the navigator, which outlives the list.
|
||||
final navigator = Navigator.of(context)..pop();
|
||||
final efforts = context.read<DeviceSettingsBloc>().state.askEfforts;
|
||||
unawaited(
|
||||
showConversation(
|
||||
navigator.context,
|
||||
|
|
@ -166,6 +168,8 @@ class const _ConversationTile({
|
|||
scope: scope,
|
||||
task: conversation.task,
|
||||
conversationId: conversation.id,
|
||||
// Its follow-ups at the effort the person asks it at (D63).
|
||||
effort: choice == null ? null : effortFor(choice, efforts),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_topi
|
|||
import 'package:halkyon_learn_flutter/src/feature/assistant/model/turn_view.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/assistant_words.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/private_sends.dart';
|
||||
import 'package:halkyon_llm/halkyon_llm.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
|
@ -40,6 +41,10 @@ Future<void> showConversation(
|
|||
String? conversationId,
|
||||
String? question,
|
||||
List<AssistantPart> chosen = const [],
|
||||
|
||||
/// The effort its questions ask at, one the assistant offers (D63); null
|
||||
/// for its own.
|
||||
LlmEffort? effort,
|
||||
}) {
|
||||
final person = PersonDependencies.of(context);
|
||||
final cubit = ConversationCubit(
|
||||
|
|
@ -53,6 +58,7 @@ Future<void> showConversation(
|
|||
task: task,
|
||||
title: title,
|
||||
answerIn: Localizations.localeOf(context).toLanguageTag(),
|
||||
effort: effort,
|
||||
);
|
||||
unawaited(() async {
|
||||
await cubit.open();
|
||||
|
|
|
|||
Loading…
Reference in a new issue