feat: AI translation in Database (#5515)

* chore: add tranlate field type

* chore: integrate ai translate

* chore: integrate client api

* chore: implement UI
This commit is contained in:
Nathan.fooo 2024-06-12 16:32:28 +08:00 committed by GitHub
parent 815c99710e
commit 3d7a500550
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
74 changed files with 1833 additions and 148 deletions

View File

@ -200,7 +200,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
@ -227,4 +227,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.15.2
COCOAPODS: 1.11.3

View File

@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -19,7 +20,10 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(minWidth: double.infinity),
constraints: BoxConstraints(
minWidth: double.infinity,
minHeight: GridSize.headerHeight,
),
child: TextButton.icon(
style: Theme.of(context).textButtonTheme.style?.copyWith(
shape: WidgetStateProperty.all<RoundedRectangleBorder>(

View File

@ -0,0 +1,111 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'translate_cell_bloc.freezed.dart';
class TranslateCellBloc extends Bloc<TranslateCellEvent, TranslateCellState> {
TranslateCellBloc({
required this.cellController,
}) : super(TranslateCellState.initial(cellController)) {
_dispatch();
_startListening();
}
final TranslateCellController cellController;
void Function()? _onCellChangedFn;
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(
onCellChanged: _onCellChangedFn!,
onFieldChanged: _onFieldChangedListener,
);
}
await cellController.dispose();
return super.close();
}
void _dispatch() {
on<TranslateCellEvent>(
(event, emit) async {
await event.when(
didReceiveCellUpdate: (cellData) {
emit(
state.copyWith(content: cellData ?? ""),
);
},
didUpdateField: (fieldInfo) {
final wrap = fieldInfo.wrapCellContent;
if (wrap != null) {
emit(state.copyWith(wrap: wrap));
}
},
updateCell: (text) async {
if (state.content != text) {
emit(state.copyWith(content: text));
await cellController.saveCellData(text);
// If the input content is "abc" that can't parsered as number then the data stored in the backend will be an empty string.
// So for every cell data that will be formatted in the backend.
// It needs to get the formatted data after saving.
add(
TranslateCellEvent.didReceiveCellUpdate(
cellController.getCellData() ?? "",
),
);
}
},
);
},
);
}
void _startListening() {
_onCellChangedFn = cellController.addListener(
onCellChanged: (cellContent) {
if (!isClosed) {
add(
TranslateCellEvent.didReceiveCellUpdate(cellContent ?? ""),
);
}
},
onFieldChanged: _onFieldChangedListener,
);
}
void _onFieldChangedListener(FieldInfo fieldInfo) {
if (!isClosed) {
add(TranslateCellEvent.didUpdateField(fieldInfo));
}
}
}
@freezed
class TranslateCellEvent with _$TranslateCellEvent {
const factory TranslateCellEvent.didReceiveCellUpdate(String? cellContent) =
_DidReceiveCellUpdate;
const factory TranslateCellEvent.didUpdateField(FieldInfo fieldInfo) =
_DidUpdateField;
const factory TranslateCellEvent.updateCell(String text) = _UpdateCell;
}
@freezed
class TranslateCellState with _$TranslateCellState {
const factory TranslateCellState({
required String content,
required bool wrap,
}) = _TranslateCellState;
factory TranslateCellState.initial(TranslateCellController cellController) {
final wrap = cellController.fieldInfo.wrapCellContent;
return TranslateCellState(
content: cellController.getCellData() ?? "",
wrap: wrap ?? true,
);
}
}

View File

@ -0,0 +1,100 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'translate_row_bloc.freezed.dart';
class TranslateRowBloc extends Bloc<TranslateRowEvent, TranslateRowState> {
TranslateRowBloc({
required this.viewId,
required this.rowId,
required this.fieldId,
}) : super(TranslateRowState.initial()) {
_dispatch();
}
final String viewId;
final String rowId;
final String fieldId;
void _dispatch() {
on<TranslateRowEvent>(
(event, emit) async {
event.when(
startTranslate: () {
final params = TranslateRowPB(
viewId: viewId,
rowId: rowId,
fieldId: fieldId,
);
emit(
state.copyWith(
loadingState: const LoadingState.loading(),
error: null,
),
);
DatabaseEventTranslateRow(params).send().then(
(result) => {
if (!isClosed)
add(TranslateRowEvent.finishTranslate(result)),
},
);
},
finishTranslate: (result) {
result.fold(
(s) => {
emit(
state.copyWith(
loadingState: const LoadingState.finish(),
error: null,
),
),
},
(err) => {
emit(
state.copyWith(
loadingState: const LoadingState.finish(),
error: err,
),
),
},
);
},
);
},
);
}
}
@freezed
class TranslateRowEvent with _$TranslateRowEvent {
const factory TranslateRowEvent.startTranslate() = _DidStartTranslate;
const factory TranslateRowEvent.finishTranslate(
FlowyResult<void, FlowyError> result,
) = _DidFinishTranslate;
}
@freezed
class TranslateRowState with _$TranslateRowState {
const factory TranslateRowState({
required LoadingState loadingState,
required FlowyError? error,
}) = _TranslateRowState;
factory TranslateRowState.initial() {
return const TranslateRowState(
loadingState: LoadingState.finish(),
error: null,
);
}
}
@freezed
class LoadingState with _$LoadingState {
const factory LoadingState.loading() = _Loading;
const factory LoadingState.finish() = _Finish;
}

View File

@ -16,6 +16,7 @@ typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
typedef URLCellController = CellController<URLCellDataPB, String>;
typedef RelationCellController = CellController<RelationCellDataPB, String>;
typedef SummaryCellController = CellController<String, String>;
typedef TranslateCellController = CellController<String, String>;
CellController makeCellController(
DatabaseController databaseController,
@ -145,6 +146,18 @@ CellController makeCellController(
),
cellDataPersistence: TextCellDataPersistence(),
);
case FieldType.Translate:
return TranslateCellController(
viewId: viewId,
fieldController: fieldController,
cellContext: cellContext,
rowCache: rowCache,
cellDataLoader: CellDataLoader(
parser: StringCellDataParser(),
reloadOnFieldChange: true,
),
cellDataPersistence: TextCellDataPersistence(),
);
}
throw UnimplementedError;
}

View File

@ -0,0 +1,72 @@
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/translate_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
part 'translate_type_option_bloc.freezed.dart';
class TranslateTypeOptionBloc
extends Bloc<TranslateTypeOptionEvent, TranslateTypeOptionState> {
TranslateTypeOptionBloc({required TranslateTypeOptionPB option})
: super(TranslateTypeOptionState.initial(option)) {
on<TranslateTypeOptionEvent>(
(event, emit) async {
event.when(
selectLanguage: (languageType) {
emit(
state.copyWith(
option: _updateLanguage(languageType),
language: languageTypeToLanguage(languageType),
),
);
},
);
},
);
}
TranslateTypeOptionPB _updateLanguage(TranslateLanguagePB languageType) {
state.option.freeze();
return state.option.rebuild((option) {
option.language = languageType;
});
}
}
@freezed
class TranslateTypeOptionEvent with _$TranslateTypeOptionEvent {
const factory TranslateTypeOptionEvent.selectLanguage(
TranslateLanguagePB languageType,
) = _SelectLanguage;
}
@freezed
class TranslateTypeOptionState with _$TranslateTypeOptionState {
const factory TranslateTypeOptionState({
required TranslateTypeOptionPB option,
required String language,
}) = _TranslateTypeOptionState;
factory TranslateTypeOptionState.initial(TranslateTypeOptionPB option) =>
TranslateTypeOptionState(
option: option,
language: languageTypeToLanguage(option.language),
);
}
String languageTypeToLanguage(TranslateLanguagePB langaugeType) {
switch (langaugeType) {
case TranslateLanguagePB.Chinese:
return 'Chinese';
case TranslateLanguagePB.English:
return 'English';
case TranslateLanguagePB.French:
return 'French';
case TranslateLanguagePB.German:
return 'German';
default:
Log.error('Unknown language type: $langaugeType');
return 'English';
}
}

View File

@ -4,8 +4,6 @@ abstract class TypeOptionParser<T> {
T fromBuffer(List<int> buffer);
}
class NumberTypeOptionDataParser extends TypeOptionParser<NumberTypeOptionPB> {
@override
NumberTypeOptionPB fromBuffer(List<int> buffer) {
@ -51,3 +49,11 @@ class RelationTypeOptionDataParser
return RelationTypeOptionPB.fromBuffer(buffer);
}
}
class TranslateTypeOptionDataParser
extends TypeOptionParser<TranslateTypeOptionPB> {
@override
TranslateTypeOptionPB fromBuffer(List<int> buffer) {
return TranslateTypeOptionPB.fromBuffer(buffer);
}
}

View File

@ -14,6 +14,7 @@ class GridSize {
static double get cellVPadding => 10 * scale;
static double get popoverItemHeight => 26 * scale;
static double get typeOptionSeparatorHeight => 4 * scale;
static double get newPropertyButtonWidth => 140 * scale;
static EdgeInsets get cellContentInsets => EdgeInsets.symmetric(
horizontal: GridSize.cellHPadding,

View File

@ -93,9 +93,12 @@ class _GridFieldCellState extends State<GridFieldCell> {
onFieldInserted: widget.onFieldInsertedOnEitherSide,
);
},
child: FieldCellButton(
field: widget.fieldInfo.field,
onTap: widget.onTap,
child: SizedBox(
height: 40,
child: FieldCellButton(
field: widget.fieldInfo.field,
onTap: widget.onTap,
),
),
);
@ -217,6 +220,12 @@ class FieldCellButton extends StatelessWidget {
field.fieldType.svgData,
color: Theme.of(context).iconTheme.color,
),
rightIcon: field.fieldType.rightIcon != null
? FlowySvg(
field.fieldType.rightIcon!,
blendMode: null,
)
: null,
radius: radius,
text: FlowyText.medium(
field.name,

View File

@ -151,7 +151,10 @@ class _CellTrailing extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: GridSize.trailHeaderPadding,
constraints: BoxConstraints(
maxWidth: GridSize.newPropertyButtonWidth,
minHeight: GridSize.headerHeight,
),
margin: EdgeInsets.only(right: GridSize.scrollBarSize + Insets.m),
decoration: BoxDecoration(
border: Border(

View File

@ -196,7 +196,10 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
@override
Widget build(BuildContext context) {
return Container(
width: 200,
constraints: BoxConstraints(
maxWidth: GridSize.newPropertyButtonWidth,
minHeight: GridSize.headerHeight,
),
decoration: _getDecoration(context),
child: FlowyButton(
margin: const EdgeInsets.symmetric(vertical: 14, horizontal: 12),

View File

@ -2,6 +2,7 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/timestamp_card_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter/widgets.dart';
@ -98,6 +99,12 @@ class CardCellBuilder {
databaseController: databaseController,
cellContext: cellContext,
),
FieldType.Translate => TranslateCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
_ => throw UnimplementedError,
};
}

View File

@ -0,0 +1,62 @@
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class TranslateCardCellStyle extends CardCellStyle {
const TranslateCardCellStyle({
required super.padding,
required this.textStyle,
});
final TextStyle textStyle;
}
class TranslateCardCell extends CardCell<TranslateCardCellStyle> {
const TranslateCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
final DatabaseController databaseController;
final CellContext cellContext;
@override
State<TranslateCardCell> createState() => _TranslateCellState();
}
class _TranslateCellState extends State<TranslateCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return TranslateCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
);
},
child: BlocBuilder<TranslateCellBloc, TranslateCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox.shrink();
}
return Container(
alignment: AlignmentDirectional.centerStart,
padding: widget.style.padding,
child: Text(state.content, style: widget.style.textStyle),
);
},
),
);
}
}

View File

@ -84,5 +84,9 @@ CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) {
padding: padding,
textStyle: textStyle,
),
FieldType.Translate: SummaryCardCellStyle(
padding: padding,
textStyle: textStyle,
),
};
}

View File

@ -84,5 +84,9 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) {
padding: padding,
textStyle: textStyle,
),
FieldType.Translate: SummaryCardCellStyle(
padding: padding,
textStyle: textStyle,
),
};
}

View File

@ -83,5 +83,9 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) {
padding: padding,
textStyle: textStyle,
),
FieldType.Translate: SummaryCardCellStyle(
padding: padding,
textStyle: textStyle,
),
};
}

View File

@ -27,50 +27,55 @@ class DesktopGridSummaryCellSkin extends IEditableSummaryCellSkin {
onExit: (p) =>
Provider.of<SummaryMouseNotifier>(context, listen: false)
.onEnter = false,
child: Stack(
children: [
TextField(
controller: textEditingController,
enabled: false,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: GridSize.headerHeight,
),
child: Stack(
children: [
TextField(
controller: textEditingController,
enabled: false,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.cellVPadding,
),
child: Consumer<SummaryMouseNotifier>(
builder: (
BuildContext context,
SummaryMouseNotifier notifier,
Widget? child,
) {
if (notifier.onEnter) {
return SummaryCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
);
} else {
return const SizedBox.shrink();
}
},
),
).positioned(right: 0, bottom: 0),
],
Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.cellVPadding,
),
child: Consumer<SummaryMouseNotifier>(
builder: (
BuildContext context,
SummaryMouseNotifier notifier,
Widget? child,
) {
if (notifier.onEnter) {
return SummaryCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
);
} else {
return const SizedBox.shrink();
}
},
),
).positioned(right: 0, bottom: 8),
],
),
),
);
},

View File

@ -0,0 +1,99 @@
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
class DesktopGridTranslateCellSkin extends IEditableTranslateCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TranslateCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return ChangeNotifierProvider(
create: (_) => TranslateMouseNotifier(),
builder: (context, child) {
return MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) =>
Provider.of<TranslateMouseNotifier>(context, listen: false)
.onEnter = true,
onExit: (p) =>
Provider.of<TranslateMouseNotifier>(context, listen: false)
.onEnter = false,
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: GridSize.headerHeight,
),
child: Stack(
children: [
TextField(
controller: textEditingController,
enabled: false,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.cellVPadding,
),
child: Consumer<TranslateMouseNotifier>(
builder: (
BuildContext context,
TranslateMouseNotifier notifier,
Widget? child,
) {
if (notifier.onEnter) {
return TranslateCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
);
} else {
return const SizedBox.shrink();
}
},
),
).positioned(right: 0, bottom: 8),
],
),
),
);
},
);
}
}
class TranslateMouseNotifier extends ChangeNotifier {
TranslateMouseNotifier();
bool _onEnter = false;
set onEnter(bool value) {
if (_onEnter != value) {
_onEnter = value;
notifyListeners();
}
}
bool get onEnter => _onEnter;
}

View File

@ -0,0 +1,53 @@
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:flutter/material.dart';
class DesktopRowDetailTranslateCellSkin extends IEditableTranslateCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TranslateCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return Column(
children: [
TextField(
controller: textEditingController,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
maxLines: null,
minLines: 1,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
),
Row(
children: [
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: TranslateCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
),
),
],
),
],
);
}
}

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
@ -120,6 +121,12 @@ class EditableCellBuilder {
skin: IEditableSummaryCellSkin.fromStyle(style),
key: key,
),
FieldType.Translate => EditableTranslateCell(
databaseController: databaseController,
cellContext: cellContext,
skin: IEditableTranslateCellSkin.fromStyle(style),
key: key,
),
_ => throw UnimplementedError(),
};
}

View File

@ -177,7 +177,7 @@ class SummaryButton extends StatelessWidget {
},
finish: (_) {
return FlowyTooltip(
message: LocaleKeys.tooltip_genSummary.tr(),
message: LocaleKeys.tooltip_aiGenerate.tr(),
child: Container(
width: 26,
height: 26,

View File

@ -0,0 +1,250 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/translate_row_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class IEditableTranslateCellSkin {
const IEditableTranslateCellSkin();
factory IEditableTranslateCellSkin.fromStyle(EditableCellStyle style) {
return switch (style) {
EditableCellStyle.desktopGrid => DesktopGridTranslateCellSkin(),
EditableCellStyle.desktopRowDetail => DesktopRowDetailTranslateCellSkin(),
EditableCellStyle.mobileGrid => MobileGridTranslateCellSkin(),
EditableCellStyle.mobileRowDetail => MobileRowDetailTranslateCellSkin(),
};
}
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TranslateCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
);
}
class EditableTranslateCell extends EditableCellWidget {
EditableTranslateCell({
super.key,
required this.databaseController,
required this.cellContext,
required this.skin,
});
final DatabaseController databaseController;
final CellContext cellContext;
final IEditableTranslateCellSkin skin;
@override
GridEditableTextCell<EditableTranslateCell> createState() =>
_TranslateCellState();
}
class _TranslateCellState extends GridEditableTextCell<EditableTranslateCell> {
late final TextEditingController _textEditingController;
late final cellBloc = TranslateCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
);
@override
void initState() {
super.initState();
_textEditingController =
TextEditingController(text: cellBloc.state.content);
}
@override
void dispose() {
_textEditingController.dispose();
cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cellBloc,
child: BlocListener<TranslateCellBloc, TranslateCellState>(
listener: (context, state) {
_textEditingController.text = state.content;
},
child: Builder(
builder: (context) {
return widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
focusNode,
_textEditingController,
);
},
),
),
);
}
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void onRequestFocus() {
focusNode.requestFocus();
}
@override
String? onCopy() => cellBloc.state.content;
@override
Future<void> focusChanged() {
if (mounted &&
!cellBloc.isClosed &&
cellBloc.state.content != _textEditingController.text.trim()) {
cellBloc.add(
TranslateCellEvent.updateCell(_textEditingController.text.trim()),
);
}
return super.focusChanged();
}
}
class TranslateCellAccessory extends StatelessWidget {
const TranslateCellAccessory({
required this.viewId,
required this.rowId,
required this.fieldId,
super.key,
});
final String viewId;
final String rowId;
final String fieldId;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => TranslateRowBloc(
viewId: viewId,
rowId: rowId,
fieldId: fieldId,
),
child: BlocBuilder<TranslateRowBloc, TranslateRowState>(
builder: (context, state) {
return const Row(
children: [TranslateButton(), HSpace(6), CopyButton()],
);
},
),
);
}
}
class TranslateButton extends StatelessWidget {
const TranslateButton({
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<TranslateRowBloc, TranslateRowState>(
builder: (context, state) {
return state.loadingState.map(
loading: (_) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
},
finish: (_) {
return FlowyTooltip(
message: LocaleKeys.tooltip_aiGenerate.tr(),
child: Container(
width: 26,
height: 26,
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
),
borderRadius: Corners.s6Border,
),
child: FlowyIconButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
fillColor: Theme.of(context).cardColor,
icon: FlowySvg(
FlowySvgs.ai_summary_generate_s,
color: Theme.of(context).colorScheme.primary,
),
onPressed: () {
context
.read<TranslateRowBloc>()
.add(const TranslateRowEvent.startTranslate());
},
),
),
);
},
);
},
);
}
}
class CopyButton extends StatelessWidget {
const CopyButton({
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<TranslateCellBloc, TranslateCellState>(
builder: (blocContext, state) {
return FlowyTooltip(
message: LocaleKeys.settings_menu_clickToCopy.tr(),
child: Container(
width: 26,
height: 26,
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
),
borderRadius: Corners.s6Border,
),
child: FlowyIconButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
fillColor: Theme.of(context).cardColor,
icon: FlowySvg(
FlowySvgs.ai_copy_s,
color: Theme.of(context).colorScheme.primary,
),
onPressed: () {
Clipboard.setData(ClipboardData(text: state.content));
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
},
),
),
);
},
);
}
}

View File

@ -0,0 +1,79 @@
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
class MobileGridTranslateCellSkin extends IEditableTranslateCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TranslateCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return ChangeNotifierProvider(
create: (_) => TranslateMouseNotifier(),
builder: (context, child) {
return MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) =>
Provider.of<TranslateMouseNotifier>(context, listen: false)
.onEnter = true,
onExit: (p) =>
Provider.of<TranslateMouseNotifier>(context, listen: false)
.onEnter = false,
child: Stack(
children: [
TextField(
controller: textEditingController,
enabled: false,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.cellVPadding,
),
child: Consumer<TranslateMouseNotifier>(
builder: (
BuildContext context,
TranslateMouseNotifier notifier,
Widget? child,
) {
if (notifier.onEnter) {
return TranslateCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
);
} else {
return const SizedBox.shrink();
}
},
),
).positioned(right: 0, bottom: 0),
],
),
);
},
);
}
}

View File

@ -0,0 +1,53 @@
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:flutter/material.dart';
class MobileRowDetailTranslateCellSkin extends IEditableTranslateCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TranslateCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return Column(
children: [
TextField(
controller: textEditingController,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
maxLines: null,
minLines: 1,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
),
Row(
children: [
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: TranslateCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
),
),
],
),
],
);
}
}

View File

@ -21,6 +21,7 @@ const List<FieldType> _supportedFieldTypes = [
FieldType.CreatedTime,
FieldType.Relation,
FieldType.Summary,
FieldType.Translate,
];
class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {

View File

@ -1,5 +1,6 @@
import 'dart:typed_data';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/translate.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
@ -33,6 +34,7 @@ abstract class TypeOptionEditorFactory {
FieldType.Checklist => const ChecklistTypeOptionEditorFactory(),
FieldType.Relation => const RelationTypeOptionEditorFactory(),
FieldType.Summary => const SummaryTypeOptionEditorFactory(),
FieldType.Translate => const TranslateTypeOptionEditorFactory(),
_ => throw UnimplementedError(),
};
}

View File

@ -0,0 +1,168 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/field/type_option/translate_type_option_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import './builder.dart';
class TranslateTypeOptionEditorFactory implements TypeOptionEditorFactory {
const TranslateTypeOptionEditorFactory();
@override
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final typeOption = TranslateTypeOptionPB.fromBuffer(field.typeOptionData);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText(
LocaleKeys.grid_field_translateTo.tr(),
),
const HSpace(6),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: BlocProvider(
create: (context) => TranslateTypeOptionBloc(option: typeOption),
child: BlocConsumer<TranslateTypeOptionBloc,
TranslateTypeOptionState>(
listenWhen: (previous, current) =>
previous.option != current.option,
listener: (context, state) {
onTypeOptionUpdated(state.option.writeToBuffer());
},
builder: (context, state) {
return _wrapLanguageListPopover(
context,
state,
popoverMutex,
SelectLanguageButton(
language: state.language,
),
);
},
),
),
),
],
),
);
}
Widget _wrapLanguageListPopover(
BuildContext blocContext,
TranslateTypeOptionState state,
PopoverMutex popoverMutex,
Widget child,
) {
return AppFlowyPopover(
mutex: popoverMutex,
asBarrier: true,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (popoverContext) {
return LanguageList(
onSelected: (language) {
blocContext
.read<TranslateTypeOptionBloc>()
.add(TranslateTypeOptionEvent.selectLanguage(language));
PopoverContainer.of(popoverContext).close();
},
selectedLanguage: state.option.language,
);
},
child: child,
);
}
}
class SelectLanguageButton extends StatelessWidget {
const SelectLanguageButton({required this.language, super.key});
final String language;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: FlowyButton(text: FlowyText(language)),
);
}
}
class LanguageList extends StatelessWidget {
const LanguageList({
super.key,
required this.onSelected,
required this.selectedLanguage,
});
final Function(TranslateLanguagePB) onSelected;
final TranslateLanguagePB selectedLanguage;
@override
Widget build(BuildContext context) {
final cells = TranslateLanguagePB.values.map((languageType) {
return LanguageCell(
languageType: languageType,
onSelected: onSelected,
isSelected: languageType == selectedLanguage,
);
}).toList();
return SizedBox(
width: 180,
child: ListView.separated(
shrinkWrap: true,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
),
);
}
}
class LanguageCell extends StatelessWidget {
const LanguageCell({
required this.languageType,
required this.onSelected,
required this.isSelected,
super.key,
});
final Function(TranslateLanguagePB) onSelected;
final TranslateLanguagePB languageType;
final bool isSelected;
@override
Widget build(BuildContext context) {
Widget? checkmark;
if (isSelected) {
checkmark = const FlowySvg(FlowySvgs.check_s);
}
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(languageTypeToLanguage(languageType)),
rightIcon: checkmark,
onTap: () => onSelected(languageType),
),
);
}
}

View File

@ -22,6 +22,7 @@ extension FieldTypeExtension on FieldType {
FieldType.CreatedTime => LocaleKeys.grid_field_createdAtFieldName.tr(),
FieldType.Relation => LocaleKeys.grid_field_relationFieldName.tr(),
FieldType.Summary => LocaleKeys.grid_field_summaryFieldName.tr(),
FieldType.Translate => LocaleKeys.grid_field_translateFieldName.tr(),
_ => throw UnimplementedError(),
};
@ -38,9 +39,16 @@ extension FieldTypeExtension on FieldType {
FieldType.CreatedTime => FlowySvgs.created_at_s,
FieldType.Relation => FlowySvgs.relation_s,
FieldType.Summary => FlowySvgs.ai_summary_s,
FieldType.Translate => FlowySvgs.ai_translate_s,
_ => throw UnimplementedError(),
};
FlowySvgData? get rightIcon => switch (this) {
FieldType.Summary => FlowySvgs.ai_indicator_s,
FieldType.Translate => FlowySvgs.ai_indicator_s,
_ => null,
};
Color get mobileIconBackgroundColor => switch (this) {
FieldType.RichText => const Color(0xFFBECCFF),
FieldType.Number => const Color(0xFFCABDFF),
@ -54,6 +62,7 @@ extension FieldTypeExtension on FieldType {
FieldType.Checklist => const Color(0xFF98F4CD),
FieldType.Relation => const Color(0xFFFDEDA7),
FieldType.Summary => const Color(0xFFBECCFF),
FieldType.Translate => const Color(0xFFBECCFF),
_ => throw UnimplementedError(),
};
@ -71,6 +80,7 @@ extension FieldTypeExtension on FieldType {
FieldType.Checklist => const Color(0xFF42AD93),
FieldType.Relation => const Color(0xFFFDEDA7),
FieldType.Summary => const Color(0xFF6859A7),
FieldType.Translate => const Color(0xFF6859A7),
_ => throw UnimplementedError(),
};
}

View File

@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"bincode",
@ -192,7 +192,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"bytes",
@ -772,7 +772,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"again",
"anyhow",
@ -819,7 +819,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"futures-channel",
"futures-util",
@ -1059,7 +1059,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"bincode",
@ -1084,7 +1084,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"async-trait",
@ -1441,7 +1441,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error",
@ -1956,6 +1956,7 @@ name = "flowy-database-pub"
version = "0.1.0"
dependencies = [
"anyhow",
"client-api",
"collab",
"collab-entity",
"lib-infra",
@ -2853,7 +2854,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"futures-util",
@ -2870,7 +2871,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error",
@ -3302,7 +3303,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"reqwest",
@ -5792,7 +5793,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error",

View File

@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ff4384fbd07a4b7394a9af8c9159cd65715d3471" }
[dependencies]
serde_json.workspace = true

View File

@ -216,7 +216,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"bincode",
@ -236,7 +236,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"bytes",
@ -562,7 +562,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"again",
"anyhow",
@ -609,7 +609,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"futures-channel",
"futures-util",
@ -787,7 +787,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"bincode",
@ -812,7 +812,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"async-trait",
@ -981,7 +981,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf 0.11.2",
"phf 0.8.0",
"smallvec",
]
@ -1026,7 +1026,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error",
@ -1341,6 +1341,7 @@ name = "flowy-database-pub"
version = "0.1.0"
dependencies = [
"anyhow",
"client-api",
"collab",
"collab-entity",
"lib-infra",
@ -1881,7 +1882,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"futures-util",
@ -1898,7 +1899,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error",
@ -2199,7 +2200,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"reqwest",
@ -2916,7 +2917,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros 0.8.0",
"phf_macros",
"phf_shared 0.8.0",
"proc-macro-hack",
]
@ -2936,7 +2937,6 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros 0.11.2",
"phf_shared 0.11.2",
]
@ -3004,19 +3004,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "phf_macros"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
@ -3901,7 +3888,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error",

View File

@ -55,7 +55,7 @@ yrs = "0.18.8"
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ff4384fbd07a4b7394a9af8c9159cd65715d3471" }

View File

@ -163,7 +163,7 @@ checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
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=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"bytes",
@ -746,7 +746,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"again",
"anyhow",
@ -793,7 +793,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"futures-channel",
"futures-util",
@ -1042,7 +1042,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"bincode",
@ -1067,7 +1067,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"async-trait",
@ -1317,7 +1317,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa 1.0.10",
"phf 0.8.0",
"phf 0.11.2",
"smallvec",
]
@ -1428,7 +1428,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error",
@ -1993,6 +1993,7 @@ name = "flowy-database-pub"
version = "0.1.0"
dependencies = [
"anyhow",
"client-api",
"collab",
"collab-entity",
"lib-infra",
@ -2927,7 +2928,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"futures-util",
@ -2944,7 +2945,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error",
@ -3381,7 +3382,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"reqwest",
@ -4888,7 +4889,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2"
dependencies = [
"bytes",
"heck 0.4.1",
"itertools 0.10.5",
"itertools 0.11.0",
"log",
"multimap",
"once_cell",
@ -4909,7 +4910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
dependencies = [
"anyhow",
"itertools 0.10.5",
"itertools 0.11.0",
"proc-macro2",
"quote",
"syn 2.0.55",
@ -5887,7 +5888,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error",

View File

@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ff4384fbd07a4b7394a9af8c9159cd65715d3471" }
[dependencies]
serde_json.workspace = true

View File

@ -0,0 +1,23 @@
<svg width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_2389_7357)">
<rect x="3.66663" y="2" width="16" height="16" rx="6" fill="url(#paint0_linear_2389_7357)" shape-rendering="crispEdges"/>
<rect x="3.66663" y="2" width="16" height="16" rx="6" fill="#806989" shape-rendering="crispEdges"/>
<path d="M11.1576 11.884H8.79963L8.42163 13H6.81063L9.09663 6.682H10.8786L13.1646 13H11.5356L11.1576 11.884ZM10.7616 10.696L9.97863 8.383L9.20463 10.696H10.7616ZM14.6794 7.456C14.4094 7.456 14.1874 7.378 14.0134 7.222C13.8454 7.06 13.7614 6.862 13.7614 6.628C13.7614 6.388 13.8454 6.19 14.0134 6.034C14.1874 5.872 14.4094 5.791 14.6794 5.791C14.9434 5.791 15.1594 5.872 15.3274 6.034C15.5014 6.19 15.5884 6.388 15.5884 6.628C15.5884 6.862 15.5014 7.06 15.3274 7.222C15.1594 7.378 14.9434 7.456 14.6794 7.456ZM15.4444 7.978V13H13.9054V7.978H15.4444Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_2389_7357" x="0.666626" y="0" width="22" height="22" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2389_7357"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2389_7357" result="shape"/>
</filter>
<linearGradient id="paint0_linear_2389_7357" x1="15.6666" y1="2.4" x2="6.86663" y2="17.2" gradientUnits="userSpaceOnUse">
<stop stop-color="#726084" stop-opacity="0.8"/>
<stop offset="1" stop-color="#5D5862"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2V7.33333H7.33333V2H2ZM6 6H3.33333V3.33333H6V6ZM2 8.66667V14H7.33333V8.66667H2ZM6 12.6667H3.33333V10H6V12.6667ZM8.66667 2V7.33333H14V2H8.66667ZM12.6667 6H10V3.33333H12.6667V6ZM8.66667 8.66667V14H14V8.66667H8.66667ZM12.6667 12.6667H10V10H12.6667V12.6667Z" fill="#750D7E"/>
</svg>

After

Width:  |  Height:  |  Size: 387 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.50584 14.2232C8.20974 14.519 7.85975 14.6668 7.45587 14.6668C7.05198 14.6668 6.70199 14.519 6.4059 14.2232L1.76115 9.58414C1.47589 9.29922 1.33325 8.95171 1.33325 8.54161C1.33325 8.13151 1.47589 7.784 1.76115 7.49909L7.47776 1.7771C7.61498 1.64004 7.77515 1.53185 7.95826 1.45251C8.14138 1.37317 8.33661 1.3335 8.54397 1.3335H13.1887C13.5952 1.3335 13.9431 1.47801 14.2325 1.76704C14.5219 2.05607 14.6666 2.40358 14.6666 2.80957V7.44866C14.6666 7.65577 14.6269 7.85077 14.5474 8.03366C14.468 8.21655 14.3597 8.37652 14.2224 8.51358L8.50584 14.2232ZM11.5647 5.40487C11.8354 5.40487 12.0654 5.31025 12.2549 5.12101C12.4444 4.93177 12.5391 4.70197 12.5391 4.43163C12.5391 4.16129 12.4444 3.9315 12.2549 3.74226C12.0654 3.55301 11.8354 3.45839 11.5647 3.45839C11.294 3.45839 11.0639 3.55301 10.8745 3.74226C10.685 3.9315 10.5903 4.16129 10.5903 4.43163C10.5903 4.70197 10.685 4.93177 10.8745 5.12101C11.0639 5.31025 11.294 5.40487 11.5647 5.40487ZM7.45199 13.1908L13.1887 7.44866V2.80957H8.54397L2.80724 8.55166L7.45199 13.1908Z" fill="#750D7E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.58008 10.0468L6.88675 8.3735L6.90675 8.3535C8.06675 7.06016 8.89341 5.5735 9.38008 4.00016H11.3334V2.66683H6.66675V1.3335H5.33341V2.66683H0.666748V3.9935H8.11341C7.66675 5.28016 6.96008 6.50016 6.00008 7.56683C5.38008 6.88016 4.86675 6.12683 4.46008 5.3335H3.12675C3.61341 6.42016 4.28008 7.44683 5.11341 8.3735L1.72008 11.7202L2.66675 12.6668L6.00008 9.3335L8.07341 11.4068L8.58008 10.0468ZM12.3334 6.66683H11.0001L8.00008 14.6668H9.33341L10.0801 12.6668H13.2467L14.0001 14.6668H15.3334L12.3334 6.66683ZM10.5867 11.3335L11.6667 8.44683L12.7467 11.3335H10.5867Z" fill="#750D7E"/>
</svg>

After

Width:  |  Height:  |  Size: 695 B

View File

@ -241,7 +241,7 @@
"viewDataBase": "View database",
"referencePage": "This {name} is referenced",
"addBlockBelow": "Add a block below",
"genSummary": "Generate summary"
"aiGenerate": "Generate"
},
"sideBar": {
"closeSidebar": "Close sidebar",
@ -873,6 +873,8 @@
"checklistFieldName": "Checklist",
"relationFieldName": "Relation",
"summaryFieldName": "AI Summary",
"translateFieldName": "AI Translate",
"translateTo": "Translate to",
"numberFormat": "Number format",
"dateFormat": "Date format",
"includeTime": "Include time",

View File

@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
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=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"bytes",
@ -664,7 +664,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"again",
"anyhow",
@ -711,7 +711,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"futures-channel",
"futures-util",
@ -920,7 +920,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"bincode",
@ -945,7 +945,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"async-trait",
@ -1165,7 +1165,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf 0.11.2",
"phf 0.8.0",
"smallvec",
]
@ -1265,7 +1265,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error",
@ -1776,6 +1776,7 @@ name = "flowy-database-pub"
version = "0.1.0"
dependencies = [
"anyhow",
"client-api",
"collab",
"collab-entity",
"lib-infra",
@ -2523,7 +2524,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"futures-util",
@ -2540,7 +2541,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error",
@ -2905,7 +2906,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"reqwest",
@ -3781,7 +3782,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros 0.8.0",
"phf_macros",
"phf_shared 0.8.0",
"proc-macro-hack",
]
@ -3801,7 +3802,6 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros 0.11.2",
"phf_shared 0.11.2",
]
@ -3869,19 +3869,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "phf_macros"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.47",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
@ -4085,7 +4072,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2"
dependencies = [
"bytes",
"heck 0.4.1",
"itertools 0.11.0",
"itertools 0.10.5",
"log",
"multimap",
"once_cell",
@ -4106,7 +4093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
dependencies = [
"anyhow",
"itertools 0.11.0",
"itertools 0.10.5",
"proc-macro2",
"quote",
"syn 2.0.47",
@ -5003,7 +4990,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error",

View File

@ -94,7 +94,7 @@ yrs = "0.18.8"
# 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 = "d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ff4384fbd07a4b7394a9af8c9159cd65715d3471" }
[profile.dev]
opt-level = 1

View File

@ -215,6 +215,14 @@ impl EventIntegrationTest {
.await;
}
pub async fn translate_row(&self, data: TranslateRowPB) {
EventBuilder::new(self.clone())
.event(DatabaseEvent::TranslateRow)
.payload(data)
.async_send()
.await;
}
pub async fn create_row(
&self,
view_id: &str,

View File

@ -1,2 +1,3 @@
// mod summarize_row;
// mod summarize_row_test;
// mod translate_row_test;
mod util;

View File

@ -0,0 +1,54 @@
use crate::database::af_cloud::util::make_test_summary_grid;
use std::time::Duration;
use tokio::time::sleep;
use event_integration_test::user_event::user_localhost_af_cloud;
use event_integration_test::EventIntegrationTest;
use flowy_database2::entities::{FieldType, TranslateRowPB};
#[tokio::test]
async fn af_cloud_translate_row_test() {
user_localhost_af_cloud().await;
let test = EventIntegrationTest::new().await;
test.af_cloud_sign_up().await;
// create document and then insert content
let current_workspace = test.get_current_workspace().await;
let initial_data = make_test_summary_grid().to_json_bytes().unwrap();
let view = test
.create_grid(
&current_workspace.id,
"translate database".to_string(),
initial_data,
)
.await;
let database_pb = test.get_database(&view.id).await;
let field = test
.get_all_database_fields(&view.id)
.await
.items
.into_iter()
.find(|field| field.field_type == FieldType::Translate)
.unwrap();
let row_id = database_pb.rows[0].id.clone();
let data = TranslateRowPB {
view_id: view.id.clone(),
row_id: row_id.clone(),
field_id: field.id.clone(),
};
test.translate_row(data).await;
sleep(Duration::from_secs(1)).await;
let cell = test
.get_text_cell(&view.id, &row_id, &field.id)
.await
.to_lowercase();
println!("cell: {}", cell);
// default translation is in French. So it should be something like this:
// Prix:2,6 $,Nom du produit:Pomme,Statut:TERMINÉ
assert!(cell.contains("pomme"));
assert!(cell.contains("produit"));
assert!(cell.contains("prix"));
}

View File

@ -6,6 +6,7 @@ use collab_database::fields::Field;
use collab_database::rows::Row;
use flowy_database2::entities::FieldType;
use flowy_database2::services::field::summary_type_option::summary::SummarizationTypeOption;
use flowy_database2::services::field::translate_type_option::translate::TranslateTypeOption;
use flowy_database2::services::field::{
FieldBuilder, NumberFormat, NumberTypeOption, SelectOption, SelectOptionColor,
SingleSelectTypeOption,
@ -61,6 +62,7 @@ fn create_fields() -> Vec<Field> {
FieldType::Number => fields.push(create_number_field("Price", NumberFormat::USD)),
FieldType::SingleSelect => fields.push(create_single_select_field("Status")),
FieldType::Summary => fields.push(create_summary_field("AI summary")),
FieldType::Translate => fields.push(create_translate_field("AI Translate")),
_ => {},
}
}
@ -124,3 +126,14 @@ fn create_summary_field(name: &str) -> Field {
.name(name)
.build()
}
#[allow(dead_code)]
fn create_translate_field(name: &str) -> Field {
let type_option = TranslateTypeOption {
auto_fill: false,
language_type: 2,
};
FieldBuilder::new(FieldType::Translate, type_option)
.name(name)
.build()
}

View File

@ -22,6 +22,7 @@ use flowy_chat_pub::cloud::{
};
use flowy_database_pub::cloud::{
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
TranslateRowContent, TranslateRowResponse,
};
use flowy_document::deps::DocumentData;
use flowy_document_pub::cloud::{DocumentCloudService, DocumentSnapshot};
@ -293,6 +294,23 @@ impl DatabaseCloudService for ServerProvider {
.await
})
}
fn translate_database_row(
&self,
workspace_id: &str,
translate_row: TranslateRowContent,
language: &str,
) -> FutureResult<TranslateRowResponse, Error> {
let workspace_id = workspace_id.to_string();
let server = self.get_server();
let language = language.to_string();
FutureResult::new(async move {
server?
.database_service()
.translate_database_row(&workspace_id, translate_row, &language)
.await
})
}
}
impl DocumentCloudService for ServerProvider {

View File

@ -9,4 +9,5 @@ edition = "2021"
lib-infra = { workspace = true }
collab-entity = { workspace = true }
collab = { workspace = true }
anyhow.workspace = true
anyhow.workspace = true
client-api = { workspace = true }

View File

@ -1,4 +1,5 @@
use anyhow::Error;
pub use client_api::entity::ai_dto::{TranslateItem, TranslateRowResponse};
use collab::core::collab::DataSource;
use collab_entity::CollabType;
use lib_infra::future::FutureResult;
@ -6,6 +7,7 @@ use std::collections::HashMap;
pub type CollabDocStateByOid = HashMap<String, DataSource>;
pub type SummaryRowContent = HashMap<String, String>;
pub type TranslateRowContent = Vec<TranslateItem>;
/// A trait for database cloud service.
/// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of
/// [flowy-server] crate for more information.
@ -39,6 +41,13 @@ pub trait DatabaseCloudService: Send + Sync {
object_id: &str,
summary_row: SummaryRowContent,
) -> FutureResult<String, Error>;
fn translate_database_row(
&self,
workspace_id: &str,
translate_row: TranslateRowContent,
language: &str,
) -> FutureResult<TranslateRowResponse, Error>;
}
pub struct DatabaseSnapshot {

View File

@ -449,6 +449,7 @@ pub enum FieldType {
CreatedTime = 9,
Relation = 10,
Summary = 11,
Translate = 12,
}
impl Display for FieldType {
@ -489,6 +490,7 @@ impl FieldType {
FieldType::CreatedTime => "Created time",
FieldType::Relation => "Relation",
FieldType::Summary => "Summarize",
FieldType::Translate => "Translate",
};
s.to_string()
}

View File

@ -109,6 +109,10 @@ impl From<&Filter> for FilterPB {
.cloned::<TextFilterPB>()
.unwrap()
.try_into(),
FieldType::Translate => condition_and_content
.cloned::<TextFilterPB>()
.unwrap()
.try_into(),
};
Self {
@ -156,6 +160,9 @@ impl TryFrom<FilterDataPB> for FilterInner {
FieldType::Summary => {
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
FieldType::Translate => {
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
};
Ok(Self::Data {

View File

@ -16,6 +16,7 @@ macro_rules! impl_into_field_type {
9 => FieldType::CreatedTime,
10 => FieldType::Relation,
11 => FieldType::Summary,
12 => FieldType::Translate,
_ => {
tracing::error!("🔴Can't parse FieldType from value: {}", ty);
FieldType::RichText

View File

@ -380,3 +380,18 @@ pub struct SummaryRowPB {
#[pb(index = 3)]
pub field_id: String,
}
#[derive(Debug, Default, Clone, ProtoBuf, Validate)]
pub struct TranslateRowPB {
#[pb(index = 1)]
#[validate(custom = "required_not_empty_str")]
pub view_id: String,
#[pb(index = 2)]
#[validate(custom = "required_not_empty_str")]
pub row_id: String,
#[pb(index = 3)]
#[validate(custom = "required_not_empty_str")]
pub field_id: String,
}

View File

@ -7,6 +7,7 @@ mod select_option_entities;
mod summary_entities;
mod text_entities;
mod timestamp_entities;
mod translate_entities;
mod url_entities;
pub use checkbox_entities::*;
@ -18,4 +19,5 @@ pub use select_option_entities::*;
pub use summary_entities::*;
pub use text_entities::*;
pub use timestamp_entities::*;
pub use translate_entities::*;
pub use url_entities::*;

View File

@ -0,0 +1,50 @@
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct TranslateTypeOptionPB {
#[pb(index = 1)]
pub auto_fill: bool,
#[pb(index = 2)]
pub language: TranslateLanguagePB,
}
impl From<TranslateTypeOption> for TranslateTypeOptionPB {
fn from(value: TranslateTypeOption) -> Self {
TranslateTypeOptionPB {
auto_fill: value.auto_fill,
language: value.language_type.into(),
}
}
}
impl From<TranslateTypeOptionPB> for TranslateTypeOption {
fn from(value: TranslateTypeOptionPB) -> Self {
TranslateTypeOption {
auto_fill: value.auto_fill,
language_type: value.language as i64,
}
}
}
#[derive(Clone, Debug, Copy, ProtoBuf_Enum, Default)]
#[repr(i64)]
pub enum TranslateLanguagePB {
Chinese = 0,
#[default]
English = 1,
French = 2,
German = 3,
}
impl From<i64> for TranslateLanguagePB {
fn from(data: i64) -> Self {
match data {
0 => TranslateLanguagePB::Chinese,
1 => TranslateLanguagePB::English,
2 => TranslateLanguagePB::French,
3 => TranslateLanguagePB::German,
_ => TranslateLanguagePB::English,
}
}
}

View File

@ -1104,3 +1104,16 @@ pub(crate) async fn summarize_row_handler(
.await?;
Ok(())
}
pub(crate) async fn translate_row_handler(
data: AFPluginData<TranslateRowPB>,
manager: AFPluginState<Weak<DatabaseManager>>,
) -> Result<(), FlowyError> {
let manager = upgrade_manager(manager)?;
let data = data.try_into_inner()?;
let row_id = RowId::from(data.row_id);
manager
.translate_row(data.view_id, row_id, data.field_id)
.await?;
Ok(())
}

View File

@ -91,6 +91,7 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
.event(DatabaseEvent::GetRelatedDatabaseRows, get_related_database_rows_handler)
// AI
.event(DatabaseEvent::SummarizeRow, summarize_row_handler)
.event(DatabaseEvent::TranslateRow, translate_row_handler)
}
/// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf)
@ -373,4 +374,7 @@ pub enum DatabaseEvent {
#[event(input = "SummaryRowPB")]
SummarizeRow = 174,
#[event(input = "TranslateRowPB")]
TranslateRow = 175,
}

View File

@ -17,15 +17,19 @@ use tracing::{event, instrument, trace};
use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig};
use collab_integrate::{CollabKVAction, CollabKVDB, CollabPersistenceConfig};
use flowy_database_pub::cloud::{DatabaseCloudService, SummaryRowContent};
use flowy_database_pub::cloud::{
DatabaseCloudService, SummaryRowContent, TranslateItem, TranslateRowContent,
};
use flowy_error::{internal_error, FlowyError, FlowyResult};
use lib_infra::box_any::BoxAny;
use lib_infra::priority_task::TaskDispatcher;
use crate::entities::{DatabaseLayoutPB, DatabaseSnapshotPB};
use crate::entities::{DatabaseLayoutPB, DatabaseSnapshotPB, FieldType};
use crate::services::cell::stringify_cell;
use crate::services::database::DatabaseEditor;
use crate::services::database_view::DatabaseLayoutDepsResolver;
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
use crate::services::field_settings::default_field_settings_by_layout_map;
use crate::services::share::csv::{CSVFormat, CSVImporter, ImportResult};
@ -459,6 +463,77 @@ impl DatabaseManager {
Ok(())
}
#[instrument(level = "debug", skip_all)]
pub async fn translate_row(
&self,
view_id: String,
row_id: RowId,
field_id: String,
) -> FlowyResult<()> {
let database = self.get_database_with_view_id(&view_id).await?;
let mut translate_row_content = TranslateRowContent::new();
let mut language = "english".to_string();
if let Some(row) = database.get_row(&view_id, &row_id) {
let fields = database.get_fields(&view_id, None);
for field in fields {
// When translate a row, skip the content in the "AI Translate" cell; it does not need to
// be translated.
if field.id != field_id {
if let Some(cell) = row.cells.get(&field.id) {
translate_row_content.push(TranslateItem {
title: field.name.clone(),
content: stringify_cell(cell, &field),
})
}
} else {
language = TranslateTypeOption::language_from_type(
field
.type_options
.get(&FieldType::Translate.to_string())
.cloned()
.map(TranslateTypeOption::from)
.unwrap_or_default()
.language_type,
)
.to_string();
}
}
}
// Call the cloud service to summarize the row.
trace!(
"[AI]:translate to {}, content:{:?}",
language,
translate_row_content
);
let response = self
.cloud_service
.translate_database_row(&self.user.workspace_id()?, translate_row_content, &language)
.await?;
// Format the response items into a single string
let content = response
.items
.into_iter()
.map(|value| {
value
.into_iter()
.map(|(_k, v)| v.to_string())
.collect::<Vec<String>>()
.join(", ")
})
.collect::<Vec<String>>()
.join(",");
trace!("[AI]:translate row response: {}", content);
// Update the cell with the response from the cloud service.
database
.update_cell_with_changeset(&view_id, &row_id, &field_id, BoxAny::new(content))
.await?;
Ok(())
}
/// Only expose this method for testing
#[cfg(debug_assertions)]
pub fn get_cloud_service(&self) -> &Arc<dyn DatabaseCloudService> {

View File

@ -262,6 +262,9 @@ impl<'a> CellBuilder<'a> {
FieldType::Summary => {
cells.insert(field_id, insert_text_cell(cell_str, field));
},
FieldType::Translate => {
cells.insert(field_id, insert_text_cell(cell_str, field));
},
}
}
}

View File

@ -1703,8 +1703,9 @@ pub async fn update_field_type_option_fn(
update.update_type_options(|type_options_update| {
event!(
tracing::Level::TRACE,
"insert type option to field type: {:?}",
field_type
"insert type option to field type: {:?}, {:?}",
field_type,
type_option_data
);
type_options_update.insert(&field_type.to_string(), type_option_data);
});

View File

@ -7,6 +7,7 @@ pub mod selection_type_option;
pub mod summary_type_option;
pub mod text_type_option;
pub mod timestamp_type_option;
pub mod translate_type_option;
mod type_option;
mod type_option_cell;
mod url_type_option;

View File

@ -85,6 +85,7 @@ impl CellDataDecoder for RichTextTypeOption {
| FieldType::CreatedTime
| FieldType::Relation => None,
FieldType::Summary => Some(StringCellData::from(stringify_cell(cell, field))),
FieldType::Translate => Some(StringCellData::from(stringify_cell(cell, field))),
}
}

View File

@ -0,0 +1,2 @@
pub mod translate;
pub mod translate_entities;

View File

@ -0,0 +1,137 @@
use crate::entities::TextFilterPB;
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::type_options::translate_type_option::translate_entities::TranslateCellData;
use crate::services::field::type_options::util::ProtobufStr;
use crate::services::field::{
TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionCellDataSerde, TypeOptionTransform,
};
use crate::services::sort::SortCondition;
use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use flowy_error::FlowyResult;
use std::cmp::Ordering;
#[derive(Debug, Clone)]
pub struct TranslateTypeOption {
pub auto_fill: bool,
/// Use [TranslateTypeOption::language_from_type] to get the language name
pub language_type: i64,
}
impl TranslateTypeOption {
pub fn language_from_type(language_type: i64) -> &'static str {
match language_type {
0 => "Chinese",
1 => "English",
2 => "French",
3 => "German",
_ => "English",
}
}
}
impl Default for TranslateTypeOption {
fn default() -> Self {
Self {
auto_fill: false,
language_type: 1,
}
}
}
impl From<TypeOptionData> for TranslateTypeOption {
fn from(value: TypeOptionData) -> Self {
let auto_fill = value.get_bool_value("auto_fill").unwrap_or_default();
let language = value.get_i64_value("language").unwrap_or_default();
Self {
auto_fill,
language_type: language,
}
}
}
impl From<TranslateTypeOption> for TypeOptionData {
fn from(value: TranslateTypeOption) -> Self {
TypeOptionDataBuilder::new()
.insert_bool_value("auto_fill", value.auto_fill)
.insert_i64_value("language", value.language_type)
.build()
}
}
impl TypeOption for TranslateTypeOption {
type CellData = TranslateCellData;
type CellChangeset = String;
type CellProtobufType = ProtobufStr;
type CellFilter = TextFilterPB;
}
impl CellDataChangeset for TranslateTypeOption {
fn apply_changeset(
&self,
changeset: String,
_cell: Option<Cell>,
) -> FlowyResult<(Cell, TranslateCellData)> {
let cell_data = TranslateCellData(changeset);
Ok((cell_data.clone().into(), cell_data))
}
}
impl TypeOptionCellDataFilter for TranslateTypeOption {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
filter.is_visible(cell_data)
}
}
impl TypeOptionCellDataCompare for TranslateTypeOption {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> Ordering {
match (cell_data.is_cell_empty(), other_cell_data.is_cell_empty()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
(false, false) => {
let order = cell_data.0.cmp(&other_cell_data.0);
sort_condition.evaluate_order(order)
},
}
}
}
impl CellDataDecoder for TranslateTypeOption {
fn decode_cell(&self, cell: &Cell) -> FlowyResult<TranslateCellData> {
Ok(TranslateCellData::from(cell))
}
fn stringify_cell_data(&self, cell_data: TranslateCellData) -> String {
cell_data.to_string()
}
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
None
}
}
impl TypeOptionTransform for TranslateTypeOption {}
impl TypeOptionCellDataSerde for TranslateTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
ProtobufStr::from(cell_data.0)
}
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(TranslateCellData::from(cell))
}
}

View File

@ -0,0 +1,46 @@
use crate::entities::FieldType;
use crate::services::field::{TypeOptionCellData, CELL_DATA};
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::{new_cell_builder, Cell};
#[derive(Default, Debug, Clone)]
pub struct TranslateCellData(pub String);
impl std::ops::Deref for TranslateCellData {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TypeOptionCellData for TranslateCellData {
fn is_cell_empty(&self) -> bool {
self.0.is_empty()
}
}
impl From<&Cell> for TranslateCellData {
fn from(cell: &Cell) -> Self {
Self(cell.get_str_value(CELL_DATA).unwrap_or_default())
}
}
impl From<TranslateCellData> for Cell {
fn from(data: TranslateCellData) -> Self {
new_cell_builder(FieldType::Translate)
.insert_str_value(CELL_DATA, data.0)
.build()
}
}
impl ToString for TranslateCellData {
fn to_string(&self) -> String {
self.0.clone()
}
}
impl AsRef<str> for TranslateCellData {
fn as_ref(&self) -> &str {
&self.0
}
}

View File

@ -11,11 +11,13 @@ use flowy_error::FlowyResult;
use crate::entities::{
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType,
MultiSelectTypeOptionPB, NumberTypeOptionPB, RelationTypeOptionPB, RichTextTypeOptionPB,
SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimestampTypeOptionPB, URLTypeOptionPB,
SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimestampTypeOptionPB,
TranslateTypeOptionPB, URLTypeOptionPB,
};
use crate::services::cell::CellDataDecoder;
use crate::services::field::checklist_type_option::ChecklistTypeOption;
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
use crate::services::field::{
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption,
RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, URLTypeOption,
@ -185,6 +187,9 @@ pub fn type_option_data_from_pb<T: Into<Bytes>>(
FieldType::Summary => {
SummarizationTypeOptionPB::try_from(bytes).map(|pb| SummarizationTypeOption::from(pb).into())
},
FieldType::Translate => {
TranslateTypeOptionPB::try_from(bytes).map(|pb| TranslateTypeOption::from(pb).into())
},
}
}
@ -252,6 +257,12 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) ->
.try_into()
.unwrap()
},
FieldType::Translate => {
let translate_type_option: TranslateTypeOption = type_option.into();
TranslateTypeOptionPB::from(translate_type_option)
.try_into()
.unwrap()
},
}
}
@ -272,5 +283,6 @@ pub fn default_type_option_data_from_type(field_type: FieldType) -> TypeOptionDa
FieldType::Checklist => ChecklistTypeOption.into(),
FieldType::Relation => RelationTypeOption::default().into(),
FieldType::Summary => SummarizationTypeOption::default().into(),
FieldType::Translate => TranslateTypeOption::default().into(),
}
}

View File

@ -11,6 +11,7 @@ use lib_infra::box_any::BoxAny;
use crate::entities::FieldType;
use crate::services::cell::{CellCache, CellDataChangeset, CellDataDecoder, CellProtobufBlob};
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
use crate::services::field::{
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, TypeOption,
@ -449,6 +450,16 @@ impl<'a> TypeOptionCellExt<'a> {
self.cell_data_cache.clone(),
)
}),
FieldType::Translate => self
.field
.get_type_option::<TranslateTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
field_type,
self.cell_data_cache.clone(),
)
}),
}
}
@ -552,6 +563,9 @@ fn get_type_option_transform_handler(
},
FieldType::Summary => Box::new(SummarizationTypeOption::from(type_option_data))
as Box<dyn TypeOptionTransformHandler>,
FieldType::Translate => {
Box::new(TranslateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},
}
}

View File

@ -281,6 +281,7 @@ impl FilterInner {
FieldType::Checkbox => BoxAny::new(CheckboxFilterPB::parse(condition as u8, content)),
FieldType::Relation => BoxAny::new(RelationFilterPB::parse(condition as u8, content)),
FieldType::Summary => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
FieldType::Translate => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
};
FilterInner::Data {
@ -367,6 +368,10 @@ impl<'a> From<&'a Filter> for FilterMap {
let filter = condition_and_content.cloned::<TextFilterPB>()?;
(filter.condition as u8, filter.content)
},
FieldType::Translate => {
let filter = condition_and_content.cloned::<TextFilterPB>()?;
(filter.condition as u8, filter.content)
},
};
Some((condition, content))
};

View File

@ -134,6 +134,7 @@ pub fn make_test_board() -> DatabaseData {
.build();
fields.push(relation_field);
},
FieldType::Translate => {},
}
}

View File

@ -132,6 +132,7 @@ pub fn make_test_grid() -> DatabaseData {
.build();
fields.push(relation_field);
},
FieldType::Translate => {},
}
}

View File

@ -83,6 +83,7 @@ async fn export_and_then_import_meta_csv_test() {
FieldType::CreatedTime => {},
FieldType::Relation => {},
FieldType::Summary => {},
FieldType::Translate => {},
}
} else {
panic!(
@ -166,6 +167,7 @@ async fn history_database_import_test() {
FieldType::CreatedTime => {},
FieldType::Relation => {},
FieldType::Summary => {},
FieldType::Translate => {},
}
} else {
panic!(

View File

@ -1,5 +1,7 @@
use anyhow::Error;
use client_api::entity::ai_dto::{SummarizeRowData, SummarizeRowParams};
use client_api::entity::ai_dto::{
SummarizeRowData, SummarizeRowParams, TranslateRowData, TranslateRowParams,
};
use client_api::entity::QueryCollabResult::{Failed, Success};
use client_api::entity::{QueryCollab, QueryCollabParams};
use client_api::error::ErrorCode::RecordNotFound;
@ -12,6 +14,7 @@ use tracing::{error, instrument};
use flowy_database_pub::cloud::{
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
TranslateRowContent, TranslateRowResponse,
};
use lib_infra::future::FutureResult;
@ -139,4 +142,26 @@ where
Ok(data.text)
})
}
fn translate_database_row(
&self,
workspace_id: &str,
translate_row: TranslateRowContent,
language: &str,
) -> FutureResult<TranslateRowResponse, Error> {
let language = language.to_string();
let workspace_id = workspace_id.to_string();
let try_get_client = self.inner.try_get_client();
FutureResult::new(async move {
let data = TranslateRowData {
cells: translate_row,
language,
include_header: false,
};
let params = TranslateRowParams { workspace_id, data };
let data = try_get_client?.translate_row(params).await?;
Ok(data)
})
}
}

View File

@ -6,6 +6,7 @@ use yrs::{Any, MapPrelim};
use flowy_database_pub::cloud::{
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
TranslateRowContent, TranslateRowResponse,
};
use lib_infra::future::FutureResult;
@ -85,4 +86,14 @@ impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl {
// TODO(lucas): local ai
FutureResult::new(async move { Ok("".to_string()) })
}
fn translate_database_row(
&self,
_workspace_id: &str,
_translate_row: TranslateRowContent,
_language: &str,
) -> FutureResult<TranslateRowResponse, Error> {
// TODO(lucas): local ai
FutureResult::new(async move { Ok(TranslateRowResponse::default()) })
}
}

View File

@ -4,6 +4,7 @@ use tokio::sync::oneshot::channel;
use flowy_database_pub::cloud::{
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
TranslateRowContent, TranslateRowResponse,
};
use lib_dispatch::prelude::af_spawn;
use lib_infra::future::FutureResult;
@ -105,4 +106,13 @@ where
) -> FutureResult<String, Error> {
FutureResult::new(async move { Ok("".to_string()) })
}
fn translate_database_row(
&self,
_workspace_id: &str,
_translate_row: TranslateRowContent,
_language: &str,
) -> FutureResult<TranslateRowResponse, Error> {
FutureResult::new(async move { Ok(TranslateRowResponse::default()) })
}
}