chore: retry load image (#7179)

* chore: retry load image

* feat: support retry count and retry duration in network image

* chore: use loading builder in network image

* feat: support retry logic in network image

* feat: disable image menu when loading image

* chore: error prompt

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
Nathan.fooo 2025-01-10 09:43:18 +08:00 committed by GitHub
parent 99a4e330e8
commit 790d5612f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 337 additions and 92 deletions

View File

@ -591,10 +591,14 @@ CustomImageBlockComponentBuilder _buildCustomImageBlockComponentBuilder(
return CustomImageBlockComponentBuilder(
configuration: configuration,
showMenu: true,
menuBuilder: (node, state) => Positioned(
menuBuilder: (node, state, imageStateNotifier) => Positioned(
top: 10,
right: 10,
child: ImageMenu(node: node, state: state),
child: ImageMenu(
node: node,
state: state,
imageStateNotifier: imageStateNotifier,
),
),
);
}

View File

@ -81,6 +81,7 @@ Node customImageNode({
typedef CustomImageBlockComponentMenuBuilder = Widget Function(
Node node,
CustomImageBlockComponentState state,
ValueNotifier<ResizableImageState> imageStateNotifier,
);
class CustomImageBlockComponentBuilder extends BlockComponentBuilder {
@ -149,6 +150,8 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
late final editorState = Provider.of<EditorState>(context, listen: false);
final showActionsNotifier = ValueNotifier<bool>(false);
final imageStateNotifier =
ValueNotifier<ResizableImageState>(ResizableImageState.loading);
bool alwaysShowMenu = false;
@ -185,6 +188,7 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
editable: editorState.editable,
alignment: alignment,
type: imageType,
onStateChange: (state) => imageStateNotifier.value = state,
onDoubleTap: () => showDialog(
context: context,
builder: (_) => InteractiveImageViewer(
@ -260,7 +264,7 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
child: child!,
),
if (value && url.isNotEmpty == true)
widget.menuBuilder!(widget.node, this),
widget.menuBuilder!(widget.node, this, imageStateNotifier),
],
);
},

View File

@ -5,6 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
@ -25,10 +26,12 @@ class ImageMenu extends StatefulWidget {
super.key,
required this.node,
required this.state,
required this.imageStateNotifier,
});
final Node node;
final CustomImageBlockComponentState state;
final ValueNotifier<ResizableImageState> imageStateNotifier;
@override
State<ImageMenu> createState() => _ImageMenuState();
@ -40,46 +43,55 @@ class _ImageMenuState extends State<ImageMenu> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
height: 32,
decoration: BoxDecoration(
color: theme.cardColor,
boxShadow: [
BoxShadow(
blurRadius: 5,
spreadRadius: 1,
color: Colors.black.withOpacity(0.1),
return ValueListenableBuilder<ResizableImageState>(
valueListenable: widget.imageStateNotifier,
builder: (_, state, child) {
if (state == ResizableImageState.loading) {
return const SizedBox.shrink();
}
return Container(
height: 32,
decoration: BoxDecoration(
color: theme.cardColor,
boxShadow: [
BoxShadow(
blurRadius: 5,
spreadRadius: 1,
color: Colors.black.withOpacity(0.1),
),
],
borderRadius: BorderRadius.circular(4.0),
),
],
borderRadius: BorderRadius.circular(4.0),
),
child: Row(
children: [
const HSpace(4),
MenuBlockButton(
tooltip: LocaleKeys.document_imageBlock_openFullScreen.tr(),
iconData: FlowySvgs.full_view_s,
onTap: openFullScreen,
child: Row(
children: [
const HSpace(4),
MenuBlockButton(
tooltip: LocaleKeys.document_imageBlock_openFullScreen.tr(),
iconData: FlowySvgs.full_view_s,
onTap: openFullScreen,
),
const HSpace(4),
MenuBlockButton(
tooltip: LocaleKeys.editor_copy.tr(),
iconData: FlowySvgs.copy_s,
onTap: copyImageLink,
),
const HSpace(4),
if (widget.state.editorState.editable) ...[
_ImageAlignButton(node: widget.node, state: widget.state),
const _Divider(),
MenuBlockButton(
tooltip: LocaleKeys.button_delete.tr(),
iconData: FlowySvgs.trash_s,
onTap: deleteImage,
),
const HSpace(4),
],
],
),
const HSpace(4),
MenuBlockButton(
tooltip: LocaleKeys.editor_copy.tr(),
iconData: FlowySvgs.copy_s,
onTap: copyImageLink,
),
const HSpace(4),
if (widget.state.editorState.editable) ...[
_ImageAlignButton(node: widget.node, state: widget.state),
const _Divider(),
MenuBlockButton(
tooltip: LocaleKeys.button_delete.tr(),
iconData: FlowySvgs.trash_s,
onTap: deleteImage,
),
const HSpace(4),
],
],
),
);
},
);
}

View File

@ -14,6 +14,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:string_validator/string_validator.dart';
enum ResizableImageState {
loading,
loaded,
failed,
}
class ResizableImage extends StatefulWidget {
const ResizableImage({
super.key,
@ -25,6 +31,7 @@ class ResizableImage extends StatefulWidget {
required this.src,
this.height,
this.onDoubleTap,
this.onStateChange,
});
final String src;
@ -34,6 +41,7 @@ class ResizableImage extends StatefulWidget {
final Alignment alignment;
final bool editable;
final VoidCallback? onDoubleTap;
final ValueChanged<ResizableImageState>? onStateChange;
final void Function(double width) onResize;
@ -96,11 +104,29 @@ class _ResizableImageState extends State<ResizableImage> {
url: widget.src,
width: imageWidth - moveDistance,
userProfilePB: _userProfilePB,
progressIndicatorBuilder: (context, _, __) => _buildLoading(context),
errorWidgetBuilder: (_, __, error) => _ImageLoadFailedWidget(
width: imageWidth,
error: error,
),
onImageLoaded: (isImageInCache) {
if (isImageInCache) {
widget.onStateChange?.call(ResizableImageState.loaded);
}
},
progressIndicatorBuilder: (context, _, progress) {
if (progress.totalSize != null) {
if (progress.progress == 1) {
widget.onStateChange?.call(ResizableImageState.loaded);
} else {
widget.onStateChange?.call(ResizableImageState.loading);
}
}
return _buildLoading(context);
},
errorWidgetBuilder: (_, __, error) {
widget.onStateChange?.call(ResizableImageState.failed);
return _ImageLoadFailedWidget(
width: imageWidth,
error: error,
);
},
);
child = _cacheImage!;

View File

@ -1,17 +1,21 @@
import 'dart:convert';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/custom_image_cache_manager.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:string_validator/string_validator.dart';
/// This widget handles the downloading and caching of either internal or network images.
///
/// It will append the access token to the URL if the URL is internal.
class FlowyNetworkImage extends StatelessWidget {
class FlowyNetworkImage extends StatefulWidget {
const FlowyNetworkImage({
super.key,
this.userProfilePB,
@ -21,57 +25,233 @@ class FlowyNetworkImage extends StatelessWidget {
this.progressIndicatorBuilder,
this.errorWidgetBuilder,
required this.url,
this.maxRetries = 3,
this.retryDuration = const Duration(seconds: 6),
this.retryErrorCodes = const {404},
this.onImageLoaded,
});
final UserProfilePB? userProfilePB;
/// The URL of the image.
final String url;
/// The width of the image.
final double? width;
/// The height of the image.
final double? height;
/// The fit of the image.
final BoxFit fit;
/// The user profile.
///
/// If the userProfilePB is not null, the image will be downloaded with the access token.
final UserProfilePB? userProfilePB;
/// The progress indicator builder.
final ProgressIndicatorBuilder? progressIndicatorBuilder;
/// The error widget builder.
final LoadingErrorWidgetBuilder? errorWidgetBuilder;
/// Retry loading the image if it fails.
final int maxRetries;
/// Retry duration
final Duration retryDuration;
/// Retry error codes.
final Set<int> retryErrorCodes;
final void Function(bool isImageInCache)? onImageLoaded;
@override
FlowyNetworkImageState createState() => FlowyNetworkImageState();
}
class FlowyNetworkImageState extends State<FlowyNetworkImage> {
final manager = CustomImageCacheManager();
final retryCounter = _FlowyNetworkRetryCounter();
// This is used to clear the retry count when the widget is disposed in case of the url is the same.
String? retryTag;
@override
void initState() {
super.initState();
assert(isURL(widget.url));
if (widget.url.isAppFlowyCloudUrl) {
assert(
widget.userProfilePB != null && widget.userProfilePB!.token.isNotEmpty,
);
}
retryTag = retryCounter.add(widget.url);
manager.getFileFromCache(widget.url).then((file) {
widget.onImageLoaded?.call(
file != null &&
file.file.path.isNotEmpty &&
file.originalUrl == widget.url,
);
});
}
@override
void reassemble() {
super.reassemble();
if (retryTag != null) {
retryCounter.clear(retryTag!);
}
}
@override
void dispose() {
if (retryTag != null) {
retryCounter.clear(retryTag!);
}
super.dispose();
}
@override
Widget build(BuildContext context) {
assert(isURL(url));
return ListenableBuilder(
listenable: retryCounter,
builder: (context, child) {
final retryCount = retryCounter.getRetryCount(widget.url);
return CachedNetworkImage(
key: ValueKey('${widget.url}_$retryCount'),
cacheManager: manager,
httpHeaders: _buildRequestHeader(),
imageUrl: widget.url,
fit: widget.fit,
width: widget.width,
height: widget.height,
progressIndicatorBuilder: widget.progressIndicatorBuilder,
errorWidget: _errorWidgetBuilder,
errorListener: (value) async {
Log.error(
'Unable to load image: ${value.toString()} - retryCount: $retryCount',
);
if (url.isAppFlowyCloudUrl) {
assert(userProfilePB != null && userProfilePB!.token.isNotEmpty);
}
final manager = CustomImageCacheManager();
return CachedNetworkImage(
cacheManager: manager,
httpHeaders: _header(),
imageUrl: url,
fit: fit,
width: width,
height: height,
progressIndicatorBuilder: progressIndicatorBuilder,
errorWidget: (context, url, error) =>
errorWidgetBuilder?.call(context, url, error) ??
const SizedBox.shrink(),
errorListener: (value) {
// try to clear the image cache.
manager.removeFile(url);
Log.error(value.toString());
// clear the cache and retry
await manager.removeFile(widget.url);
_retryLoadImage();
},
);
},
);
}
Map<String, String> _header() {
/// if the error is 404 and the retry count is less than the max retries, it return a loading indicator.
Widget _errorWidgetBuilder(BuildContext context, String url, Object error) {
final retryCount = retryCounter.getRetryCount(url);
if (error is HttpExceptionWithStatus) {
if (widget.retryErrorCodes.contains(error.statusCode) &&
retryCount < widget.maxRetries) {
final fakeDownloadProgress = DownloadProgress(url, null, 0);
return widget.progressIndicatorBuilder?.call(
context,
url,
fakeDownloadProgress,
) ??
const Center(
child: _SensitiveContent(),
);
}
if (error.statusCode == 422) {
// Unprocessable Entity: Used when the server understands the request but cannot process it due to
//semantic issues (e.g., sensitive keywords).
return const _SensitiveContent();
}
}
return widget.errorWidgetBuilder?.call(context, url, error) ??
const SizedBox.shrink();
}
Map<String, String> _buildRequestHeader() {
final header = <String, String>{};
final token = userProfilePB?.token;
final token = widget.userProfilePB?.token;
if (token != null) {
try {
final decodedToken = jsonDecode(token);
header['Authorization'] = 'Bearer ${decodedToken['access_token']}';
} catch (e) {
Log.error('unable to decode token: $e');
Log.error('Unable to decode token: $e');
}
}
return header;
}
void _retryLoadImage() {
final retryCount = retryCounter.getRetryCount(widget.url);
if (retryCount < widget.maxRetries) {
Future.delayed(widget.retryDuration, () {
Log.debug(
'Retry load image: ${widget.url}, retry count: $retryCount',
);
// Increment the retry count for the URL to trigger the image rebuild.
retryCounter.increment(widget.url);
});
}
}
}
/// This class is used to count the number of retries for a given URL.
class _FlowyNetworkRetryCounter with ChangeNotifier {
_FlowyNetworkRetryCounter._();
factory _FlowyNetworkRetryCounter() => _instance;
static final _instance = _FlowyNetworkRetryCounter._();
final Map<String, int> _values = <String, int>{};
Map<String, int> get values => {..._values};
/// Get the retry count for a given URL.
int getRetryCount(String url) => _values[url] ?? 0;
/// Add a new URL to the retry counter. Don't call notifyListeners() here.
///
/// This function will return a tag, use it to clear the retry count.
/// Because the url may be the same, we need to add a unique tag to the url.
String add(String url) {
_values.putIfAbsent(url, () => 0);
return url + uuid();
}
/// Increment the retry count for a given URL.
void increment(String url) {
final count = _values[url];
if (count == null) {
_values[url] = 1;
} else {
_values[url] = count + 1;
}
notifyListeners();
}
/// Clear the retry count for a given URL.
void clear(String tag) {
_values.remove(tag);
}
/// Reset the retry counter.
void reset() {
_values.clear();
}
}
class _SensitiveContent extends StatelessWidget {
const _SensitiveContent();
@override
Widget build(BuildContext context) {
return FlowyText(LocaleKeys.ai_sensitiveKeyword.tr());
}
}

View File

@ -3068,5 +3068,8 @@
"answerFive": "Unsatisfied"
}
}
},
"ai":{
"sensitiveKeyword": "Image generation failed due to sensitive content. Please rephrase your input and try again"
}
}

View File

@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b47a635cfc9e42030706aebd47e40b95361cbdee#b47a635cfc9e42030706aebd47e40b95361cbdee"
dependencies = [
"anyhow",
"bincode",
@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b47a635cfc9e42030706aebd47e40b95361cbdee#b47a635cfc9e42030706aebd47e40b95361cbdee"
dependencies = [
"anyhow",
"bytes",
@ -786,7 +786,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b47a635cfc9e42030706aebd47e40b95361cbdee#b47a635cfc9e42030706aebd47e40b95361cbdee"
dependencies = [
"again",
"anyhow",
@ -843,7 +843,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b47a635cfc9e42030706aebd47e40b95361cbdee#b47a635cfc9e42030706aebd47e40b95361cbdee"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -856,7 +856,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b47a635cfc9e42030706aebd47e40b95361cbdee#b47a635cfc9e42030706aebd47e40b95361cbdee"
dependencies = [
"futures-channel",
"futures-util",
@ -1128,7 +1128,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b47a635cfc9e42030706aebd47e40b95361cbdee#b47a635cfc9e42030706aebd47e40b95361cbdee"
dependencies = [
"anyhow",
"bincode",
@ -1153,7 +1153,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b47a635cfc9e42030706aebd47e40b95361cbdee#b47a635cfc9e42030706aebd47e40b95361cbdee"
dependencies = [
"anyhow",
"async-trait",
@ -1400,7 +1400,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf 0.8.0",
"phf 0.11.2",
"smallvec",
]
@ -1548,7 +1548,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b47a635cfc9e42030706aebd47e40b95361cbdee#b47a635cfc9e42030706aebd47e40b95361cbdee"
dependencies = [
"anyhow",
"app-error",
@ -2970,7 +2970,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b47a635cfc9e42030706aebd47e40b95361cbdee#b47a635cfc9e42030706aebd47e40b95361cbdee"
dependencies = [
"anyhow",
"futures-util",
@ -2987,7 +2987,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b47a635cfc9e42030706aebd47e40b95361cbdee#b47a635cfc9e42030706aebd47e40b95361cbdee"
dependencies = [
"anyhow",
"app-error",
@ -3598,7 +3598,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b47a635cfc9e42030706aebd47e40b95361cbdee#b47a635cfc9e42030706aebd47e40b95361cbdee"
dependencies = [
"anyhow",
"bytes",
@ -4624,7 +4624,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros",
"phf_macros 0.8.0",
"phf_shared 0.8.0",
"proc-macro-hack",
]
@ -4644,6 +4644,7 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros 0.11.3",
"phf_shared 0.11.2",
]
@ -4711,6 +4712,19 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.94",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
@ -6140,7 +6154,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b47a635cfc9e42030706aebd47e40b95361cbdee#b47a635cfc9e42030706aebd47e40b95361cbdee"
dependencies = [
"anyhow",
"app-error",

View File

@ -103,8 +103,8 @@ dashmap = "6.0.1"
# Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2bd6da228d3e3f0f258c982b7a2a3571718d3688" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2bd6da228d3e3f0f258c982b7a2a3571718d3688" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "b47a635cfc9e42030706aebd47e40b95361cbdee" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "b47a635cfc9e42030706aebd47e40b95361cbdee" }
[profile.dev]
opt-level = 0

View File

@ -271,12 +271,13 @@ impl Chat {
}
chat_notification_builder(&chat_id, ChatNotification::FinishStreaming).send();
trace!("[Chat] finish streaming");
if answer_stream_buffer.lock().await.is_empty() {
return Ok(());
}
let content = answer_stream_buffer.lock().await.take_content();
let metadata = answer_stream_buffer.lock().await.take_metadata();
let answer = cloud_service
.create_answer(&workspace_id, &chat_id, &content, question_id, metadata)
.await?;

View File

@ -70,6 +70,7 @@ pub fn create_log_filter(
// filters.push(format!("lib_dispatch={}", level));
filters.push(format!("client_api={}", level));
filters.push(format!("infra={}", level));
#[cfg(feature = "profiling")]
filters.push(format!("tokio={}", level));
#[cfg(feature = "profiling")]