mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-10-03 20:33:50 +00:00
feat: support skipping in-memory update transaction (#6856)
* feat: support skipping in-memory update transaction * fix: flutter analyze * feat: add sentence mode * test: support skipping in-memory update transaction * test: add sentence mode * test: add sentence mode (2) * chore: set enableDocumentInternalLog to false * fix: integration test * fix: integration test
This commit is contained in:
parent
e0226e54a5
commit
e86d584ea7
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
|
@ -246,10 +246,16 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
(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}',
|
||||
|
@ -404,8 +404,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
||||
}
|
||||
|
||||
List<SelectionMenuItem> _customSlashMenuItems() {
|
||||
final isLocalMode = context.read<DocumentBloc>().isLocalMode;
|
||||
return [
|
||||
aiWriterSlashMenuItem,
|
||||
if (!isLocalMode) aiWriterSlashMenuItem,
|
||||
textSlashMenuItem,
|
||||
heading1SlashMenuItem,
|
||||
heading2SlashMenuItem,
|
||||
|
@ -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<void> insertCharacter(String line, Duration delay) async {
|
||||
final iterator = line.runes.iterator;
|
||||
while (iterator.moveNext()) {
|
||||
await editorState.insertTextAtCurrentSelection(
|
||||
iterator.currentAsString,
|
||||
);
|
||||
await Future.delayed(delay);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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<void> insertSentence(String line, Duration delay) async {
|
||||
await editorState.insertTextAtCurrentSelection(line);
|
||||
await Future.delayed(delay);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _splitText(String text, String separator) {
|
||||
final parts = text.split(RegExp(separator));
|
||||
final result = <String>[];
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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<void> _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() {
|
||||
|
@ -88,8 +88,7 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
|
||||
|
||||
@override
|
||||
late EditorState editorState = context.read<EditorState>();
|
||||
late Stream<(TransactionTime, Transaction)> stream =
|
||||
editorState.transactionStream;
|
||||
late Stream<EditorTransactionValue> stream = editorState.transactionStream;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -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
|
||||
|
@ -40,7 +40,7 @@ class EditorTransactionService extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _EditorTransactionServiceState extends State<EditorTransactionService> {
|
||||
StreamSubscription<(TransactionTime, Transaction)>? transactionSubscription;
|
||||
StreamSubscription<EditorTransactionValue>? transactionSubscription;
|
||||
|
||||
bool isUndoRedo = false;
|
||||
bool isPaste = false;
|
||||
@ -131,8 +131,11 @@ class _EditorTransactionServiceState extends State<EditorTransactionService> {
|
||||
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<EditorTransactionService> {
|
||||
handler.type: handler.livesInDelta ? <MentionBlockData>[] : <Node>[],
|
||||
};
|
||||
|
||||
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) {
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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.",
|
||||
];
|
Loading…
x
Reference in New Issue
Block a user