Skip to content

Commit

Permalink
feat: add ai message content to document (#7041)
Browse files Browse the repository at this point in the history
* feat: add ai response content to page

* chore: apply suggestions from code review

Co-authored-by: Lucas <[email protected]>

* chore: apply suggestions from code review

* chore: reorganize code

* chore: i18n

* chore: enable opening the document in the sidebar

* fix: async await

* chore: rename ai message action bar widget

* feat: make transactions be reflected in the opened document

* chore: don't forget to close the bloc

* fix: isLastLineEmpty

* chore: code cleanup

* fix: sync after EditorState.apply

* chore: decrease visibility of DocumentBlocMap

* chore: add back missing assert

---------

Co-authored-by: Lucas <[email protected]>
  • Loading branch information
richardshiue and LucasXu0 authored Dec 26, 2024
1 parent 956d2df commit 200b367
Show file tree
Hide file tree
Showing 15 changed files with 690 additions and 219 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import 'dart:async';

import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart';

class ChatEditDocumentService {
static Future<ViewPB?> saveMessagesToNewPage(
String chatPageName,
String parentViewId,
List<TextMessage> messages,
) async {
if (messages.isEmpty) {
return null;
}

// Convert messages to markdown and trim the last empty newline.
final completeMessage = messages.map((m) => m.text).join('\n').trimRight();
if (completeMessage.isEmpty) {
return null;
}

final document = customMarkdownToDocument(completeMessage);
final initialBytes =
DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();
if (initialBytes == null) {
Log.error('Failed to convert messages to document');
return null;
}

return ViewBackendService.createView(
name: LocaleKeys.chat_addToNewPageName.tr(args: [chatPageName]),
layoutType: ViewLayoutPB.Document,
parentViewId: parentViewId,
initialDataBytes: initialBytes,
).toNullable();
}

static Future<void> addMessageToPage(
String documentId,
TextMessage message,
) async {
if (message.text.isEmpty) {
Log.error('Message is empty');
return;
}

final bloc = DocumentBloc(
documentId: documentId,
saveToBlocMap: false,
)..add(const DocumentEvent.initial());

if (bloc.state.editorState == null) {
await bloc.stream.firstWhere((state) => state.editorState != null);
}

final editorState = bloc.state.editorState;
if (editorState == null) {
Log.error("Can't get EditorState of document");
return;
}

final messageDocument = customMarkdownToDocument(message.text);
if (messageDocument.isEmpty) {
Log.error('Failed to convert message to document');
return;
}

final lastNodeOrNull = editorState.document.root.children.lastOrNull;

final rootIsEmpty = lastNodeOrNull == null;
final isLastLineEmpty = lastNodeOrNull?.children.isNotEmpty == false &&
lastNodeOrNull?.delta?.isNotEmpty == false;

final nodes = [
if (rootIsEmpty || !isLastLineEmpty) paragraphNode(),
...messageDocument.root.children,
];
final insertPath = rootIsEmpty || listEquals(lastNodeOrNull.path, const [0])
? const [0]
: lastNodeOrNull.path.next;

final transaction = editorState.transaction..insertNodes(insertPath, nodes);
await editorState.apply(transaction);
await bloc.close();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,8 @@ class ChatSource {
}

class ChatSettingsCubit extends Cubit<ChatSettingsState> {
ChatSettingsCubit({required this.chatId})
: super(ChatSettingsState.initial());
ChatSettingsCubit() : super(ChatSettingsState.initial());

final String chatId;
List<String> selectedSourceIds = [];
ChatSource? source;
List<ChatSource> selectedSources = [];
Expand Down
21 changes: 3 additions & 18 deletions frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import 'dart:async';
import 'dart:io';

import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
Expand Down Expand Up @@ -36,6 +31,7 @@ import 'presentation/chat_welcome_page.dart';
import 'presentation/layout_define.dart';
import 'presentation/message/ai_text_message.dart';
import 'presentation/message/error_text_message.dart';
import 'presentation/message/message_util.dart';
import 'presentation/message/user_text_message.dart';
import 'presentation/scroll_to_bottom.dart';

Expand Down Expand Up @@ -355,19 +351,8 @@ class _ChatContentPage extends StatelessWidget {
} else {
final sidebarView =
await ViewBackendService.getView(metadata.id).toNullable();
if (sidebarView == null) {
return;
}
if (UniversalPlatform.isDesktop) {
getIt<TabsBloc>().add(
TabsEvent.openSecondaryPlugin(
plugin: sidebarView.plugin(),
),
);
} else {
if (context.mounted) {
unawaited(context.pushView(sidebarView));
}
if (context.mounted) {
openPageFromMessage(context, sidebarView);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
top: null,
child: TextFieldTapRegion(
child: _PromptBottomActions(
chatId: widget.chatId,
textController: textController,
overlayController: overlayController,
focusNode: focusNode,
Expand Down Expand Up @@ -483,7 +482,6 @@ class _FocusNextItemIntent extends Intent {

class _PromptBottomActions extends StatelessWidget {
const _PromptBottomActions({
required this.chatId,
required this.textController,
required this.overlayController,
required this.focusNode,
Expand All @@ -493,7 +491,6 @@ class _PromptBottomActions extends StatelessWidget {
required this.onUpdateSelectedSources,
});

final String chatId;
final TextEditingController textController;
final OverlayPortalController overlayController;
final FocusNode focusNode;
Expand Down Expand Up @@ -539,7 +536,6 @@ class _PromptBottomActions extends StatelessWidget {

Widget _selectSourcesButton(BuildContext context) {
return PromptInputDesktopSelectSourcesButton(
chatId: chatId,
onUpdateSelectedSources: onUpdateSelectedSources,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,6 @@ class _MobileAIPromptInputState extends State<MobileAIPromptInput> {
alignment: Alignment.bottomCenter,
padding: const EdgeInsets.only(bottom: 8.0),
child: _LeadingActions(
chatId: widget.chatId,
textController: textController,
// onMention: () {
// textController.text += '@';
Expand Down Expand Up @@ -303,12 +302,10 @@ class _MobileAIPromptInputState extends State<MobileAIPromptInput> {

class _LeadingActions extends StatelessWidget {
const _LeadingActions({
required this.chatId,
required this.textController,
required this.onUpdateSelectedSources,
});

final String chatId;
final TextEditingController textController;
final void Function(List<String>) onUpdateSelectedSources;

Expand All @@ -317,7 +314,6 @@ class _LeadingActions extends StatelessWidget {
return Material(
color: Theme.of(context).cardColor,
child: PromptInputMobileSelectSourcesButton(
chatId: chatId,
onUpdateSelectedSources: onUpdateSelectedSources,
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,9 @@ import 'select_sources_menu.dart';
class PromptInputMobileSelectSourcesButton extends StatefulWidget {
const PromptInputMobileSelectSourcesButton({
super.key,
required this.chatId,
required this.onUpdateSelectedSources,
});

final String chatId;
final void Function(List<String>) onUpdateSelectedSources;

@override
Expand All @@ -34,7 +32,7 @@ class PromptInputMobileSelectSourcesButton extends StatefulWidget {

class _PromptInputMobileSelectSourcesButtonState
extends State<PromptInputMobileSelectSourcesButton> {
late final cubit = ChatSettingsCubit(chatId: widget.chatId);
late final cubit = ChatSettingsCubit();

@override
void initState() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:math';

import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
Expand All @@ -10,6 +11,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_w
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
Expand All @@ -22,11 +24,9 @@ import 'chat_mention_page_menu.dart';
class PromptInputDesktopSelectSourcesButton extends StatefulWidget {
const PromptInputDesktopSelectSourcesButton({
super.key,
required this.chatId,
required this.onUpdateSelectedSources,
});

final String chatId;
final void Function(List<String>) onUpdateSelectedSources;

@override
Expand All @@ -36,7 +36,7 @@ class PromptInputDesktopSelectSourcesButton extends StatefulWidget {

class _PromptInputDesktopSelectSourcesButtonState
extends State<PromptInputDesktopSelectSourcesButton> {
late final cubit = ChatSettingsCubit(chatId: widget.chatId);
late final cubit = ChatSettingsCubit();
final popoverController = PopoverController();

@override
Expand Down Expand Up @@ -280,6 +280,7 @@ class ChatSourceTreeItem extends StatefulWidget {
required this.isSelectedSection,
required this.onSelected,
required this.height,
this.showCheckbox = true,
});

final ChatSource chatSource;
Expand All @@ -295,6 +296,8 @@ class ChatSourceTreeItem extends StatefulWidget {

final double height;

final bool showCheckbox;

@override
State<ChatSourceTreeItem> createState() => _ChatSourceTreeItemState();
}
Expand All @@ -309,18 +312,21 @@ class _ChatSourceTreeItemState extends State<ChatSourceTreeItem> {
level: widget.level,
isDescendentOfSpace: widget.isDescendentOfSpace,
isSelectedSection: widget.isSelectedSection,
showCheckbox: widget.showCheckbox,
onSelected: widget.onSelected,
),
);

final disabledEnabledChild =
widget.chatSource.ignoreStatus == IgnoreViewType.disable
? FlowyTooltip(
message: switch (widget.chatSource.view.layout) {
ViewLayoutPB.Document =>
"You can only select up to 3 top-level documents and its children",
_ => "We don't support chatting with databases at this time",
},
message: widget.showCheckbox
? switch (widget.chatSource.view.layout) {
ViewLayoutPB.Document =>
LocaleKeys.chat_sourcesLimitReached.tr(),
_ => LocaleKeys.chat_sourceUnsupported.tr(),
}
: "",
child: Opacity(
opacity: 0.5,
child: MouseRegion(
Expand Down Expand Up @@ -358,6 +364,7 @@ class _ChatSourceTreeItemState extends State<ChatSourceTreeItem> {
isSelectedSection: widget.isSelectedSection,
onSelected: widget.onSelected,
height: widget.height,
showCheckbox: widget.showCheckbox,
),
),
],
Expand All @@ -374,13 +381,15 @@ class ChatSourceTreeItemInner extends StatelessWidget {
required this.level,
required this.isDescendentOfSpace,
required this.isSelectedSection,
required this.showCheckbox,
this.onSelected,
});

final ChatSource chatSource;
final int level;
final bool isDescendentOfSpace;
final bool isSelectedSection;
final bool showCheckbox;
final void Function(ChatSource)? onSelected;

@override
Expand All @@ -403,7 +412,7 @@ class ChatSourceTreeItemInner extends StatelessWidget {
),
const HSpace(2.0),
// checkbox
if (!chatSource.view.isSpace) ...[
if (!chatSource.view.isSpace && showCheckbox) ...[
SourceSelectedStatusCheckbox(
chatSource: chatSource,
),
Expand Down
Loading

0 comments on commit 200b367

Please sign in to comment.