🐛 Bug fixes for the AI thought

This commit is contained in:
2025-12-21 21:48:46 +08:00
parent 87a54625aa
commit eb89d9223a
7 changed files with 403 additions and 386 deletions

View File

@@ -310,6 +310,7 @@
"settingsServerUrl": "Server URL", "settingsServerUrl": "Server URL",
"settingsApplied": "The settings has been applied.", "settingsApplied": "The settings has been applied.",
"notifications": "Notifications", "notifications": "Notifications",
"notificationsDescription": "See what's happended related to you recently.",
"posts": "Posts", "posts": "Posts",
"settingsBackgroundImage": "Background Image", "settingsBackgroundImage": "Background Image",
"settingsBackgroundImageClear": "Clear Background Image", "settingsBackgroundImageClear": "Clear Background Image",
@@ -1135,6 +1136,7 @@
"installUpdate": "Install update", "installUpdate": "Install update",
"openReleasePage": "Open release page", "openReleasePage": "Open release page",
"postCompose": "Compose Post", "postCompose": "Compose Post",
"postComposeDescription": "Compose a new post",
"postPublish": "Publish Post", "postPublish": "Publish Post",
"restoreDraftTitle": "Restore Draft", "restoreDraftTitle": "Restore Draft",
"restoreDraftMessage": "A draft was found. Do you want to restore it?", "restoreDraftMessage": "A draft was found. Do you want to restore it?",

View File

@@ -10,17 +10,20 @@ import "package:island/widgets/thought/thought_shared.dart";
import "package:material_symbols_icons/material_symbols_icons.dart"; import "package:material_symbols_icons/material_symbols_icons.dart";
class ThoughtSheet extends HookConsumerWidget { class ThoughtSheet extends HookConsumerWidget {
final String? initialMessage;
final List<Map<String, dynamic>> attachedMessages; final List<Map<String, dynamic>> attachedMessages;
final List<String> attachedPosts; final List<String> attachedPosts;
const ThoughtSheet({ const ThoughtSheet({
super.key, super.key,
this.initialMessage,
this.attachedMessages = const [], this.attachedMessages = const [],
this.attachedPosts = const [], this.attachedPosts = const [],
}); });
static Future<void> show( static Future<void> show(
BuildContext context, { BuildContext context, {
String? initialMessage,
List<Map<String, dynamic>> attachedMessages = const [], List<Map<String, dynamic>> attachedMessages = const [],
List<String> attachedPosts = const [], List<String> attachedPosts = const [],
}) { }) {
@@ -28,8 +31,8 @@ class ThoughtSheet extends HookConsumerWidget {
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
useSafeArea: true, useSafeArea: true,
builder: builder: (context) => ThoughtSheet(
(context) => ThoughtSheet( initialMessage: initialMessage,
attachedMessages: attachedMessages, attachedMessages: attachedMessages,
attachedPosts: attachedPosts, attachedPosts: attachedPosts,
), ),
@@ -40,6 +43,7 @@ class ThoughtSheet extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final chatState = useThoughtChat( final chatState = useThoughtChat(
ref, ref,
initialMessage: initialMessage,
attachedMessages: attachedMessages, attachedMessages: attachedMessages,
attachedPosts: attachedPosts, attachedPosts: attachedPosts,
); );
@@ -95,8 +99,7 @@ class ThoughtSheet extends HookConsumerWidget {
], ],
); );
}, },
orElse: orElse: () => ThoughtChatInterface(
() => ThoughtChatInterface(
attachedMessages: attachedMessages, attachedMessages: attachedMessages,
attachedPosts: attachedPosts, attachedPosts: attachedPosts,
), ),

View File

@@ -38,3 +38,16 @@ class ShowComposeSheetEvent {
class ShowNotificationSheetEvent { class ShowNotificationSheetEvent {
const ShowNotificationSheetEvent(); const ShowNotificationSheetEvent();
} }
/// Event fired to show the thought sheet
class ShowThoughtSheetEvent {
final String? initialMessage;
final List<Map<String, dynamic>> attachedMessages;
final List<String> attachedPosts;
const ShowThoughtSheetEvent({
this.initialMessage,
this.attachedMessages = const [],
this.attachedPosts = const [],
});
}

View File

@@ -19,6 +19,7 @@ import 'package:island/widgets/content/network_status_sheet.dart';
import 'package:island/widgets/tour/tour.dart'; import 'package:island/widgets/tour/tour.dart';
import 'package:island/widgets/post/compose_sheet.dart'; import 'package:island/widgets/post/compose_sheet.dart';
import 'package:island/screens/notification.dart'; import 'package:island/screens/notification.dart';
import 'package:island/screens/thought/think_sheet.dart';
import 'package:island/services/event_bus.dart'; import 'package:island/services/event_bus.dart';
import 'package:tray_manager/tray_manager.dart'; import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@@ -38,6 +39,7 @@ class _AppWrapperState extends ConsumerState<AppWrapper>
StreamSubscription? composeSheetSubs; StreamSubscription? composeSheetSubs;
StreamSubscription? notificationSheetSubs; StreamSubscription? notificationSheetSubs;
StreamSubscription? thoughtSheetSubs;
@override @override
void initState() { void initState() {
@@ -70,6 +72,12 @@ class _AppWrapperState extends ConsumerState<AppWrapper>
} }
}); });
thoughtSheetSubs = eventBus.on<ShowThoughtSheetEvent>().listen((event) {
if (mounted) {
_showThoughtSheet(event);
}
});
final initialUrl = await protocolHandler.getInitialUrl(); final initialUrl = await protocolHandler.getInitialUrl();
if (initialUrl != null && mounted) { if (initialUrl != null && mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -87,6 +95,7 @@ class _AppWrapperState extends ConsumerState<AppWrapper>
ntySubs?.cancel(); ntySubs?.cancel();
composeSheetSubs?.cancel(); composeSheetSubs?.cancel();
notificationSheetSubs?.cancel(); notificationSheetSubs?.cancel();
thoughtSheetSubs?.cancel();
super.dispose(); super.dispose();
} }
@@ -154,6 +163,15 @@ class _AppWrapperState extends ConsumerState<AppWrapper>
); );
} }
void _showThoughtSheet(ShowThoughtSheetEvent event) {
ThoughtSheet.show(
context,
initialMessage: event.initialMessage,
attachedMessages: event.attachedMessages,
attachedPosts: event.attachedPosts,
);
}
void _handleDeepLink(Uri uri, WidgetRef ref) async { void _handleDeepLink(Uri uri, WidgetRef ref) async {
String path = '/${uri.host}${uri.path}'; String path = '/${uri.host}${uri.path}';

View File

@@ -29,16 +29,17 @@ class CommandPattleWidget extends HookConsumerWidget {
static List<SpecialAction> _getSpecialActions(BuildContext context) { static List<SpecialAction> _getSpecialActions(BuildContext context) {
return [ return [
SpecialAction( SpecialAction(
name: 'Compose Post', name: 'postCompose'.tr(),
description: 'Create a new post', description: 'postComposeDescription'.tr(),
icon: Symbols.edit, icon: Symbols.edit,
action: () { action: () {
eventBus.fire(const ShowComposeSheetEvent()); eventBus.fire(const ShowComposeSheetEvent());
}, },
), ),
SpecialAction( SpecialAction(
name: 'Notifications', name: 'notifications'.tr(),
description: 'View your notifications', description: 'notificationsDescription'.tr(),
searchableAliases: ['notifications', 'alert', 'bell'],
icon: Symbols.notifications, icon: Symbols.notifications,
action: () { action: () {
eventBus.fire(const ShowNotificationSheetEvent()); eventBus.fire(const ShowNotificationSheetEvent());
@@ -149,7 +150,7 @@ class CommandPattleWidget extends HookConsumerWidget {
filteredChats.isEmpty && filteredChats.isEmpty &&
filteredSpecialActions.isEmpty && filteredSpecialActions.isEmpty &&
filteredRoutes.isEmpty filteredRoutes.isEmpty
? _getFallbackActions(searchQuery.value) ? _getFallbackActions(context, searchQuery.value)
: <FallbackAction>[]; : <FallbackAction>[];
// Combine results: fallbacks first, then chats, special actions, routes // Combine results: fallbacks first, then chats, special actions, routes
@@ -202,17 +203,7 @@ class CommandPattleWidget extends HookConsumerWidget {
if (event.logicalKey == LogicalKeyboardKey.enter || if (event.logicalKey == LogicalKeyboardKey.enter ||
event.logicalKey == LogicalKeyboardKey.numpadEnter) { event.logicalKey == LogicalKeyboardKey.numpadEnter) {
final item = allResults[focusedIndex.value ?? 0]; final item = allResults[focusedIndex.value ?? 0];
if (item is SnChatRoom) { _executeItem(context, ref, item);
_navigateToChat(context, ref, item);
} else if (item is SpecialAction) {
onDismiss();
item.action();
} else if (item is RouteItem) {
_navigateToRoute(context, ref, item);
} else if (item is FallbackAction) {
onDismiss();
item.action();
}
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
if (allResults.isNotEmpty) { if (allResults.isNotEmpty) {
if (focusedIndex.value == null) { if (focusedIndex.value == null) {
@@ -291,6 +282,13 @@ class CommandPattleWidget extends HookConsumerWidget {
leading: CircleAvatar( leading: CircleAvatar(
child: const Icon(Symbols.keyboard_command_key), child: const Icon(Symbols.keyboard_command_key),
).padding(horizontal: 8), ).padding(horizontal: 8),
onSubmitted: !isDesktop() && allResults.isNotEmpty
? (value) => _executeItem(
context,
ref,
allResults[0],
)
: null,
), ),
AnimatedSize( AnimatedSize(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
@@ -389,25 +387,63 @@ class CommandPattleWidget extends HookConsumerWidget {
ref.read(routerProvider).go(route.path); ref.read(routerProvider).go(route.path);
} }
static List<FallbackAction> _getFallbackActions(String query) { void _executeItem(BuildContext context, WidgetRef ref, dynamic item) {
if (item is SnChatRoom) {
_navigateToChat(context, ref, item);
} else if (item is SpecialAction) {
onDismiss();
item.action();
} else if (item is RouteItem) {
_navigateToRoute(context, ref, item);
} else if (item is FallbackAction) {
onDismiss();
item.action();
}
}
static List<FallbackAction> _getFallbackActions(
BuildContext context,
String query,
) {
final List<FallbackAction> actions = []; final List<FallbackAction> actions = [];
// Check if query is a URL // Check if query is a URL
final Uri? uri = Uri.tryParse(query); final Uri? uri = Uri.tryParse(query);
if (uri != null && (uri.scheme == 'http' || uri.scheme == 'https')) { final isValidUrl =
uri != null && (uri.scheme == 'http' || uri.scheme == 'https');
final isDomain = RegExp(
r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$',
).hasMatch(query);
if (isValidUrl || isDomain) {
final finalUri = isDomain ? Uri.parse('https://$query') : uri!;
actions.add( actions.add(
FallbackAction( FallbackAction(
name: 'Open URL', name: 'Open URL',
description: 'Open $query in browser', description: 'Open ${finalUri.toString()} in browser',
icon: Symbols.open_in_new, icon: Symbols.open_in_new,
action: () async { action: () async {
if (await canLaunchUrl(uri)) { if (await canLaunchUrl(finalUri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication); await launchUrl(finalUri, mode: LaunchMode.externalApplication);
} }
}, },
), ),
); );
} else { }
// Ask the AI
// Bugged, DO NOT USE
// actions.add(
// FallbackAction(
// name: 'Ask the AI',
// description: 'Ask "$query" to the AI',
// icon: Symbols.bubble_chart,
// action: () {
// eventBus.fire(ShowThoughtSheetEvent(initialMessage: query));
// },
// ),
// );
// Search the web // Search the web
actions.add( actions.add(
FallbackAction( FallbackAction(
@@ -424,7 +460,6 @@ class CommandPattleWidget extends HookConsumerWidget {
}, },
), ),
); );
}
return actions; return actions;
} }

View File

@@ -28,62 +28,23 @@ class ThoughtContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (isStreaming) { final content = streamingText.isNotEmpty
// Streaming text with spinner ? streamingText
if (streamingText.isNotEmpty) { : thought != null
final isStreamingError = streamingText.startsWith('Error:'); ? thought!.parts
return Container(
padding: isStreamingError ? const EdgeInsets.all(8) : EdgeInsets.zero,
decoration:
isStreamingError
? BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.error,
width: 1,
),
borderRadius: BorderRadius.circular(8),
)
: null,
child: MarkdownTextContent(
isSelectable: true,
content: streamingText,
extraBlockSyntaxList: [ProposalBlockSyntax()],
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
color:
isStreamingError ? Theme.of(context).colorScheme.error : null,
),
extraGenerators: [
ProposalGenerator(
backgroundColor:
Theme.of(context).colorScheme.secondaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onSecondaryContainer,
borderColor: Theme.of(context).colorScheme.outline,
),
],
),
);
}
return const SizedBox.shrink();
} else {
// Regular thought content - render parts
if (thought!.parts.isNotEmpty) {
final textParts = thought!.parts
.where((p) => p.type == ThinkingMessagePartType.text) .where((p) => p.type == ThinkingMessagePartType.text)
.map((p) => p.text ?? '') .map((p) => p.text ?? '')
.join(''); .join('')
if (textParts.isNotEmpty) { : '';
if (content.isEmpty) return const SizedBox.shrink();
final isError = content.startsWith('Error:') || _isErrorMessage;
return Container( return Container(
padding: padding: isError ? const EdgeInsets.all(8) : EdgeInsets.zero,
_isErrorMessage decoration: isError
? const EdgeInsets.symmetric(horizontal: 12, vertical: 4)
: EdgeInsets.zero,
decoration:
_isErrorMessage
? BoxDecoration( ? BoxDecoration(
color: Theme.of(
context,
).colorScheme.error.withOpacity(0.1),
border: Border.all( border: Border.all(
color: Theme.of(context).colorScheme.error, color: Theme.of(context).colorScheme.error,
width: 1, width: 1,
@@ -93,28 +54,19 @@ class ThoughtContent extends StatelessWidget {
: null, : null,
child: MarkdownTextContent( child: MarkdownTextContent(
isSelectable: true, isSelectable: true,
content: textParts, content: content,
extraBlockSyntaxList: [ProposalBlockSyntax()], extraBlockSyntaxList: [ProposalBlockSyntax()],
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: color: isError ? Theme.of(context).colorScheme.error : null,
_isErrorMessage
? Theme.of(context).colorScheme.error
: null,
), ),
extraGenerators: [ extraGenerators: [
ProposalGenerator( ProposalGenerator(
backgroundColor: backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
Theme.of(context).colorScheme.secondaryContainer, foregroundColor: Theme.of(context).colorScheme.onSecondaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onSecondaryContainer,
borderColor: Theme.of(context).colorScheme.outline, borderColor: Theme.of(context).colorScheme.outline,
), ),
], ],
), ),
); );
} }
}
return const SizedBox.shrink();
}
}
} }

View File

@@ -74,6 +74,7 @@ ThoughtChatState useThoughtChat(
String? initialSequenceId, String? initialSequenceId,
List<SnThinkingThought>? initialThoughts, List<SnThinkingThought>? initialThoughts,
String? initialTopic, String? initialTopic,
String? initialMessage,
List<Map<String, dynamic>> attachedMessages = const [], List<Map<String, dynamic>> attachedMessages = const [],
List<String> attachedPosts = const [], List<String> attachedPosts = const [],
VoidCallback? onSequenceIdChanged, VoidCallback? onSequenceIdChanged,
@@ -117,11 +118,13 @@ ThoughtChatState useThoughtChat(
useEffect(() { useEffect(() {
if (localThoughts.value.isNotEmpty || isStreaming.value) { if (localThoughts.value.isNotEmpty || isStreaming.value) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
scrollController.animateTo( scrollController.animateTo(
0, 0,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeOut, curve: Curves.easeOut,
); );
}
}); });
} }
return null; return null;
@@ -141,10 +144,12 @@ ThoughtChatState useThoughtChat(
return () => scrollController.removeListener(onScroll); return () => scrollController.removeListener(onScroll);
}, [scrollController]); }, [scrollController]);
Future<void> sendMessage() async { Future<void> sendMessage({String? message}) async {
if (messageController.text.trim().isEmpty) return; if (message == null && messageController.text.trim().isEmpty) {
return;
}
final userMessage = messageController.text.trim(); final userMessage = message ?? messageController.text.trim();
// Add user message to local thoughts // Add user message to local thoughts
final userInfo = ref.read(userInfoProvider); final userInfo = ref.read(userInfoProvider);
@@ -177,8 +182,9 @@ ThoughtChatState useThoughtChat(
accpetProposals: ['post_create'], accpetProposals: ['post_create'],
attachedMessages: attachedMessages, attachedMessages: attachedMessages,
attachedPosts: attachedPosts, attachedPosts: attachedPosts,
serviceId: serviceId: selectedServiceId.value.isNotEmpty
selectedServiceId.value.isNotEmpty ? selectedServiceId.value : null, ? selectedServiceId.value
: null,
); );
try { try {
@@ -368,6 +374,15 @@ ThoughtChatState useThoughtChat(
} }
} }
useEffect(() {
if (initialMessage?.isNotEmpty ?? false) {
WidgetsBinding.instance.addPostFrameCallback((_) {
sendMessage(message: initialMessage);
});
}
return null;
}, [initialMessage]);
return ThoughtChatState( return ThoughtChatState(
sequenceId: sequenceId, sequenceId: sequenceId,
localThoughts: localThoughts, localThoughts: localThoughts,
@@ -388,6 +403,7 @@ class ThoughtChatInterface extends HookConsumerWidget {
final List<SnThinkingThought>? initialThoughts; final List<SnThinkingThought>? initialThoughts;
final String? initialSequenceId; final String? initialSequenceId;
final String? initialTopic; final String? initialTopic;
final String? initialMessage;
final List<Map<String, dynamic>> attachedMessages; final List<Map<String, dynamic>> attachedMessages;
final List<String> attachedPosts; final List<String> attachedPosts;
final bool isDisabled; final bool isDisabled;
@@ -397,6 +413,7 @@ class ThoughtChatInterface extends HookConsumerWidget {
this.initialThoughts, this.initialThoughts,
this.initialSequenceId, this.initialSequenceId,
this.initialTopic, this.initialTopic,
this.initialMessage,
this.attachedMessages = const [], this.attachedMessages = const [],
this.attachedPosts = const [], this.attachedPosts = const [],
this.isDisabled = false, this.isDisabled = false,
@@ -415,6 +432,7 @@ class ThoughtChatInterface extends HookConsumerWidget {
initialSequenceId: initialSequenceId, initialSequenceId: initialSequenceId,
initialThoughts: initialThoughts, initialThoughts: initialThoughts,
initialTopic: initialTopic, initialTopic: initialTopic,
initialMessage: initialMessage,
attachedMessages: attachedMessages, attachedMessages: attachedMessages,
attachedPosts: attachedPosts, attachedPosts: attachedPosts,
); );
@@ -453,17 +471,14 @@ class ThoughtChatInterface extends HookConsumerWidget {
), ),
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
curve: Curves.easeOut, curve: Curves.easeOut,
builder: builder: (context, height, child) =>
(context, height, child) =>
SuperListView.builder( SuperListView.builder(
listController: chatState.listController, listController: chatState.listController,
controller: chatState.scrollController, controller: chatState.scrollController,
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: 16, top: 16,
bottom: bottom:
MediaQuery.of( MediaQuery.of(context).padding.bottom +
context,
).padding.bottom +
8 + 8 +
height, height,
), ),
@@ -484,8 +499,7 @@ class ThoughtChatInterface extends HookConsumerWidget {
chatState.isStreaming.value chatState.isStreaming.value
? index - 1 ? index - 1
: index; : index;
final thought = final thought = chatState
chatState
.localThoughts .localThoughts
.value[thoughtIndex]; .value[thoughtIndex];
return ThoughtItem(thought: thought); return ThoughtItem(thought: thought);
@@ -510,12 +524,10 @@ class ThoughtChatInterface extends HookConsumerWidget {
if (chatState.isStreaming.value && index == 0) { if (chatState.isStreaming.value && index == 0) {
return ThoughtItem( return ThoughtItem(
isStreaming: true, isStreaming: true,
streamingItems: streamingItems: chatState.streamingItems.value,
chatState.streamingItems.value,
); );
} }
final thoughtIndex = final thoughtIndex = chatState.isStreaming.value
chatState.isStreaming.value
? index - 1 ? index - 1
: index; : index;
final thought = final thought =
@@ -531,18 +543,14 @@ class ThoughtChatInterface extends HookConsumerWidget {
// Bottom gradient - appears when scrolling towards newer thoughts (behind thought input) // Bottom gradient - appears when scrolling towards newer thoughts (behind thought input)
AnimatedBuilder( AnimatedBuilder(
animation: chatState.bottomGradientNotifier.value, animation: chatState.bottomGradientNotifier.value,
builder: builder: (context, child) => Positioned(
(context, child) => Positioned(
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
child: Opacity( child: Opacity(
opacity: chatState.bottomGradientNotifier.value.value, opacity: chatState.bottomGradientNotifier.value.value,
child: Container( child: Container(
height: math.min( height: math.min(MediaQuery.of(context).size.height * 0.1, 128),
MediaQuery.of(context).size.height * 0.1,
128,
),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.bottomCenter, begin: Alignment.bottomCenter,
@@ -746,48 +754,33 @@ class ThoughtInput extends HookWidget {
maxLines: 5, maxLines: 5,
minLines: 1, minLines: 1,
textInputAction: TextInputAction.send, textInputAction: TextInputAction.send,
onSubmitted: onSubmitted: (!isStreaming && !isDisabled)
(!isStreaming && !isDisabled)
? (_) => onSend() ? (_) => onSend()
: null, : null,
), ),
), ),
IconButton( Row(
icon: Icon(isStreaming ? Symbols.stop : Icons.send),
color: Theme.of(context).colorScheme.primary,
onPressed: (!isStreaming && !isDisabled) ? onSend : null,
),
],
),
Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
child: Row(
children: [ children: [
if (services.isNotEmpty) if (services.isNotEmpty)
DropdownButtonHideUnderline( SizedBox(
height: 40,
child: DropdownButtonHideUnderline(
child: DropdownButton2<String>( child: DropdownButton2<String>(
value: value: selectedServiceId.value.isEmpty
selectedServiceId.value.isEmpty
? null ? null
: selectedServiceId.value, : selectedServiceId.value,
customButton: Container( customButton: Container(
padding: EdgeInsets.all(4), padding: const EdgeInsets.symmetric(
decoration: BoxDecoration( horizontal: 4,
border: BoxBorder.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
), ),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(16), Radius.circular(24),
), ),
), ),
child: Row( child: Row(
spacing: 8, spacing: 8,
children: [ children: [
const Icon(
Symbols.network_intelligence,
size: 20,
),
Text(selectedServiceId.value), Text(selectedServiceId.value),
const Icon( const Icon(
Symbols.keyboard_arrow_down, Symbols.keyboard_arrow_down,
@@ -796,8 +789,7 @@ class ThoughtInput extends HookWidget {
], ],
).padding(vertical: 2, horizontal: 6), ).padding(vertical: 2, horizontal: 6),
), ),
items: items: services
services
.map( .map(
(service) => DropdownMenuItem<String>( (service) => DropdownMenuItem<String>(
value: service.serviceId, value: service.serviceId,
@@ -807,21 +799,20 @@ class ThoughtInput extends HookWidget {
children: [ children: [
Text( Text(
service.serviceId, service.serviceId,
style: DefaultTextStyle.of( style: DefaultTextStyle.of(context)
context, .style
).style.copyWith( .copyWith(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
Text( Text(
'Rate: ${service.billingMultiplier}x, Level: P${service.perkLevel}', '${service.billingMultiplier}x, T${service.perkLevel}',
style: DefaultTextStyle.of( style: DefaultTextStyle.of(context)
context, .style
).style.copyWith( .copyWith(
fontSize: 12, fontSize: 12,
color: color: Theme.of(context)
Theme.of(context)
.colorScheme .colorScheme
.onSurfaceVariant, .onSurfaceVariant,
), ),
@@ -831,8 +822,7 @@ class ThoughtInput extends HookWidget {
), ),
) )
.toList(), .toList(),
onChanged: onChanged: !isStreaming && !isDisabled
!isStreaming && !isDisabled
? (value) { ? (value) {
if (value != null) { if (value != null) {
selectedServiceId.value = value; selectedServiceId.value = value;
@@ -844,7 +834,7 @@ class ThoughtInput extends HookWidget {
buttonStyleData: ButtonStyleData( buttonStyleData: ButtonStyleData(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(16), Radius.circular(24),
), ),
), ),
), ),
@@ -860,8 +850,15 @@ class ThoughtInput extends HookWidget {
), ),
), ),
), ),
),
], ],
), ),
IconButton(
icon: Icon(isStreaming ? Symbols.stop : Icons.send),
color: Theme.of(context).colorScheme.primary,
onPressed: (!isStreaming && !isDisabled) ? onSend : null,
),
],
), ),
], ],
), ),
@@ -923,8 +920,7 @@ class ThoughtItem extends StatelessWidget {
} }
List<Widget> buildWidgetsList() { List<Widget> buildWidgetsList() {
final List<StreamItem> items = final List<StreamItem> items = isStreaming
isStreaming
? (streamingItems ?? []) ? (streamingItems ?? [])
: thought!.parts.map((p) { : thought!.parts.map((p) {
String type; String type;
@@ -950,8 +946,7 @@ class ThoughtItem extends StatelessWidget {
final isAI = final isAI =
isStreaming || isStreaming ||
(!isStreaming && thought!.role == ThinkingThoughtRole.assistant); (!isStreaming && thought!.role == ThinkingThoughtRole.assistant);
final List<Map<String, String>> proposals = final List<Map<String, String>> proposals = !isStreaming
!isStreaming
? _extractProposals( ? _extractProposals(
thought!.parts thought!.parts
.where((p) => p.type == ThinkingMessagePartType.text) .where((p) => p.type == ThinkingMessagePartType.text)
@@ -986,8 +981,7 @@ class ThoughtItem extends StatelessWidget {
isFinish: result != null, isFinish: result != null,
isStreaming: isStreaming, isStreaming: isStreaming,
callData: JsonEncoder.withIndent(' ').convert(item.data.toJson()), callData: JsonEncoder.withIndent(' ').convert(item.data.toJson()),
resultData: resultData: result != null
result != null
? JsonEncoder.withIndent(' ').convert(result.data.toJson()) ? JsonEncoder.withIndent(' ').convert(result.data.toJson())
: null, : null,
), ),