diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 2fc2c2e7c4..489ba2e14a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -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, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart index 7a68ad99b9..f3f5000157 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart @@ -81,6 +81,7 @@ Node customImageNode({ typedef CustomImageBlockComponentMenuBuilder = Widget Function( Node node, CustomImageBlockComponentState state, + ValueNotifier imageStateNotifier, ); class CustomImageBlockComponentBuilder extends BlockComponentBuilder { @@ -149,6 +150,8 @@ class CustomImageBlockComponentState extends State late final editorState = Provider.of(context, listen: false); final showActionsNotifier = ValueNotifier(false); + final imageStateNotifier = + ValueNotifier(ResizableImageState.loading); bool alwaysShowMenu = false; @@ -185,6 +188,7 @@ class CustomImageBlockComponentState extends State 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 child: child!, ), if (value && url.isNotEmpty == true) - widget.menuBuilder!(widget.node, this), + widget.menuBuilder!(widget.node, this, imageStateNotifier), ], ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart index cae8d66985..d5b02e755d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart @@ -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 imageStateNotifier; @override State createState() => _ImageMenuState(); @@ -40,46 +43,55 @@ class _ImageMenuState extends State { @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( + 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), - ], - ], - ), + ); + }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart index 97f6365337..7551947bce 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart @@ -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? onStateChange; final void Function(double width) onResize; @@ -96,11 +104,29 @@ class _ResizableImageState extends State { 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!; diff --git a/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart b/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart index d37b2e0838..30144d12b9 100644 --- a/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart +++ b/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart @@ -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 retryErrorCodes; + + final void Function(bool isImageInCache)? onImageLoaded; + + @override + FlowyNetworkImageState createState() => FlowyNetworkImageState(); +} + +class FlowyNetworkImageState extends State { + 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 _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 _buildRequestHeader() { final header = {}; - 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 _values = {}; + Map 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()); + } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 9fe043ede1..be01b7e06f 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -3068,5 +3068,8 @@ "answerFive": "Unsatisfied" } } + }, + "ai":{ + "sensitiveKeyword": "Image generation failed due to sensitive content. Please rephrase your input and try again" } } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 4a44b6d427..6bd772670b 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -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", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index e2fa00d709..a3feb2e0d3 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -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 diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 27eecf8638..00b41778a2 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -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?; diff --git a/frontend/rust-lib/flowy-core/src/log_filter.rs b/frontend/rust-lib/flowy-core/src/log_filter.rs index 3fd887b973..63877862f0 100644 --- a/frontend/rust-lib/flowy-core/src/log_filter.rs +++ b/frontend/rust-lib/flowy-core/src/log_filter.rs @@ -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")]