Skip to content

Commit fa0a334

Browse files
committed
feat: refactor the gpt3 api and support multi line completion
1 parent 310236d commit fa0a334

File tree

8 files changed

+230
-151
lines changed

8 files changed

+230
-151
lines changed

frontend/app_flowy/packages/appflowy_editor/example/lib/pages/simple_editor.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
44
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
55
import 'package:example/plugin/AI/continue_to_write.dart';
66
import 'package:example/plugin/AI/auto_completion.dart';
7-
import 'package:example/plugin/AI/getgpt3completions.dart';
7+
import 'package:example/plugin/AI/gpt3.dart';
88
import 'package:example/plugin/AI/smart_edit.dart';
99
import 'package:flutter/material.dart';
1010

frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/AI/auto_completion.dart

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import 'package:appflowy_editor/appflowy_editor.dart';
2-
import 'package:example/plugin/AI/getgpt3completions.dart';
2+
import 'package:example/plugin/AI/gpt3.dart';
33
import 'package:example/plugin/AI/text_robot.dart';
44
import 'package:flutter/material.dart';
55
import 'package:flutter/services.dart';
@@ -37,12 +37,18 @@ SelectionMenuItem autoCompletionMenuItem = SelectionMenuItem(
3737
Navigator.of(context).pop();
3838
// fetch the result and insert it
3939
final textRobot = TextRobot(editorState: editorState);
40-
getGPT3Completion(apiKey, controller.text, '', (result) async {
41-
await textRobot.insertText(
42-
result,
43-
inputType: TextRobotInputType.character,
44-
);
45-
});
40+
const gpt3 = GPT3APIClient(apiKey: apiKey);
41+
gpt3.getGPT3Completion(
42+
controller.text,
43+
'',
44+
onResult: (result) async {
45+
await textRobot.insertText(
46+
result,
47+
inputType: TextRobotInputType.character,
48+
);
49+
},
50+
onError: () async {},
51+
);
4652
} else if (key.logicalKey == LogicalKeyboardKey.escape) {
4753
Navigator.of(context).pop();
4854
}

frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/AI/continue_to_write.dart

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import 'package:appflowy_editor/appflowy_editor.dart';
2-
import 'package:example/plugin/AI/getgpt3completions.dart';
2+
import 'package:example/plugin/AI/gpt3.dart';
33
import 'package:example/plugin/AI/text_robot.dart';
44
import 'package:flutter/material.dart';
55

@@ -14,35 +14,96 @@ SelectionMenuItem continueToWriteMenuItem = SelectionMenuItem(
1414
),
1515
keywords: ['continue to write'],
1616
handler: ((editorState, menuService, context) async {
17-
// get the current text
17+
// Two cases
18+
// 1. if there is content in the text node where the cursor is located,
19+
// then we use the current text content as data.
20+
// 2. if there is no content in the text node where the cursor is located,
21+
// then we use the previous / next text node's content as data.
22+
1823
final selection =
1924
editorState.service.selectionService.currentSelection.value;
20-
final textNodes = editorState.service.selectionService.currentSelectedNodes;
21-
if (selection == null || !selection.isCollapsed || textNodes.length != 1) {
25+
if (selection == null || !selection.isCollapsed) {
26+
return;
27+
}
28+
29+
final textNodes = editorState.service.selectionService.currentSelectedNodes
30+
.whereType<TextNode>();
31+
if (textNodes.isEmpty) {
2232
return;
2333
}
24-
final textNode = textNodes.first as TextNode;
25-
final prompt = textNode.delta.slice(0, selection.startIndex).toPlainText();
26-
final suffix = textNode.delta
27-
.slice(
28-
selection.endIndex,
29-
textNode.toPlainText().length,
30-
)
31-
.toPlainText();
34+
3235
final textRobot = TextRobot(editorState: editorState);
33-
getGPT3Completion(
34-
apiKey,
36+
const gpt3 = GPT3APIClient(apiKey: apiKey);
37+
final textNode = textNodes.first;
38+
39+
var prompt = '';
40+
var suffix = '';
41+
42+
void continueToWriteInSingleLine() {
43+
prompt = textNode.delta.slice(0, selection.startIndex).toPlainText();
44+
suffix = textNode.delta
45+
.slice(
46+
selection.endIndex,
47+
textNode.toPlainText().length,
48+
)
49+
.toPlainText();
50+
}
51+
52+
void continueToWriteInMulitLines() {
53+
final parent = textNode.parent;
54+
if (parent != null) {
55+
for (final node in parent.children) {
56+
if (node is! TextNode || node.toPlainText().isEmpty) continue;
57+
if (node.path < textNode.path) {
58+
prompt += '${node.toPlainText()}\n';
59+
} else if (node.path > textNode.path) {
60+
suffix += '${node.toPlainText()}\n';
61+
}
62+
}
63+
}
64+
}
65+
66+
if (textNodes.first.toPlainText().isNotEmpty) {
67+
continueToWriteInSingleLine();
68+
} else {
69+
continueToWriteInMulitLines();
70+
}
71+
72+
if (prompt.isEmpty && suffix.isEmpty) {
73+
return;
74+
}
75+
76+
late final BuildContext diglogContext;
77+
78+
showDialog(
79+
context: context,
80+
builder: (context) {
81+
diglogContext = context;
82+
return AlertDialog(
83+
content: Column(
84+
mainAxisSize: MainAxisSize.min,
85+
children: const [
86+
CircularProgressIndicator(),
87+
SizedBox(height: 10),
88+
Text('Loading'),
89+
],
90+
),
91+
);
92+
},
93+
);
94+
95+
gpt3.getGPT3Completion(
3596
prompt,
3697
suffix,
37-
(result) async {
38-
if (result == '\\n') {
39-
await editorState.insertNewLineAtCurrentSelection();
40-
} else {
41-
await textRobot.insertText(
42-
result,
43-
inputType: TextRobotInputType.word,
44-
);
45-
}
98+
onResult: (result) async {
99+
Navigator.of(diglogContext).pop(true);
100+
await textRobot.insertText(
101+
result,
102+
inputType: TextRobotInputType.word,
103+
);
104+
},
105+
onError: () async {
106+
Navigator.of(diglogContext).pop(true);
46107
},
47108
);
48109
}),

frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/AI/getgpt3completions.dart

Lines changed: 0 additions & 111 deletions
This file was deleted.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import 'package:http/http.dart' as http;
2+
import 'dart:async';
3+
import 'dart:convert';
4+
5+
// Please fill in your own API key
6+
const apiKey = '';
7+
8+
enum GPT3API {
9+
completion,
10+
edit,
11+
}
12+
13+
extension on GPT3API {
14+
Uri get uri {
15+
switch (this) {
16+
case GPT3API.completion:
17+
return Uri.parse('https://api.openai.com/v1/completions');
18+
case GPT3API.edit:
19+
return Uri.parse('https://api.openai.com/v1/edits');
20+
}
21+
}
22+
}
23+
24+
class GPT3APIClient {
25+
const GPT3APIClient({
26+
required this.apiKey,
27+
});
28+
29+
final String apiKey;
30+
31+
/// Get completions from GPT-3
32+
///
33+
/// [prompt] is the prompt text
34+
/// [suffix] is the suffix text
35+
/// [onResult] is the callback function to handle the result
36+
/// [maxTokens] is the maximum number of tokens to generate
37+
/// [temperature] is the temperature of the model
38+
///
39+
/// See https://beta.openai.com/docs/api-reference/completions/create
40+
Future<void> getGPT3Completion(
41+
String prompt,
42+
String suffix, {
43+
required Future<void> Function(String result) onResult,
44+
required Future<void> Function() onError,
45+
int maxTokens = 200,
46+
double temperature = .3,
47+
}) async {
48+
final data = {
49+
'model': 'text-davinci-003',
50+
'prompt': prompt,
51+
'suffix': suffix,
52+
'max_tokens': maxTokens,
53+
'temperature': temperature,
54+
'stream': false,
55+
};
56+
57+
final headers = {
58+
'Authorization': apiKey,
59+
'Content-Type': 'application/json',
60+
};
61+
62+
final response = await http.post(
63+
GPT3API.completion.uri,
64+
headers: headers,
65+
body: json.encode(data),
66+
);
67+
68+
if (response.statusCode == 200) {
69+
final result = json.decode(response.body);
70+
final choices = result['choices'];
71+
if (choices != null && choices is List) {
72+
for (final choice in choices) {
73+
final text = choice['text'];
74+
await onResult(text);
75+
}
76+
}
77+
} else {
78+
await onError();
79+
}
80+
}
81+
82+
Future<void> getGPT3Edit(
83+
String apiKey,
84+
String input,
85+
String instruction, {
86+
required Future<void> Function(List<String> result) onResult,
87+
required Future<void> Function() onError,
88+
int n = 1,
89+
double temperature = .3,
90+
}) async {
91+
final data = {
92+
'model': 'text-davinci-edit-001',
93+
'input': input,
94+
'instruction': instruction,
95+
'temperature': temperature,
96+
'n': n,
97+
};
98+
99+
final headers = {
100+
'Authorization': apiKey,
101+
'Content-Type': 'application/json',
102+
};
103+
104+
final response = await http.post(
105+
Uri.parse('https://api.openai.com/v1/edits'),
106+
headers: headers,
107+
body: json.encode(data),
108+
);
109+
if (response.statusCode == 200) {
110+
final result = json.decode(response.body);
111+
final choices = result['choices'];
112+
if (choices != null && choices is List) {
113+
await onResult(choices.map((e) => e['text'] as String).toList());
114+
}
115+
} else {
116+
await onError();
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)