Skip to content

Commit

Permalink
feat: add openai service (#1858)
Browse files Browse the repository at this point in the history
* feat: add openai service

* feat: add openai auto completion plugin

* feat: add visible icon for open ai input field

* chore: optimize user experience

* feat: add auto completion node plugin

* feat: support keep and discard the auto generated text

* fix: can't delete the auto completion node

* feat: disable ai plugins if open ai key is null

* fix: wrong auto completion node card color

* fix: make sure the previous text node is pure when using auto generator
  • Loading branch information
LucasXu0 authored Feb 16, 2023
1 parent 2f9823d commit 7c3a823
Show file tree
Hide file tree
Showing 23 changed files with 777 additions and 18 deletions.
16 changes: 14 additions & 2 deletions frontend/app_flowy/assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,12 @@
"signIn": "Sign In",
"signOut": "Sign Out",
"complete": "Complete",
"save": "Save"
"save": "Save",
"generate": "Generate",
"esc": "ESC",
"keep": "Keep",
"tryAgain": "Try again",
"discard": "Discard"
},
"label": {
"welcome": "Welcome!",
Expand Down Expand Up @@ -334,7 +339,14 @@
},
"plugins": {
"referencedBoard": "Referenced Board",
"referencedGrid": "Referenced Grid"
"referencedGrid": "Referenced Grid",
"autoCompletionMenuItemName": "Auto Completion",
"autoGeneratorMenuItemName": "Auto Generator",
"autoGeneratorTitleName": "Open AI: Auto Generator",
"autoGeneratorLearnMore": "Learn more",
"autoGeneratorGenerate": "Generate",
"autoGeneratorHintText": "Tell us what you want to generate by OpenAI ...",
"autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key"
}
},
"board": {
Expand Down
16 changes: 16 additions & 0 deletions frontend/app_flowy/lib/plugins/document/application/doc_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'dart:convert';
import 'package:app_flowy/plugins/trash/application/trash_service.dart';
import 'package:app_flowy/user/application/user_service.dart';
import 'package:app_flowy/workspace/application/view/view_listener.dart';
import 'package:app_flowy/plugins/document/application/doc_service.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
show EditorState, Document, Transaction;
import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';
Expand All @@ -12,6 +14,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dartz/dartz.dart';
import 'dart:async';
import 'package:app_flowy/util/either_extension.dart';

part 'doc_bloc.freezed.dart';

Expand Down Expand Up @@ -73,6 +76,16 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
}

Future<void> _initial(Initial value, Emitter<DocumentState> emit) async {
final userProfile = await UserService.getCurrentUserProfile();
if (userProfile.isRight()) {
emit(
state.copyWith(
loadingState:
DocumentLoadingState.finish(right(userProfile.asRight())),
),
);
return;
}
final result = await _documentService.openDocument(view: view);
result.fold(
(documentData) {
Expand All @@ -82,6 +95,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
emit(
state.copyWith(
loadingState: DocumentLoadingState.finish(left(unit)),
userProfilePB: userProfile.asLeft(),
),
);
},
Expand Down Expand Up @@ -142,12 +156,14 @@ class DocumentState with _$DocumentState {
required DocumentLoadingState loadingState,
required bool isDeleted,
required bool forceClose,
UserProfilePB? userProfilePB,
}) = _DocumentState;

factory DocumentState.initial() => const DocumentState(
loadingState: _Loading(),
isDeleted: false,
forceClose: false,
userProfilePB: null,
);
}

Expand Down
15 changes: 14 additions & 1 deletion frontend/app_flowy/lib/plugins/document/document_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import 'package:app_flowy/plugins/document/presentation/plugins/board/board_menu
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
import 'package:app_flowy/plugins/document/presentation/plugins/grid/grid_menu_item.dart';
import 'package:app_flowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
import 'package:app_flowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart';
import 'package:app_flowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
Expand Down Expand Up @@ -83,6 +85,7 @@ class _DocumentPageState extends State<DocumentPage> {
if (state.isDeleted) _renderBanner(context),
// AppFlowy Editor
_renderAppFlowyEditor(
context,
context.read<DocumentBloc>().editorState,
),
],
Expand All @@ -99,7 +102,11 @@ class _DocumentPageState extends State<DocumentPage> {
);
}

Widget _renderAppFlowyEditor(EditorState editorState) {
Widget _renderAppFlowyEditor(BuildContext context, EditorState editorState) {
// enable open ai features if needed.
final userProfilePB = context.read<DocumentBloc>().state.userProfilePB;
final openAIKey = userProfilePB?.openaiKey;

final theme = Theme.of(context);
final editor = AppFlowyEditor(
editorState: editorState,
Expand All @@ -117,6 +124,8 @@ class _DocumentPageState extends State<DocumentPage> {
kGridType: GridNodeWidgetBuilder(),
// Card
kCalloutType: CalloutNodeWidgetBuilder(),
// Auto Generator,
kAutoCompletionInputType: AutoCompletionInputBuilder(),
},
shortcutEvents: [
// Divider
Expand All @@ -141,6 +150,10 @@ class _DocumentPageState extends State<DocumentPage> {
gridMenuItem,
// Callout
calloutMenuItem,
// AI
if (openAIKey != null && openAIKey.isNotEmpty) ...[
autoGeneratorMenuItem,
]
],
themeData: theme.copyWith(extensions: [
...theme.extensions.values,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'error.freezed.dart';
part 'error.g.dart';

@freezed
class OpenAIError with _$OpenAIError {
const factory OpenAIError({
String? code,
required String message,
}) = _OpenAIError;

factory OpenAIError.fromJson(Map<String, Object?> json) =>
_$OpenAIErrorFromJson(json);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import 'dart:convert';

import 'text_completion.dart';
import 'package:dartz/dartz.dart';
import 'dart:async';

import 'error.dart';
import 'package:http/http.dart' as http;

// Please fill in your own API key
const apiKey = '';

enum OpenAIRequestType {
textCompletion,
textEdit;

Uri get uri {
switch (this) {
case OpenAIRequestType.textCompletion:
return Uri.parse('https://api.openai.com/v1/completions');
case OpenAIRequestType.textEdit:
return Uri.parse('https://api.openai.com/v1/edits');
}
}
}

abstract class OpenAIRepository {
/// Get completions from GPT-3
///
/// [prompt] is the prompt text
/// [suffix] is the suffix text
/// [maxTokens] is the maximum number of tokens to generate
/// [temperature] is the temperature of the model
///
Future<Either<OpenAIError, TextCompletionResponse>> getCompletions({
required String prompt,
String? suffix,
int maxTokens = 50,
double temperature = .3,
});
}

class HttpOpenAIRepository implements OpenAIRepository {
const HttpOpenAIRepository({
required this.client,
required this.apiKey,
});

final http.Client client;
final String apiKey;

Map<String, String> get headers => {
'Authorization': 'Bearer $apiKey',
'Content-Type': 'application/json',
};

@override
Future<Either<OpenAIError, TextCompletionResponse>> getCompletions({
required String prompt,
String? suffix,
int maxTokens = 50,
double temperature = 0.3,
}) async {
final parameters = {
'model': 'text-davinci-003',
'prompt': prompt,
'suffix': suffix,
'max_tokens': maxTokens,
'temperature': temperature,
'stream': false,
};

final response = await http.post(
OpenAIRequestType.textCompletion.uri,
headers: headers,
body: json.encode(parameters),
);

if (response.statusCode == 200) {
return Right(TextCompletionResponse.fromJson(json.decode(response.body)));
} else {
return Left(OpenAIError.fromJson(json.decode(response.body)['error']));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'text_completion.freezed.dart';
part 'text_completion.g.dart';

@freezed
class TextCompletionChoice with _$TextCompletionChoice {
factory TextCompletionChoice({
required String text,
required int index,
// ignore: invalid_annotation_target
@JsonKey(name: 'finish_reason') required String finishReason,
}) = _TextCompletionChoice;

factory TextCompletionChoice.fromJson(Map<String, Object?> json) =>
_$TextCompletionChoiceFromJson(json);
}

@freezed
class TextCompletionResponse with _$TextCompletionResponse {
const factory TextCompletionResponse({
required List<TextCompletionChoice> choices,
}) = _TextCompletionResponse;

factory TextCompletionResponse.fromJson(Map<String, Object?> json) =>
_$TextCompletionResponseFromJson(json);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'package:appflowy_editor/appflowy_editor.dart';

enum TextRobotInputType {
character,
word,
}

extension TextRobot on EditorState {
Future<void> autoInsertText(
String text, {
TextRobotInputType inputType = TextRobotInputType.word,
Duration delay = const Duration(milliseconds: 10),
}) async {
final lines = text.split('\n');
for (final line in lines) {
if (line.isEmpty) continue;
switch (inputType) {
case TextRobotInputType.character:
final iterator = line.runes.iterator;
while (iterator.moveNext()) {
await insertTextAtCurrentSelection(
iterator.currentAsString,
);
await Future.delayed(delay, () {});
}
break;
case TextRobotInputType.word:
final words = line.split(' ').map((e) => '$e ');
for (final word in words) {
await insertTextAtCurrentSelection(
word,
);
await Future.delayed(delay, () {});
}
break;
}

// insert new line
if (lines.length > 1) {
await insertNewLineAtCurrentSelection();
}
}
}
}
Loading

0 comments on commit 7c3a823

Please sign in to comment.