Thinking billing check

This commit is contained in:
2025-11-16 01:18:20 +08:00
parent a713b30d93
commit a9fd75cc45
6 changed files with 190 additions and 25 deletions

View File

@@ -1323,5 +1323,6 @@
"popularity": "Popularity", "popularity": "Popularity",
"descendingOrder": "Descending Order", "descendingOrder": "Descending Order",
"selectDate": "Select Date", "selectDate": "Select Date",
"pinnedPosts": "Pinned Posts" "pinnedPosts": "Pinned Posts",
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders"
} }

View File

@@ -1090,5 +1090,6 @@
"thoughtNewConversation": "开始新对话", "thoughtNewConversation": "开始新对话",
"thoughtParseError": "解析 AI 响应失败", "thoughtParseError": "解析 AI 响应失败",
"aiThought": "寻思", "aiThought": "寻思",
"aiThoughtTitle": "让 SN 酱寻思寻思" "aiThoughtTitle": "让 SN 酱寻思寻思",
"thoughtUnpaidHint": "寻思因为有未支付的订单而被禁用"
} }

View File

@@ -6,6 +6,7 @@ import "package:riverpod_annotation/riverpod_annotation.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:island/models/thought.dart"; import "package:island/models/thought.dart";
import "package:island/pods/network.dart"; import "package:island/pods/network.dart";
import "package:island/widgets/alert.dart";
import "package:island/widgets/app_scaffold.dart"; import "package:island/widgets/app_scaffold.dart";
import "package:island/widgets/response.dart"; import "package:island/widgets/response.dart";
import "package:island/widgets/thought/thought_sequence_list.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'; part 'think.g.dart';
@riverpod
Future<bool> thoughtAvailableStaus(Ref ref) async {
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get('/insight/billing/status');
return response.data['status'] == 'ok';
}
@riverpod @riverpod
Future<List<SnThinkingThought>> thoughtSequence( Future<List<SnThinkingThought>> thoughtSequence(
Ref ref, Ref ref,
@@ -47,6 +55,8 @@ class ThoughtScreen extends HookConsumerWidget {
? initialThoughts.first.sequence!.topic ? initialThoughts.first.sequence!.topic
: 'aiThought'.tr(); : 'aiThought'.tr();
final statusAsync = ref.watch(thoughtAvailableStausProvider);
return AppScaffold( return AppScaffold(
isNoBackground: false, isNoBackground: false,
appBar: AppBar( appBar: AppBar(
@@ -71,23 +81,91 @@ class ThoughtScreen extends HookConsumerWidget {
const Gap(8), const Gap(8),
], ],
), ),
body: thoughts.when( body: statusAsync.maybeWhen(
data: data: (status) {
(thoughtList) => ThoughtChatInterface( final retry = useMemoized(
initialThoughts: thoughtList, () => () async {
initialTopic: initialTopic, showLoadingModal(context);
), try {
loading: () => const Center(child: CircularProgressIndicator()), await ref
error: .read(apiClientProvider)
(error, _) => ResponseErrorWidget( .post('/insight/billing/retry');
error: error, showSnackBar('Retried billing process');
onRetry: ref.invalidate(thoughtAvailableStausProvider);
() => } catch (e) {
selectedSequenceId.value != null showSnackBar('Failed to retry billing');
? ref.invalidate( }
thoughtSequenceProvider(selectedSequenceId.value!), hideLoadingModal(context);
) },
: null, [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,
),
), ),
), ),
); );

View File

@@ -6,6 +6,25 @@ part of 'think.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$thoughtAvailableStausHash() =>
r'720e04e56bff8c4d4ca6854ce997da4e7926c84c';
/// See also [thoughtAvailableStaus].
@ProviderFor(thoughtAvailableStaus)
final thoughtAvailableStausProvider = AutoDisposeFutureProvider<bool>.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<bool>;
String _$thoughtSequenceHash() => r'2a93c0a04f9a720ba474c02a36502940fb7f3ed7'; String _$thoughtSequenceHash() => r'2a93c0a04f9a720ba474c02a36502940fb7f3ed7';
/// Copied from Dart SDK /// Copied from Dart SDK

View File

@@ -1,8 +1,13 @@
import "package:easy_localization/easy_localization.dart"; import "package:easy_localization/easy_localization.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.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/content/sheet.dart";
import "package:island/widgets/thought/thought_shared.dart"; import "package:island/widgets/thought/thought_shared.dart";
import "package:material_symbols_icons/material_symbols_icons.dart";
class ThoughtSheet extends HookConsumerWidget { class ThoughtSheet extends HookConsumerWidget {
final List<Map<String, dynamic>> attachedMessages; final List<Map<String, dynamic>> attachedMessages;
@@ -39,11 +44,62 @@ class ThoughtSheet extends HookConsumerWidget {
attachedPosts: attachedPosts, attachedPosts: attachedPosts,
); );
final statusAsync = ref.watch(thoughtAvailableStausProvider);
return SheetScaffold( return SheetScaffold(
titleText: chatState.currentTopic.value ?? 'aiThought'.tr(), titleText: chatState.currentTopic.value ?? 'aiThought'.tr(),
child: ThoughtChatInterface( child: statusAsync.maybeWhen(
attachedMessages: attachedMessages, data: (status) {
attachedPosts: attachedPosts, 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,
),
), ),
); );
} }

View File

@@ -359,6 +359,7 @@ class ThoughtChatInterface extends HookConsumerWidget {
final String? initialTopic; final String? initialTopic;
final List<Map<String, dynamic>> attachedMessages; final List<Map<String, dynamic>> attachedMessages;
final List<String> attachedPosts; final List<String> attachedPosts;
final bool isDisabled;
const ThoughtChatInterface({ const ThoughtChatInterface({
super.key, super.key,
@@ -366,6 +367,7 @@ class ThoughtChatInterface extends HookConsumerWidget {
this.initialTopic, this.initialTopic,
this.attachedMessages = const [], this.attachedMessages = const [],
this.attachedPosts = const [], this.attachedPosts = const [],
this.isDisabled = false,
}); });
@override @override
@@ -466,6 +468,7 @@ class ThoughtChatInterface extends HookConsumerWidget {
onSend: chatState.sendMessage, onSend: chatState.sendMessage,
attachedMessages: attachedMessages, attachedMessages: attachedMessages,
attachedPosts: attachedPosts, attachedPosts: attachedPosts,
isDisabled: isDisabled,
), ),
), ),
), ),
@@ -509,6 +512,7 @@ class ThoughtInput extends HookWidget {
final VoidCallback onSend; final VoidCallback onSend;
final List<Map<String, dynamic>>? attachedMessages; final List<Map<String, dynamic>>? attachedMessages;
final List<String>? attachedPosts; final List<String>? attachedPosts;
final bool isDisabled;
const ThoughtInput({ const ThoughtInput({
super.key, super.key,
@@ -517,6 +521,7 @@ class ThoughtInput extends HookWidget {
required this.onSend, required this.onSend,
this.attachedMessages, this.attachedMessages,
this.attachedPosts, this.attachedPosts,
this.isDisabled = false,
}); });
@override @override
@@ -607,11 +612,13 @@ class ThoughtInput extends HookWidget {
child: TextField( child: TextField(
controller: messageController, controller: messageController,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
enabled: !isStreaming, enabled: !isStreaming && !isDisabled,
decoration: InputDecoration( decoration: InputDecoration(
hintText: hintText:
(isStreaming (isStreaming
? 'thoughtStreamingHint' ? 'thoughtStreamingHint'
: isDisabled
? 'thoughtUnpaidHint'.tr()
: 'thoughtInputHint') : 'thoughtInputHint')
.tr(), .tr(),
border: InputBorder.none, border: InputBorder.none,
@@ -624,13 +631,16 @@ class ThoughtInput extends HookWidget {
maxLines: 5, maxLines: 5,
minLines: 1, minLines: 1,
textInputAction: TextInputAction.send, textInputAction: TextInputAction.send,
onSubmitted: (_) => onSend(), onSubmitted:
(!isStreaming && !isDisabled)
? (_) => onSend()
: null,
), ),
), ),
IconButton( IconButton(
icon: Icon(isStreaming ? Symbols.stop : Icons.send), icon: Icon(isStreaming ? Symbols.stop : Icons.send),
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
onPressed: onSend, onPressed: (!isStreaming && !isDisabled) ? onSend : null,
), ),
], ],
), ),