diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 08e73200..b5410d8c 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -1323,5 +1323,6 @@ "popularity": "Popularity", "descendingOrder": "Descending Order", "selectDate": "Select Date", - "pinnedPosts": "Pinned Posts" + "pinnedPosts": "Pinned Posts", + "thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders" } diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 9637c7e9..24d5208c 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -1090,5 +1090,6 @@ "thoughtNewConversation": "开始新对话", "thoughtParseError": "解析 AI 响应失败", "aiThought": "寻思", - "aiThoughtTitle": "让 SN 酱寻思寻思" + "aiThoughtTitle": "让 SN 酱寻思寻思", + "thoughtUnpaidHint": "寻思因为有未支付的订单而被禁用" } diff --git a/lib/screens/thought/think.dart b/lib/screens/thought/think.dart index 0e100ec0..36c9684c 100644 --- a/lib/screens/thought/think.dart +++ b/lib/screens/thought/think.dart @@ -6,6 +6,7 @@ import "package:riverpod_annotation/riverpod_annotation.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:island/models/thought.dart"; import "package:island/pods/network.dart"; +import "package:island/widgets/alert.dart"; import "package:island/widgets/app_scaffold.dart"; import "package:island/widgets/response.dart"; import "package:island/widgets/thought/thought_sequence_list.dart"; @@ -14,6 +15,13 @@ import "package:material_symbols_icons/material_symbols_icons.dart"; part 'think.g.dart'; +@riverpod +Future thoughtAvailableStaus(Ref ref) async { + final apiClient = ref.watch(apiClientProvider); + final response = await apiClient.get('/insight/billing/status'); + return response.data['status'] == 'ok'; +} + @riverpod Future> thoughtSequence( Ref ref, @@ -47,6 +55,8 @@ class ThoughtScreen extends HookConsumerWidget { ? initialThoughts.first.sequence!.topic : 'aiThought'.tr(); + final statusAsync = ref.watch(thoughtAvailableStausProvider); + return AppScaffold( isNoBackground: false, appBar: AppBar( @@ -71,23 +81,91 @@ class ThoughtScreen extends HookConsumerWidget { const Gap(8), ], ), - body: thoughts.when( - data: - (thoughtList) => ThoughtChatInterface( - initialThoughts: thoughtList, - initialTopic: initialTopic, - ), - loading: () => const Center(child: CircularProgressIndicator()), - error: - (error, _) => ResponseErrorWidget( - error: error, - onRetry: - () => - selectedSequenceId.value != null - ? ref.invalidate( - thoughtSequenceProvider(selectedSequenceId.value!), - ) - : null, + body: statusAsync.maybeWhen( + data: (status) { + final retry = useMemoized( + () => () async { + showLoadingModal(context); + try { + await ref + .read(apiClientProvider) + .post('/insight/billing/retry'); + showSnackBar('Retried billing process'); + ref.invalidate(thoughtAvailableStausProvider); + } catch (e) { + showSnackBar('Failed to retry billing'); + } + hideLoadingModal(context); + }, + [context, ref], + ); + + final thoughtsBody = thoughts.when( + data: + (thoughtList) => ThoughtChatInterface( + initialThoughts: thoughtList, + initialTopic: initialTopic, + isDisabled: !status, + ), + loading: () => const Center(child: CircularProgressIndicator()), + error: + (error, _) => ResponseErrorWidget( + error: error, + onRetry: + () => + selectedSequenceId.value != null + ? ref.invalidate( + thoughtSequenceProvider( + selectedSequenceId.value!, + ), + ) + : null, + ), + ); + return status + ? thoughtsBody + : 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()), + ), + ], + ), + Expanded(child: thoughtsBody), + ], + ); + }, + orElse: + () => thoughts.when( + data: + (thoughtList) => ThoughtChatInterface( + initialThoughts: thoughtList, + initialTopic: initialTopic, + ), + loading: () => const Center(child: CircularProgressIndicator()), + error: + (error, _) => ResponseErrorWidget( + error: error, + onRetry: + () => + selectedSequenceId.value != null + ? ref.invalidate( + thoughtSequenceProvider( + selectedSequenceId.value!, + ), + ) + : null, + ), ), ), ); diff --git a/lib/screens/thought/think.g.dart b/lib/screens/thought/think.g.dart index 0ba5d80e..318d97a7 100644 --- a/lib/screens/thought/think.g.dart +++ b/lib/screens/thought/think.g.dart @@ -6,6 +6,25 @@ part of 'think.dart'; // RiverpodGenerator // ************************************************************************** +String _$thoughtAvailableStausHash() => + r'720e04e56bff8c4d4ca6854ce997da4e7926c84c'; + +/// See also [thoughtAvailableStaus]. +@ProviderFor(thoughtAvailableStaus) +final thoughtAvailableStausProvider = AutoDisposeFutureProvider.internal( + thoughtAvailableStaus, + name: r'thoughtAvailableStausProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$thoughtAvailableStausHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef ThoughtAvailableStausRef = AutoDisposeFutureProviderRef; String _$thoughtSequenceHash() => r'2a93c0a04f9a720ba474c02a36502940fb7f3ed7'; /// Copied from Dart SDK diff --git a/lib/screens/thought/think_sheet.dart b/lib/screens/thought/think_sheet.dart index f14effa6..f2b9572b 100644 --- a/lib/screens/thought/think_sheet.dart +++ b/lib/screens/thought/think_sheet.dart @@ -1,8 +1,13 @@ import "package:easy_localization/easy_localization.dart"; import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:island/pods/network.dart"; +import "package:island/screens/thought/think.dart"; +import "package:island/widgets/alert.dart"; import "package:island/widgets/content/sheet.dart"; import "package:island/widgets/thought/thought_shared.dart"; +import "package:material_symbols_icons/material_symbols_icons.dart"; class ThoughtSheet extends HookConsumerWidget { final List> attachedMessages; @@ -39,11 +44,62 @@ class ThoughtSheet extends HookConsumerWidget { attachedPosts: attachedPosts, ); + final statusAsync = ref.watch(thoughtAvailableStausProvider); + return SheetScaffold( titleText: chatState.currentTopic.value ?? 'aiThought'.tr(), - child: ThoughtChatInterface( - attachedMessages: attachedMessages, - attachedPosts: attachedPosts, + child: statusAsync.maybeWhen( + data: (status) { + final retry = useMemoized( + () => () async { + showLoadingModal(context); + try { + await ref + .read(apiClientProvider) + .post('/insight/billing/retry'); + showSnackBar('Retried billing process'); + ref.invalidate(thoughtAvailableStausProvider); + } catch (e) { + showSnackBar('Failed to retry billing'); + } + hideLoadingModal(context); + }, + [context, ref], + ); + + final chatInterface = ThoughtChatInterface( + attachedMessages: attachedMessages, + attachedPosts: attachedPosts, + isDisabled: !status, + ); + 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()), + ), + ], + ), + Expanded(child: chatInterface), + ], + ); + }, + orElse: + () => ThoughtChatInterface( + attachedMessages: attachedMessages, + attachedPosts: attachedPosts, + ), ), ); } diff --git a/lib/widgets/thought/thought_shared.dart b/lib/widgets/thought/thought_shared.dart index ed86199e..c259cec9 100644 --- a/lib/widgets/thought/thought_shared.dart +++ b/lib/widgets/thought/thought_shared.dart @@ -359,6 +359,7 @@ class ThoughtChatInterface extends HookConsumerWidget { final String? initialTopic; final List> attachedMessages; final List attachedPosts; + final bool isDisabled; const ThoughtChatInterface({ super.key, @@ -366,6 +367,7 @@ class ThoughtChatInterface extends HookConsumerWidget { this.initialTopic, this.attachedMessages = const [], this.attachedPosts = const [], + this.isDisabled = false, }); @override @@ -466,6 +468,7 @@ class ThoughtChatInterface extends HookConsumerWidget { onSend: chatState.sendMessage, attachedMessages: attachedMessages, attachedPosts: attachedPosts, + isDisabled: isDisabled, ), ), ), @@ -509,6 +512,7 @@ class ThoughtInput extends HookWidget { final VoidCallback onSend; final List>? attachedMessages; final List? attachedPosts; + final bool isDisabled; const ThoughtInput({ super.key, @@ -517,6 +521,7 @@ class ThoughtInput extends HookWidget { required this.onSend, this.attachedMessages, this.attachedPosts, + this.isDisabled = false, }); @override @@ -607,11 +612,13 @@ class ThoughtInput extends HookWidget { child: TextField( controller: messageController, keyboardType: TextInputType.multiline, - enabled: !isStreaming, + enabled: !isStreaming && !isDisabled, decoration: InputDecoration( hintText: (isStreaming ? 'thoughtStreamingHint' + : isDisabled + ? 'thoughtUnpaidHint'.tr() : 'thoughtInputHint') .tr(), border: InputBorder.none, @@ -624,13 +631,16 @@ class ThoughtInput extends HookWidget { maxLines: 5, minLines: 1, textInputAction: TextInputAction.send, - onSubmitted: (_) => onSend(), + onSubmitted: + (!isStreaming && !isDisabled) + ? (_) => onSend() + : null, ), ), IconButton( icon: Icon(isStreaming ? Symbols.stop : Icons.send), color: Theme.of(context).colorScheme.primary, - onPressed: onSend, + onPressed: (!isStreaming && !isDisabled) ? onSend : null, ), ], ),