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",
"descendingOrder": "Descending Order",
"selectDate": "Select Date",
"pinnedPosts": "Pinned Posts"
"pinnedPosts": "Pinned Posts",
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders"
}

View File

@@ -1090,5 +1090,6 @@
"thoughtNewConversation": "开始新对话",
"thoughtParseError": "解析 AI 响应失败",
"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: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<bool> thoughtAvailableStaus(Ref ref) async {
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get('/insight/billing/status');
return response.data['status'] == 'ok';
}
@riverpod
Future<List<SnThinkingThought>> 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,
),
),
),
);

View File

@@ -6,6 +6,25 @@ part of 'think.dart';
// 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';
/// Copied from Dart SDK

View File

@@ -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<Map<String, dynamic>> 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,
),
),
);
}

View File

@@ -359,6 +359,7 @@ class ThoughtChatInterface extends HookConsumerWidget {
final String? initialTopic;
final List<Map<String, dynamic>> attachedMessages;
final List<String> 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<Map<String, dynamic>>? attachedMessages;
final List<String>? 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,
),
],
),