mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: generate image by Stability AI / openAI (#3642)
This commit is contained in:
parent
41d4351176
commit
dace02d34d
@ -0,0 +1,105 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:dartz/dartz.dart' hide State;
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class OpenAIImageWidget extends StatefulWidget {
|
||||||
|
const OpenAIImageWidget({
|
||||||
|
super.key,
|
||||||
|
required this.onSelectNetworkImage,
|
||||||
|
});
|
||||||
|
|
||||||
|
final void Function(String url) onSelectNetworkImage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<OpenAIImageWidget> createState() => _OpenAIImageWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OpenAIImageWidgetState extends State<OpenAIImageWidget> {
|
||||||
|
Future<Either<OpenAIError, List<String>>>? future;
|
||||||
|
String query = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FlowyTextField(
|
||||||
|
autoFocus: true,
|
||||||
|
hintText: LocaleKeys.document_imageBlock_ai_placeholder.tr(),
|
||||||
|
onChanged: (value) => query = value,
|
||||||
|
onEditingComplete: _search,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const HSpace(4.0),
|
||||||
|
FlowyButton(
|
||||||
|
useIntrinsicWidth: true,
|
||||||
|
text: FlowyText(
|
||||||
|
LocaleKeys.search_label.tr(),
|
||||||
|
),
|
||||||
|
onTap: _search,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const VSpace(12.0),
|
||||||
|
if (future != null)
|
||||||
|
Expanded(
|
||||||
|
child: FutureBuilder(
|
||||||
|
future: future,
|
||||||
|
builder: (context, value) {
|
||||||
|
final data = value.data;
|
||||||
|
if (!value.hasData ||
|
||||||
|
value.connectionState != ConnectionState.done ||
|
||||||
|
data == null) {
|
||||||
|
return const CircularProgressIndicator.adaptive();
|
||||||
|
}
|
||||||
|
return data.fold(
|
||||||
|
(l) => Center(
|
||||||
|
child: FlowyText(
|
||||||
|
l.message,
|
||||||
|
maxLines: 3,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(r) => GridView.count(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
mainAxisSpacing: 16.0,
|
||||||
|
crossAxisSpacing: 10.0,
|
||||||
|
childAspectRatio: 4 / 3,
|
||||||
|
children: r
|
||||||
|
.map(
|
||||||
|
(e) => GestureDetector(
|
||||||
|
onTap: () => widget.onSelectNetworkImage(e),
|
||||||
|
child: Image.network(e),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _search() async {
|
||||||
|
final openAI = await getIt.getAsync<OpenAIRepository>();
|
||||||
|
setState(() {
|
||||||
|
future = openAI.generateImage(
|
||||||
|
prompt: query,
|
||||||
|
n: 6,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_error.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:dartz/dartz.dart' hide State;
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra/uuid.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
class StabilityAIImageWidget extends StatefulWidget {
|
||||||
|
const StabilityAIImageWidget({
|
||||||
|
super.key,
|
||||||
|
required this.onSelectImage,
|
||||||
|
});
|
||||||
|
|
||||||
|
final void Function(String url) onSelectImage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StabilityAIImageWidget> createState() => _StabilityAIImageWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StabilityAIImageWidgetState extends State<StabilityAIImageWidget> {
|
||||||
|
Future<Either<StabilityAIRequestError, List<String>>>? future;
|
||||||
|
String query = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FlowyTextField(
|
||||||
|
autoFocus: true,
|
||||||
|
hintText: LocaleKeys
|
||||||
|
.document_imageBlock_stability_ai_placeholder
|
||||||
|
.tr(),
|
||||||
|
onChanged: (value) => query = value,
|
||||||
|
onEditingComplete: _search,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const HSpace(4.0),
|
||||||
|
FlowyButton(
|
||||||
|
useIntrinsicWidth: true,
|
||||||
|
text: FlowyText(
|
||||||
|
LocaleKeys.search_label.tr(),
|
||||||
|
),
|
||||||
|
onTap: _search,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const VSpace(12.0),
|
||||||
|
if (future != null)
|
||||||
|
Expanded(
|
||||||
|
child: FutureBuilder(
|
||||||
|
future: future,
|
||||||
|
builder: (context, value) {
|
||||||
|
final data = value.data;
|
||||||
|
if (!value.hasData ||
|
||||||
|
value.connectionState != ConnectionState.done ||
|
||||||
|
data == null) {
|
||||||
|
return const CircularProgressIndicator.adaptive();
|
||||||
|
}
|
||||||
|
return data.fold(
|
||||||
|
(l) => Center(
|
||||||
|
child: FlowyText(
|
||||||
|
l.message,
|
||||||
|
maxLines: 3,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(r) => GridView.count(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
mainAxisSpacing: 16.0,
|
||||||
|
crossAxisSpacing: 10.0,
|
||||||
|
childAspectRatio: 4 / 3,
|
||||||
|
children: r.map(
|
||||||
|
(e) {
|
||||||
|
final base64Image = base64Decode(e);
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
final tempDirectory = await getTemporaryDirectory();
|
||||||
|
final path = p.join(
|
||||||
|
tempDirectory.path,
|
||||||
|
'${uuid()}.png',
|
||||||
|
);
|
||||||
|
File(path).writeAsBytesSync(base64Image);
|
||||||
|
widget.onSelectImage(path);
|
||||||
|
},
|
||||||
|
child: Image.memory(base64Image),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _search() async {
|
||||||
|
final stabilityAI = await getIt.getAsync<StabilityAIRepository>();
|
||||||
|
setState(() {
|
||||||
|
future = stabilityAI.generateImage(
|
||||||
|
prompt: query,
|
||||||
|
n: 6,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,10 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart';
|
||||||
|
import 'package:appflowy/user/application/user_service.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
@ -11,7 +14,8 @@ enum UploadImageType {
|
|||||||
local,
|
local,
|
||||||
url,
|
url,
|
||||||
unsplash,
|
unsplash,
|
||||||
ai;
|
stabilityAI,
|
||||||
|
openAI;
|
||||||
|
|
||||||
String get description {
|
String get description {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
@ -21,8 +25,10 @@ enum UploadImageType {
|
|||||||
return LocaleKeys.document_imageBlock_embedLink_label.tr();
|
return LocaleKeys.document_imageBlock_embedLink_label.tr();
|
||||||
case UploadImageType.unsplash:
|
case UploadImageType.unsplash:
|
||||||
return 'Unsplash';
|
return 'Unsplash';
|
||||||
case UploadImageType.ai:
|
case UploadImageType.openAI:
|
||||||
return 'Generate from AI';
|
return LocaleKeys.document_imageBlock_ai_label.tr();
|
||||||
|
case UploadImageType.stabilityAI:
|
||||||
|
return LocaleKeys.document_imageBlock_stability_ai_label.tr();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,11 +49,39 @@ class UploadImageMenu extends StatefulWidget {
|
|||||||
|
|
||||||
class _UploadImageMenuState extends State<UploadImageMenu> {
|
class _UploadImageMenuState extends State<UploadImageMenu> {
|
||||||
int currentTabIndex = 0;
|
int currentTabIndex = 0;
|
||||||
|
List<UploadImageType> values = UploadImageType.values;
|
||||||
|
bool supportOpenAI = false;
|
||||||
|
bool supportStabilityAI = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
UserBackendService.getCurrentUserProfile().then(
|
||||||
|
(value) {
|
||||||
|
final supportOpenAI = value.fold(
|
||||||
|
(l) => false,
|
||||||
|
(r) => r.openaiKey.isNotEmpty,
|
||||||
|
);
|
||||||
|
final supportStabilityAI = value.fold(
|
||||||
|
(l) => false,
|
||||||
|
(r) => r.stabilityAiKey.isNotEmpty,
|
||||||
|
);
|
||||||
|
if (supportOpenAI != this.supportOpenAI ||
|
||||||
|
supportStabilityAI != this.supportStabilityAI) {
|
||||||
|
setState(() {
|
||||||
|
this.supportOpenAI = supportOpenAI;
|
||||||
|
this.supportStabilityAI = supportStabilityAI;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DefaultTabController(
|
return DefaultTabController(
|
||||||
length: 3, // UploadImageType.values.length, // ai is not implemented yet
|
length: values.length,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -62,10 +96,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
|
|||||||
),
|
),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
// splashBorderRadius: BorderRadius.circular(4),
|
// splashBorderRadius: BorderRadius.circular(4),
|
||||||
tabs: UploadImageType.values
|
tabs: values
|
||||||
.where(
|
|
||||||
(element) => element != UploadImageType.ai,
|
|
||||||
) // ai is not implemented yet
|
|
||||||
.map(
|
.map(
|
||||||
(e) => FlowyHover(
|
(e) => FlowyHover(
|
||||||
style: const HoverStyle(borderRadius: BorderRadius.zero),
|
style: const HoverStyle(borderRadius: BorderRadius.zero),
|
||||||
@ -115,8 +146,39 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
case UploadImageType.ai:
|
case UploadImageType.openAI:
|
||||||
return const FlowyText.medium('ai');
|
return supportOpenAI
|
||||||
|
? Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: OpenAIImageWidget(
|
||||||
|
onSelectNetworkImage: widget.onSubmit,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: FlowyText(
|
||||||
|
LocaleKeys.document_imageBlock_pleaseInputYourOpenAIKey.tr(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case UploadImageType.stabilityAI:
|
||||||
|
return supportStabilityAI
|
||||||
|
? Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: StabilityAIImageWidget(
|
||||||
|
onSelectImage: widget.onPickFile,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: FlowyText(
|
||||||
|
LocaleKeys.document_imageBlock_pleaseInputYourStabilityAIKey
|
||||||
|
.tr(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart';
|
||||||
|
|
||||||
import 'text_completion.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import 'dart:async';
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import 'error.dart';
|
import 'error.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'text_completion.dart';
|
||||||
|
|
||||||
// Please fill in your own API key
|
// Please fill in your own API key
|
||||||
const apiKey = '';
|
const apiKey = '';
|
||||||
|
|
||||||
enum OpenAIRequestType {
|
enum OpenAIRequestType {
|
||||||
textCompletion,
|
textCompletion,
|
||||||
textEdit;
|
textEdit,
|
||||||
|
imageGenerations;
|
||||||
|
|
||||||
Uri get uri {
|
Uri get uri {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
@ -22,6 +22,8 @@ enum OpenAIRequestType {
|
|||||||
return Uri.parse('https://api.openai.com/v1/completions');
|
return Uri.parse('https://api.openai.com/v1/completions');
|
||||||
case OpenAIRequestType.textEdit:
|
case OpenAIRequestType.textEdit:
|
||||||
return Uri.parse('https://api.openai.com/v1/edits');
|
return Uri.parse('https://api.openai.com/v1/edits');
|
||||||
|
case OpenAIRequestType.imageGenerations:
|
||||||
|
return Uri.parse('https://api.openai.com/v1/images/generations');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,6 +66,17 @@ abstract class OpenAIRepository {
|
|||||||
required String instruction,
|
required String instruction,
|
||||||
double temperature = 0.3,
|
double temperature = 0.3,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Generate image from GPT-3
|
||||||
|
///
|
||||||
|
/// [prompt] is the prompt text
|
||||||
|
/// [n] is the number of images to generate
|
||||||
|
///
|
||||||
|
/// the result is a list of urls
|
||||||
|
Future<Either<OpenAIError, List<String>>> generateImage({
|
||||||
|
required String prompt,
|
||||||
|
int n = 1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class HttpOpenAIRepository implements OpenAIRepository {
|
class HttpOpenAIRepository implements OpenAIRepository {
|
||||||
@ -228,4 +241,40 @@ class HttpOpenAIRepository implements OpenAIRepository {
|
|||||||
return Left(OpenAIError.fromJson(json.decode(response.body)['error']));
|
return Left(OpenAIError.fromJson(json.decode(response.body)['error']));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<OpenAIError, List<String>>> generateImage({
|
||||||
|
required String prompt,
|
||||||
|
int n = 1,
|
||||||
|
}) async {
|
||||||
|
final parameters = {
|
||||||
|
'prompt': prompt,
|
||||||
|
'n': n,
|
||||||
|
'size': '512x512',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await client.post(
|
||||||
|
OpenAIRequestType.imageGenerations.uri,
|
||||||
|
headers: headers,
|
||||||
|
body: json.encode(parameters),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = json.decode(
|
||||||
|
utf8.decode(response.bodyBytes),
|
||||||
|
)['data'] as List;
|
||||||
|
final urls = data
|
||||||
|
.map((e) => e.values)
|
||||||
|
.expand((e) => e)
|
||||||
|
.map((e) => e.toString())
|
||||||
|
.toList();
|
||||||
|
return Right(urls);
|
||||||
|
} else {
|
||||||
|
return Left(OpenAIError.fromJson(json.decode(response.body)['error']));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return Left(OpenAIError(message: error.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_error.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
enum StabilityAIRequestType {
|
||||||
|
imageGenerations;
|
||||||
|
|
||||||
|
Uri get uri {
|
||||||
|
switch (this) {
|
||||||
|
case StabilityAIRequestType.imageGenerations:
|
||||||
|
return Uri.parse(
|
||||||
|
'https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class StabilityAIRepository {
|
||||||
|
/// Generate image from Stability AI
|
||||||
|
///
|
||||||
|
/// [prompt] is the prompt text
|
||||||
|
/// [n] is the number of images to generate
|
||||||
|
///
|
||||||
|
/// the return value is a list of base64 encoded images
|
||||||
|
Future<Either<StabilityAIRequestError, List<String>>> generateImage({
|
||||||
|
required String prompt,
|
||||||
|
int n = 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class HttpStabilityAIRepository implements StabilityAIRepository {
|
||||||
|
const HttpStabilityAIRepository({
|
||||||
|
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<StabilityAIRequestError, List<String>>> generateImage({
|
||||||
|
required String prompt,
|
||||||
|
int n = 1,
|
||||||
|
}) async {
|
||||||
|
final parameters = {
|
||||||
|
'text_prompts': [
|
||||||
|
{
|
||||||
|
'text': prompt,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'samples': n,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await client.post(
|
||||||
|
StabilityAIRequestType.imageGenerations.uri,
|
||||||
|
headers: headers,
|
||||||
|
body: json.encode(parameters),
|
||||||
|
);
|
||||||
|
|
||||||
|
final data = json.decode(
|
||||||
|
utf8.decode(response.bodyBytes),
|
||||||
|
);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final artifacts = data['artifacts'] as List;
|
||||||
|
final base64Images = artifacts
|
||||||
|
.map(
|
||||||
|
(e) => e['base64'].toString(),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
return Right(base64Images);
|
||||||
|
} else {
|
||||||
|
return Left(
|
||||||
|
StabilityAIRequestError(
|
||||||
|
data['message'].toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return Left(
|
||||||
|
StabilityAIRequestError(
|
||||||
|
error.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
class StabilityAIRequestError {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
StabilityAIRequestError(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'StabilityAIRequestError{message: $message}';
|
||||||
|
}
|
||||||
|
}
|
@ -9,12 +9,13 @@ import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc
|
|||||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart';
|
||||||
import 'package:appflowy/plugins/trash/application/prelude.dart';
|
import 'package:appflowy/plugins/trash/application/prelude.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/auth/af_cloud_auth_service.dart';
|
import 'package:appflowy/user/application/auth/af_cloud_auth_service.dart';
|
||||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
import 'package:appflowy/user/application/auth/supabase_mock_auth_service.dart';
|
|
||||||
import 'package:appflowy/user/application/auth/supabase_auth_service.dart';
|
import 'package:appflowy/user/application/auth/supabase_auth_service.dart';
|
||||||
|
import 'package:appflowy/user/application/auth/supabase_mock_auth_service.dart';
|
||||||
import 'package:appflowy/user/application/prelude.dart';
|
import 'package:appflowy/user/application/prelude.dart';
|
||||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||||
import 'package:appflowy/user/application/user_listener.dart';
|
import 'package:appflowy/user/application/user_listener.dart';
|
||||||
@ -85,6 +86,23 @@ void _resolveCommonService(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
getIt.registerFactoryAsync<StabilityAIRepository>(
|
||||||
|
() async {
|
||||||
|
final result = await UserBackendService.getCurrentUserProfile();
|
||||||
|
return result.fold(
|
||||||
|
(l) {
|
||||||
|
throw Exception('Failed to get user profile: ${l.msg}');
|
||||||
|
},
|
||||||
|
(r) {
|
||||||
|
return HttpStabilityAIRepository(
|
||||||
|
client: http.Client(),
|
||||||
|
apiKey: r.stabilityAiKey,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
getIt.registerFactory<ClipboardService>(
|
getIt.registerFactory<ClipboardService>(
|
||||||
() => ClipboardService(),
|
() => ClipboardService(),
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
|
||||||
class UserBackendService {
|
class UserBackendService {
|
||||||
@ -26,6 +26,7 @@ class UserBackendService {
|
|||||||
String? email,
|
String? email,
|
||||||
String? iconUrl,
|
String? iconUrl,
|
||||||
String? openAIKey,
|
String? openAIKey,
|
||||||
|
String? stabilityAiKey,
|
||||||
}) {
|
}) {
|
||||||
final payload = UpdateUserProfilePayloadPB.create()..id = userId;
|
final payload = UpdateUserProfilePayloadPB.create()..id = userId;
|
||||||
|
|
||||||
@ -49,6 +50,10 @@ class UserBackendService {
|
|||||||
payload.openaiKey = openAIKey;
|
payload.openaiKey = openAIKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stabilityAiKey != null) {
|
||||||
|
payload.stabilityAiKey = stabilityAiKey;
|
||||||
|
}
|
||||||
|
|
||||||
return UserEventUpdateUserProfile(payload).send();
|
return UserEventUpdateUserProfile(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,9 +3,9 @@ import 'package:appflowy/user/application/user_service.dart';
|
|||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
|
|
||||||
part 'settings_user_bloc.freezed.dart';
|
part 'settings_user_bloc.freezed.dart';
|
||||||
|
|
||||||
@ -60,6 +60,16 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
updateUserStabilityAIKey: (stabilityAIKey) {
|
||||||
|
_userService
|
||||||
|
.updateUserProfile(stabilityAiKey: stabilityAIKey)
|
||||||
|
.then((result) {
|
||||||
|
result.fold(
|
||||||
|
(l) => null,
|
||||||
|
(err) => Log.error(err),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
didLoadHistoricalUsers: (List<HistoricalUserPB> historicalUsers) {
|
didLoadHistoricalUsers: (List<HistoricalUserPB> historicalUsers) {
|
||||||
emit(state.copyWith(historicalUsers: historicalUsers));
|
emit(state.copyWith(historicalUsers: historicalUsers));
|
||||||
},
|
},
|
||||||
@ -119,6 +129,9 @@ class SettingsUserEvent with _$SettingsUserEvent {
|
|||||||
const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon;
|
const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon;
|
||||||
const factory SettingsUserEvent.updateUserOpenAIKey(String openAIKey) =
|
const factory SettingsUserEvent.updateUserOpenAIKey(String openAIKey) =
|
||||||
_UpdateUserOpenaiKey;
|
_UpdateUserOpenaiKey;
|
||||||
|
const factory SettingsUserEvent.updateUserStabilityAIKey(
|
||||||
|
String stabilityAIKey,
|
||||||
|
) = _UpdateUserStabilityAIKey;
|
||||||
const factory SettingsUserEvent.didReceiveUserProfile(
|
const factory SettingsUserEvent.didReceiveUserProfile(
|
||||||
UserProfilePB newUserProfile,
|
UserProfilePB newUserProfile,
|
||||||
) = _DidReceiveUserProfile;
|
) = _DidReceiveUserProfile;
|
||||||
|
@ -61,6 +61,8 @@ class SettingsUserView extends StatelessWidget {
|
|||||||
const VSpace(12),
|
const VSpace(12),
|
||||||
_renderCurrentOpenaiKey(context),
|
_renderCurrentOpenaiKey(context),
|
||||||
const VSpace(12),
|
const VSpace(12),
|
||||||
|
_renderCurrentStabilityAIKey(context),
|
||||||
|
const VSpace(12),
|
||||||
_renderLoginOrLogoutButton(context, state),
|
_renderLoginOrLogoutButton(context, state),
|
||||||
const VSpace(12),
|
const VSpace(12),
|
||||||
],
|
],
|
||||||
@ -207,9 +209,29 @@ class SettingsUserView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _renderCurrentOpenaiKey(BuildContext context) {
|
Widget _renderCurrentOpenaiKey(BuildContext context) {
|
||||||
final String openAIKey =
|
final String accessKey =
|
||||||
context.read<SettingsUserViewBloc>().state.userProfile.openaiKey;
|
context.read<SettingsUserViewBloc>().state.userProfile.openaiKey;
|
||||||
return _OpenaiKeyInput(openAIKey);
|
return _AIAccessKeyInput(
|
||||||
|
accessKey: accessKey,
|
||||||
|
title: 'OpenAI Key',
|
||||||
|
hintText: LocaleKeys.settings_user_pleaseInputYourOpenAIKey.tr(),
|
||||||
|
callback: (key) => context
|
||||||
|
.read<SettingsUserViewBloc>()
|
||||||
|
.add(SettingsUserEvent.updateUserOpenAIKey(key)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _renderCurrentStabilityAIKey(BuildContext context) {
|
||||||
|
final String accessKey =
|
||||||
|
context.read<SettingsUserViewBloc>().state.userProfile.stabilityAiKey;
|
||||||
|
return _AIAccessKeyInput(
|
||||||
|
accessKey: accessKey,
|
||||||
|
title: 'Stability AI Key',
|
||||||
|
hintText: LocaleKeys.settings_user_pleaseInputYourStabilityAIKey.tr(),
|
||||||
|
callback: (key) => context
|
||||||
|
.read<SettingsUserViewBloc>()
|
||||||
|
.add(SettingsUserEvent.updateUserStabilityAIKey(key)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _avatarOverlay({
|
Widget _avatarOverlay({
|
||||||
@ -379,18 +401,24 @@ class UserEmailInputState extends State<UserEmailInput> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _OpenaiKeyInput extends StatefulWidget {
|
class _AIAccessKeyInput extends StatefulWidget {
|
||||||
final String openAIKey;
|
const _AIAccessKeyInput({
|
||||||
const _OpenaiKeyInput(
|
required this.accessKey,
|
||||||
this.openAIKey, {
|
required this.title,
|
||||||
Key? key,
|
required this.hintText,
|
||||||
}) : super(key: key);
|
required this.callback,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String accessKey;
|
||||||
|
final String title;
|
||||||
|
final String hintText;
|
||||||
|
final void Function(String key) callback;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_OpenaiKeyInput> createState() => _OpenaiKeyInputState();
|
State<_AIAccessKeyInput> createState() => _AIAccessKeyInputState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
|
class _AIAccessKeyInputState extends State<_AIAccessKeyInput> {
|
||||||
bool visible = false;
|
bool visible = false;
|
||||||
final textEditingController = TextEditingController();
|
final textEditingController = TextEditingController();
|
||||||
final debounce = Debounce();
|
final debounce = Debounce();
|
||||||
@ -399,7 +427,7 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
textEditingController.text = widget.openAIKey;
|
textEditingController.text = widget.accessKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -415,12 +443,12 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
|
|||||||
focusedBorder: UnderlineInputBorder(
|
focusedBorder: UnderlineInputBorder(
|
||||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
|
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
labelText: 'OpenAI Key',
|
labelText: widget.title,
|
||||||
labelStyle: Theme.of(context)
|
labelStyle: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleMedium!
|
.titleMedium!
|
||||||
.copyWith(fontWeight: FontWeight.w500),
|
.copyWith(fontWeight: FontWeight.w500),
|
||||||
hintText: LocaleKeys.settings_user_pleaseInputYourOpenAIKey.tr(),
|
hintText: widget.hintText,
|
||||||
suffixIcon: FlowyIconButton(
|
suffixIcon: FlowyIconButton(
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
@ -437,9 +465,7 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
|
|||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
debounce.call(() {
|
debounce.call(() {
|
||||||
context
|
widget.callback(value);
|
||||||
.read<SettingsUserViewBloc>()
|
|
||||||
.add(SettingsUserEvent.updateUserOpenAIKey(value));
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -54,8 +54,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: af8d96b
|
ref: "0abcf7f"
|
||||||
resolved-ref: af8d96bc1aab07046f4febdd991e1787c75c6e38
|
resolved-ref: "0abcf7f6d273b838c895abdc17f6833540613729"
|
||||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.4.3"
|
version: "1.4.3"
|
||||||
|
@ -47,7 +47,7 @@ dependencies:
|
|||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||||
ref: 'af8d96b'
|
ref: "0abcf7f"
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
path: packages/appflowy_popover
|
path: packages/appflowy_popover
|
||||||
|
|
||||||
@ -134,7 +134,6 @@ dev_dependencies:
|
|||||||
url_launcher_platform_interface: any
|
url_launcher_platform_interface: any
|
||||||
run_with_network_images: ^0.0.1
|
run_with_network_images: ^0.0.1
|
||||||
|
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
http: ^1.0.0
|
http: ^1.0.0
|
||||||
|
|
||||||
|
@ -353,6 +353,7 @@
|
|||||||
"tooltipSelectIcon": "Select icon",
|
"tooltipSelectIcon": "Select icon",
|
||||||
"selectAnIcon": "Select an icon",
|
"selectAnIcon": "Select an icon",
|
||||||
"pleaseInputYourOpenAIKey": "please input your OpenAI key",
|
"pleaseInputYourOpenAIKey": "please input your OpenAI key",
|
||||||
|
"pleaseInputYourStabilityAIKey": "please input your Stability AI key",
|
||||||
"clickToLogout": "Click to logout the current user"
|
"clickToLogout": "Click to logout the current user"
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
@ -652,6 +653,14 @@
|
|||||||
"label": "Image URL",
|
"label": "Image URL",
|
||||||
"placeholder": "Enter image URL"
|
"placeholder": "Enter image URL"
|
||||||
},
|
},
|
||||||
|
"ai": {
|
||||||
|
"label": "Generate image from OpenAI",
|
||||||
|
"placeholder": "Please input the prompt for OpenAI to generate image"
|
||||||
|
},
|
||||||
|
"stability_ai": {
|
||||||
|
"label": "Generate image from Stability AI",
|
||||||
|
"placeholder": "Please input the prompt for Stability AI to generate image"
|
||||||
|
},
|
||||||
"support": "Image size limit is 5MB. Supported formats: JPEG, PNG, GIF, SVG",
|
"support": "Image size limit is 5MB. Supported formats: JPEG, PNG, GIF, SVG",
|
||||||
"error": {
|
"error": {
|
||||||
"invalidImage": "Invalid image",
|
"invalidImage": "Invalid image",
|
||||||
@ -663,7 +672,9 @@
|
|||||||
"label": "Embed link",
|
"label": "Embed link",
|
||||||
"placeholder": "Paste or type an image link"
|
"placeholder": "Paste or type an image link"
|
||||||
},
|
},
|
||||||
"searchForAnImage": "Search for an image"
|
"searchForAnImage": "Search for an image",
|
||||||
|
"pleaseInputYourOpenAIKey": "please input your OpenAI key in Settings page",
|
||||||
|
"pleaseInputYourStabilityAIKey": "please input your Stability AI key in Settings page"
|
||||||
},
|
},
|
||||||
"codeBlock": {
|
"codeBlock": {
|
||||||
"language": {
|
"language": {
|
||||||
|
@ -116,6 +116,7 @@ where
|
|||||||
token: token_from_client(client).await.unwrap_or("".to_string()),
|
token: token_from_client(client).await.unwrap_or("".to_string()),
|
||||||
icon_url: "".to_owned(),
|
icon_url: "".to_owned(),
|
||||||
openai_key: "".to_owned(),
|
openai_key: "".to_owned(),
|
||||||
|
stability_ai_key: "".to_owned(),
|
||||||
workspace_id: match profile.latest_workspace_id {
|
workspace_id: match profile.latest_workspace_id {
|
||||||
Some(w) => w.to_string(),
|
Some(w) => w.to_string(),
|
||||||
None => "".to_string(),
|
None => "".to_string(),
|
||||||
|
@ -215,6 +215,7 @@ where
|
|||||||
token: "".to_string(),
|
token: "".to_string(),
|
||||||
icon_url: "".to_string(),
|
icon_url: "".to_string(),
|
||||||
openai_key: "".to_string(),
|
openai_key: "".to_string(),
|
||||||
|
stability_ai_key: "".to_string(),
|
||||||
workspace_id: response.latest_workspace_id,
|
workspace_id: response.latest_workspace_id,
|
||||||
auth_type: AuthType::Supabase,
|
auth_type: AuthType::Supabase,
|
||||||
encryption_type: EncryptionType::from_sign(&response.encryption_sign),
|
encryption_type: EncryptionType::from_sign(&response.encryption_sign),
|
||||||
|
@ -64,6 +64,7 @@ async fn supabase_update_user_profile_test() {
|
|||||||
password: None,
|
password: None,
|
||||||
icon_url: None,
|
icon_url: None,
|
||||||
openai_key: None,
|
openai_key: None,
|
||||||
|
stability_ai_key: None,
|
||||||
encryption_sign: None,
|
encryption_sign: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
ALTER TABLE user_table
|
||||||
|
DROP COLUMN stability_ai_key;
|
@ -0,0 +1,3 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
ALTER TABLE user_table
|
||||||
|
ADD COLUMN stability_ai_key TEXT NOT NULL DEFAULT "";
|
@ -31,6 +31,7 @@ diesel::table! {
|
|||||||
email -> Text,
|
email -> Text,
|
||||||
auth_type -> Integer,
|
auth_type -> Integer,
|
||||||
encryption_type -> Text,
|
encryption_type -> Text,
|
||||||
|
stability_ai_key -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +47,29 @@ async fn user_update_with_name() {
|
|||||||
assert_eq!(user_profile.name, new_name,);
|
assert_eq!(user_profile.name, new_name,);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn user_update_with_ai_key() {
|
||||||
|
let sdk = FlowyCoreTest::new();
|
||||||
|
let user = sdk.init_user().await;
|
||||||
|
let openai_key = "openai_key".to_owned();
|
||||||
|
let stability_ai_key = "stability_ai_key".to_owned();
|
||||||
|
let request = UpdateUserProfilePayloadPB::new(user.id)
|
||||||
|
.openai_key(&openai_key)
|
||||||
|
.stability_ai_key(&stability_ai_key);
|
||||||
|
let _ = EventBuilder::new(sdk.clone())
|
||||||
|
.event(UpdateUserProfile)
|
||||||
|
.payload(request)
|
||||||
|
.sync_send();
|
||||||
|
|
||||||
|
let user_profile = EventBuilder::new(sdk.clone())
|
||||||
|
.event(GetUserProfile)
|
||||||
|
.sync_send()
|
||||||
|
.parse::<UserProfilePB>();
|
||||||
|
|
||||||
|
assert_eq!(user_profile.openai_key, openai_key,);
|
||||||
|
assert_eq!(user_profile.stability_ai_key, stability_ai_key,);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn user_update_with_email() {
|
async fn user_update_with_email() {
|
||||||
let sdk = FlowyCoreTest::new();
|
let sdk = FlowyCoreTest::new();
|
||||||
|
@ -191,6 +191,7 @@ pub struct UserProfile {
|
|||||||
pub token: String,
|
pub token: String,
|
||||||
pub icon_url: String,
|
pub icon_url: String,
|
||||||
pub openai_key: String,
|
pub openai_key: String,
|
||||||
|
pub stability_ai_key: String,
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
pub auth_type: AuthType,
|
pub auth_type: AuthType,
|
||||||
// If the encryption_sign is not empty, which means the user has enabled the encryption.
|
// If the encryption_sign is not empty, which means the user has enabled the encryption.
|
||||||
@ -252,6 +253,7 @@ where
|
|||||||
workspace_id: value.latest_workspace().id.to_owned(),
|
workspace_id: value.latest_workspace().id.to_owned(),
|
||||||
auth_type: auth_type.clone(),
|
auth_type: auth_type.clone(),
|
||||||
encryption_type: value.encryption_type(),
|
encryption_type: value.encryption_type(),
|
||||||
|
stability_ai_key: "".to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -264,6 +266,7 @@ pub struct UpdateUserProfileParams {
|
|||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
pub icon_url: Option<String>,
|
pub icon_url: Option<String>,
|
||||||
pub openai_key: Option<String>,
|
pub openai_key: Option<String>,
|
||||||
|
pub stability_ai_key: Option<String>,
|
||||||
pub encryption_sign: Option<String>,
|
pub encryption_sign: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,6 +303,11 @@ impl UpdateUserProfileParams {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_stability_ai_key(mut self, stability_ai_key: &str) -> Self {
|
||||||
|
self.stability_ai_key = Some(stability_ai_key.to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_encryption_type(mut self, encryption_type: EncryptionType) -> Self {
|
pub fn with_encryption_type(mut self, encryption_type: EncryptionType) -> Self {
|
||||||
let sign = match encryption_type {
|
let sign = match encryption_type {
|
||||||
EncryptionType::NoEncryption => "".to_string(),
|
EncryptionType::NoEncryption => "".to_string(),
|
||||||
@ -316,6 +324,7 @@ impl UpdateUserProfileParams {
|
|||||||
&& self.icon_url.is_none()
|
&& self.icon_url.is_none()
|
||||||
&& self.openai_key.is_none()
|
&& self.openai_key.is_none()
|
||||||
&& self.encryption_sign.is_none()
|
&& self.encryption_sign.is_none()
|
||||||
|
&& self.stability_ai_key.is_none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ pub use user_id::*;
|
|||||||
pub use user_name::*;
|
pub use user_name::*;
|
||||||
pub use user_openai_key::*;
|
pub use user_openai_key::*;
|
||||||
pub use user_password::*;
|
pub use user_password::*;
|
||||||
|
pub use user_stability_ai_key::*;
|
||||||
|
|
||||||
// https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
|
// https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
|
||||||
mod user_email;
|
mod user_email;
|
||||||
@ -12,3 +13,4 @@ mod user_id;
|
|||||||
mod user_name;
|
mod user_name;
|
||||||
mod user_openai_key;
|
mod user_openai_key;
|
||||||
mod user_password;
|
mod user_password;
|
||||||
|
mod user_stability_ai_key;
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
use flowy_error::ErrorCode;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UserStabilityAIKey(pub String);
|
||||||
|
|
||||||
|
impl UserStabilityAIKey {
|
||||||
|
pub fn parse(s: String) -> Result<UserStabilityAIKey, ErrorCode> {
|
||||||
|
Ok(Self(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for UserStabilityAIKey {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,8 @@ use crate::entities::AuthTypePB;
|
|||||||
use crate::errors::ErrorCode;
|
use crate::errors::ErrorCode;
|
||||||
use crate::services::entities::HistoricalUser;
|
use crate::services::entities::HistoricalUser;
|
||||||
|
|
||||||
|
use super::parser::UserStabilityAIKey;
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf)]
|
||||||
pub struct UserTokenPB {
|
pub struct UserTokenPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
@ -51,6 +53,9 @@ pub struct UserProfilePB {
|
|||||||
|
|
||||||
#[pb(index = 10)]
|
#[pb(index = 10)]
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 11)]
|
||||||
|
pub stability_ai_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)]
|
#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)]
|
||||||
@ -82,6 +87,7 @@ impl std::convert::From<UserProfile> for UserProfilePB {
|
|||||||
encryption_sign,
|
encryption_sign,
|
||||||
encryption_type: encryption_ty,
|
encryption_type: encryption_ty,
|
||||||
workspace_id: user_profile.workspace_id,
|
workspace_id: user_profile.workspace_id,
|
||||||
|
stability_ai_key: user_profile.stability_ai_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -105,6 +111,9 @@ pub struct UpdateUserProfilePayloadPB {
|
|||||||
|
|
||||||
#[pb(index = 6, one_of)]
|
#[pb(index = 6, one_of)]
|
||||||
pub openai_key: Option<String>,
|
pub openai_key: Option<String>,
|
||||||
|
|
||||||
|
#[pb(index = 7, one_of)]
|
||||||
|
pub stability_ai_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateUserProfilePayloadPB {
|
impl UpdateUserProfilePayloadPB {
|
||||||
@ -139,6 +148,11 @@ impl UpdateUserProfilePayloadPB {
|
|||||||
self.openai_key = Some(openai_key.to_owned());
|
self.openai_key = Some(openai_key.to_owned());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn stability_ai_key(mut self, stability_ai_key: &str) -> Self {
|
||||||
|
self.stability_ai_key = Some(stability_ai_key.to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryInto<UpdateUserProfileParams> for UpdateUserProfilePayloadPB {
|
impl TryInto<UpdateUserProfileParams> for UpdateUserProfilePayloadPB {
|
||||||
@ -170,6 +184,11 @@ impl TryInto<UpdateUserProfileParams> for UpdateUserProfilePayloadPB {
|
|||||||
Some(openai_key) => Some(UserOpenaiKey::parse(openai_key)?.0),
|
Some(openai_key) => Some(UserOpenaiKey::parse(openai_key)?.0),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let stability_ai_key = match self.stability_ai_key {
|
||||||
|
None => None,
|
||||||
|
Some(stability_ai_key) => Some(UserStabilityAIKey::parse(stability_ai_key)?.0),
|
||||||
|
};
|
||||||
|
|
||||||
Ok(UpdateUserProfileParams {
|
Ok(UpdateUserProfileParams {
|
||||||
uid: self.id,
|
uid: self.id,
|
||||||
name,
|
name,
|
||||||
@ -178,6 +197,7 @@ impl TryInto<UpdateUserProfileParams> for UpdateUserProfilePayloadPB {
|
|||||||
icon_url,
|
icon_url,
|
||||||
openai_key,
|
openai_key,
|
||||||
encryption_sign: None,
|
encryption_sign: None,
|
||||||
|
stability_ai_key,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ pub struct UserTable {
|
|||||||
pub(crate) email: String,
|
pub(crate) email: String,
|
||||||
pub(crate) auth_type: i32,
|
pub(crate) auth_type: i32,
|
||||||
pub(crate) encryption_type: String,
|
pub(crate) encryption_type: String,
|
||||||
|
pub(crate) stability_ai_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserTable {
|
impl UserTable {
|
||||||
@ -41,6 +42,7 @@ impl From<(UserProfile, AuthType)> for UserTable {
|
|||||||
email: user_profile.email,
|
email: user_profile.email,
|
||||||
auth_type: auth_type as i32,
|
auth_type: auth_type as i32,
|
||||||
encryption_type,
|
encryption_type,
|
||||||
|
stability_ai_key: user_profile.stability_ai_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,6 +59,7 @@ impl From<UserTable> for UserProfile {
|
|||||||
workspace_id: table.workspace,
|
workspace_id: table.workspace,
|
||||||
auth_type: AuthType::from(table.auth_type),
|
auth_type: AuthType::from(table.auth_type),
|
||||||
encryption_type: EncryptionType::from_str(&table.encryption_type).unwrap_or_default(),
|
encryption_type: EncryptionType::from_str(&table.encryption_type).unwrap_or_default(),
|
||||||
|
stability_ai_key: table.stability_ai_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,6 +74,7 @@ pub struct UserTableChangeset {
|
|||||||
pub icon_url: Option<String>,
|
pub icon_url: Option<String>,
|
||||||
pub openai_key: Option<String>,
|
pub openai_key: Option<String>,
|
||||||
pub encryption_type: Option<String>,
|
pub encryption_type: Option<String>,
|
||||||
|
pub stability_ai_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserTableChangeset {
|
impl UserTableChangeset {
|
||||||
@ -87,6 +91,7 @@ impl UserTableChangeset {
|
|||||||
icon_url: params.icon_url,
|
icon_url: params.icon_url,
|
||||||
openai_key: params.openai_key,
|
openai_key: params.openai_key,
|
||||||
encryption_type,
|
encryption_type,
|
||||||
|
stability_ai_key: params.stability_ai_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,6 +105,7 @@ impl UserTableChangeset {
|
|||||||
icon_url: Some(user_profile.icon_url),
|
icon_url: Some(user_profile.icon_url),
|
||||||
openai_key: Some(user_profile.openai_key),
|
openai_key: Some(user_profile.openai_key),
|
||||||
encryption_type: Some(encryption_type),
|
encryption_type: Some(encryption_type),
|
||||||
|
stability_ai_key: Some(user_profile.stability_ai_key),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user