diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart new file mode 100644 index 0000000000..a207a31021 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart @@ -0,0 +1,47 @@ +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../../shared/constants.dart'; +import '../../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('AI Writer:', () { + testWidgets('the ai writer transaction should only apply in memory', + (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + ); + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + + const pageName = 'Document'; + await tester.createNewPageInSpace( + spaceName: Constants.generalSpaceName, + layout: ViewLayoutPB.Document, + pageName: pageName, + ); + + await tester.editor.tapLineOfEditorAt(0); + await tester.editor.showSlashMenu(); + await tester.editor.tapSlashMenuItemWithName( + LocaleKeys.document_slashMenu_name_aiWriter.tr(), + ); + expect(find.byType(AutoCompletionBlockComponent), findsOneWidget); + + // switch to another page + await tester.openPage(Constants.gettingStartedPageName); + // switch back to the page + await tester.openPage(pageName); + + // expect the ai writer block is not in the document + expect(find.byType(AutoCompletionBlockComponent), findsNothing); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_test_runner.dart index 88d7db8a71..58a9d7398b 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_test_runner.dart @@ -1,5 +1,6 @@ import 'package:integration_test/integration_test.dart'; +import 'document_ai_writer_test.dart' as document_ai_writer_test; import 'document_copy_link_to_block_test.dart' as document_copy_link_to_block_test; import 'document_option_actions_test.dart' as document_option_actions_test; @@ -11,4 +12,5 @@ void main() { document_option_actions_test.main(); document_copy_link_to_block_test.main(); document_publish_test.main(); + document_ai_writer_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart index b6c2e4cc8c..c6fb6f7293 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart @@ -26,7 +26,6 @@ import '../../shared/util.dart'; const _testImageUrls = [ 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640', - 'https://mathiasbynens.be/demo/animated-webp-supported.webp', 'https://www.easygifanimator.net/images/samples/eglite.gif', 'https://people.math.sc.edu/Burkardt/data/bmp/snail.bmp', 'https://file-examples.com/storage/fe9566cb7d67345489a5a97/2017/10/file_example_JPG_100kB.jpg', diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_multi_image_block_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_multi_image_block_test.dart index d8b0784a39..68ad7db7e5 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_multi_image_block_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_multi_image_block_test.dart @@ -48,7 +48,7 @@ void main() { await tester.editor.showSlashMenu(); await tester.editor.tapSlashMenuItemWithName( LocaleKeys.document_slashMenu_name_photoGallery.tr(), - offset: 100, + offset: 80, ); expect(find.byType(MultiImageBlockComponent), findsOneWidget); expect(find.byType(MultiImagePlaceholder), findsOneWidget); @@ -146,7 +146,7 @@ void main() { await tester.editor.showSlashMenu(); await tester.editor.tapSlashMenuItemWithName( LocaleKeys.document_slashMenu_name_photoGallery.tr(), - offset: 100, + offset: 80, ); expect(find.byType(MultiImageBlockComponent), findsOneWidget); expect(find.byType(MultiImagePlaceholder), findsOneWidget); diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index dcf1c2e2b7..78a0d8d981 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -246,10 +246,16 @@ class DocumentBloc extends Bloc { (event) async { final time = event.$1; final transaction = event.$2; + final options = event.$3; if (time != TransactionTime.before) { return; } + if (options.inMemoryUpdate) { + Log.info('skip transaction for in-memory update'); + return; + } + if (enableDocumentInternalLog) { Log.debug( '[TransactionAdapter] 1. transaction before apply: ${transaction.hashCode}', diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index fdee715c78..bbdbd56446 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -404,8 +404,9 @@ class _AppFlowyEditorPageState extends State } List _customSlashMenuItems() { + final isLocalMode = context.read().isLocalMode; return [ - aiWriterSlashMenuItem, + if (!isLocalMode) aiWriterSlashMenuItem, textSlashMenuItem, heading1SlashMenuItem, heading2SlashMenuItem, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/text_robot.dart index f50cf7f0e8..445a504657 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/text_robot.dart @@ -3,6 +3,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; enum TextRobotInputType { character, word, + sentence, } class TextRobot { @@ -16,44 +17,78 @@ class TextRobot { String text, { TextRobotInputType inputType = TextRobotInputType.word, Duration delay = const Duration(milliseconds: 10), + String separator = '\n', }) async { - if (text == '\n') { - return editorState.insertNewLine(); + if (text == separator) { + await editorState.insertNewLine(); + await Future.delayed(delay); + return; } - final lines = text.split('\n'); + final lines = _splitText(text, separator); for (final line in lines) { if (line.isEmpty) { await editorState.insertNewLine(); + await Future.delayed(delay); continue; } switch (inputType) { case TextRobotInputType.character: - final iterator = line.runes.iterator; - while (iterator.moveNext()) { - await editorState.insertTextAtCurrentSelection( - iterator.currentAsString, - ); - await Future.delayed(delay); - } + await insertCharacter(line, delay); break; case TextRobotInputType.word: - final words = line.split(' '); - if (words.length == 1 || - (words.length == 2 && - (words.first.isEmpty || words.last.isEmpty))) { - await editorState.insertTextAtCurrentSelection( - line, - ); - } else { - for (final word in words.map((e) => '$e ')) { - await editorState.insertTextAtCurrentSelection( - word, - ); - } - } - await Future.delayed(delay); + await insertWord(line, delay); + break; + case TextRobotInputType.sentence: + await insertSentence(line, delay); break; } } } + + Future insertCharacter(String line, Duration delay) async { + final iterator = line.runes.iterator; + while (iterator.moveNext()) { + await editorState.insertTextAtCurrentSelection( + iterator.currentAsString, + ); + await Future.delayed(delay); + } + } + + Future insertWord(String line, Duration delay) async { + final words = line.split(' '); + if (words.length == 1 || + (words.length == 2 && (words.first.isEmpty || words.last.isEmpty))) { + await editorState.insertTextAtCurrentSelection( + line, + ); + } else { + for (final word in words.map((e) => '$e ')) { + await editorState.insertTextAtCurrentSelection( + word, + ); + } + } + await Future.delayed(delay); + } + + Future insertSentence(String line, Duration delay) async { + await editorState.insertTextAtCurrentSelection(line); + await Future.delayed(delay); + } +} + +List _splitText(String text, String separator) { + final parts = text.split(RegExp(separator)); + final result = []; + + for (int i = 0; i < parts.length; i++) { + result.add(parts[i]); + // Only add empty string if it's not the last part and the next part is not empty + if (i < parts.length - 1 && parts[i + 1].isNotEmpty) { + result.add(''); + } + } + + return result; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart index 7d637b1273..489579e707 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart @@ -194,7 +194,7 @@ class _AutoCompletionBlockComponentState final transaction = editorState.transaction..deleteNode(widget.node); await editorState.apply( transaction, - options: const ApplyOptions(recordUndo: false), + options: const ApplyOptions(recordUndo: false, inMemoryUpdate: true), withUpdateSelection: false, ); } @@ -231,6 +231,8 @@ class _AutoCompletionBlockComponentState onProcess: (text) async { await textRobot.autoInsertText( text, + separator: r'\n\n', + inputType: TextRobotInputType.sentence, delay: Duration.zero, ); }, @@ -267,7 +269,10 @@ class _AutoCompletionBlockComponentState start, end.last - start.last + 1, ); - await editorState.apply(transaction); + await editorState.apply( + transaction, + options: const ApplyOptions(inMemoryUpdate: true), + ); await _makeSurePreviousNodeIsEmptyParagraphNode(); } } @@ -318,6 +323,8 @@ class _AutoCompletionBlockComponentState onProcess: (text) async { await textRobot.autoInsertText( text, + inputType: TextRobotInputType.sentence, + separator: r'\n\n', delay: Duration.zero, ); }, @@ -394,12 +401,13 @@ class _AutoCompletionBlockComponentState Future _makeSurePreviousNodeIsEmptyParagraphNode() async { // make sure the previous node is a empty paragraph node without any styles. - final transaction = editorState.transaction; + final previous = widget.node.previous; final Selection selection; if (previous == null || previous.type != ParagraphBlockKeys.type || (previous.delta?.toPlainText().isNotEmpty ?? false)) { + final transaction = editorState.transaction; selection = Selection.single( path: widget.node.path, startOffset: 0, @@ -408,17 +416,22 @@ class _AutoCompletionBlockComponentState widget.node.path, paragraphNode(), ); + await editorState.apply(transaction); } else { selection = Selection.single( path: previous.path, startOffset: 0, ); } + final transaction = editorState.transaction; transaction.updateNode(widget.node, { AutoCompletionBlockKeys.startSelection: selection.toJson(), }); transaction.afterSelection = selection; - await editorState.apply(transaction); + await editorState.apply( + transaction, + options: const ApplyOptions(inMemoryUpdate: true), + ); } void _subscribeSelectionGesture() { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart index f5d1148692..8fc3fd9145 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart @@ -88,8 +88,7 @@ class _OutlineBlockWidgetState extends State @override late EditorState editorState = context.read(); - late Stream<(TransactionTime, Transaction)> stream = - editorState.transactionStream; + late Stream stream = editorState.transactionStream; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items.dart index fdebfddfc7..a06d3abb37 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items.dart @@ -465,6 +465,7 @@ SelectionMenuItem outlineSlashMenuItem = SelectionMenuItem( editorState.apply(transaction); }, ); + // math equation SelectionMenuItem mathEquationSlashMenuItem = SelectionMenuItem.node( getName: () => LocaleKeys.document_slashMenu_name_mathEquation.tr(), @@ -541,20 +542,37 @@ SelectionMenuItem emojiSlashMenuItem = SelectionMenuItem( ); // auto generate menu item -SelectionMenuItem aiWriterSlashMenuItem = SelectionMenuItem.node( - getName: () => LocaleKeys.document_slashMenu_name_aiWriter.tr(), - nameBuilder: _slashMenuItemNameBuilder, - iconBuilder: (editorState, isSelected, style) => SelectableSvgWidget( +SelectionMenuItem aiWriterSlashMenuItem = SelectionMenuItem( + getName: LocaleKeys.document_slashMenu_name_aiWriter.tr, + icon: (editorState, isSelected, style) => SelectableSvgWidget( data: FlowySvgs.slash_menu_icon_ai_writer_s, isSelected: isSelected, style: style, ), keywords: ['ai', 'openai', 'writer', 'ai writer', 'autogenerator'], - nodeBuilder: (editorState, _) { - final node = autoCompletionNode(start: editorState.selection!); - return node; + handler: (editorState, menuService, context) { + final selection = editorState.selection; + if (selection == null || !selection.isCollapsed) { + return; + } + final node = editorState.getNodeAtPath(selection.end.path); + final delta = node?.delta; + if (node == null || delta == null) { + return; + } + final newNode = autoCompletionNode(start: selection); + + final transaction = editorState.transaction; + //default insert after + final path = node.path.next; + transaction + ..insertNode(path, newNode) + ..afterSelection = null; + editorState.apply( + transaction, + options: const ApplyOptions(inMemoryUpdate: true), + ); }, - replace: (_, node) => false, ); // table menu item diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart index 1c50b8fbe5..29cc7435ff 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart @@ -40,7 +40,7 @@ class EditorTransactionService extends StatefulWidget { } class _EditorTransactionServiceState extends State { - StreamSubscription<(TransactionTime, Transaction)>? transactionSubscription; + StreamSubscription? transactionSubscription; bool isUndoRedo = false; bool isPaste = false; @@ -131,8 +131,11 @@ class _EditorTransactionServiceState extends State { return matchingNodes; } - void onEditorTransaction((TransactionTime, Transaction) event) { - if (event.$1 == TransactionTime.before) { + void onEditorTransaction(EditorTransactionValue event) { + final time = event.$1; + final transaction = event.$2; + + if (time == TransactionTime.before) { return; } @@ -145,7 +148,7 @@ class _EditorTransactionServiceState extends State { handler.type: handler.livesInDelta ? [] : [], }; - for (final op in event.$2.operations) { + for (final op in transaction.operations) { if (op is InsertOperation) { for (final n in op.nodes) { for (final handler in _transactionHandlers) { diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index c27af2c406..f9d70b44e6 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -61,8 +61,8 @@ packages: dependency: "direct main" description: path: "." - ref: ea81e3c - resolved-ref: ea81e3c1647344aff45970c39556902ffad4373d + ref: "5b3878d" + resolved-ref: "5b3878dcc5876ae7a329b308ff82763f02cf8c5f" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "4.0.0" @@ -70,8 +70,8 @@ packages: dependency: "direct main" description: path: "packages/appflowy_editor_plugins" - ref: "27c898d" - resolved-ref: "27c898d1343f52d80444a0f469b8ee403606cf36" + ref: "3f82111" + resolved-ref: "3f82111f958b0ac9f06aa80fd19a629f3a649ec0" url: "https://github.com/AppFlowy-IO/AppFlowy-plugins.git" source: git version: "0.0.6" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index c4f06fbfb5..17e6be1e63 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -172,13 +172,13 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "ea81e3c" + ref: "5b3878d" appflowy_editor_plugins: git: url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git path: "packages/appflowy_editor_plugins" - ref: "27c898d" + ref: "3f82111" sheet: git: diff --git a/frontend/appflowy_flutter/test/unit_test/document/text_robot/text_robot_test.dart b/frontend/appflowy_flutter/test/unit_test/document/text_robot/text_robot_test.dart new file mode 100644 index 0000000000..ca08e4310b --- /dev/null +++ b/frontend/appflowy_flutter/test/unit_test/document/text_robot/text_robot_test.dart @@ -0,0 +1,549 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('text robot:', () { + setUpAll(() { + Log.shared.disableLog = true; + }); + + tearDownAll(() { + Log.shared.disableLog = false; + }); + + test('auto insert text with sentence mode (1)', () async { + final editorState = EditorState.blank(); + editorState.selection = Selection.collapsed(Position(path: [0])); + final textRobot = TextRobot( + editorState: editorState, + ); + for (final text in _sample1) { + await textRobot.autoInsertText( + text, + separator: r'\n\n', + inputType: TextRobotInputType.sentence, + delay: Duration.zero, + ); + } + + final p1 = editorState.document.nodeAtPath([0])!.delta!.toPlainText(); + final p2 = editorState.document.nodeAtPath([1])!.delta!.toPlainText(); + final p3 = editorState.document.nodeAtPath([2])!.delta!.toPlainText(); + + expect( + p1, + 'In a quaint village nestled between rolling hills, a young girl named Elara discovered a hidden garden. She stumbled upon it while chasing a mischievous rabbit through a narrow, winding path. ', + ); + expect( + p2, + 'The garden was a vibrant oasis, brimming with colorful flowers and whispering trees. Elara felt an inexplicable connection to the place, as if it held secrets from a forgotten time. ', + ); + expect( + p3, + 'Determined to uncover its mysteries, she visited daily, unraveling tales of ancient magic and wisdom. The garden transformed her spirit, teaching her the importance of harmony and the beauty of nature\'s wonders.', + ); + }); + + test('auto insert text with sentence mode (2)', () async { + final editorState = EditorState.blank(); + editorState.selection = Selection.collapsed(Position(path: [0])); + final textRobot = TextRobot( + editorState: editorState, + ); + + var breakCount = 0; + for (final text in _sample2) { + if (text.contains('\n\n')) { + breakCount++; + } + await textRobot.autoInsertText( + text, + separator: r'\n\n', + inputType: TextRobotInputType.sentence, + delay: Duration.zero, + ); + } + + final len = editorState.document.root.children.length; + expect(len, breakCount + 1); + expect(len, 7); + + final p1 = editorState.document.nodeAtPath([0])!.delta!.toPlainText(); + final p2 = editorState.document.nodeAtPath([1])!.delta!.toPlainText(); + final p3 = editorState.document.nodeAtPath([2])!.delta!.toPlainText(); + final p4 = editorState.document.nodeAtPath([3])!.delta!.toPlainText(); + final p5 = editorState.document.nodeAtPath([4])!.delta!.toPlainText(); + final p6 = editorState.document.nodeAtPath([5])!.delta!.toPlainText(); + final p7 = editorState.document.nodeAtPath([6])!.delta!.toPlainText(); + + expect( + p1, + 'Once upon a time in the small, whimsical village of Greenhollow, nestled between rolling hills and lush forests, there lived a young girl named Elara. Unlike the other villagers, Elara had a unique gift: she could communicate with animals. This extraordinary ability made her both a beloved and mysterious figure in Greenhollow.', + ); + expect( + p2, + 'One crisp autumn morning, as golden leaves danced in the breeze, Elara heard a distressed call from the forest. Following the sound, she discovered a young fox trapped in a hunter\'s snare. With gentle hands and a calming voice, she freed the frightened creature, who introduced himself as Rufus. Grateful for her help, Rufus promised to assist Elara whenever she needed.', + ); + expect( + p3, + 'Word of Elara\'s kindness spread among the forest animals, and soon she found herself surrounded by a diverse group of animal friends, from wise old owls to playful otters. Together, they shared stories, solved problems, and looked out for one another.', + ); + expect( + p4, + 'One day, the village faced an unexpected threat: a severe drought that threatened their crops and water supply. The villagers grew anxious, unsure of how to cope with the impending scarcity. Elara, determined to help, turned to her animal friends for guidance.', + ); + expect( + p5, + 'The animals led Elara to a hidden spring deep within the forest, a source of fresh water unknown to the villagers. With Rufus\'s clever planning and the otters\' help in directing the flow, they managed to channel the spring water to the village, saving the crops and quenching the villagers\' thirst.', + ); + expect( + p6, + 'Grateful and amazed, the villagers hailed Elara as a hero. They came to understand the importance of living harmoniously with nature and the wonders that could be achieved through kindness and cooperation.', + ); + expect( + p7, + 'From that day on, Greenhollow thrived as a community where humans and animals lived together in harmony, cherishing the bonds that Elara had helped forge. And whenever challenges arose, the villagers knew they could rely on Elara and her extraordinary friends to guide them through, ensuring that the spirit of unity and compassion always prevailed.', + ); + }); + }); +} + +final _sample1 = [ + "In", + " a quaint", + " village", + " nestled", + " between", + " rolling", + " hills", + ",", + " a", + " young", + " girl", + " named", + " El", + "ara discovered", + " a hidden", + " garden", + ".", + " She stumbled", + " upon", + " it", + " while", + " chasing", + " a", + " misch", + "iev", + "ous rabbit", + " through", + " a", + " narrow,", + " winding path", + ".", + " \n\n", + "The", + " garden", + " was", + " a", + " vibrant", + " oasis", + ",", + " br", + "imming with", + " colorful", + " flowers", + " and whisper", + "ing", + " trees", + ".", + " El", + "ara", + " felt", + " an inexp", + "licable", + " connection", + " to", + " the", + " place,", + " as", + " if", + " it held", + " secrets", + " from", + " a", + " forgotten", + " time", + ".", + " \n\n", + "Determ", + "ined to", + " uncover", + " its", + " mysteries", + ",", + " she", + " visited", + " daily,", + " unravel", + "ing", + " tales", + " of", + " ancient", + " magic", + " and", + " wisdom", + ".", + " The", + " garden transformed", + " her", + " spirit", + ", teaching", + " her the", + " importance of harmony and", + " the", + " beauty", + " of", + " nature", + "'s wonders.", +]; + +final _sample2 = [ + "Once", + " upon", + " a", + " time", + " in", + " the small", + ",", + " whimsical", + " village", + " of", + " Green", + "h", + "ollow", + ",", + " nestled", + " between", + " rolling hills", + " and", + " lush", + " forests", + ",", + " there", + " lived", + " a young", + " girl", + " named", + " Elara.", + " Unlike the", + " other", + " villagers", + ",", + " El", + "ara", + " had", + " a unique", + " gift", + ":", + " she could", + " communicate", + " with", + " animals", + ".", + " This", + " extraordinary", + " ability", + " made", + " her both a", + " beloved", + " and", + " mysterious", + " figure", + " in", + " Green", + "h", + "ollow", + ".\n\n", + "One", + " crisp", + " autumn", + " morning,", + " as", + " golden", + " leaves", + " danced", + " in", + " the", + " breeze", + ", El", + "ara heard", + " a distressed", + " call", + " from", + " the", + " forest", + ".", + " Following", + " the", + " sound", + ",", + " she", + " discovered", + " a", + " young", + " fox", + " trapped", + " in", + " a", + " hunter's", + " snare", + ".", + " With", + " gentle", + " hands", + " and", + " a", + " calming", + " voice", + ",", + " she", + " freed", + " the", + " frightened", + " creature", + ", who", + " introduced", + " himself", + " as Ruf", + "us.", + " Gr", + "ateful", + " for", + " her", + " help", + ",", + " Rufus promised", + " to assist", + " Elara", + " whenever", + " she", + " needed.\n\n", + "Word", + " of", + " Elara", + "'s kindness", + " spread among", + " the forest", + " animals", + ",", + " and soon", + " she", + " found", + " herself", + " surrounded", + " by", + " a", + " diverse", + " group", + " of", + " animal", + " friends", + ",", + " from", + " wise", + " old ow", + "ls to playful", + " ot", + "ters.", + " Together,", + " they", + " shared stories", + ",", + " solved problems", + ",", + " and", + " looked", + " out", + " for", + " one", + " another", + ".\n\n", + "One", + " day", + ", the village faced", + " an unexpected", + " threat", + ":", + " a", + " severe", + " drought", + " that", + " threatened", + " their", + " crops", + " and", + " water supply", + ".", + " The", + " villagers", + " grew", + " anxious", + ",", + " unsure", + " of", + " how to", + " cope", + " with", + " the", + " impending", + " scarcity", + ".", + " El", + "ara", + ",", + " determined", + " to", + " help", + ",", + " turned", + " to her", + " animal friends", + " for", + " guidance", + ".\n\nThe", + " animals", + " led", + " El", + "ara", + " to", + " a", + " hidden", + " spring", + " deep", + " within", + " the forest,", + " a source", + " of", + " fresh", + " water unknown", + " to the", + " villagers", + ".", + " With", + " Ruf", + "us's", + " clever planning", + " and the", + " ot", + "ters", + "'", + " help", + " in directing", + " the", + " flow", + ",", + " they", + " managed", + " to", + " channel the", + " spring", + " water", + " to", + " the", + " village,", + " saving the", + " crops", + " and", + " quenching", + " the", + " villagers", + "'", + " thirst", + ".\n\n", + "Gr", + "ateful and", + " amazed,", + " the", + " villagers", + " hailed El", + "ara as", + " a", + " hero", + ".", + " They", + " came", + " to", + " understand the", + " importance", + " of living", + " harmon", + "iously", + " with", + " nature", + " and", + " the", + " wonders", + " that", + " could", + " be", + " achieved", + " through kindness", + " and cooperation", + ".\n\nFrom", + " that day", + " on", + ",", + " Greenh", + "ollow", + " thr", + "ived", + " as", + " a", + " community", + " where", + " humans", + " and", + " animals", + " lived together", + " in", + " harmony", + ",", + " cher", + "ishing", + " the", + " bonds that", + " El", + "ara", + " had", + " helped", + " forge", + ".", + " And whenever", + " challenges arose", + ", the", + " villagers", + " knew", + " they", + " could", + " rely on", + " El", + "ara and", + " her", + " extraordinary", + " friends", + " to", + " guide them", + " through", + ",", + " ensuring", + " that", + " the", + " spirit", + " of", + " unity", + " and", + " compassion", + " always prevailed.", +];