Skip to content

Commit 4a5eda6

Browse files
appflowyXazin
andauthored
chore: enable billing (#5779)
* chore: enable billing * chore: adjust bright mode UI * chore: show corresponding error in sidebar * chore: dismiss dialog in ai writter when hit ai response * fix: improvements from test session * chore: ai error message for database * chore: different prompt for workspace owner * feat: cancel plan survey * chore: show ai repsonse limit on chat * fix: sidebar toast after merge * chore: remove unused debug print * fix: popover close on action * fix: minor copy changes * chore: disable billing * chore: disbale billing --------- Co-authored-by: Mathias Mogensen <[email protected]>
1 parent b5d7996 commit 4a5eda6

File tree

34 files changed

+1158
-432
lines changed

34 files changed

+1158
-432
lines changed

frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,23 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
1717
required this.questionId,
1818
}) : super(ChatAIMessageState.initial(message)) {
1919
if (state.stream != null) {
20-
_subscription = state.stream!.listen((text) {
21-
if (isClosed) {
22-
return;
23-
}
24-
25-
if (text.startsWith("data:")) {
26-
add(ChatAIMessageEvent.newText(text.substring(5)));
27-
} else if (text.startsWith("error:")) {
28-
add(ChatAIMessageEvent.receiveError(text.substring(5)));
29-
}
30-
});
20+
_subscription = state.stream!.listen(
21+
onData: (text) {
22+
if (!isClosed) {
23+
add(ChatAIMessageEvent.updateText(text));
24+
}
25+
},
26+
onError: (error) {
27+
if (!isClosed) {
28+
add(ChatAIMessageEvent.receiveError(error.toString()));
29+
}
30+
},
31+
onAIResponseLimit: () {
32+
if (!isClosed) {
33+
add(const ChatAIMessageEvent.onAIResponseLimit());
34+
}
35+
},
36+
);
3137

3238
if (state.stream!.error != null) {
3339
Future.delayed(const Duration(milliseconds: 300), () {
@@ -42,11 +48,16 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
4248
(event, emit) async {
4349
await event.when(
4450
initial: () async {},
45-
newText: (newText) {
46-
emit(state.copyWith(text: state.text + newText, error: null));
51+
updateText: (newText) {
52+
emit(
53+
state.copyWith(
54+
text: newText,
55+
messageState: const MessageState.ready(),
56+
),
57+
);
4758
},
4859
receiveError: (error) {
49-
emit(state.copyWith(error: error));
60+
emit(state.copyWith(messageState: MessageState.onError(error)));
5061
},
5162
retry: () {
5263
if (questionId is! Int64) {
@@ -55,8 +66,7 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
5566
}
5667
emit(
5768
state.copyWith(
58-
retryState: const LoadingState.loading(),
59-
error: null,
69+
messageState: const MessageState.loading(),
6070
),
6171
);
6272

@@ -82,8 +92,14 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
8292
emit(
8393
state.copyWith(
8494
text: text,
85-
error: null,
86-
retryState: const LoadingState.finish(),
95+
messageState: const MessageState.ready(),
96+
),
97+
);
98+
},
99+
onAIResponseLimit: () {
100+
emit(
101+
state.copyWith(
102+
messageState: const MessageState.onAIResponseLimit(),
87103
),
88104
);
89105
},
@@ -98,34 +114,42 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
98114
return super.close();
99115
}
100116

101-
StreamSubscription<AnswerStreamElement>? _subscription;
117+
StreamSubscription<String>? _subscription;
102118
final String chatId;
103119
final Int64? questionId;
104120
}
105121

106122
@freezed
107123
class ChatAIMessageEvent with _$ChatAIMessageEvent {
108124
const factory ChatAIMessageEvent.initial() = Initial;
109-
const factory ChatAIMessageEvent.newText(String text) = _NewText;
125+
const factory ChatAIMessageEvent.updateText(String text) = _UpdateText;
110126
const factory ChatAIMessageEvent.receiveError(String error) = _ReceiveError;
111127
const factory ChatAIMessageEvent.retry() = _Retry;
112128
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
129+
const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;
113130
}
114131

115132
@freezed
116133
class ChatAIMessageState with _$ChatAIMessageState {
117134
const factory ChatAIMessageState({
118135
AnswerStream? stream,
119-
String? error,
120136
required String text,
121-
required LoadingState retryState,
137+
required MessageState messageState,
122138
}) = _ChatAIMessageState;
123139

124140
factory ChatAIMessageState.initial(dynamic text) {
125141
return ChatAIMessageState(
126142
text: text is String ? text : "",
127143
stream: text is AnswerStream ? text : null,
128-
retryState: const LoadingState.finish(),
144+
messageState: const MessageState.ready(),
129145
);
130146
}
131147
}
148+
149+
@freezed
150+
class MessageState with _$MessageState {
151+
const factory MessageState.onError(String error) = _Error;
152+
const factory MessageState.onAIResponseLimit() = _AIResponseLimit;
153+
const factory MessageState.ready() = _Ready;
154+
const factory MessageState.loading() = _Loading;
155+
}

frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -525,42 +525,84 @@ OnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {
525525
return null;
526526
}
527527

528-
typedef AnswerStreamElement = String;
529-
530528
class AnswerStream {
531529
AnswerStream() {
532530
_port.handler = _controller.add;
533531
_subscription = _controller.stream.listen(
534532
(event) {
535533
if (event.startsWith("data:")) {
536534
_hasStarted = true;
535+
final newText = event.substring(5);
536+
_text += newText;
537+
if (_onData != null) {
538+
_onData!(_text);
539+
}
537540
} else if (event.startsWith("error:")) {
538541
_error = event.substring(5);
542+
if (_onError != null) {
543+
_onError!(_error!);
544+
}
545+
} else if (event == "AI_RESPONSE_LIMIT") {
546+
if (_onAIResponseLimit != null) {
547+
_onAIResponseLimit!();
548+
}
549+
}
550+
},
551+
onDone: () {
552+
if (_onEnd != null) {
553+
_onEnd!();
554+
}
555+
},
556+
onError: (error) {
557+
if (_onError != null) {
558+
_onError!(error.toString());
539559
}
540560
},
541561
);
542562
}
543563

544564
final RawReceivePort _port = RawReceivePort();
545-
final StreamController<AnswerStreamElement> _controller =
546-
StreamController.broadcast();
547-
late StreamSubscription<AnswerStreamElement> _subscription;
565+
final StreamController<String> _controller = StreamController.broadcast();
566+
late StreamSubscription<String> _subscription;
548567
bool _hasStarted = false;
549568
String? _error;
569+
String _text = "";
570+
571+
// Callbacks
572+
void Function(String text)? _onData;
573+
void Function()? _onStart;
574+
void Function()? _onEnd;
575+
void Function(String error)? _onError;
576+
void Function()? _onAIResponseLimit;
550577

551578
int get nativePort => _port.sendPort.nativePort;
552579
bool get hasStarted => _hasStarted;
553580
String? get error => _error;
581+
String get text => _text;
554582

555583
Future<void> dispose() async {
556584
await _controller.close();
557585
await _subscription.cancel();
558586
_port.close();
559587
}
560588

561-
StreamSubscription<AnswerStreamElement> listen(
562-
void Function(AnswerStreamElement event)? onData,
563-
) {
564-
return _controller.stream.listen(onData);
589+
StreamSubscription<String> listen({
590+
void Function(String text)? onData,
591+
void Function()? onStart,
592+
void Function()? onEnd,
593+
void Function(String error)? onError,
594+
void Function()? onAIResponseLimit,
595+
}) {
596+
_onData = onData;
597+
_onStart = onStart;
598+
_onEnd = onEnd;
599+
_onError = onError;
600+
_onAIResponseLimit = onAIResponseLimit;
601+
602+
if (_onStart != null) {
603+
_onStart!();
604+
}
605+
606+
return _subscription;
565607
}
566608
}

frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import 'package:appflowy/generated/locale_keys.g.dart';
22
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
3-
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
43
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
54
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
65
import 'package:easy_localization/easy_localization.dart';
@@ -38,25 +37,34 @@ class ChatAITextMessageWidget extends StatelessWidget {
3837
)..add(const ChatAIMessageEvent.initial()),
3938
child: BlocBuilder<ChatAIMessageBloc, ChatAIMessageState>(
4039
builder: (context, state) {
41-
if (state.error != null) {
42-
return StreamingError(
43-
onRetryPressed: () {
44-
context.read<ChatAIMessageBloc>().add(
45-
const ChatAIMessageEvent.retry(),
46-
);
47-
},
48-
);
49-
}
50-
51-
if (state.retryState == const LoadingState.loading()) {
52-
return const ChatAILoading();
53-
}
54-
55-
if (state.text.isEmpty) {
56-
return const ChatAILoading();
57-
} else {
58-
return AIMarkdownText(markdown: state.text);
59-
}
40+
return state.messageState.when(
41+
onError: (err) {
42+
return StreamingError(
43+
onRetryPressed: () {
44+
context.read<ChatAIMessageBloc>().add(
45+
const ChatAIMessageEvent.retry(),
46+
);
47+
},
48+
);
49+
},
50+
onAIResponseLimit: () {
51+
return FlowyText(
52+
LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(),
53+
maxLines: 10,
54+
lineHeight: 1.5,
55+
);
56+
},
57+
ready: () {
58+
if (state.text.isEmpty) {
59+
return const ChatAILoading();
60+
} else {
61+
return AIMarkdownText(markdown: state.text);
62+
}
63+
},
64+
loading: () {
65+
return const ChatAILoading();
66+
},
67+
);
6068
},
6169
),
6270
);

frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter/services.dart';
3+
14
import 'package:appflowy/generated/flowy_svgs.g.dart';
25
import 'package:appflowy/generated/locale_keys.g.dart';
36
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
@@ -7,19 +10,18 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller_build
710
import 'package:appflowy/plugins/database/application/database_controller.dart';
811
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart';
912
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart';
13+
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
1014
import 'package:appflowy/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart';
1115
import 'package:appflowy/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart';
1216
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
13-
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
1417
import 'package:appflowy/workspace/presentation/home/toast.dart';
18+
import 'package:appflowy_backend/dispatch/error.dart';
1519
import 'package:easy_localization/easy_localization.dart';
1620
import 'package:flowy_infra/size.dart';
1721
import 'package:flowy_infra/theme_extension.dart';
1822
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
1923
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
2024
import 'package:flowy_infra_ui/widget/spacing.dart';
21-
import 'package:flutter/material.dart';
22-
import 'package:flutter/services.dart';
2325
import 'package:flutter_bloc/flutter_bloc.dart';
2426

2527
abstract class IEditableSummaryCellSkin {
@@ -149,7 +151,22 @@ class SummaryCellAccessory extends StatelessWidget {
149151
rowId: rowId,
150152
fieldId: fieldId,
151153
),
152-
child: BlocBuilder<SummaryRowBloc, SummaryRowState>(
154+
child: BlocConsumer<SummaryRowBloc, SummaryRowState>(
155+
listenWhen: (previous, current) {
156+
return previous.error != current.error;
157+
},
158+
listener: (context, state) {
159+
if (state.error != null) {
160+
if (state.error!.isAIResponseLimitExceeded) {
161+
showSnackBarMessage(
162+
context,
163+
LocaleKeys.sideBar_aiResponseLimitDialogTitle.tr(),
164+
);
165+
} else {
166+
showSnackBarMessage(context, state.error!.msg);
167+
}
168+
}
169+
},
153170
builder: (context, state) {
154171
return const Row(
155172
children: [SummaryButton(), HSpace(6), CopyButton()],
@@ -169,13 +186,13 @@ class SummaryButton extends StatelessWidget {
169186
Widget build(BuildContext context) {
170187
return BlocBuilder<SummaryRowBloc, SummaryRowState>(
171188
builder: (context, state) {
172-
return state.loadingState.map(
173-
loading: (_) {
189+
return state.loadingState.when(
190+
loading: () {
174191
return const Center(
175192
child: CircularProgressIndicator.adaptive(),
176193
);
177194
},
178-
finish: (_) {
195+
finish: () {
179196
return FlowyTooltip(
180197
message: LocaleKeys.tooltip_aiGenerate.tr(),
181198
child: Container(

0 commit comments

Comments
 (0)