Skip to content

Commit

Permalink
Generate App description with AI (#1590)
Browse files Browse the repository at this point in the history
  • Loading branch information
beastoin authored Dec 31, 2024
2 parents c28a3db + 4aa09ca commit 90b3c3d
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 35 deletions.
3 changes: 3 additions & 0 deletions app/assets/images/ai_magic.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions app/lib/backend/http/api/apps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,21 @@ Future<List<PaymentPlan>> getPaymentPlansServer() async {
return [];
}
}

Future<String> getGenratedDescription(String name, String description) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/app/generate-description',
headers: {},
body: jsonEncode({'name': name, 'description': description}),
method: 'POST',
);
try {
if (response == null || response.statusCode != 200) return '';
log('getGenratedDescription: ${response.body}');
return jsonDecode(response.body)['description'];
} catch (e, stackTrace) {
debugPrint(e.toString());
CrashReporting.reportHandledCrash(e, stackTrace);
return '';
}
}
4 changes: 4 additions & 0 deletions app/lib/gen/assets.gen.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion app/lib/pages/apps/add_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class _AddAppPageState extends State<AddAppPage> {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.primary,
appBar: AppBar(
title: const Text('Submit Your App'),
title: const Text('Submit App'),
backgroundColor: Theme.of(context).colorScheme.primary,
),
extendBody: true,
Expand Down Expand Up @@ -104,6 +104,7 @@ class _AddAppPageState extends State<AddAppPage> {
pickImage: () async {
await provider.pickImage();
},
generatingDescription: provider.isGenratingDescription,
allowPaidApps: provider.allowPaidApps,
appPricing: provider.isPaid ? 'Paid' : 'Free',
appNameController: provider.appNameController,
Expand Down
15 changes: 15 additions & 0 deletions app/lib/pages/apps/providers/add_app_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class AddAppProvider extends ChangeNotifier {
bool isUpdating = false;
bool isSubmitting = false;
bool isValid = false;
bool isGenratingDescription = false;

bool allowPaidApps = false;

Expand Down Expand Up @@ -647,4 +648,18 @@ class AddAppProvider extends ChangeNotifier {
checkValidity();
notifyListeners();
}

Future<void> generateDescription() async {
setIsGenratingDescription(true);
var res = await getGenratedDescription(appNameController.text, appDescriptionController.text);
appDescriptionController.text = res.decodeString;
checkValidity();
setIsGenratingDescription(false);
notifyListeners();
}

void setIsGenratingDescription(bool genrating) {
isGenratingDescription = genrating;
notifyListeners();
}
}
1 change: 1 addition & 0 deletions app/lib/pages/apps/update_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class _UpdateAppPageState extends State<UpdateAppPage> {
pickImage: () async {
await provider.updateImage();
},
generatingDescription: provider.isGenratingDescription,
allowPaidApps: provider.allowPaidApps,
appPricing: provider.isPaid ? 'Paid' : 'Free',
imageFile: provider.imageFile,
Expand Down
86 changes: 60 additions & 26 deletions app/lib/pages/apps/widgets/app_metadata_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import 'dart:io';

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:friend_private/backend/schema/app.dart';
import 'package:friend_private/gen/assets.gen.dart';
import 'package:friend_private/pages/apps/providers/add_app_provider.dart';
import 'package:provider/provider.dart';
import 'package:skeletonizer/skeletonizer.dart';

class AppMetadataWidget extends StatelessWidget {
final File? imageFile;
Expand All @@ -19,6 +22,7 @@ class AppMetadataWidget extends StatelessWidget {
final String? category;
final String? appPricing;
final bool allowPaidApps;
final bool generatingDescription;

const AppMetadataWidget({
super.key,
Expand All @@ -34,6 +38,7 @@ class AppMetadataWidget extends StatelessWidget {
this.category,
this.appPricing,
required this.allowPaidApps,
required this.generatingDescription,
});

@override
Expand Down Expand Up @@ -349,35 +354,64 @@ class AppMetadataWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(10.0),
),
width: double.infinity,
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: MediaQuery.sizeOf(context).height * 0.1,
maxHeight: MediaQuery.sizeOf(context).height * 0.4,
),
child: Scrollbar(
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
reverse: false,
child: TextFormField(
maxLines: null,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please provide a valid description';
}
return null;
},
controller: appDescriptionController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.only(top: 6, bottom: 2),
isDense: true,
border: InputBorder.none,
hintText:
'My Awesome App is a great app that does amazing things. It is the best app ever!',
hintMaxLines: 4,
child: Stack(
children: [
ConstrainedBox(
constraints: BoxConstraints(
minHeight: MediaQuery.sizeOf(context).height * 0.1,
maxHeight: MediaQuery.sizeOf(context).height * 0.4,
),
child: Scrollbar(
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
reverse: false,
child: generatingDescription
? Skeletonizer.zone(
enabled: generatingDescription,
effect: ShimmerEffect(
baseColor: Colors.grey[700]!,
highlightColor: Colors.grey[600]!,
duration: Duration(seconds: 1),
),
child: Bone.multiText(),
)
: TextFormField(
maxLines: null,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please provide a valid description';
}
return null;
},
controller: appDescriptionController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.only(top: 6, bottom: 2),
isDense: true,
border: InputBorder.none,
hintText:
'My Awesome App is a great app that does amazing things. It is the best app ever!',
hintMaxLines: 4,
),
),
),
),
),
),
appDescriptionController.text.isNotEmpty && appNameController.text.isNotEmpty
? Positioned(
bottom: 2,
right: 0,
child: GestureDetector(
onTap: () async {
await context.read<AddAppProvider>().generateDescription();
},
child: SvgPicture.asset(
Assets.images.aiMagic,
color: Colors.white,
),
),
)
: SizedBox.shrink(),
],
),
),
const SizedBox(
Expand Down
18 changes: 17 additions & 1 deletion backend/routers/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
decrease_app_installs_count, enable_app, disable_app, delete_app_cache_by_id
from utils.apps import get_available_apps, get_available_app_by_id, get_approved_available_apps, \
get_available_app_by_id_with_reviews, set_app_review, get_app_reviews, add_tester, is_tester, \
add_app_access_for_tester, remove_app_access_for_tester, upsert_app_payment_link, get_is_user_paid_app, is_permit_payment_plan_get
add_app_access_for_tester, remove_app_access_for_tester, upsert_app_payment_link, get_is_user_paid_app, \
is_permit_payment_plan_get
from utils.llm import generate_description

from utils.notifications import send_notification
from utils.other import endpoints as auth
Expand Down Expand Up @@ -283,13 +285,15 @@ def get_plugin_capabilities():
]}
]


# @deprecated
@router.get('/v1/app/payment-plans', tags=['v1'])
def get_payment_plans_v1():
return [
{'title': 'Monthly Recurring', 'id': 'monthly_recurring'},
]


@router.get('/v1/app/plans', tags=['v1'])
def get_payment_plans(uid: str = Depends(auth.get_current_user_uid)):
if not uid or len(uid) == 0 or not is_permit_payment_plan_get(uid):
Expand All @@ -299,6 +303,18 @@ def get_payment_plans(uid: str = Depends(auth.get_current_user_uid)):
]


@router.post('/v1/app/generate-description', tags=['v1'])
def generate_description_endpoint(data: dict, uid: str = Depends(auth.get_current_user_uid)):
if data['name'] == '':
raise HTTPException(status_code=422, detail='App Name is required')
if data['description'] == '':
raise HTTPException(status_code=422, detail='App Description is required')
desc = generate_description(data['name'], data['description'])
return {
'description': desc,
}


# ******************************************************
# **************** ENABLE/DISABLE APPS *****************
# ******************************************************
Expand Down
Loading

0 comments on commit 90b3c3d

Please sign in to comment.