🐛 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",
"settingsApplied": "The settings has been applied.",
"notifications": "Notifications",
"notificationsDescription": "See what's happended related to you recently.",
"posts": "Posts",
"settingsBackgroundImage": "Background Image",
"settingsBackgroundImageClear": "Clear Background Image",
@@ -1135,6 +1136,7 @@
"installUpdate": "Install update",
"openReleasePage": "Open release page",
"postCompose": "Compose Post",
"postComposeDescription": "Compose a new post",
"postPublish": "Publish Post",
"restoreDraftTitle": "Restore Draft",
"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";
class ThoughtSheet extends HookConsumerWidget {
final String? initialMessage;
final List<Map<String, dynamic>> attachedMessages;
final List<String> attachedPosts;
const ThoughtSheet({
super.key,
this.initialMessage,
this.attachedMessages = const [],
this.attachedPosts = const [],
});
static Future<void> show(
BuildContext context, {
String? initialMessage,
List<Map<String, dynamic>> attachedMessages = const [],
List<String> attachedPosts = const [],
}) {
@@ -28,11 +31,11 @@ class ThoughtSheet extends HookConsumerWidget {
context: context,
isScrollControlled: true,
useSafeArea: true,
builder:
(context) => ThoughtSheet(
attachedMessages: attachedMessages,
attachedPosts: attachedPosts,
),
builder: (context) => ThoughtSheet(
initialMessage: initialMessage,
attachedMessages: attachedMessages,
attachedPosts: attachedPosts,
),
);
}
@@ -40,6 +43,7 @@ class ThoughtSheet extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final chatState = useThoughtChat(
ref,
initialMessage: initialMessage,
attachedMessages: attachedMessages,
attachedPosts: attachedPosts,
);
@@ -75,31 +79,30 @@ class ThoughtSheet extends HookConsumerWidget {
return status
? chatInterface
: Column(
children: [
MaterialBanner(
leading: const Icon(Symbols.error),
content: const Text(
'You have unpaid orders. Please settle your payment to continue using the service.',
style: TextStyle(fontWeight: FontWeight.bold),
),
actions: [
TextButton(
onPressed: () {
retry();
},
child: Text('retry'.tr()),
children: [
MaterialBanner(
leading: const Icon(Symbols.error),
content: const Text(
'You have unpaid orders. Please settle your payment to continue using the service.',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
Expanded(child: chatInterface),
],
);
actions: [
TextButton(
onPressed: () {
retry();
},
child: Text('retry'.tr()),
),
],
),
Expanded(child: chatInterface),
],
);
},
orElse:
() => ThoughtChatInterface(
attachedMessages: attachedMessages,
attachedPosts: attachedPosts,
),
orElse: () => ThoughtChatInterface(
attachedMessages: attachedMessages,
attachedPosts: attachedPosts,
),
),
);
}

View File

@@ -38,3 +38,16 @@ class ShowComposeSheetEvent {
class 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/post/compose_sheet.dart';
import 'package:island/screens/notification.dart';
import 'package:island/screens/thought/think_sheet.dart';
import 'package:island/services/event_bus.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart';
@@ -38,6 +39,7 @@ class _AppWrapperState extends ConsumerState<AppWrapper>
StreamSubscription? composeSheetSubs;
StreamSubscription? notificationSheetSubs;
StreamSubscription? thoughtSheetSubs;
@override
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();
if (initialUrl != null && mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -87,6 +95,7 @@ class _AppWrapperState extends ConsumerState<AppWrapper>
ntySubs?.cancel();
composeSheetSubs?.cancel();
notificationSheetSubs?.cancel();
thoughtSheetSubs?.cancel();
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 {
String path = '/${uri.host}${uri.path}';

View File

@@ -29,16 +29,17 @@ class CommandPattleWidget extends HookConsumerWidget {
static List<SpecialAction> _getSpecialActions(BuildContext context) {
return [
SpecialAction(
name: 'Compose Post',
description: 'Create a new post',
name: 'postCompose'.tr(),
description: 'postComposeDescription'.tr(),
icon: Symbols.edit,
action: () {
eventBus.fire(const ShowComposeSheetEvent());
},
),
SpecialAction(
name: 'Notifications',
description: 'View your notifications',
name: 'notifications'.tr(),
description: 'notificationsDescription'.tr(),
searchableAliases: ['notifications', 'alert', 'bell'],
icon: Symbols.notifications,
action: () {
eventBus.fire(const ShowNotificationSheetEvent());
@@ -149,7 +150,7 @@ class CommandPattleWidget extends HookConsumerWidget {
filteredChats.isEmpty &&
filteredSpecialActions.isEmpty &&
filteredRoutes.isEmpty
? _getFallbackActions(searchQuery.value)
? _getFallbackActions(context, searchQuery.value)
: <FallbackAction>[];
// Combine results: fallbacks first, then chats, special actions, routes
@@ -202,17 +203,7 @@ class CommandPattleWidget extends HookConsumerWidget {
if (event.logicalKey == LogicalKeyboardKey.enter ||
event.logicalKey == LogicalKeyboardKey.numpadEnter) {
final item = allResults[focusedIndex.value ?? 0];
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();
}
_executeItem(context, ref, item);
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
if (allResults.isNotEmpty) {
if (focusedIndex.value == null) {
@@ -291,6 +282,13 @@ class CommandPattleWidget extends HookConsumerWidget {
leading: CircleAvatar(
child: const Icon(Symbols.keyboard_command_key),
).padding(horizontal: 8),
onSubmitted: !isDesktop() && allResults.isNotEmpty
? (value) => _executeItem(
context,
ref,
allResults[0],
)
: null,
),
AnimatedSize(
duration: const Duration(milliseconds: 200),
@@ -389,43 +387,80 @@ class CommandPattleWidget extends HookConsumerWidget {
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 = [];
// Check if query is a URL
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(
FallbackAction(
name: 'Open URL',
description: 'Open $query in browser',
description: 'Open ${finalUri.toString()} in browser',
icon: Symbols.open_in_new,
action: () async {
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
},
),
);
} else {
// Search the web
actions.add(
FallbackAction(
name: 'Search the web',
description: 'Search "$query" on Google',
icon: Symbols.search,
action: () async {
final searchUri = Uri.https('www.google.com', '/search', {
'q': query,
});
if (await canLaunchUrl(searchUri)) {
await launchUrl(searchUri, mode: LaunchMode.externalApplication);
if (await canLaunchUrl(finalUri)) {
await launchUrl(finalUri, mode: LaunchMode.externalApplication);
}
},
),
);
}
// 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
actions.add(
FallbackAction(
name: 'Search the web',
description: 'Search "$query" on Google',
icon: Symbols.search,
action: () async {
final searchUri = Uri.https('www.google.com', '/search', {
'q': query,
});
if (await canLaunchUrl(searchUri)) {
await launchUrl(searchUri, mode: LaunchMode.externalApplication);
}
},
),
);
return actions;
}
}

View File

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

View File

@@ -74,6 +74,7 @@ ThoughtChatState useThoughtChat(
String? initialSequenceId,
List<SnThinkingThought>? initialThoughts,
String? initialTopic,
String? initialMessage,
List<Map<String, dynamic>> attachedMessages = const [],
List<String> attachedPosts = const [],
VoidCallback? onSequenceIdChanged,
@@ -117,11 +118,13 @@ ThoughtChatState useThoughtChat(
useEffect(() {
if (localThoughts.value.isNotEmpty || isStreaming.value) {
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
if (scrollController.hasClients) {
scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
return null;
@@ -141,10 +144,12 @@ ThoughtChatState useThoughtChat(
return () => scrollController.removeListener(onScroll);
}, [scrollController]);
Future<void> sendMessage() async {
if (messageController.text.trim().isEmpty) return;
Future<void> sendMessage({String? message}) async {
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
final userInfo = ref.read(userInfoProvider);
@@ -177,8 +182,9 @@ ThoughtChatState useThoughtChat(
accpetProposals: ['post_create'],
attachedMessages: attachedMessages,
attachedPosts: attachedPosts,
serviceId:
selectedServiceId.value.isNotEmpty ? selectedServiceId.value : null,
serviceId: selectedServiceId.value.isNotEmpty
? selectedServiceId.value
: null,
);
try {
@@ -309,8 +315,8 @@ ThoughtChatState useThoughtChat(
final now = DateTime.now();
final errorMessage =
error is DioException && error.response?.data is ResponseBody
? 'toughtParseError'.tr()
: error.toString();
? 'toughtParseError'.tr()
: error.toString();
final errorThought = SnThinkingThought(
id: 'error-${DateTime.now().millisecondsSinceEpoch}',
parts: [
@@ -368,6 +374,15 @@ ThoughtChatState useThoughtChat(
}
}
useEffect(() {
if (initialMessage?.isNotEmpty ?? false) {
WidgetsBinding.instance.addPostFrameCallback((_) {
sendMessage(message: initialMessage);
});
}
return null;
}, [initialMessage]);
return ThoughtChatState(
sequenceId: sequenceId,
localThoughts: localThoughts,
@@ -388,6 +403,7 @@ class ThoughtChatInterface extends HookConsumerWidget {
final List<SnThinkingThought>? initialThoughts;
final String? initialSequenceId;
final String? initialTopic;
final String? initialMessage;
final List<Map<String, dynamic>> attachedMessages;
final List<String> attachedPosts;
final bool isDisabled;
@@ -397,6 +413,7 @@ class ThoughtChatInterface extends HookConsumerWidget {
this.initialThoughts,
this.initialSequenceId,
this.initialTopic,
this.initialMessage,
this.attachedMessages = const [],
this.attachedPosts = const [],
this.isDisabled = false,
@@ -415,6 +432,7 @@ class ThoughtChatInterface extends HookConsumerWidget {
initialSequenceId: initialSequenceId,
initialThoughts: initialThoughts,
initialTopic: initialTopic,
initialMessage: initialMessage,
attachedMessages: attachedMessages,
attachedPosts: attachedPosts,
);
@@ -445,84 +463,78 @@ class ThoughtChatInterface extends HookConsumerWidget {
Expanded(
child:
previousInputHeight != null &&
previousInputHeight != inputHeight.value
? TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: previousInputHeight,
end: inputHeight.value,
),
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
builder:
(context, height, child) =>
SuperListView.builder(
listController: chatState.listController,
controller: chatState.scrollController,
padding: EdgeInsets.only(
top: 16,
bottom:
MediaQuery.of(
context,
).padding.bottom +
8 +
height,
),
reverse: true,
itemCount:
chatState.localThoughts.value.length +
(chatState.isStreaming.value ? 1 : 0),
itemBuilder: (context, index) {
if (chatState.isStreaming.value &&
index == 0) {
return ThoughtItem(
isStreaming: true,
streamingItems:
chatState.streamingItems.value,
);
}
final thoughtIndex =
chatState.isStreaming.value
? index - 1
: index;
final thought =
chatState
.localThoughts
.value[thoughtIndex];
return ThoughtItem(thought: thought);
},
),
)
: SuperListView.builder(
listController: chatState.listController,
controller: chatState.scrollController,
padding: EdgeInsets.only(
top: 16,
bottom:
MediaQuery.of(context).padding.bottom +
8 +
inputHeight.value,
),
reverse: true,
itemCount:
chatState.localThoughts.value.length +
(chatState.isStreaming.value ? 1 : 0),
itemBuilder: (context, index) {
if (chatState.isStreaming.value && index == 0) {
return ThoughtItem(
isStreaming: true,
streamingItems:
chatState.streamingItems.value,
);
}
final thoughtIndex =
chatState.isStreaming.value
previousInputHeight != inputHeight.value
? TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: previousInputHeight,
end: inputHeight.value,
),
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
builder: (context, height, child) =>
SuperListView.builder(
listController: chatState.listController,
controller: chatState.scrollController,
padding: EdgeInsets.only(
top: 16,
bottom:
MediaQuery.of(context).padding.bottom +
8 +
height,
),
reverse: true,
itemCount:
chatState.localThoughts.value.length +
(chatState.isStreaming.value ? 1 : 0),
itemBuilder: (context, index) {
if (chatState.isStreaming.value &&
index == 0) {
return ThoughtItem(
isStreaming: true,
streamingItems:
chatState.streamingItems.value,
);
}
final thoughtIndex =
chatState.isStreaming.value
? index - 1
: index;
final thought =
chatState.localThoughts.value[thoughtIndex];
return ThoughtItem(thought: thought);
},
final thought = chatState
.localThoughts
.value[thoughtIndex];
return ThoughtItem(thought: thought);
},
),
)
: SuperListView.builder(
listController: chatState.listController,
controller: chatState.scrollController,
padding: EdgeInsets.only(
top: 16,
bottom:
MediaQuery.of(context).padding.bottom +
8 +
inputHeight.value,
),
reverse: true,
itemCount:
chatState.localThoughts.value.length +
(chatState.isStreaming.value ? 1 : 0),
itemBuilder: (context, index) {
if (chatState.isStreaming.value && index == 0) {
return ThoughtItem(
isStreaming: true,
streamingItems: chatState.streamingItems.value,
);
}
final thoughtIndex = chatState.isStreaming.value
? index - 1
: index;
final thought =
chatState.localThoughts.value[thoughtIndex];
return ThoughtItem(thought: thought);
},
),
),
],
),
@@ -531,35 +543,31 @@ class ThoughtChatInterface extends HookConsumerWidget {
// Bottom gradient - appears when scrolling towards newer thoughts (behind thought input)
AnimatedBuilder(
animation: chatState.bottomGradientNotifier.value,
builder:
(context, child) => Positioned(
left: 0,
right: 0,
bottom: 0,
child: Opacity(
opacity: chatState.bottomGradientNotifier.value.value,
child: Container(
height: math.min(
MediaQuery.of(context).size.height * 0.1,
128,
),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.8),
Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.0),
],
),
),
builder: (context, child) => Positioned(
left: 0,
right: 0,
bottom: 0,
child: Opacity(
opacity: chatState.bottomGradientNotifier.value.value,
child: Container(
height: math.min(MediaQuery.of(context).size.height * 0.1, 128),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.8),
Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.0),
],
),
),
),
),
),
),
// Thought Input positioned above gradient (higher z-index)
Positioned(
@@ -746,58 +754,42 @@ class ThoughtInput extends HookWidget {
maxLines: 5,
minLines: 1,
textInputAction: TextInputAction.send,
onSubmitted:
(!isStreaming && !isDisabled)
? (_) => onSend()
: null,
onSubmitted: (!isStreaming && !isDisabled)
? (_) => onSend()
: null,
),
),
IconButton(
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: [
if (services.isNotEmpty)
DropdownButtonHideUnderline(
child: DropdownButton2<String>(
value:
selectedServiceId.value.isEmpty
Row(
children: [
if (services.isNotEmpty)
SizedBox(
height: 40,
child: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
value: selectedServiceId.value.isEmpty
? null
: selectedServiceId.value,
customButton: Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
border: BoxBorder.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
borderRadius: const BorderRadius.all(
Radius.circular(16),
),
),
child: Row(
spacing: 8,
children: [
const Icon(
Symbols.network_intelligence,
size: 20,
customButton: Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
),
Text(selectedServiceId.value),
const Icon(
Symbols.keyboard_arrow_down,
size: 14,
).padding(right: 4),
],
).padding(vertical: 2, horizontal: 6),
),
items:
services
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
),
child: Row(
spacing: 8,
children: [
Text(selectedServiceId.value),
const Icon(
Symbols.keyboard_arrow_down,
size: 14,
).padding(right: 4),
],
).padding(vertical: 2, horizontal: 6),
),
items: services
.map(
(service) => DropdownMenuItem<String>(
value: service.serviceId,
@@ -807,61 +799,66 @@ class ThoughtInput extends HookWidget {
children: [
Text(
service.serviceId,
style: DefaultTextStyle.of(
context,
).style.copyWith(
fontSize: 14,
fontWeight: FontWeight.w500,
),
style: DefaultTextStyle.of(context)
.style
.copyWith(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
'Rate: ${service.billingMultiplier}x, Level: P${service.perkLevel}',
style: DefaultTextStyle.of(
context,
).style.copyWith(
fontSize: 12,
color:
Theme.of(context)
'${service.billingMultiplier}x, T${service.perkLevel}',
style: DefaultTextStyle.of(context)
.style
.copyWith(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
),
],
),
),
)
.toList(),
onChanged:
!isStreaming && !isDisabled
onChanged: !isStreaming && !isDisabled
? (value) {
if (value != null) {
selectedServiceId.value = value;
if (value != null) {
selectedServiceId.value = value;
}
}
}
: null,
hint: const Text('Select Service'),
isDense: true,
buttonStyleData: ButtonStyleData(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(16),
hint: const Text('Select Service'),
isDense: true,
buttonStyleData: ButtonStyleData(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
),
),
menuItemStyleData: MenuItemStyleData(
selectedMenuItemBuilder: (context, child) {
return child;
},
height: 56,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 8,
),
),
),
),
menuItemStyleData: MenuItemStyleData(
selectedMenuItemBuilder: (context, child) {
return child;
},
height: 56,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 8,
),
),
),
),
],
),
],
),
IconButton(
icon: Icon(isStreaming ? Symbols.stop : Icons.send),
color: Theme.of(context).colorScheme.primary,
onPressed: (!isStreaming && !isDisabled) ? onSend : null,
),
],
),
],
),
@@ -923,42 +920,40 @@ class ThoughtItem extends StatelessWidget {
}
List<Widget> buildWidgetsList() {
final List<StreamItem> items =
isStreaming
? (streamingItems ?? [])
: thought!.parts.map((p) {
String type;
switch (p.type) {
case ThinkingMessagePartType.text:
type = 'text';
break;
case ThinkingMessagePartType.functionCall:
type = 'function_call';
break;
case ThinkingMessagePartType.functionResult:
type = 'function_result';
break;
}
return StreamItem(
type,
p.type == ThinkingMessagePartType.text
? p.text ?? ''
: p.functionCall ?? p.functionResult,
);
}).toList();
final List<StreamItem> items = isStreaming
? (streamingItems ?? [])
: thought!.parts.map((p) {
String type;
switch (p.type) {
case ThinkingMessagePartType.text:
type = 'text';
break;
case ThinkingMessagePartType.functionCall:
type = 'function_call';
break;
case ThinkingMessagePartType.functionResult:
type = 'function_result';
break;
}
return StreamItem(
type,
p.type == ThinkingMessagePartType.text
? p.text ?? ''
: p.functionCall ?? p.functionResult,
);
}).toList();
final isAI =
isStreaming ||
(!isStreaming && thought!.role == ThinkingThoughtRole.assistant);
final List<Map<String, String>> proposals =
!isStreaming
? _extractProposals(
thought!.parts
.where((p) => p.type == ThinkingMessagePartType.text)
.map((p) => p.text ?? '')
.join(),
)
: [];
final List<Map<String, String>> proposals = !isStreaming
? _extractProposals(
thought!.parts
.where((p) => p.type == ThinkingMessagePartType.text)
.map((p) => p.text ?? '')
.join(),
)
: [];
final List<Widget> widgets = [];
String currentText = '';
@@ -986,10 +981,9 @@ class ThoughtItem extends StatelessWidget {
isFinish: result != null,
isStreaming: isStreaming,
callData: JsonEncoder.withIndent(' ').convert(item.data.toJson()),
resultData:
result != null
? JsonEncoder.withIndent(' ').convert(result.data.toJson())
: null,
resultData: result != null
? JsonEncoder.withIndent(' ').convert(result.data.toJson())
: null,
),
);
} else if (item.type == 'function_result') {