diff --git a/app/assets/images/ai_magic.svg b/app/assets/images/ai_magic.svg
new file mode 100644
index 000000000..daae7d07b
--- /dev/null
+++ b/app/assets/images/ai_magic.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/lib/backend/http/api/apps.dart b/app/lib/backend/http/api/apps.dart
index 384310f9e..146609d82 100644
--- a/app/lib/backend/http/api/apps.dart
+++ b/app/lib/backend/http/api/apps.dart
@@ -358,3 +358,21 @@ Future> getPaymentPlansServer() async {
return [];
}
}
+
+Future 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 '';
+ }
+}
diff --git a/app/lib/gen/assets.gen.dart b/app/lib/gen/assets.gen.dart
index 5fbb42468..fce474a12 100644
--- a/app/lib/gen/assets.gen.dart
+++ b/app/lib/gen/assets.gen.dart
@@ -67,6 +67,9 @@ class $AssetsFontsGen {
class $AssetsImagesGen {
const $AssetsImagesGen();
+ /// File path: assets/images/ai_magic.svg
+ String get aiMagic => 'assets/images/ai_magic.svg';
+
/// File path: assets/images/app_launcher_icon.png
AssetGenImage get appLauncherIcon =>
const AssetGenImage('assets/images/app_launcher_icon.png');
@@ -132,6 +135,7 @@ class $AssetsImagesGen {
/// List of all assets
List get values => [
+ aiMagic,
appLauncherIcon,
background,
blob,
diff --git a/app/lib/pages/apps/add_app.dart b/app/lib/pages/apps/add_app.dart
index dcdf0482d..3ed05cb61 100644
--- a/app/lib/pages/apps/add_app.dart
+++ b/app/lib/pages/apps/add_app.dart
@@ -37,7 +37,7 @@ class _AddAppPageState extends State {
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,
@@ -104,6 +104,7 @@ class _AddAppPageState extends State {
pickImage: () async {
await provider.pickImage();
},
+ generatingDescription: provider.isGenratingDescription,
allowPaidApps: provider.allowPaidApps,
appPricing: provider.isPaid ? 'Paid' : 'Free',
appNameController: provider.appNameController,
diff --git a/app/lib/pages/apps/providers/add_app_provider.dart b/app/lib/pages/apps/providers/add_app_provider.dart
index c631f8760..9083eac0d 100644
--- a/app/lib/pages/apps/providers/add_app_provider.dart
+++ b/app/lib/pages/apps/providers/add_app_provider.dart
@@ -60,6 +60,7 @@ class AddAppProvider extends ChangeNotifier {
bool isUpdating = false;
bool isSubmitting = false;
bool isValid = false;
+ bool isGenratingDescription = false;
bool allowPaidApps = false;
@@ -647,4 +648,18 @@ class AddAppProvider extends ChangeNotifier {
checkValidity();
notifyListeners();
}
+
+ Future 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();
+ }
}
diff --git a/app/lib/pages/apps/update_app.dart b/app/lib/pages/apps/update_app.dart
index 82ed97e7a..a125299b0 100644
--- a/app/lib/pages/apps/update_app.dart
+++ b/app/lib/pages/apps/update_app.dart
@@ -105,6 +105,7 @@ class _UpdateAppPageState extends State {
pickImage: () async {
await provider.updateImage();
},
+ generatingDescription: provider.isGenratingDescription,
allowPaidApps: provider.allowPaidApps,
appPricing: provider.isPaid ? 'Paid' : 'Free',
imageFile: provider.imageFile,
diff --git a/app/lib/pages/apps/widgets/app_metadata_widget.dart b/app/lib/pages/apps/widgets/app_metadata_widget.dart
index 6493b7349..2ee71e650 100644
--- a/app/lib/pages/apps/widgets/app_metadata_widget.dart
+++ b/app/lib/pages/apps/widgets/app_metadata_widget.dart
@@ -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;
@@ -19,6 +22,7 @@ class AppMetadataWidget extends StatelessWidget {
final String? category;
final String? appPricing;
final bool allowPaidApps;
+ final bool generatingDescription;
const AppMetadataWidget({
super.key,
@@ -34,6 +38,7 @@ class AppMetadataWidget extends StatelessWidget {
this.category,
this.appPricing,
required this.allowPaidApps,
+ required this.generatingDescription,
});
@override
@@ -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().generateDescription();
+ },
+ child: SvgPicture.asset(
+ Assets.images.aiMagic,
+ color: Colors.white,
+ ),
+ ),
+ )
+ : SizedBox.shrink(),
+ ],
),
),
const SizedBox(
diff --git a/backend/routers/apps.py b/backend/routers/apps.py
index e370a3245..42f89226d 100644
--- a/backend/routers/apps.py
+++ b/backend/routers/apps.py
@@ -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
@@ -283,6 +285,7 @@ def get_plugin_capabilities():
]}
]
+
# @deprecated
@router.get('/v1/app/payment-plans', tags=['v1'])
def get_payment_plans_v1():
@@ -290,6 +293,7 @@ def get_payment_plans_v1():
{'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):
@@ -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 *****************
# ******************************************************
diff --git a/backend/utils/llm.py b/backend/utils/llm.py
index bed3075d8..680c72077 100644
--- a/backend/utils/llm.py
+++ b/backend/utils/llm.py
@@ -239,7 +239,7 @@ class TopicsContext(BaseModel):
class DatesContext(BaseModel):
dates_range: List[datetime] = Field(default=[],
examples=[['2024-12-23T00:00:00+07:00', '2024-12-23T23:59:00+07:00']],
- description="Dates range. (Optional)",)
+ description="Dates range. (Optional)", )
def requires_context_v1(messages: List[Message]) -> bool:
@@ -257,6 +257,7 @@ def requires_context_v1(messages: List[Message]) -> bool:
except ValidationError:
return False
+
def requires_context(question: str) -> bool:
prompt = f'''
Based on the current question your task is to determine whether the user is asking a question that requires context outside the conversation to be answered.
@@ -300,6 +301,7 @@ def retrieve_is_an_omi_question_v1(messages: List[Message]) -> bool:
except ValidationError:
return False
+
def retrieve_is_an_omi_question_v2(messages: List[Message]) -> bool:
prompt = f'''
Task: Analyze the conversation to identify if the user is inquiring about the functionalities or usage of the app, Omi or Friend. Focus on detecting questions related to the app's operations or capabilities.
@@ -405,6 +407,7 @@ def retrieve_context_dates(messages: List[Message], tz: str) -> List[datetime]:
response: DatesContext = with_parser.invoke(prompt)
return response.dates_range
+
def retrieve_context_dates_by_question(question: str, tz: str) -> List[datetime]:
prompt = f'''
You MUST determine the appropriate date range in {tz} that provides context for answering the question provided.
@@ -424,6 +427,7 @@ def retrieve_context_dates_by_question(question: str, tz: str) -> List[datetime]
response: DatesContext = with_parser.invoke(prompt)
return response.dates_range
+
def retrieve_context_dates_by_question_v2(question: str, tz: str) -> List[datetime]:
prompt = f'''
**Task:** Determine the appropriate date range in UTC that provides context for answering the question.
@@ -474,6 +478,7 @@ def retrieve_context_dates_by_question_v2(question: str, tz: str) -> List[dateti
response: DatesContext = with_parser.invoke(prompt)
return response.dates_range
+
def retrieve_context_dates_by_question_v1(question: str, tz: str) -> List[datetime]:
prompt = f'''
Task: Identify the relevant date range needed to provide context for answering the user's recent question.
@@ -565,7 +570,9 @@ def answer_omi_question(messages: List[Message], context: str) -> str:
""".replace(' ', '').strip()
return llm_mini.invoke(prompt).content
-def qa_rag(uid: str, question: str, context: str, plugin: Optional[Plugin] = None, cited: Optional[bool] = False, messages: List[Message] = [], tz: Optional[str] = "UTC") -> str:
+
+def qa_rag(uid: str, question: str, context: str, plugin: Optional[Plugin] = None, cited: Optional[bool] = False,
+ messages: List[Message] = [], tz: Optional[str] = "UTC") -> str:
user_name, facts_str = get_prompt_facts(uid)
facts_str = '\n'.join(facts_str.split('\n')[1:]).strip()
@@ -688,7 +695,9 @@ def qa_rag_v3(uid: str, question: str, context: str, plugin: Optional[Plugin] =
# print('qa_rag prompt', prompt)
return ChatOpenAI(model='gpt-4o').invoke(prompt).content
-def qa_rag_v2(uid: str, question: str, context: str, plugin: Optional[Plugin] = None, cited: Optional[bool] = False) -> str:
+
+def qa_rag_v2(uid: str, question: str, context: str, plugin: Optional[Plugin] = None,
+ cited: Optional[bool] = False) -> str:
user_name, facts_str = get_prompt_facts(uid)
facts_str = '\n'.join(facts_str.split('\n')[1:]).strip()
@@ -737,6 +746,7 @@ def qa_rag_v2(uid: str, question: str, context: str, plugin: Optional[Plugin] =
# print('qa_rag prompt', prompt)
return ChatOpenAI(model='gpt-4o').invoke(prompt).content
+
def qa_rag_v1(uid: str, question: str, context: str, plugin: Optional[Plugin] = None) -> str:
user_name, facts_str = get_prompt_facts(uid)
facts_str = '\n'.join(facts_str.split('\n')[1:]).strip()
@@ -1038,7 +1048,7 @@ class OutputQuestion(BaseModel):
def extract_question_from_conversation(messages: List[Message]) -> str:
# user last messages
user_message_idx = len(messages)
- for i in range(len(messages)-1, -1,-1):
+ for i in range(len(messages) - 1, -1, -1):
if messages[i].sender == MessageSender.ai:
break
if messages[i].sender == MessageSender.human:
@@ -1091,10 +1101,11 @@ def extract_question_from_conversation(messages: List[Message]) -> str:
# print(prompt)
return llm_mini.with_structured_output(OutputQuestion).invoke(prompt).question
+
def extract_question_from_conversation_v3(messages: List[Message]) -> str:
# user last messages
user_message_idx = len(messages)
- for i in range(len(messages)-1, -1,-1):
+ for i in range(len(messages) - 1, -1, -1):
if messages[i].sender == MessageSender.ai:
break
if messages[i].sender == MessageSender.human:
@@ -1146,7 +1157,7 @@ def extract_question_from_conversation_v3(messages: List[Message]) -> str:
def extract_question_from_conversation_v2(messages: List[Message]) -> str:
# user last messages
user_message_idx = len(messages)
- for i in range(len(messages)-1, -1,-1):
+ for i in range(len(messages) - 1, -1, -1):
if messages[i].sender == MessageSender.ai:
break
if messages[i].sender == MessageSender.human:
@@ -1196,8 +1207,9 @@ def extract_question_from_conversation_v1(messages: List[Message]) -> str:
'''.replace(' ', '').strip()
return llm_mini.with_structured_output(OutputQuestion).invoke(prompt).question
+
def retrieve_metadata_fields_from_transcript(
- uid: str, created_at: datetime, transcript_segment: List[dict], tz: str
+ uid: str, created_at: datetime, transcript_segment: List[dict], tz: str
) -> ExtractedInformation:
transcript = ''
for segment in transcript_segment:
@@ -1403,3 +1415,19 @@ def get_proactive_message(uid: str, plugin_prompt: str, params: [str], context:
# print(prompt)
return llm_mini.invoke(prompt).content
+
+
+# **************************************************
+# *************** APPS AI GENERATE *****************
+# **************************************************
+
+def generate_description(app_name: str, description: str) -> str:
+ prompt = f"""
+ You are an AI assistant specializing in crafting detailed and engaging descriptions for apps.
+ You will be provided with the app's name and a brief description which might not be that good. Your task is to expand on the given information, creating a captivating and detailed app description that highlights the app's features, functionality, and benefits.
+ The description should be concise, professional, and not more than 40 words, ensuring clarity and appeal. Respond with only the description, tailored to the app's concept and purpose.
+ App Name: {app_name}
+ Description: {description}
+ """
+ prompt = prompt.replace(' ', '').strip()
+ return llm_mini.invoke(prompt).content