🐛 Bug fixes for the AI thought
This commit is contained in:
@@ -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?",
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 [],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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}';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user