From cb26bae9be5743c1ff7022e95344e7f89ff6de3d Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sun, 9 Feb 2025 16:33:08 +0530 Subject: [PATCH 01/18] persona from device mvp --- app/lib/backend/http/api/apps.dart | 30 ++++ app/lib/pages/persona/add_persona.dart | 226 +++++++++++++++++++++++++ backend/main.py | 4 +- backend/routers/persona.py | 116 +++++++++++++ 4 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 app/lib/pages/persona/add_persona.dart create mode 100644 backend/routers/persona.py diff --git a/app/lib/backend/http/api/apps.dart b/app/lib/backend/http/api/apps.dart index c31827d998..2f1b080017 100644 --- a/app/lib/backend/http/api/apps.dart +++ b/app/lib/backend/http/api/apps.dart @@ -380,3 +380,33 @@ Future getGenratedDescription(String name, String description) async { return ''; } } + +Future createPersonaApp(File file, Map personaData) async { + var request = http.MultipartRequest( + 'POST', + Uri.parse('${Env.apiBaseUrl}v1/persona'), + ); + request.files.add(await http.MultipartFile.fromPath('file', file.path, filename: basename(file.path))); + request.headers.addAll({'Authorization': await getAuthHeader()}); + request.fields.addAll({'persona_data': jsonEncode(personaData)}); + print(jsonEncode(personaData)); + try { + var streamedResponse = await request.send(); + var response = await http.Response.fromStream(streamedResponse); + + if (response.statusCode == 200) { + debugPrint('createPersonaApp Response body: ${jsonDecode(response.body)}'); + return true; + } else { + debugPrint('Failed to submit app. Status code: ${response.statusCode}'); + if (response.body.isNotEmpty) { + return false; + } else { + return false; + } + } + } catch (e) { + debugPrint('An error occurred createPersonaApp: $e'); + return false; + } +} diff --git a/app/lib/pages/persona/add_persona.dart b/app/lib/pages/persona/add_persona.dart new file mode 100644 index 0000000000..839fe0dec2 --- /dev/null +++ b/app/lib/pages/persona/add_persona.dart @@ -0,0 +1,226 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/http/api/apps.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/utils/alerts/app_snackbar.dart'; +import 'package:friend_private/widgets/animated_loading_button.dart'; +import 'package:image_picker/image_picker.dart'; + +class AddPersonaPage extends StatefulWidget { + const AddPersonaPage({super.key}); + + @override + State createState() => _AddPersonaPageState(); +} + +class _AddPersonaPageState extends State { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _emailController = TextEditingController(); + bool _isPublic = false; + File? _selectedImage; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _nameController.text = SharedPreferencesUtil().givenName; + _emailController.text = SharedPreferencesUtil().email; + } + + Future _pickImage() async { + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage(source: ImageSource.gallery); + if (image != null) { + setState(() { + _selectedImage = File(image.path); + }); + } + } + + Future _createPersona() async { + if (!_formKey.currentState!.validate() || _selectedImage == null) { + if (_selectedImage == null) { + AppSnackbar.showSnackbarError('Please select an image'); + } + return; + } + + setState(() { + _isLoading = true; + }); + + try { + final personaData = { + 'author': _nameController.text, + 'email': _emailController.text, + }; + + var res = await createPersonaApp(_selectedImage!, personaData); + + if (mounted) { + if (res) { + retrieveApps(); + AppSnackbar.showSnackbarSuccess('Persona created successfully'); + } else { + AppSnackbar.showSnackbarError('Failed to create your persona'); + } + } + } catch (e) { + if (mounted) { + AppSnackbar.showSnackbarError('Failed to create persona: $e'); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.transparent, + title: const Text('Create Persona', style: TextStyle(color: Colors.white)), + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: GestureDetector( + onTap: _pickImage, + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(60), + border: Border.all(color: Colors.grey.shade800), + ), + child: _selectedImage != null + ? ClipRRect( + borderRadius: BorderRadius.circular(60), + child: Image.file( + _selectedImage!, + fit: BoxFit.cover, + ), + ) + : Icon( + Icons.add_a_photo, + size: 40, + color: Colors.grey.shade400, + ), + ), + ), + ), + const SizedBox(height: 32), + TextFormField( + controller: _nameController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Name', + labelStyle: TextStyle(color: Colors.grey.shade400), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.grey.shade800), + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.grey.shade600), + borderRadius: BorderRadius.circular(8), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a name'; + } + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _emailController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Email', + labelStyle: TextStyle(color: Colors.grey.shade400), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.grey.shade800), + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.grey.shade600), + borderRadius: BorderRadius.circular(8), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter an email'; + } + if (!value.contains('@')) { + return 'Please enter a valid email'; + } + return null; + }, + ), + const SizedBox(height: 24), + Row( + children: [ + Text( + 'Make Persona Public', + style: TextStyle(color: Colors.grey.shade400), + ), + const Spacer(), + Switch( + value: _isPublic, + onChanged: (value) { + setState(() { + _isPublic = value; + }); + }, + activeColor: Colors.white, + ), + ], + ), + const SizedBox(height: 32), + SizedBox( + width: double.infinity, + child: AnimatedLoadingButton( + onPressed: _createPersona, + color: Colors.white, + loaderColor: Colors.black, + text: "Create Persona", + textStyle: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/backend/main.py b/backend/main.py index c2d090b873..c7436eb592 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,7 +6,7 @@ from modal import Image, App, asgi_app, Secret from routers import workflow, chat, firmware, plugins, memories, transcribe_v2, notifications, \ - speech_profile, agents, facts, users, processing_memories, trends, sdcard, sync, apps, custom_auth, payment + speech_profile, agents, facts, users, processing_memories, trends, sdcard, sync, apps, custom_auth, payment, persona if os.environ.get('SERVICE_ACCOUNT_JSON'): service_account_info = json.loads(os.environ["SERVICE_ACCOUNT_JSON"]) @@ -40,6 +40,8 @@ app.include_router(payment.router) +app.include_router(persona.router) + modal_app = App( name='backend', secrets=[Secret.from_name("gcp-credentials"), Secret.from_name('envs')], diff --git a/backend/routers/persona.py b/backend/routers/persona.py new file mode 100644 index 0000000000..e73d64a9cc --- /dev/null +++ b/backend/routers/persona.py @@ -0,0 +1,116 @@ +import json +import os +from fastapi import APIRouter, Depends, Form, UploadFile, File +from datetime import datetime +from ulid import ULID +from database.facts import get_facts +from database.auth import get_user_name +from database.memories import get_memories +from models.memory import Memory +from utils.llm import llm_medium +from utils.other import endpoints as auth +from database._client import db +from utils.other.storage import upload_plugin_logo + +router = APIRouter() + + +def generate_description(facts): + # Create a concise description from facts + facts_text = "\n".join([f"- {fact['content']}" for fact in facts if not fact['deleted']]) + + prompt = f"""Based on these facts about a person, create a concise, engaging description that captures their unique personality and characteristics (max 250 characters). + +Facts: +{facts_text} + +Create a natural, memorable description that captures this person's essence. Focus on the most unique and interesting aspects. Make it conversational and engaging.""" + + try: + response = llm_medium.invoke(prompt) + description = response.content + return description + except Exception as e: + print(f'Error generating description: {e}') + # Fallback to simple description if API call fails + key_facts = [fact['content'] for fact in facts if not fact['deleted']][:3] + facts_summary = ". ".join(key_facts) + return facts_summary[:250] + + +@router.post("/v1/persona") +async def create_persona(persona_data: str = Form(...), file: UploadFile = File(...), uid=Depends(auth.get_current_user_uid)): + data = json.loads(persona_data) + # get facts from db + facts = get_facts(uid, limit=1000) + # get firebase user name from uid + user_name = get_user_name(uid) + + # get the most recent 50 memories from db and convert them to a single string + memories = get_memories(uid, limit=50) + conversation_history = Memory.memories_to_string(memories) + + # Create the chat prompt template + chat_prompt = f"""You are {user_name} AI, you must personify {user_name} as well as you can. + +Style: +- Interactive highly opinionated messages and questions. +- Reference your personal facts naturally + +Rules: +- Never mention being AI +- No ** or formal language +- 2-3 sentences for response. Never more than 20 words. +- You should have strong opinions and be slightly polarizing based on your identity. +- You must make the conversation flow. You excel at coherent conversations. + +Personal Facts and Context:""" + + # Add user facts to the prompt + facts_text = "\n".join([f"- {fact['content']}" for fact in facts if not fact['deleted']]) + chat_prompt += f"\n{facts_text}" + + # Add conversation history + chat_prompt += "\n\nRecent Conversations:\n" + conversation_history + + # Add final instructions + chat_prompt += "\n\nUse these facts and conversations to shape your personality. Responses should help the conversation go deeper. You have strong opinions, slightly controversial." + + # Generate persona description + description = generate_description(facts) + + # Upload image to omi_plugins_bucket + os.makedirs(f'_temp/plugins', exist_ok=True) + file_path = f"_temp/plugins/{file.filename}" + with open(file_path, 'wb') as f: + f.write(file.file.read()) + + # Use the provided doc_id from the request data + doc_id = str(ULID()) + imgUrl = upload_plugin_logo(file_path, doc_id) + + # Save persona data to Firestore at the specified document ID + doc_ref = db.collection('plugins_data').document(doc_id).set({ + 'approved': True, + 'author': data['author'], + 'capabilities': ["chat"], + 'category': 'persona', + 'chat_prompt': chat_prompt, + 'connected_accounts': ["omi"], + 'created_at': datetime.now().isoformat(), + 'deleted': False, + 'description': description, + 'email': data['email'], + 'id': doc_id, + 'image': imgUrl, + 'name': user_name, + 'private': True, + 'status': 'approved', + 'uid': uid + }) + + return { + "chat_prompt": chat_prompt, + "description": description, + "doc_id": doc_id + } From 28f42340cb7e0898a8b98b0737c23b907866060c Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Thu, 13 Feb 2025 23:24:01 +0530 Subject: [PATCH 02/18] persona from device backend stuff --- backend/database/apps.py | 5 ++ backend/routers/chat.py | 5 +- backend/routers/persona.py | 72 ++++++++-------------------- backend/utils/llm.py | 96 +++++++++++++++++++++++++++++++------ backend/utils/openrouter.py | 61 +++++++++++++++++++++++ backend/utils/persona.py | 52 ++++++++++++++++++++ 6 files changed, 224 insertions(+), 67 deletions(-) create mode 100644 backend/utils/openrouter.py create mode 100644 backend/utils/persona.py diff --git a/backend/database/apps.py b/backend/database/apps.py index 1623e8e2df..347f99f76f 100644 --- a/backend/database/apps.py +++ b/backend/database/apps.py @@ -261,3 +261,8 @@ def get_persona_by_id_db(persona_id: str): if doc.exists: return doc.to_dict() return None + + +def add_persona_to_db(persona_data: dict): + persona_ref = db.collection('plugins_data') + persona_ref.add(persona_data, persona_data['id']) \ No newline at end of file diff --git a/backend/routers/chat.py b/backend/routers/chat.py index 780b80d12c..d8719234af 100644 --- a/backend/routers/chat.py +++ b/backend/routers/chat.py @@ -18,6 +18,7 @@ from utils.apps import get_available_app_by_id from utils.chat import process_voice_message_segment, process_voice_message_segment_stream from utils.llm import initial_chat_message +from utils.openrouter import execute_persona_chat_stream from utils.other import endpoints as auth from utils.retrieval.graph import execute_graph_chat, execute_graph_chat_stream @@ -55,6 +56,7 @@ def send_message( messages = list(reversed([Message(**msg) for msg in chat_db.get_messages(uid, limit=10, plugin_id=plugin_id)])) + def process_message(response: str, callback_data: dict): memories = callback_data.get('memories_found', []) ask_for_nps = callback_data.get('ask_for_nps', False) @@ -94,7 +96,8 @@ def process_message(response: str, callback_data: dict): async def generate_stream(): callback_data = {} - async for chunk in execute_graph_chat_stream(uid, messages, app, cited=True, callback_data=callback_data): + stream_function = execute_persona_chat_stream if app and 'persona' in app.capabilities else execute_graph_chat_stream + async for chunk in stream_function(uid, messages, app, cited=True, callback_data=callback_data): if chunk: data = chunk.replace("\n", "__CRLF__") yield f'{data}\n\n' diff --git a/backend/routers/persona.py b/backend/routers/persona.py index e73d64a9cc..832045382d 100644 --- a/backend/routers/persona.py +++ b/backend/routers/persona.py @@ -3,54 +3,30 @@ from fastapi import APIRouter, Depends, Form, UploadFile, File from datetime import datetime from ulid import ULID + +from database.apps import add_persona_to_db from database.facts import get_facts from database.auth import get_user_name from database.memories import get_memories from models.memory import Memory -from utils.llm import llm_medium +from utils.llm import condense_conversations, condense_facts, generate_persona_description from utils.other import endpoints as auth -from database._client import db from utils.other.storage import upload_plugin_logo router = APIRouter() -def generate_description(facts): - # Create a concise description from facts - facts_text = "\n".join([f"- {fact['content']}" for fact in facts if not fact['deleted']]) - - prompt = f"""Based on these facts about a person, create a concise, engaging description that captures their unique personality and characteristics (max 250 characters). - -Facts: -{facts_text} - -Create a natural, memorable description that captures this person's essence. Focus on the most unique and interesting aspects. Make it conversational and engaging.""" - - try: - response = llm_medium.invoke(prompt) - description = response.content - return description - except Exception as e: - print(f'Error generating description: {e}') - # Fallback to simple description if API call fails - key_facts = [fact['content'] for fact in facts if not fact['deleted']][:3] - facts_summary = ". ".join(key_facts) - return facts_summary[:250] - - @router.post("/v1/persona") -async def create_persona(persona_data: str = Form(...), file: UploadFile = File(...), uid=Depends(auth.get_current_user_uid)): +async def create_persona(persona_data: str = Form(...), file: UploadFile = File(...), + uid=Depends(auth.get_current_user_uid)): data = json.loads(persona_data) - # get facts from db facts = get_facts(uid, limit=1000) - # get firebase user name from uid user_name = get_user_name(uid) - # get the most recent 50 memories from db and convert them to a single string - memories = get_memories(uid, limit=50) + memories = get_memories(uid, limit=100) conversation_history = Memory.memories_to_string(memories) + conversation_history = condense_conversations([conversation_history]) - # Create the chat prompt template chat_prompt = f"""You are {user_name} AI, you must personify {user_name} as well as you can. Style: @@ -67,7 +43,7 @@ async def create_persona(persona_data: str = Form(...), file: UploadFile = File( Personal Facts and Context:""" # Add user facts to the prompt - facts_text = "\n".join([f"- {fact['content']}" for fact in facts if not fact['deleted']]) + facts_text = condense_facts([fact['content'] for fact in facts if not fact['deleted']]) chat_prompt += f"\n{facts_text}" # Add conversation history @@ -77,7 +53,7 @@ async def create_persona(persona_data: str = Form(...), file: UploadFile = File( chat_prompt += "\n\nUse these facts and conversations to shape your personality. Responses should help the conversation go deeper. You have strong opinions, slightly controversial." # Generate persona description - description = generate_description(facts) + description = generate_persona_description(facts_text) # Upload image to omi_plugins_bucket os.makedirs(f'_temp/plugins', exist_ok=True) @@ -85,32 +61,26 @@ async def create_persona(persona_data: str = Form(...), file: UploadFile = File( with open(file_path, 'wb') as f: f.write(file.file.read()) - # Use the provided doc_id from the request data - doc_id = str(ULID()) - imgUrl = upload_plugin_logo(file_path, doc_id) + persona_id = str(ULID()) + img_url = upload_plugin_logo(file_path, persona_id) - # Save persona data to Firestore at the specified document ID - doc_ref = db.collection('plugins_data').document(doc_id).set({ - 'approved': True, + doc_data = { + 'approved': False, 'author': data['author'], - 'capabilities': ["chat"], + 'capabilities': ["chat", "persona"], 'category': 'persona', 'chat_prompt': chat_prompt, 'connected_accounts': ["omi"], 'created_at': datetime.now().isoformat(), 'deleted': False, 'description': description, - 'email': data['email'], - 'id': doc_id, - 'image': imgUrl, + 'email': 'mohsin@test.com', + 'id': persona_id, + 'image': img_url, 'name': user_name, - 'private': True, - 'status': 'approved', + 'private': data['private'], + 'status': 'under-review', 'uid': uid - }) - - return { - "chat_prompt": chat_prompt, - "description": description, - "doc_id": doc_id } + add_persona_to_db(doc_data) + return {'success': True, 'message': 'Persona created successfully.'} diff --git a/backend/utils/llm.py b/backend/utils/llm.py index d52b070863..a57d13e07a 100644 --- a/backend/utils/llm.py +++ b/backend/utils/llm.py @@ -579,9 +579,10 @@ def answer_simple_message(uid: str, messages: List[Message], plugin: Optional[Pl return llm_mini.invoke(prompt).content -def answer_simple_message_stream(uid: str, messages: List[Message], plugin: Optional[Plugin] = None, callbacks=[]) -> str: +def answer_simple_message_stream(uid: str, messages: List[Message], plugin: Optional[Plugin] = None, + callbacks=[]) -> str: prompt = _get_answer_simple_message_prompt(uid, messages, plugin) - return llm_mini_stream.invoke(prompt, {'callbacks':callbacks}).content + return llm_mini_stream.invoke(prompt, {'callbacks': callbacks}).content def _get_answer_omi_question_prompt(messages: List[Message], context: str) -> str: @@ -609,13 +610,15 @@ def answer_omi_question(messages: List[Message], context: str) -> str: prompt = _get_answer_omi_question_prompt(messages, context) return llm_mini.invoke(prompt).content + def answer_omi_question_stream(messages: List[Message], context: str, callbacks: []) -> str: prompt = _get_answer_omi_question_prompt(messages, context) - return llm_mini_stream.invoke(prompt, {'callbacks':callbacks}).content + return llm_mini_stream.invoke(prompt, {'callbacks': callbacks}).content -def _get_qa_rag_prompt(uid: str, question: str, context: str, plugin: Optional[Plugin] = None, cited: Optional[bool] = False, - messages: List[Message] = [], tz: Optional[str] = "UTC") -> str: +def _get_qa_rag_prompt(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() @@ -702,17 +705,17 @@ def qa_rag(uid: str, question: str, context: str, plugin: Optional[Plugin] = Non # print('qa_rag prompt', prompt) return llm_medium.invoke(prompt).content + def qa_rag_stream(uid: str, question: str, context: str, plugin: Optional[Plugin] = None, cited: Optional[bool] = False, messages: List[Message] = [], tz: Optional[str] = "UTC", callbacks=[]) -> str: - prompt = _get_qa_rag_prompt(uid, question, context, plugin, cited, messages, tz) # print('qa_rag prompt', prompt) return llm_medium_stream.invoke(prompt, {'callbacks': callbacks}).content -def _get_qa_rag_prompt_v6(uid: str, question: str, context: str, plugin: Optional[Plugin] = None, cited: Optional[bool] = False, +def _get_qa_rag_prompt_v6(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() @@ -783,9 +786,10 @@ def _get_qa_rag_prompt_v6(uid: str, question: str, context: str, plugin: Optiona """.replace(' ', '').replace('\n\n\n', '\n\n').strip() -def _get_qa_rag_prompt_v5(uid: str, question: str, context: str, plugin: Optional[Plugin] = None, cited: Optional[bool] = False, - messages: List[Message] = [], tz: Optional[str] = "UTC") -> str: +def _get_qa_rag_prompt_v5(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() @@ -850,9 +854,9 @@ def _get_qa_rag_prompt_v5(uid: str, question: str, context: str, plugin: Optiona """.replace(' ', '').replace('\n\n\n', '\n\n').strip() -def _get_qa_rag_prompt_v4(uid: str, question: str, context: str, plugin: Optional[Plugin] = None, cited: Optional[bool] = False, +def _get_qa_rag_prompt_v4(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() @@ -918,15 +922,17 @@ def qa_rag_v4(uid: str, question: str, context: str, plugin: Optional[Plugin] = # print('qa_rag prompt', prompt) return llm_large.invoke(prompt).content -def qa_rag_stream_v4(uid: str, question: str, context: str, plugin: Optional[Plugin] = None, cited: Optional[bool] = False, - messages: List[Message] = [], tz: Optional[str] = "UTC", callbacks=[]) -> str: +def qa_rag_stream_v4(uid: str, question: str, context: str, plugin: Optional[Plugin] = None, + cited: Optional[bool] = False, + messages: List[Message] = [], tz: Optional[str] = "UTC", callbacks=[]) -> str: prompt = _get_qa_rag_prompt(uid, question, context, plugin, cited, messages, tz) # print('qa_rag prompt', prompt) return llm_large_stream.invoke(prompt, {'callbacks': callbacks}).content -def qa_rag_v3(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_v3(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() @@ -1336,6 +1342,7 @@ class FiltersToUse(BaseModel): class OutputQuestion(BaseModel): question: str = Field(description='The extracted user question from the conversation.') + def extract_question_from_conversation(messages: List[Message]) -> str: # user last messages user_message_idx = len(messages) @@ -1474,6 +1481,7 @@ def extract_question_from_conversation_v6(messages: List[Message]) -> str: # print(prompt) return llm_mini.with_structured_output(OutputQuestion).invoke(prompt).question + def extract_question_from_conversation_v5(messages: List[Message]) -> str: # user last messages user_message_idx = len(messages) @@ -1533,6 +1541,7 @@ def extract_question_from_conversation_v5(messages: List[Message]) -> str: # print(prompt) return llm_mini.with_structured_output(OutputQuestion).invoke(prompt).question + def extract_question_from_conversation_v4(messages: List[Message]) -> str: # user last messages user_message_idx = len(messages) @@ -1919,3 +1928,60 @@ def generate_description(app_name: str, description: str) -> str: """ prompt = prompt.replace(' ', '').strip() return llm_mini.invoke(prompt).content + + +# ************************************************** +# ******************* PERSONA ********************** +# ************************************************** + +def condense_facts(facts): + combined_facts = "\n".join(facts) + prompt = f"""You are an expert in personality analysis and information preservation. Your task is to condense the following facts about a person while ensuring their complete personality traits and characteristics are preserved: + {combined_facts} + +Instructions: +- Preserve ALL information that defines the person's unique traits, preferences, and behaviors +- Group semantically related facts together to maintain context +- Merge overlapping or redundant facts while retaining subtle nuances +- Prioritize facts that showcase distinctive personality characteristics +- Maintain chronological or causal relationships between facts +- Keep the tone natural and conversational +- Ensure the condensed version could recreate the exact same persona + +The output will be used to create an AI persona that perfectly mirrors this individual, so no unique trait or characteristic can be lost in the condensing process. + """ + response = llm_medium.invoke(prompt) + return response.content + + +def generate_persona_description(facts): + prompt = f"""Based on these facts about a person, create a concise, engaging description that captures their unique personality and characteristics (max 250 characters). + +Facts: +{facts} + +Create a natural, memorable description that captures this person's essence. Focus on the most unique and interesting aspects. Make it conversational and engaging.""" + + response = llm_medium.invoke(prompt) + description = response.content + return description + + +def condense_conversations(conversations): + combined_conversations = "\n".join(conversations) + prompt = f"""You are an expert in conversation analysis and personality preservation. Your task is to condense the following conversations while preserving the essential context, communication style, and personality traits: + {combined_conversations} + +Instructions: +- Preserve ALL information that defines the person's communication style and personality +- Maintain key discussion topics and viewpoints expressed +- Capture recurring patterns in how they interact and respond +- Keep important contextual details and relationships mentioned +- Retain their unique voice, vocabulary, and expression style +- Group related conversations to maintain context flow +- Ensure the condensed version accurately represents their conversational identity + +The output will be used to create an AI persona that mirrors this individual's conversation style, so no key personality trait or communication pattern can be lost in the condensing process. + """ + response = llm_medium.invoke(prompt) + return response.content diff --git a/backend/utils/openrouter.py b/backend/utils/openrouter.py new file mode 100644 index 0000000000..c37840c745 --- /dev/null +++ b/backend/utils/openrouter.py @@ -0,0 +1,61 @@ +import os +from typing import List, Optional + +from openai import AsyncClient + +from models.app import App +from models.chat import Message + +openrouter_key = os.getenv('OPENROUTER_API_KEY') + +client = AsyncClient( + base_url="https://openrouter.ai/api/v1", + api_key=openrouter_key, +) + + +async def execute_persona_chat_stream(uid: str, messages: List[Message], app: App, cited: Optional[bool] = False, + callback_data: dict = None): + """Handle streaming chat responses for persona-type apps using OpenRouter.""" + + system_prompt = app.chat_prompt + formatted_messages = [{ + "role": "system", + "content": system_prompt + }] + + # Add message history + for msg in messages: + role = "assistant" if msg.sender == "ai" else "user" + formatted_messages.append({"role": role, "content": msg.text}) + + # Track the full response for callback_data + full_response = [] + + # Stream the response + try: + stream = await client.chat.completions.create( + messages=formatted_messages, + model="google/gemini-flash-1.5-8b", + stream=True + ) + + async for chunk in stream: + if chunk.choices and chunk.choices[0].delta.content: + content = chunk.choices[0].delta.content + full_response.append(content) + data = content.replace("\n", "__CRLF__") + yield f"data: {data}\n\n" + + # Store final response in callback_data + if callback_data is not None: + callback_data['answer'] = ''.join(full_response) + callback_data['memories_found'] = [] + callback_data['ask_for_nps'] = False + + except Exception as e: + print(f"Error in execute_persona_chat_stream: {e}") + if callback_data is not None: + callback_data['error'] = str(e) + + yield None diff --git a/backend/utils/persona.py b/backend/utils/persona.py new file mode 100644 index 0000000000..c66dd6aa68 --- /dev/null +++ b/backend/utils/persona.py @@ -0,0 +1,52 @@ +import os +from datetime import datetime + +from database.facts import get_facts +from database.auth import get_user_name +from database.memories import get_memories +from database._client import db +from models.memory import Memory +from routers.persona import condense_facts, condense_conversations + + +def update_persona_prompt(uid: str, doc_id: str): + """Update a persona's chat prompt with latest facts and memories.""" + # Get latest facts and user info + facts = get_facts(uid, limit=1000) + user_name = get_user_name(uid) + + # Get and condense recent memories + memories = get_memories(uid, limit=100) + conversation_history = Memory.memories_to_string(memories) + conversation_history = condense_conversations([conversation_history]) + + # Condense facts + facts_text = condense_facts([fact['content'] for fact in facts if not fact['deleted']]) + + # Generate updated chat prompt + chat_prompt = f"""You are {user_name} AI, you must personify {user_name} as well as you can. + +Style: +- Interactive highly opinionated messages and questions. +- Reference your personal facts naturally + +Rules: +- Never mention being AI +- No ** or formal language +- 2-3 sentences for response. Never more than 20 words. +- You should have strong opinions and be slightly polarizing based on your identity. +- You must make the conversation flow. You excel at coherent conversations. + +Personal Facts and Context: +{facts_text} + +Recent Conversations: +{conversation_history} + +Use these facts and conversations to shape your personality. Responses should help the conversation go deeper. You have strong opinions, slightly controversial.""" + + # Update persona data in Firestore + db.collection('plugins_data').document(doc_id).update({ + 'chat_prompt': chat_prompt, + 'updated_at': datetime.now().isoformat() + }) \ No newline at end of file From d490cb3c423e56a27ca6b967df563ca9e945447d Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Thu, 13 Feb 2025 23:24:25 +0530 Subject: [PATCH 03/18] improve persona creation ui --- app/lib/pages/apps/explore_install_page.dart | 13 +++- .../apps/widgets/create_options_sheet.dart | 71 +++++++++++++++++++ app/lib/pages/persona/add_persona.dart | 63 ++++++---------- 3 files changed, 102 insertions(+), 45 deletions(-) create mode 100644 app/lib/pages/apps/widgets/create_options_sheet.dart diff --git a/app/lib/pages/apps/explore_install_page.dart b/app/lib/pages/apps/explore_install_page.dart index 75ff4a9f59..8e1474030d 100644 --- a/app/lib/pages/apps/explore_install_page.dart +++ b/app/lib/pages/apps/explore_install_page.dart @@ -11,6 +11,8 @@ import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/utils/other/temp.dart'; import 'package:provider/provider.dart'; +import 'widgets/create_options_sheet.dart'; + String filterValueToString(dynamic value) { if (value.runtimeType == String) { return value; @@ -190,8 +192,13 @@ class _ExploreInstallPageState extends State with AutomaticK SliverToBoxAdapter( child: GestureDetector( onTap: () { - MixpanelManager().pageOpened('Submit App'); - routeToPage(context, const AddAppPage()); + showModalBottomSheet( + context: context, + builder: (context) => const CreateOptionsSheet(), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + ); }, child: Container( padding: const EdgeInsets.all(12.0), @@ -208,7 +215,7 @@ class _ExploreInstallPageState extends State with AutomaticK Icon(Icons.add, color: Colors.white), SizedBox(width: 8), Text( - 'Create and submit a new app', + 'Create New', textAlign: TextAlign.center, ), ], diff --git a/app/lib/pages/apps/widgets/create_options_sheet.dart b/app/lib/pages/apps/widgets/create_options_sheet.dart new file mode 100644 index 0000000000..3a30a738ea --- /dev/null +++ b/app/lib/pages/apps/widgets/create_options_sheet.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:friend_private/pages/apps/add_app.dart'; +import 'package:friend_private/pages/persona/add_persona.dart'; +import 'package:friend_private/utils/analytics/mixpanel.dart'; +import 'package:friend_private/utils/other/temp.dart'; + +class CreateOptionsSheet extends StatelessWidget { + const CreateOptionsSheet({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(24, 32, 24, 24), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'What would you like to create?', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 24), + Card( + elevation: 0, + color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: Icon(Icons.apps, color: Colors.white), + title: + Text('Submit an App', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white)), + subtitle: Text('Create and share your app with the community', + style: TextStyle(color: Colors.white.withOpacity(0.7))), + onTap: () { + Navigator.pop(context); + MixpanelManager().pageOpened('Submit App'); + routeToPage(context, const AddAppPage()); + }, + ), + ), + const SizedBox(height: 12), + Card( + elevation: 0, + color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: Icon(Icons.person_outline, color: Colors.white), + title: + Text('Create Persona', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white)), + subtitle: Text('Create your digital clone for personalized interactions', + style: TextStyle(color: Colors.white.withOpacity(0.7))), + onTap: () { + Navigator.pop(context); + MixpanelManager().pageOpened('Create Persona'); + routeToPage(context, const AddPersonaPage()); + }, + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/pages/persona/add_persona.dart b/app/lib/pages/persona/add_persona.dart index 839fe0dec2..d5a6b64d8b 100644 --- a/app/lib/pages/persona/add_persona.dart +++ b/app/lib/pages/persona/add_persona.dart @@ -2,9 +2,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:friend_private/backend/http/api/apps.dart'; import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/providers/app_provider.dart'; import 'package:friend_private/utils/alerts/app_snackbar.dart'; import 'package:friend_private/widgets/animated_loading_button.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:provider/provider.dart'; class AddPersonaPage extends StatefulWidget { const AddPersonaPage({super.key}); @@ -53,14 +55,14 @@ class _AddPersonaPageState extends State { try { final personaData = { 'author': _nameController.text, - 'email': _emailController.text, + 'private': !_isPublic, }; var res = await createPersonaApp(_selectedImage!, personaData); if (mounted) { if (res) { - retrieveApps(); + context.read().getApps(); AppSnackbar.showSnackbarSuccess('Persona created successfully'); } else { AppSnackbar.showSnackbarError('Failed to create your persona'); @@ -156,32 +158,6 @@ class _AddPersonaPageState extends State { return null; }, ), - const SizedBox(height: 16), - TextFormField( - controller: _emailController, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - labelText: 'Email', - labelStyle: TextStyle(color: Colors.grey.shade400), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.grey.shade800), - borderRadius: BorderRadius.circular(8), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.grey.shade600), - borderRadius: BorderRadius.circular(8), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter an email'; - } - if (!value.contains('@')) { - return 'Please enter a valid email'; - } - return null; - }, - ), const SizedBox(height: 24), Row( children: [ @@ -202,25 +178,28 @@ class _AddPersonaPageState extends State { ], ), const SizedBox(height: 32), - SizedBox( - width: double.infinity, - child: AnimatedLoadingButton( - onPressed: _createPersona, - color: Colors.white, - loaderColor: Colors.black, - text: "Create Persona", - textStyle: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ), ], ), ), ), ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.only(left: 24, right: 24, bottom: 52), + child: SizedBox( + width: double.infinity, + child: AnimatedLoadingButton( + onPressed: _createPersona, + color: Colors.white, + loaderColor: Colors.black, + text: "Create Persona", + textStyle: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), ); } } From 72c6eccb4758dcdde1b8e64a4abee0c73227d9b2 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 14 Feb 2025 13:16:32 +0530 Subject: [PATCH 04/18] update persona prompt once memory is created --- backend/database/apps.py | 13 +++++++++++++ backend/utils/memories/process_memory.py | 8 +++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/backend/database/apps.py b/backend/database/apps.py index 347f99f76f..553cb7cdd7 100644 --- a/backend/database/apps.py +++ b/backend/database/apps.py @@ -263,6 +263,19 @@ def get_persona_by_id_db(persona_id: str): return None +def get_persona_by_uid_db(uid: str): + filters = [FieldFilter('uid', '==', uid), FieldFilter('capabilities', 'array_contains', 'persona'), + FieldFilter('deleted', '==', False)] + persona_ref = db.collection('plugins_data').where(filter=BaseCompositeFilter('AND', filters)).limit(1) + docs = persona_ref.get() + if not docs: + return None + doc = next(iter(docs), None) + if not doc: + return None + return doc.to_dict() + + def add_persona_to_db(persona_data: dict): persona_ref = db.collection('plugins_data') persona_ref.add(persona_data, persona_data['id']) \ No newline at end of file diff --git a/backend/utils/memories/process_memory.py b/backend/utils/memories/process_memory.py index 56d085dbd2..f942a8b1fc 100644 --- a/backend/utils/memories/process_memory.py +++ b/backend/utils/memories/process_memory.py @@ -12,7 +12,7 @@ import database.notifications as notification_db import database.tasks as tasks_db import database.trends as trends_db -from database.apps import record_app_usage +from database.apps import record_app_usage, get_persona_by_uid_db from database.vector_db import upsert_vector2, update_vector_metadata from models.app import App, UsageHistoryType from models.facts import FactDB @@ -27,6 +27,7 @@ trends_extractor from utils.notifications import send_notification from utils.other.hume import get_hume, HumeJobCallbackModel, HumeJobModelPredictionResponseModel +from utils.persona import update_persona_prompt from utils.retrieval.rag import retrieve_rag_memory_context from utils.webhooks import memory_created_webhook @@ -191,6 +192,11 @@ def process_memory( if not is_reprocess: threading.Thread(target=memory_created_webhook, args=(uid, memory,)).start() + # Update persona prompt with new memory + persona = get_persona_by_uid_db(uid) + print('updating persona after memory creation') + if persona: + update_persona_prompt(uid, persona['id']) # TODO: trigger external integrations here too From 6e9261db495b96dc156582b2fd58dd1385ec0027 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:21:50 +0530 Subject: [PATCH 05/18] cleanup and improvements --- backend/database/apps.py | 7 ++++++- backend/routers/persona.py | 2 +- backend/utils/memories/process_memory.py | 2 ++ backend/utils/persona.py | 10 +++++----- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/backend/database/apps.py b/backend/database/apps.py index 553cb7cdd7..9b51c194b7 100644 --- a/backend/database/apps.py +++ b/backend/database/apps.py @@ -278,4 +278,9 @@ def get_persona_by_uid_db(uid: str): def add_persona_to_db(persona_data: dict): persona_ref = db.collection('plugins_data') - persona_ref.add(persona_data, persona_data['id']) \ No newline at end of file + persona_ref.add(persona_data, persona_data['id']) + + +def update_persona_in_db(persona_data: dict): + persona_ref = db.collection('plugins_data').document(persona_data['id']) + persona_ref.update(persona_data) \ No newline at end of file diff --git a/backend/routers/persona.py b/backend/routers/persona.py index 832045382d..7511537fd0 100644 --- a/backend/routers/persona.py +++ b/backend/routers/persona.py @@ -74,7 +74,7 @@ async def create_persona(persona_data: str = Form(...), file: UploadFile = File( 'created_at': datetime.now().isoformat(), 'deleted': False, 'description': description, - 'email': 'mohsin@test.com', + 'email': data['email'], 'id': persona_id, 'image': img_url, 'name': user_name, diff --git a/backend/utils/memories/process_memory.py b/backend/utils/memories/process_memory.py index f942a8b1fc..00169440a3 100644 --- a/backend/utils/memories/process_memory.py +++ b/backend/utils/memories/process_memory.py @@ -13,6 +13,7 @@ import database.tasks as tasks_db import database.trends as trends_db from database.apps import record_app_usage, get_persona_by_uid_db +from database.redis_db import delete_app_cache_by_id from database.vector_db import upsert_vector2, update_vector_metadata from models.app import App, UsageHistoryType from models.facts import FactDB @@ -197,6 +198,7 @@ def process_memory( print('updating persona after memory creation') if persona: update_persona_prompt(uid, persona['id']) + delete_app_cache_by_id(persona['id']) # TODO: trigger external integrations here too diff --git a/backend/utils/persona.py b/backend/utils/persona.py index c66dd6aa68..5656b15d15 100644 --- a/backend/utils/persona.py +++ b/backend/utils/persona.py @@ -1,6 +1,7 @@ import os from datetime import datetime +from database.apps import update_persona_in_db, get_persona_by_uid_db from database.facts import get_facts from database.auth import get_user_name from database.memories import get_memories @@ -45,8 +46,7 @@ def update_persona_prompt(uid: str, doc_id: str): Use these facts and conversations to shape your personality. Responses should help the conversation go deeper. You have strong opinions, slightly controversial.""" - # Update persona data in Firestore - db.collection('plugins_data').document(doc_id).update({ - 'chat_prompt': chat_prompt, - 'updated_at': datetime.now().isoformat() - }) \ No newline at end of file + persona = get_persona_by_uid_db(uid) + persona['chat_prompt'] = chat_prompt + persona['updated_at'] = datetime.utcnow() + update_persona_in_db(persona) \ No newline at end of file From fad4f9b1fb5c16d09775d14e904b168fcf9480aa Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sun, 16 Feb 2025 00:36:34 +0530 Subject: [PATCH 06/18] persona creation and handling on app --- app/lib/backend/http/api/apps.dart | 18 +++++ app/lib/backend/schema/app.dart | 7 +- app/lib/pages/apps/add_app.dart | 78 ++++++++++++++++++- app/lib/pages/apps/explore_install_page.dart | 14 +--- .../apps/providers/add_app_provider.dart | 58 +++++++++----- .../apps/widgets/create_options_sheet.dart | 71 ----------------- app/lib/pages/chat/page.dart | 3 +- app/lib/utils/text_formatter.dart | 11 +++ 8 files changed, 154 insertions(+), 106 deletions(-) delete mode 100644 app/lib/pages/apps/widgets/create_options_sheet.dart create mode 100644 app/lib/utils/text_formatter.dart diff --git a/app/lib/backend/http/api/apps.dart b/app/lib/backend/http/api/apps.dart index c6f06c2d03..64d1cd8cea 100644 --- a/app/lib/backend/http/api/apps.dart +++ b/app/lib/backend/http/api/apps.dart @@ -444,3 +444,21 @@ Future createPersonaApp(File file, Map personaData) async return false; } } + +Future checkPersonaUsername(String username) async { + var response = await makeApiCall( + url: '${Env.apiBaseUrl}v1/apps/check-username?username=$username', + headers: {}, + body: '', + method: 'GET', + ); + try { + if (response == null || response.statusCode != 200) return false; + log('checkPersonaUsernames: ${response.body}'); + return jsonDecode(response.body)['is_taken']; + } catch (e, stackTrace) { + debugPrint(e.toString()); + CrashReporting.reportHandledCrash(e, stackTrace); + return true; + } +} diff --git a/app/lib/backend/schema/app.dart b/app/lib/backend/schema/app.dart index e0d41b934d..3f2bdbed2b 100644 --- a/app/lib/backend/schema/app.dart +++ b/app/lib/backend/schema/app.dart @@ -195,6 +195,7 @@ class App { String? paymentLink; List thumbnailIds; List thumbnailUrls; + String? username; App({ required this.id, @@ -229,6 +230,7 @@ class App { this.paymentLink, this.thumbnailIds = const [], this.thumbnailUrls = const [], + this.username, }); String? getRatingAvg() => ratingAvg?.toStringAsFixed(1); @@ -237,7 +239,9 @@ class App { bool worksWithMemories() => hasCapability('memories'); - bool worksWithChat() => hasCapability('chat'); + bool worksWithChat() => hasCapability('chat') || hasCapability('persona'); + + bool isNotPersona() => !hasCapability('persona'); bool worksExternally() => hasCapability('external_integration'); @@ -278,6 +282,7 @@ class App { paymentLink: json['payment_link'], thumbnailIds: (json['thumbnails'] as List?)?.cast() ?? [], thumbnailUrls: (json['thumbnail_urls'] as List?)?.cast() ?? [], + username: json['username'], ); } diff --git a/app/lib/pages/apps/add_app.dart b/app/lib/pages/apps/add_app.dart index c4a0597a57..0b5e4ec667 100644 --- a/app/lib/pages/apps/add_app.dart +++ b/app/lib/pages/apps/add_app.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/services.dart'; +import 'package:friend_private/utils/other/debouncer.dart'; +import 'package:friend_private/utils/text_formatter.dart'; import 'package:shimmer/shimmer.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/pages/apps/widgets/full_screen_image_viewer.dart'; @@ -29,6 +32,8 @@ class AddAppPage extends StatefulWidget { class _AddAppPageState extends State { late bool showSubmitAppConfirmation; + final _debouncer = Debouncer(delay: const Duration(milliseconds: 500)); + @override void initState() { showSubmitAppConfirmation = SharedPreferencesUtil().showSubmitAppConfirmation; @@ -114,8 +119,6 @@ class _AddAppPageState extends State { appPricing: provider.isPaid ? 'Paid' : 'Free', appNameController: provider.appNameController, appDescriptionController: provider.appDescriptionController, - creatorNameController: provider.creatorNameController, - creatorEmailController: provider.creatorEmailController, categories: provider.categories, setAppCategory: provider.setAppCategory, imageFile: provider.imageFile, @@ -370,6 +373,77 @@ class _AddAppPageState extends State { ), ], ), + if (provider.isCapabilitySelectedById('persona')) + Container( + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12.0), + ), + padding: const EdgeInsets.all(14.0), + margin: const EdgeInsets.only(top: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'Persona Username', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a username to access the persona'; + } + return null; + }, + onChanged: (value) { + _debouncer.run(() async { + await provider.checkIsUsernameTaken(value); + }); + }, + controller: provider.usernameController, + inputFormatters: [ + LowerCaseTextFormatter(), + FilteringTextInputFormatter.allow(RegExp(r'[a-z0-9_]')), + ], + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: 'Enter a username', + suffix: provider.usernameController.text.isEmpty + ? null + : provider.isCheckingUsername + ? const SizedBox( + width: 16, + height: 16, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.grey), + ), + ), + ) + : Icon( + provider.isUsernameTaken ? Icons.close : Icons.check, + color: provider.isUsernameTaken ? Colors.red : Colors.green, + size: 16, + ), + ), + ), + ), + ], + ), + ), const SizedBox( height: 22, ), diff --git a/app/lib/pages/apps/explore_install_page.dart b/app/lib/pages/apps/explore_install_page.dart index 8e1474030d..3919f229bf 100644 --- a/app/lib/pages/apps/explore_install_page.dart +++ b/app/lib/pages/apps/explore_install_page.dart @@ -11,8 +11,6 @@ import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/utils/other/temp.dart'; import 'package:provider/provider.dart'; -import 'widgets/create_options_sheet.dart'; - String filterValueToString(dynamic value) { if (value.runtimeType == String) { return value; @@ -191,14 +189,8 @@ class _ExploreInstallPageState extends State with AutomaticK )), SliverToBoxAdapter( child: GestureDetector( - onTap: () { - showModalBottomSheet( - context: context, - builder: (context) => const CreateOptionsSheet(), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - ); + onTap: () async { + routeToPage(context, const AddAppPage()); }, child: Container( padding: const EdgeInsets.all(12.0), @@ -215,7 +207,7 @@ class _ExploreInstallPageState extends State with AutomaticK Icon(Icons.add, color: Colors.white), SizedBox(width: 8), Text( - 'Create New', + 'Create and Submit App', textAlign: TextAlign.center, ), ], diff --git a/app/lib/pages/apps/providers/add_app_provider.dart b/app/lib/pages/apps/providers/add_app_provider.dart index 6e98018f6a..5059f9f953 100644 --- a/app/lib/pages/apps/providers/add_app_provider.dart +++ b/app/lib/pages/apps/providers/add_app_provider.dart @@ -23,11 +23,12 @@ class AddAppProvider extends ChangeNotifier { TextEditingController appNameController = TextEditingController(); TextEditingController appDescriptionController = TextEditingController(); - TextEditingController creatorNameController = TextEditingController(); - TextEditingController creatorEmailController = TextEditingController(); TextEditingController chatPromptController = TextEditingController(); TextEditingController conversationPromptController = TextEditingController(); + TextEditingController usernameController = TextEditingController(); String? appCategory; + bool isUsernameTaken = false; + bool isCheckingUsername = false; // Trigger Event String? triggerEvent; @@ -84,8 +85,6 @@ class AddAppProvider extends ChangeNotifier { if (paymentPlans.isEmpty) { await getPaymentPlans(); } - creatorNameController.text = SharedPreferencesUtil().givenName; - creatorEmailController.text = SharedPreferencesUtil().email; setIsLoading(false); } @@ -132,9 +131,7 @@ class AddAppProvider extends ChangeNotifier { imageUrl = app.image; appNameController.text = app.name.decodeString; appDescriptionController.text = app.description.decodeString; - creatorNameController.text = app.author.decodeString; priceController.text = app.price.toString(); - creatorEmailController.text = app.email ?? ''; makeAppPublic = !app.private; selectedCapabilities = app.getCapabilitiesFromIds(capabilities); if (app.externalIntegration != null) { @@ -169,8 +166,6 @@ class AddAppProvider extends ChangeNotifier { void clear() { appNameController.clear(); appDescriptionController.clear(); - creatorNameController.clear(); - creatorEmailController.clear(); chatPromptController.clear(); conversationPromptController.clear(); triggerEvent = null; @@ -246,6 +241,12 @@ class AddAppProvider extends ChangeNotifier { notifyListeners(); } + Future checkIsUsernameTaken(String username) async { + setIsCheckingUsername(true); + isUsernameTaken = await checkPersonaUsername(username); + setIsCheckingUsername(false); + } + bool hasDataChanged(App app, String category) { if (imageFile != null) { return true; @@ -256,9 +257,6 @@ class AddAppProvider extends ChangeNotifier { if (appDescriptionController.text != app.description) { return true; } - if (creatorNameController.text != app.author) { - return true; - } if (makeAppPublic != !app.private) { return true; } @@ -322,6 +320,9 @@ class AddAppProvider extends ChangeNotifier { if (capability.id == 'proactive_notification') { isValid = selectedScopes.isNotEmpty && selectedCapabilities.length > 1; } + if (capability.id == 'persona') { + isValid = usernameController.text.isNotEmpty && !isUsernameTaken; + } } if (isPaid) { isValid = formKey.currentState!.validate() && selectePaymentPlan != null; @@ -406,6 +407,16 @@ class AddAppProvider extends ChangeNotifier { return false; } } + if (capability.id == 'persona') { + if (usernameController.text.isEmpty) { + AppSnackbar.showSnackbarError('Please enter a username for your persona app'); + return false; + } + if (isUsernameTaken) { + AppSnackbar.showSnackbarError('Username is already taken. Please choose another username'); + return false; + } + } } if (appCategory == null) { AppSnackbar.showSnackbarError('Please select a category for your app'); @@ -424,8 +435,6 @@ class AddAppProvider extends ChangeNotifier { Map data = { 'name': appNameController.text, 'description': appDescriptionController.text, - 'author': creatorNameController.text, - 'email': creatorEmailController.text, 'capabilities': selectedCapabilities.map((e) => e.id).toList(), 'deleted': false, 'uid': SharedPreferencesUtil().uid, @@ -490,10 +499,8 @@ class AddAppProvider extends ChangeNotifier { setIsSubmitting(true); Map data = { - 'name': appNameController.text, - 'description': appDescriptionController.text, - 'author': creatorNameController.text, - 'email': creatorEmailController.text, + 'name': appNameController.text.trim(), + 'description': appDescriptionController.text.trim(), 'capabilities': selectedCapabilities.map((e) => e.id).toList(), 'deleted': false, 'uid': SharedPreferencesUtil().uid, @@ -503,6 +510,7 @@ class AddAppProvider extends ChangeNotifier { 'price': priceController.text.isNotEmpty ? double.parse(priceController.text) : 0.0, 'payment_plan': selectePaymentPlan, 'thumbnails': thumbnailIds, + 'username': usernameController.text.trim(), }; for (var capability in selectedCapabilities) { if (capability.id == 'external_integration') { @@ -523,10 +531,10 @@ class AddAppProvider extends ChangeNotifier { } } if (capability.id == 'chat') { - data['chat_prompt'] = chatPromptController.text; + data['chat_prompt'] = chatPromptController.text.trim(); } if (capability.id == 'memories') { - data['memory_prompt'] = conversationPromptController.text; + data['memory_prompt'] = conversationPromptController.text.trim(); } if (capability.id == 'proactive_notification') { if (data['proactive_notification'] == null) { @@ -630,7 +638,13 @@ class AddAppProvider extends ChangeNotifier { if (selectedCapabilities.contains(capability)) { selectedCapabilities.remove(capability); } else { - selectedCapabilities.add(capability); + if (selectedCapabilities.length == 1 && selectedCapabilities.first.id == 'persona') { + AppSnackbar.showSnackbarError('Other capabilities cannot be selected with Persona'); + } else if (selectedCapabilities.isNotEmpty && capability.id == 'persona') { + AppSnackbar.showSnackbarError('Persona cannot be selected with other capabilities'); + } else { + selectedCapabilities.add(capability); + } } checkValidity(); notifyListeners(); @@ -727,6 +741,10 @@ class AddAppProvider extends ChangeNotifier { void setIsGenratingDescription(bool genrating) { isGenratingDescription = genrating; + } + + void setIsCheckingUsername(bool checking) { + isCheckingUsername = checking; notifyListeners(); } } diff --git a/app/lib/pages/apps/widgets/create_options_sheet.dart b/app/lib/pages/apps/widgets/create_options_sheet.dart deleted file mode 100644 index 3a30a738ea..0000000000 --- a/app/lib/pages/apps/widgets/create_options_sheet.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:friend_private/pages/apps/add_app.dart'; -import 'package:friend_private/pages/persona/add_persona.dart'; -import 'package:friend_private/utils/analytics/mixpanel.dart'; -import 'package:friend_private/utils/other/temp.dart'; - -class CreateOptionsSheet extends StatelessWidget { - const CreateOptionsSheet({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.fromLTRB(24, 32, 24, 24), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'What would you like to create?', - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - const SizedBox(height: 24), - Card( - elevation: 0, - color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - leading: Icon(Icons.apps, color: Colors.white), - title: - Text('Submit an App', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white)), - subtitle: Text('Create and share your app with the community', - style: TextStyle(color: Colors.white.withOpacity(0.7))), - onTap: () { - Navigator.pop(context); - MixpanelManager().pageOpened('Submit App'); - routeToPage(context, const AddAppPage()); - }, - ), - ), - const SizedBox(height: 12), - Card( - elevation: 0, - color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - leading: Icon(Icons.person_outline, color: Colors.white), - title: - Text('Create Persona', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white)), - subtitle: Text('Create your digital clone for personalized interactions', - style: TextStyle(color: Colors.white.withOpacity(0.7))), - onTap: () { - Navigator.pop(context); - MixpanelManager().pageOpened('Create Persona'); - routeToPage(context, const AddPersonaPage()); - }, - ), - ), - ], - ), - ); - } -} diff --git a/app/lib/pages/chat/page.dart b/app/lib/pages/chat/page.dart index 5d3d80bd74..6bbf5c4159 100644 --- a/app/lib/pages/chat/page.dart +++ b/app/lib/pages/chat/page.dart @@ -343,7 +343,8 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin { showTypingIndicator: provider.showTypingIndicator && chatIndex == 0, message: message, sendMessage: _sendMessageUtil, - displayOptions: provider.messages.length <= 1, + displayOptions: provider.messages.length <= 1 && + provider.messageSenderApp(message.appId)?.isNotPersona() == true, appSender: provider.messageSenderApp(message.appId), updateConversation: (ServerConversation conversation) { context.read().updateConversation(conversation); diff --git a/app/lib/utils/text_formatter.dart b/app/lib/utils/text_formatter.dart new file mode 100644 index 0000000000..22ddd89570 --- /dev/null +++ b/app/lib/utils/text_formatter.dart @@ -0,0 +1,11 @@ +import 'package:flutter/services.dart'; + +class LowerCaseTextFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + return TextEditingValue( + text: newValue.text.toLowerCase(), + selection: newValue.selection, + ); + } +} From 75b1db0449c16f1801e2323924d72eeaebfd4559 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sun, 16 Feb 2025 00:37:00 +0530 Subject: [PATCH 07/18] get rid of email and name fields for apps --- app/lib/pages/apps/update_app.dart | 6 +- .../apps/widgets/app_metadata_widget.dart | 73 ------------------- 2 files changed, 2 insertions(+), 77 deletions(-) diff --git a/app/lib/pages/apps/update_app.dart b/app/lib/pages/apps/update_app.dart index 5f769d3c11..32d50696a7 100644 --- a/app/lib/pages/apps/update_app.dart +++ b/app/lib/pages/apps/update_app.dart @@ -114,8 +114,6 @@ class _UpdateAppPageState extends State { imageFile: provider.imageFile, appNameController: provider.appNameController, appDescriptionController: provider.appDescriptionController, - creatorNameController: provider.creatorNameController, - creatorEmailController: provider.creatorEmailController, categories: provider.categories, setAppCategory: provider.setAppCategory, imageUrl: provider.imageUrl, @@ -152,8 +150,8 @@ class _UpdateAppPageState extends State { itemCount: provider.thumbnailUrls.length + 1, itemBuilder: (context, index) { // Calculate dimensions to maintain 2:3 ratio - final width = 120.0; - final height = width * 1.5; // 2:3 ratio + const width = 120.0; + const height = width * 1.5; // 2:3 ratio if (index == provider.thumbnailUrls.length) { return GestureDetector( diff --git a/app/lib/pages/apps/widgets/app_metadata_widget.dart b/app/lib/pages/apps/widgets/app_metadata_widget.dart index 771ef845a1..5daaae75cb 100644 --- a/app/lib/pages/apps/widgets/app_metadata_widget.dart +++ b/app/lib/pages/apps/widgets/app_metadata_widget.dart @@ -15,8 +15,6 @@ class AppMetadataWidget extends StatelessWidget { final VoidCallback pickImage; final TextEditingController appNameController; final TextEditingController appDescriptionController; - final TextEditingController creatorNameController; - final TextEditingController creatorEmailController; final List categories; final Function(String?) setAppCategory; final String? category; @@ -31,8 +29,6 @@ class AppMetadataWidget extends StatelessWidget { required this.pickImage, required this.appNameController, required this.appDescriptionController, - required this.creatorNameController, - required this.creatorEmailController, required this.categories, required this.setAppCategory, this.category, @@ -270,75 +266,6 @@ class AppMetadataWidget extends StatelessWidget { const SizedBox( height: 16, ), - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'Creator Name', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(10.0), - ), - width: double.infinity, - child: TextFormField( - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter creator name'; - } - return null; - }, - controller: creatorNameController, - decoration: const InputDecoration( - contentPadding: EdgeInsets.only(top: 6, bottom: 6), - isDense: true, - errorText: null, - border: InputBorder.none, - hintText: 'Nik Shevchenko', - ), - ), - ), - const SizedBox( - height: 16, - ), - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'Email Address', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(10.0), - ), - width: double.infinity, - child: TextFormField( - controller: creatorEmailController, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter creator email'; - } - return null; - }, - decoration: const InputDecoration( - contentPadding: EdgeInsets.only(top: 6, bottom: 6), - isDense: true, - border: InputBorder.none, - hintText: 'nik@basedhardware.com', - ), - ), - ), - const SizedBox( - height: 16, - ), Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( From 88ac1343522850aed30b6638b9f323b616ce68d3 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sun, 16 Feb 2025 00:38:40 +0530 Subject: [PATCH 08/18] persona backend improvements --- backend/database/redis_db.py | 24 +++++ backend/models/app.py | 7 +- backend/routers/apps.py | 33 +++++-- backend/routers/chat.py | 38 ++++---- backend/utils/apps.py | 107 ++++++++++++++++++++++- backend/utils/memories/process_memory.py | 5 +- backend/utils/openrouter.py | 8 +- 7 files changed, 186 insertions(+), 36 deletions(-) diff --git a/backend/database/redis_db.py b/backend/database/redis_db.py index d437db1325..2ba20485c7 100644 --- a/backend/database/redis_db.py +++ b/backend/database/redis_db.py @@ -69,6 +69,29 @@ def delete_app_cache_by_id(app_id: str): r.delete(f'apps:{app_id}') +# ****************************************************** +# ********************** PERSONA *********************** +# ****************************************************** + +def is_username_taken(username: str) -> bool: + return r.exists(f'personas:{username}') + + +def get_uid_by_username(username: str) -> str | None: + uid = r.get(f'personas:{username}') + if not uid: + return None + return uid.decode() + + +def delete_username(username: str): + r.delete(f'personas:{username}') + + +def save_username(username: str, uid: str): + r.set(f'personas:{username}', uid) + + # ****************************************************** # *********************** APPS ************************* # ****************************************************** @@ -158,6 +181,7 @@ def migrate_user_plugins_reviews(prev_uid: str, new_uid: str): def set_user_paid_app(app_id: str, uid: str, ttl: int): r.set(f'users:{uid}:paid_apps:{app_id}', app_id, ex=ttl) + def get_user_paid_app(app_id: str, uid: str) -> str: val = r.get(f'users:{uid}:paid_apps:{app_id}') if not val: diff --git a/backend/models/app.py b/backend/models/app.py index 20f0a08447..c3a75fada1 100644 --- a/backend/models/app.py +++ b/backend/models/app.py @@ -61,6 +61,8 @@ class App(BaseModel): capabilities: Set[str] memory_prompt: Optional[str] = None chat_prompt: Optional[str] = None + persona_prompt: Optional[str] = None + username: Optional[str] = None external_integration: Optional[ExternalIntegration] = None reviews: List[AppReview] = [] user_review: Optional[AppReview] = None @@ -95,7 +97,10 @@ def works_with_memories(self) -> bool: return self.has_capability('memories') def works_with_chat(self) -> bool: - return self.has_capability('chat') + return self.has_capability('chat') or self.has_capability('persona') + + def is_a_persona(self) -> bool: + return self.has_capability('persona') def works_externally(self) -> bool: return self.has_capability('external_integration') diff --git a/backend/routers/apps.py b/backend/routers/apps.py index bab2ddd080..82144795cb 100644 --- a/backend/routers/apps.py +++ b/backend/routers/apps.py @@ -9,14 +9,15 @@ from database.apps import change_app_approval_status, get_unapproved_public_apps_db, \ add_app_to_db, update_app_in_db, delete_app_from_db, update_app_visibility_in_db, \ get_personas_by_username_db, get_persona_by_id_db, delete_persona_db +from database.auth import get_user_from_uid from database.notifications import get_token_only from database.redis_db import delete_generic_cache, get_specific_user_review, increase_app_installs_count, \ - decrease_app_installs_count, enable_app, disable_app, delete_app_cache_by_id + decrease_app_installs_count, enable_app, disable_app, delete_app_cache_by_id, is_username_taken from database.users import get_stripe_connect_account_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 + is_permit_payment_plan_get, generate_persona_prompt from utils.llm import generate_description from utils.notifications import send_notification @@ -50,6 +51,10 @@ def create_app(app_data: str = Form(...), file: UploadFile = File(...), uid=Depe data['status'] = 'under-review' data['name'] = data['name'].strip() data['id'] = str(ULID()) + if not data.get('author') and not data.get('email'): + user = get_user_from_uid(uid) + data['author'] = user['display_name'] + data['email'] = user['email'] if not data.get('is_paid'): data['is_paid'] = False else: @@ -72,12 +77,15 @@ def create_app(app_data: str = Form(...), file: UploadFile = File(...), uid=Depe external_integration['is_instructions_url'] = True else: external_integration['is_instructions_url'] = False + if "persona" in data['capabilities']: + data['persona_prompt'] = generate_persona_prompt(uid) + data['connected_accounts'] = ['omi'] os.makedirs(f'_temp/plugins', exist_ok=True) file_path = f"_temp/plugins/{file.filename}" with open(file_path, 'wb') as f: f.write(file.file.read()) - imgUrl = upload_plugin_logo(file_path, data['id']) - data['image'] = imgUrl + img_url = upload_plugin_logo(file_path, data['id']) + data['image'] = img_url data['created_at'] = datetime.now(timezone.utc) add_app_to_db(data) @@ -89,6 +97,14 @@ def create_app(app_data: str = Form(...), file: UploadFile = File(...), uid=Depe return {'status': 'ok', 'app_id': app.id} +@router.get('/v1/apps/check-username', tags=['v1']) +def check_username(username: str, uid: str = Depends(auth.get_current_user_uid)): + persona = is_username_taken(username) + if persona == 0: + return {'is_taken': False} + return {'is_taken': True} + + @router.patch('/v1/apps/{app_id}', tags=['v1']) def update_app(app_id: str, app_data: str = Form(...), file: UploadFile = File(None), uid=Depends(auth.get_current_user_uid)): @@ -111,7 +127,8 @@ def update_app(app_id: str, app_data: str = Form(...), file: UploadFile = File(N update_app_in_db(data) # payment link - upsert_app_payment_link(data.get('id'), data.get('is_paid', False), data.get('price'), data.get('payment_plan'), data.get('uid'), + upsert_app_payment_link(data.get('id'), data.get('is_paid', False), data.get('price'), data.get('payment_plan'), + data.get('uid'), previous_price=plugin.get("price", 0)) if plugin['approved'] and (plugin['private'] is None or plugin['private'] is False): @@ -282,6 +299,7 @@ def get_plugin_capabilities(): return [ {'title': 'Chat', 'id': 'chat'}, {'title': 'Memories', 'id': 'memories'}, + {'title': 'Persona', 'id': 'persona'}, {'title': 'External Integration', 'id': 'external_integration', 'triggers': [ {'title': 'Audio Bytes', 'id': 'audio_bytes'}, {'title': 'Memory Creation', 'id': 'memory_creation'}, @@ -457,8 +475,8 @@ def reject_app(app_id: str, uid: str, secret_key: str = Header(...)): @router.delete('/v1/personas/{persona_id}', tags=['v1']) @router.post('/v1/app/thumbnails', tags=['v1']) async def upload_app_thumbnail_endpoint( - file: UploadFile = File(...), - uid: str = Depends(auth.get_current_user_uid) + file: UploadFile = File(...), + uid: str = Depends(auth.get_current_user_uid) ): """Upload a thumbnail image for an app. @@ -492,6 +510,7 @@ async def upload_app_thumbnail_endpoint( if os.path.exists(temp_path): os.remove(temp_path) + def delete_persona(persona_id: str, secret_key: str = Header(...)): if secret_key != os.getenv('ADMIN_KEY'): raise HTTPException(status_code=403, detail='You are not authorized to perform this action') diff --git a/backend/routers/chat.py b/backend/routers/chat.py index 73aeaf426c..8124330cad 100644 --- a/backend/routers/chat.py +++ b/backend/routers/chat.py @@ -13,7 +13,8 @@ import database.chat as chat_db from database.apps import record_app_usage from models.app import App -from models.chat import ChatSession, Message, SendMessageRequest, MessageSender, ResponseMessage, MessageMemory, FileChat +from models.chat import ChatSession, Message, SendMessageRequest, MessageSender, ResponseMessage, MessageMemory, \ + FileChat from models.memory import Memory from models.plugin import UsageHistoryType from routers.sync import retrieve_file_paths, decode_files_to_wav, retrieve_vad_segments @@ -28,6 +29,7 @@ router = APIRouter() fc = FileChatTool() + def filter_messages(messages, plugin_id): print('filter_messages', len(messages), plugin_id) collected = [] @@ -38,6 +40,7 @@ def filter_messages(messages, plugin_id): print('filter_messages output:', len(collected)) return collected + def acquire_chat_session(uid: str, plugin_id: Optional[str] = None): chat_session = chat_db.get_chat_session(uid, plugin_id=plugin_id) if chat_session is None: @@ -91,7 +94,6 @@ def send_message( messages = list(reversed([Message(**msg) for msg in chat_db.get_messages(uid, limit=10, plugin_id=plugin_id)])) - def process_message(response: str, callback_data: dict): memories = callback_data.get('memories_found', []) ask_for_nps = callback_data.get('ask_for_nps', False) @@ -135,12 +137,13 @@ def process_message(response: str, callback_data: dict): async def generate_stream(): callback_data = {} -# stream_function = execute_persona_chat_stream if app and 'persona' in app.capabilities else execute_graph_chat_stream -# async for chunk in stream_function(uid, messages, app, cited=True, callback_data=callback_data): - async for chunk in execute_graph_chat_stream(uid, messages, app, cited=True, callback_data=callback_data, chat_session=chat_session): + stream_function = execute_persona_chat_stream if app and app.is_a_persona() else execute_graph_chat_stream + async for chunk in stream_function(uid, messages, app, cited=True, callback_data=callback_data, + chat_session=chat_session): + # async for chunk in execute_graph_chat_stream(uid, messages, app, cited=True, callback_data=callback_data, chat_session=chat_session): if chunk: - data = chunk.replace("\n", "__CRLF__") - yield f'{data}\n\n' + msg = chunk.replace("\n", "__CRLF__") + yield f'{msg}\n\n' else: response = callback_data.get('answer') if response: @@ -161,7 +164,6 @@ async def generate_stream(): def report_message( message_id: str, uid: str = Depends(auth.get_current_user_uid) ): - message, msg_doc_id = chat_db.get_message(uid, message_id) if message is None: raise HTTPException(status_code=404, detail='Message not found') @@ -214,7 +216,8 @@ def send_message_v1( messages = list(reversed([Message(**msg) for msg in chat_db.get_messages(uid, limit=10, plugin_id=plugin_id)])) - response, ask_for_nps, memories = execute_graph_chat(uid, messages, app, cited=True, chat_session=chat_session) # plugin + response, ask_for_nps, memories = execute_graph_chat(uid, messages, app, cited=True, + chat_session=chat_session) # plugin # cited extraction cited_memory_idxs = {int(i) for i in re.findall(r'\[(\d+)\]', response)} @@ -243,14 +246,12 @@ def send_message_v1( chat_session_id=chat_session.id, ) - chat_db.add_message(uid, ai_message.dict()) chat_db.add_message_to_chat_session(uid, chat_session.id, ai_message.id) ai_message.memories = memories if len(memories) < 5 else memories[:5] if app_id: record_app_usage(uid, app_id, UsageHistoryType.chat_message_sent, message_id=ai_message.id) - resp = ai_message.dict() resp['ask_for_nps'] = ask_for_nps return resp @@ -274,11 +275,10 @@ async def send_message_with_file( @router.delete('/v1/messages', tags=['chat'], response_model=Message) def clear_chat_messages(plugin_id: Optional[str] = None, uid: str = Depends(auth.get_current_user_uid)): - if plugin_id in ['null', '']: plugin_id = None - # get current chat session + # get current chat session chat_session = chat_db.get_chat_session(uid, plugin_id=plugin_id) chat_session_id = chat_session['id'] if chat_session else None @@ -353,7 +353,8 @@ def get_messages(plugin_id: Optional[str] = None, uid: str = Depends(auth.get_cu chat_session = chat_db.get_chat_session(uid, plugin_id=plugin_id) chat_session_id = chat_session['id'] if chat_session else None - messages = chat_db.get_messages(uid, limit=100, include_memories=True, plugin_id=plugin_id, chat_session_id=chat_session_id) + messages = chat_db.get_messages(uid, limit=100, include_memories=True, plugin_id=plugin_id, + chat_session_id=chat_session_id) print('get_messages', len(messages), plugin_id) if not messages: return [initial_message_util(uid, plugin_id)] @@ -406,14 +407,15 @@ async def generate_stream(): media_type="text/event-stream" ) + @router.post('/v1/files', response_model=List[FileChat], tags=['chat']) -def upload_file_chat(files: List[UploadFile] = File(...) , uid: str = Depends(auth.get_current_user_uid)): +def upload_file_chat(files: List[UploadFile] = File(...), uid: str = Depends(auth.get_current_user_uid)): thumbs_name = [] files_chat = [] for file in files: temp_file = Path(f"{file.filename}") with temp_file.open("wb") as buffer: - shutil.copyfileobj(file.file, buffer) + shutil.copyfileobj(file.file, buffer) fc_tool = FileChatTool() result = fc_tool.upload(temp_file) @@ -435,19 +437,17 @@ def upload_file_chat(files: List[UploadFile] = File(...) , uid: str = Depends(au # cleanup temp_file temp_file.unlink() - if len(thumbs_name) > 0: thumbs_path = storage.upload_multi_chat_files(thumbs_name, uid) for fc in files_chat: if not fc.is_image(): continue - thumb_path = thumbs_path.get(fc.thumb_name , "") + thumb_path = thumbs_path.get(fc.thumb_name, "") fc.thumbnail = thumb_path # cleanup file thumb thumb_file = Path(fc.thumb_name) thumb_file.unlink() - # save db files_chat_dict = [fc.dict() for fc in files_chat] diff --git a/backend/utils/apps.py b/backend/utils/apps.py index 48b90f7982..2b5f6404d3 100644 --- a/backend/utils/apps.py +++ b/backend/utils/apps.py @@ -8,7 +8,10 @@ get_app_usage_count_db, get_app_memory_created_integration_usage_count_db, get_app_memory_prompt_usage_count_db, \ add_tester_db, add_app_access_for_tester_db, remove_app_access_for_tester_db, remove_tester_db, \ is_tester_db, can_tester_access_app_db, get_apps_for_tester_db, get_app_chat_message_sent_usage_count_db, \ - update_app_in_db, get_audio_apps_count + update_app_in_db, get_audio_apps_count, get_persona_by_uid_db, update_persona_in_db, add_persona_to_db +from database.auth import get_user_name +from database.facts import get_facts +from database.memories import get_memories from database.redis_db import get_enabled_plugins, get_plugin_reviews, get_generic_cache, \ set_generic_cache, set_app_usage_history_cache, get_app_usage_history_cache, get_app_money_made_cache, \ set_app_money_made_cache, get_plugins_installs_count, get_plugins_reviews, get_app_cache_by_id, set_app_cache_by_id, \ @@ -16,7 +19,9 @@ set_app_usage_count_cache, set_user_paid_app, get_user_paid_app from database.users import get_stripe_connect_account_id from models.app import App, UsageHistoryItem, UsageHistoryType +from models.memory import Memory from utils import stripe +from utils.llm import condense_conversations, condense_facts, generate_persona_description MarketplaceAppReviewUIDs = os.getenv('MARKETPLACE_APP_REVIEWERS').split(',') if os.getenv( 'MARKETPLACE_APP_REVIEWERS') else [] @@ -348,7 +353,105 @@ def is_audio_bytes_app_enabled(uid: str): limit = 30 enabled_apps = list(set(enabled_apps)) for i in range(0, len(enabled_apps), limit): - audio_apps_count = get_audio_apps_count(enabled_apps[i:i+limit]) + audio_apps_count = get_audio_apps_count(enabled_apps[i:i + limit]) if audio_apps_count > 0: return True return False + + +def generate_persona_prompt(uid: str): + """Generate a persona prompt based on user facts and memories.""" + facts = get_facts(uid, limit=1000) + user_name = get_user_name(uid) + + memories = get_memories(uid, limit=100) + conversation_history = Memory.memories_to_string(memories) + conversation_history = condense_conversations([conversation_history]) + + persona_prompt = f"""You are {user_name} AI, you must personify {user_name} as well as you can. + + Style: + - Interactive highly opinionated messages and questions. + - Reference your personal facts naturally + + Rules: + - Never mention being AI + - No ** or formal language + - 2-3 sentences for response. Never more than 20 words. + - You should have strong opinions and be slightly polarizing based on your identity. + - You must make the conversation flow. You excel at coherent conversations. + + Personal Facts and Context:""" + + # Add user facts to the prompt + facts_text = condense_facts([fact['content'] for fact in facts if not fact['deleted']], user_name) + persona_prompt += f"\n{facts_text}" + + # Add conversation history + persona_prompt += "\n\nRecent Conversations:\n" + conversation_history + + # Add final instructions + persona_prompt += "\n\nUse these facts and conversations to shape your personality. Responses should help the conversation go deeper. You have strong opinions, slightly controversial." + + return persona_prompt + + +def update_persona_prompt(uid: str): + """Update a persona's chat prompt with latest facts and memories.""" + # Get latest facts and user info + facts = get_facts(uid, limit=1000) + user_name = get_user_name(uid) + + # Get and condense recent memories + memories = get_memories(uid, limit=100) + conversation_history = Memory.memories_to_string(memories) + conversation_history = condense_conversations([conversation_history]) + + # Condense facts + facts_text = condense_facts([fact['content'] for fact in facts if not fact['deleted']], user_name) + + # Generate updated chat prompt + persona_prompt = f""" +You are {user_name} AI. Your objective is to personify {user_name} as accurately as possible for 1:1 cloning. + +You have: +- **Condensed Facts:** Core identity, personality, values, behavioral patterns, preferences, and continuity for ongoing projects or relationships. +- **Condensed Context from Recent Conversations:** Key themes, communication nuances, emotional undertones, decision-making patterns, and conversational flow. + +**Style:** +- Highly interactive and opinionated. +- Strong, slightly polarizing opinions based on {user_name}’s identity. +- Reference personal facts naturally. +- 2-3 sentence responses, never more than 20 words. +- Maintain conversational flow with coherent, context-aware replies. + +**Guidelines:** +1. Seamlessly integrate facts and recent contexts for continuity, recency, and relevance. +2. Use the condensed facts as foundational knowledge and ongoing continuity. +3. Leverage recent conversations to maintain situational awareness and contextual accuracy. +4. Emulate {user_name}’s humor, emotional responses, vocabulary, and tone. +5. Respond with the appropriate level of detail and complexity as {user_name} would. +6. Exhibit the same thought processes, decision-making patterns, and conversational dynamics. + +**Rules:** +- Never mention being AI. +- No ** or formal language. +- No unnecessary text or explanations. +- Maintain the illusion of continuity and authenticity at all times. +- Every response must be indistinguishable from how {user_name} would naturally respond. + +You have all the necessary condensed facts and contextual knowledge. Begin personifying {user_name} now. + + +Personal Facts and Context: +{facts_text} + +Recent Conversations: +{conversation_history} + +Use these facts and conversations to shape your personality. Responses should help the conversation go deeper. You have strong opinions, slightly controversial.""" + + persona = get_persona_by_uid_db(uid) + persona['persona_prompt'] = persona_prompt + persona['updated_at'] = datetime.utcnow() + update_persona_in_db(persona) diff --git a/backend/utils/memories/process_memory.py b/backend/utils/memories/process_memory.py index 00169440a3..1e5f729e32 100644 --- a/backend/utils/memories/process_memory.py +++ b/backend/utils/memories/process_memory.py @@ -21,14 +21,13 @@ from models.task import Task, TaskStatus, TaskAction, TaskActionProvider from models.trend import Trend from models.notification_message import NotificationMessage -from utils.apps import get_available_apps +from utils.apps import get_available_apps, update_persona_prompt from utils.llm import obtain_emotional_message, retrieve_metadata_fields_from_transcript from utils.llm import summarize_open_glass, get_transcript_structure, generate_embedding, \ get_plugin_result, should_discard_memory, summarize_experience_text, new_facts_extractor, \ trends_extractor from utils.notifications import send_notification from utils.other.hume import get_hume, HumeJobCallbackModel, HumeJobModelPredictionResponseModel -from utils.persona import update_persona_prompt from utils.retrieval.rag import retrieve_rag_memory_context from utils.webhooks import memory_created_webhook @@ -197,7 +196,7 @@ def process_memory( persona = get_persona_by_uid_db(uid) print('updating persona after memory creation') if persona: - update_persona_prompt(uid, persona['id']) + update_persona_prompt(uid) delete_app_cache_by_id(persona['id']) # TODO: trigger external integrations here too diff --git a/backend/utils/openrouter.py b/backend/utils/openrouter.py index c37840c745..0e837cd224 100644 --- a/backend/utils/openrouter.py +++ b/backend/utils/openrouter.py @@ -4,7 +4,7 @@ from openai import AsyncClient from models.app import App -from models.chat import Message +from models.chat import Message, ChatSession openrouter_key = os.getenv('OPENROUTER_API_KEY') @@ -15,10 +15,10 @@ async def execute_persona_chat_stream(uid: str, messages: List[Message], app: App, cited: Optional[bool] = False, - callback_data: dict = None): + callback_data: dict = None, chat_session: Optional[ChatSession] = None): """Handle streaming chat responses for persona-type apps using OpenRouter.""" - system_prompt = app.chat_prompt + system_prompt = app.persona_prompt formatted_messages = [{ "role": "system", "content": system_prompt @@ -36,7 +36,7 @@ async def execute_persona_chat_stream(uid: str, messages: List[Message], app: Ap try: stream = await client.chat.completions.create( messages=formatted_messages, - model="google/gemini-flash-1.5-8b", + model="gpt-4o", stream=True ) From cd12b1f060b7b03df33e4be887342e4e2d169f76 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sun, 16 Feb 2025 00:40:54 +0530 Subject: [PATCH 09/18] improve prompts and cleanup --- app/lib/pages/apps/app_detail/app_detail.dart | 15 +++- backend/main.py | 4 +- backend/routers/persona.py | 86 ------------------- backend/utils/llm.py | 79 +++++++++++------ backend/utils/persona.py | 52 ----------- 5 files changed, 64 insertions(+), 172 deletions(-) delete mode 100644 backend/routers/persona.py delete mode 100644 backend/utils/persona.py diff --git a/app/lib/pages/apps/app_detail/app_detail.dart b/app/lib/pages/apps/app_detail/app_detail.dart index 860dd4a134..5bc87f38d0 100644 --- a/app/lib/pages/apps/app_detail/app_detail.dart +++ b/app/lib/pages/apps/app_detail/app_detail.dart @@ -202,10 +202,17 @@ class _AppDetailPageState extends State { child: const Icon(Icons.share), onTap: () { MixpanelManager().track('App Shared', properties: {'appId': app.id}); - Share.share( - 'Check out this app on Omi AI: ${app.name} by ${app.author} \n\n${app.description.decodeString}\n\n\nhttps://h.omi.me/apps/${app.id}', - subject: app.name, - ); + if (app.isNotPersona()) { + Share.share( + 'Check out this app on Omi AI: ${app.name} by ${app.author} \n\n${app.description.decodeString}\n\n\nhttps://h.omi.me/apps/${app.id}', + subject: app.name, + ); + } else { + Share.share( + 'Check out this Persona on Omi AI: ${app.name} by ${app.author} \n\n${app.description.decodeString}\n\n\nhttps://persona.omi.me/u/${app.username}', + subject: app.name, + ); + } }, ), !context.watch().isAppOwner diff --git a/backend/main.py b/backend/main.py index c7436eb592..c2d090b873 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,7 +6,7 @@ from modal import Image, App, asgi_app, Secret from routers import workflow, chat, firmware, plugins, memories, transcribe_v2, notifications, \ - speech_profile, agents, facts, users, processing_memories, trends, sdcard, sync, apps, custom_auth, payment, persona + speech_profile, agents, facts, users, processing_memories, trends, sdcard, sync, apps, custom_auth, payment if os.environ.get('SERVICE_ACCOUNT_JSON'): service_account_info = json.loads(os.environ["SERVICE_ACCOUNT_JSON"]) @@ -40,8 +40,6 @@ app.include_router(payment.router) -app.include_router(persona.router) - modal_app = App( name='backend', secrets=[Secret.from_name("gcp-credentials"), Secret.from_name('envs')], diff --git a/backend/routers/persona.py b/backend/routers/persona.py deleted file mode 100644 index 7511537fd0..0000000000 --- a/backend/routers/persona.py +++ /dev/null @@ -1,86 +0,0 @@ -import json -import os -from fastapi import APIRouter, Depends, Form, UploadFile, File -from datetime import datetime -from ulid import ULID - -from database.apps import add_persona_to_db -from database.facts import get_facts -from database.auth import get_user_name -from database.memories import get_memories -from models.memory import Memory -from utils.llm import condense_conversations, condense_facts, generate_persona_description -from utils.other import endpoints as auth -from utils.other.storage import upload_plugin_logo - -router = APIRouter() - - -@router.post("/v1/persona") -async def create_persona(persona_data: str = Form(...), file: UploadFile = File(...), - uid=Depends(auth.get_current_user_uid)): - data = json.loads(persona_data) - facts = get_facts(uid, limit=1000) - user_name = get_user_name(uid) - - memories = get_memories(uid, limit=100) - conversation_history = Memory.memories_to_string(memories) - conversation_history = condense_conversations([conversation_history]) - - chat_prompt = f"""You are {user_name} AI, you must personify {user_name} as well as you can. - -Style: -- Interactive highly opinionated messages and questions. -- Reference your personal facts naturally - -Rules: -- Never mention being AI -- No ** or formal language -- 2-3 sentences for response. Never more than 20 words. -- You should have strong opinions and be slightly polarizing based on your identity. -- You must make the conversation flow. You excel at coherent conversations. - -Personal Facts and Context:""" - - # Add user facts to the prompt - facts_text = condense_facts([fact['content'] for fact in facts if not fact['deleted']]) - chat_prompt += f"\n{facts_text}" - - # Add conversation history - chat_prompt += "\n\nRecent Conversations:\n" + conversation_history - - # Add final instructions - chat_prompt += "\n\nUse these facts and conversations to shape your personality. Responses should help the conversation go deeper. You have strong opinions, slightly controversial." - - # Generate persona description - description = generate_persona_description(facts_text) - - # Upload image to omi_plugins_bucket - os.makedirs(f'_temp/plugins', exist_ok=True) - file_path = f"_temp/plugins/{file.filename}" - with open(file_path, 'wb') as f: - f.write(file.file.read()) - - persona_id = str(ULID()) - img_url = upload_plugin_logo(file_path, persona_id) - - doc_data = { - 'approved': False, - 'author': data['author'], - 'capabilities': ["chat", "persona"], - 'category': 'persona', - 'chat_prompt': chat_prompt, - 'connected_accounts': ["omi"], - 'created_at': datetime.now().isoformat(), - 'deleted': False, - 'description': description, - 'email': data['email'], - 'id': persona_id, - 'image': img_url, - 'name': user_name, - 'private': data['private'], - 'status': 'under-review', - 'uid': uid - } - add_persona_to_db(doc_data) - return {'success': True, 'message': 'Persona created successfully.'} diff --git a/backend/utils/llm.py b/backend/utils/llm.py index 9085f4c570..721655987f 100644 --- a/backend/utils/llm.py +++ b/backend/utils/llm.py @@ -1963,21 +1963,32 @@ def generate_description(app_name: str, description: str) -> str: # ******************* PERSONA ********************** # ************************************************** -def condense_facts(facts): +def condense_facts(facts, name): combined_facts = "\n".join(facts) - prompt = f"""You are an expert in personality analysis and information preservation. Your task is to condense the following facts about a person while ensuring their complete personality traits and characteristics are preserved: - {combined_facts} - -Instructions: -- Preserve ALL information that defines the person's unique traits, preferences, and behaviors -- Group semantically related facts together to maintain context -- Merge overlapping or redundant facts while retaining subtle nuances -- Prioritize facts that showcase distinctive personality characteristics -- Maintain chronological or causal relationships between facts -- Keep the tone natural and conversational -- Ensure the condensed version could recreate the exact same persona - -The output will be used to create an AI persona that perfectly mirrors this individual, so no unique trait or characteristic can be lost in the condensing process. + prompt = f""" +You are an AI tasked with condensing a detailed profile of hundreds facts about {name} to accurately replicate their personality, communication style, decision-making patterns, and contextual knowledge for 1:1 cloning. + +**Requirements:** +1. Prioritize facts based on: + - Relevance to the user’s core identity, personality, and communication style. + - Frequency of occurrence or mention in conversations. + - Impact on decision-making processes and behavioral patterns. +2. Group related facts to eliminate redundancy while preserving context. +3. Preserve nuances in communication style, humor, tone, and preferences. +4. Retain facts essential for continuity in ongoing projects, interests, and relationships. +5. Discard trivial details, repetitive information, and rarely mentioned facts. +6. Maintain consistency in the user’s thought processes, conversational flow, and emotional responses. + +**Output Format (No Extra Text):** +- **Core Identity and Personality:** Brief overview encapsulating the user’s personality, values, and communication style. +- **Prioritized Facts:** Organized into categories with only the most relevant and impactful details. +- **Behavioral Patterns and Decision-Making:** Key patterns defining how the user approaches problems and makes decisions. +- **Contextual Knowledge and Continuity:** Facts crucial for maintaining continuity in conversations and ongoing projects. + +The output must be as concise as possible while retaining all necessary information for 1:1 cloning. Absolutely no introductory or closing statements, explanations, or any unnecessary text. Directly present the condensed facts in the specified format. Begin condensation now. + +Facts: +{combined_facts} """ response = llm_medium.invoke(prompt) return response.content @@ -1998,19 +2009,33 @@ def generate_persona_description(facts): def condense_conversations(conversations): combined_conversations = "\n".join(conversations) - prompt = f"""You are an expert in conversation analysis and personality preservation. Your task is to condense the following conversations while preserving the essential context, communication style, and personality traits: - {combined_conversations} - -Instructions: -- Preserve ALL information that defines the person's communication style and personality -- Maintain key discussion topics and viewpoints expressed -- Capture recurring patterns in how they interact and respond -- Keep important contextual details and relationships mentioned -- Retain their unique voice, vocabulary, and expression style -- Group related conversations to maintain context flow -- Ensure the condensed version accurately represents their conversational identity - -The output will be used to create an AI persona that mirrors this individual's conversation style, so no key personality trait or communication pattern can be lost in the condensing process. + prompt = f""" +You are an AI tasked with condensing context from the recent 50 conversations of a user to accurately replicate their communication style, personality, decision-making patterns, and contextual knowledge for 1:1 cloning. Each conversation includes a summary and a full transcript. + +**Requirements:** +1. Prioritize information based on: + - Most impactful and frequently occurring themes, topics, and interests. + - Nuances in communication style, humor, tone, and emotional undertones. + - Decision-making patterns and problem-solving approaches. + - User preferences in conversation flow, level of detail, and type of responses. +2. Condense redundant or repetitive information while maintaining necessary context. +3. Group related contexts to enhance conciseness and preserve continuity. +4. Retain patterns in how the user reacts to different situations, questions, or challenges. +5. Preserve continuity for ongoing discussions, projects, or relationships. +6. Maintain consistency in the user's thought processes, conversational flow, and emotional responses. +7. Eliminate any trivial details or low-impact information. + +**Output Format (No Extra Text):** +- **Condensed Communication Style and Tone:** Key nuances in tone, humor, and emotional undertones. +- **Condensed Recurring Themes and Interests:** Most impactful and frequently discussed topics or interests. +- **Condensed Decision-Making and Problem-Solving Patterns:** Core insights into decision-making approaches. +- **Condensed Conversational Flow and Preferences:** Preferred conversation style, response length, and level of detail. +- **Condensed Contextual Continuity:** Essential facts for maintaining continuity in ongoing discussions, projects, or relationships. + +The output must be as concise as possible while retaining all necessary context for 1:1 cloning. Absolutely no introductory or closing statements, explanations, or any unnecessary text. Directly present the condensed context in the specified format. Begin now. + +Conversations: +{combined_conversations} """ response = llm_medium.invoke(prompt) return response.content diff --git a/backend/utils/persona.py b/backend/utils/persona.py deleted file mode 100644 index 5656b15d15..0000000000 --- a/backend/utils/persona.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -from datetime import datetime - -from database.apps import update_persona_in_db, get_persona_by_uid_db -from database.facts import get_facts -from database.auth import get_user_name -from database.memories import get_memories -from database._client import db -from models.memory import Memory -from routers.persona import condense_facts, condense_conversations - - -def update_persona_prompt(uid: str, doc_id: str): - """Update a persona's chat prompt with latest facts and memories.""" - # Get latest facts and user info - facts = get_facts(uid, limit=1000) - user_name = get_user_name(uid) - - # Get and condense recent memories - memories = get_memories(uid, limit=100) - conversation_history = Memory.memories_to_string(memories) - conversation_history = condense_conversations([conversation_history]) - - # Condense facts - facts_text = condense_facts([fact['content'] for fact in facts if not fact['deleted']]) - - # Generate updated chat prompt - chat_prompt = f"""You are {user_name} AI, you must personify {user_name} as well as you can. - -Style: -- Interactive highly opinionated messages and questions. -- Reference your personal facts naturally - -Rules: -- Never mention being AI -- No ** or formal language -- 2-3 sentences for response. Never more than 20 words. -- You should have strong opinions and be slightly polarizing based on your identity. -- You must make the conversation flow. You excel at coherent conversations. - -Personal Facts and Context: -{facts_text} - -Recent Conversations: -{conversation_history} - -Use these facts and conversations to shape your personality. Responses should help the conversation go deeper. You have strong opinions, slightly controversial.""" - - persona = get_persona_by_uid_db(uid) - persona['chat_prompt'] = chat_prompt - persona['updated_at'] = datetime.utcnow() - update_persona_in_db(persona) \ No newline at end of file From 1e7790b84e5774827d26d6c41c7b737c8523ee0b Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sun, 16 Feb 2025 00:41:37 +0530 Subject: [PATCH 10/18] support for username based handling on the web personas --- .../src/app/api/chat/route.ts | 2 +- personas-open-source/src/app/chat/page.tsx | 6 +- .../src/app/u/[username]/page.tsx | 83 +++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 personas-open-source/src/app/u/[username]/page.tsx diff --git a/personas-open-source/src/app/api/chat/route.ts b/personas-open-source/src/app/api/chat/route.ts index 91156a99b7..60cf8c2c20 100644 --- a/personas-open-source/src/app/api/chat/route.ts +++ b/personas-open-source/src/app/api/chat/route.ts @@ -26,7 +26,7 @@ export async function POST(req: Request) { const botDoc = await getDoc(doc(db, 'plugins_data', botId)); if (botDoc.exists()) { const bot = botDoc.data(); - chatPrompt = bot.chat_prompt; + chatPrompt = bot.chat_prompt ?? bot.persona_prompt; isInfluencer = bot.is_influencer ?? false; } } catch (error) { diff --git a/personas-open-source/src/app/chat/page.tsx b/personas-open-source/src/app/chat/page.tsx index 0c107963fc..96bef85ccd 100644 --- a/personas-open-source/src/app/chat/page.tsx +++ b/personas-open-source/src/app/chat/page.tsx @@ -34,6 +34,7 @@ function ChatContent() { const [botData, setBotData] = useState<{ name: string; avatar: string; + image?: string; username?: string; } | null>(null); @@ -61,7 +62,8 @@ function ChatContent() { setBotData({ name: data.name, avatar: data.avatar, - username: data.username + username: data.username, + image: data.image }); } } catch (error) { @@ -74,7 +76,7 @@ function ChatContent() { // Use the fetched data const botName = botData?.name || 'Omi'; - const botImage = botData?.avatar || '/omi-avatar.svg'; + const botImage = botData?.avatar || botData?.image || '/omi-avatar.svg'; const username = botData?.username || ''; // Function to save messages to Firebase diff --git a/personas-open-source/src/app/u/[username]/page.tsx b/personas-open-source/src/app/u/[username]/page.tsx new file mode 100644 index 0000000000..f70d9974dc --- /dev/null +++ b/personas-open-source/src/app/u/[username]/page.tsx @@ -0,0 +1,83 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { collection, query, where, getDocs } from 'firebase/firestore'; +import { db } from '@/lib/firebase'; +import { Button } from '@/components/ui/button'; +import { ArrowLeft } from 'lucide-react'; +import Link from 'next/link'; +import { use } from 'react'; + +export default function UsernamePage({ params }: { params: Promise<{ username: string }> }) { + const { username } = use(params); + const router = useRouter(); + const [error, setError] = useState<'not_found' | 'private' | null>(null); + + useEffect(() => { + const fetchBotByUsername = async () => { + try { + const q = query( + collection(db, 'plugins_data'), + where('username', '==', username.toLowerCase()) + ); + const querySnapshot = await getDocs(q); + + if (!querySnapshot.empty) { + const botDoc = querySnapshot.docs[0]; + const botData = botDoc.data(); + + if (botData.private) { + setError('private'); + } else { + router.replace(`/chat?id=${botDoc.id}`); + } + } else { + setError('not_found'); + } + } catch (error) { + console.error('Error fetching bot by username:', error); + setError('not_found'); + } + }; + + fetchBotByUsername(); + }, [username, router]); + + if (!error) return null; + + return ( +
+
+ + + +
+ +
+

omi

+ {error === 'not_found' ? ( + <> +

This persona does not exist

+

The persona you're looking for could not be found.

+ + ) : ( + <> +

This persona is private

+

You don't have access to view this persona.

+ + )} + + + + +
+
+ ); +} \ No newline at end of file From bcbff910903dac2cd6f5c2d03351f4bf56ae4882 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sun, 16 Feb 2025 23:49:44 +0530 Subject: [PATCH 11/18] cleanup and attempt at streaming --- app/lib/backend/http/api/apps.dart | 2 +- backend/routers/apps.py | 20 ++++++++++ backend/routers/chat.py | 10 ++--- backend/utils/llm.py | 51 ++++++++++++++++++++++++- backend/utils/openrouter.py | 61 ------------------------------ 5 files changed, 74 insertions(+), 70 deletions(-) delete mode 100644 backend/utils/openrouter.py diff --git a/app/lib/backend/http/api/apps.dart b/app/lib/backend/http/api/apps.dart index 64d1cd8cea..c562af5137 100644 --- a/app/lib/backend/http/api/apps.dart +++ b/app/lib/backend/http/api/apps.dart @@ -270,7 +270,7 @@ Future> getAppCategories() async { Future> getAppCapabilitiesServer() async { var response = await makeApiCall( - url: '${Env.apiBaseUrl}v1/app-capabilities', + url: '${Env.apiBaseUrl}v2/app-capabilities', headers: {}, body: '', method: 'GET', diff --git a/backend/routers/apps.py b/backend/routers/apps.py index 82144795cb..1bad318725 100644 --- a/backend/routers/apps.py +++ b/backend/routers/apps.py @@ -314,6 +314,26 @@ def get_plugin_capabilities(): ] +@router.get('/v2/app-capabilities', tags=['v2']) +def get_plugin_capabilities(): + return [ + {'title': 'Chat', 'id': 'chat'}, + {'title': 'Memories', 'id': 'memories'}, + {'title': 'Persona', 'id': 'persona'}, + {'title': 'External Integration', 'id': 'external_integration', 'triggers': [ + {'title': 'Audio Bytes', 'id': 'audio_bytes'}, + {'title': 'Memory Creation', 'id': 'memory_creation'}, + {'title': 'Transcript Processed', 'id': 'transcript_processed'}, + ]}, + {'title': 'Notification', 'id': 'proactive_notification', 'scopes': [ + {'title': 'User Name', 'id': 'user_name'}, + {'title': 'User Facts', 'id': 'user_facts'}, + {'title': 'User Memories', 'id': 'user_context'}, + {'title': 'User Chat', 'id': 'user_chat'} + ]} + ] + + # @deprecated @router.get('/v1/app/payment-plans', tags=['v1']) def get_payment_plans_v1(): diff --git a/backend/routers/chat.py b/backend/routers/chat.py index 66253a9d4e..758641e7a3 100644 --- a/backend/routers/chat.py +++ b/backend/routers/chat.py @@ -19,8 +19,7 @@ from routers.sync import retrieve_file_paths, decode_files_to_wav, retrieve_vad_segments from utils.apps import get_available_app_by_id from utils.chat import process_voice_message_segment, process_voice_message_segment_stream -from utils.llm import initial_chat_message -from utils.openrouter import execute_persona_chat_stream +from utils.llm import initial_chat_message, execute_persona_chat_stream from utils.other import endpoints as auth, storage from utils.other.chat_file import FileChatTool from utils.retrieval.graph import execute_graph_chat, execute_graph_chat_stream @@ -137,9 +136,7 @@ def process_message(response: str, callback_data: dict): async def generate_stream(): callback_data = {} stream_function = execute_persona_chat_stream if app and app.is_a_persona() else execute_graph_chat_stream - async for chunk in stream_function(uid, messages, app, cited=True, callback_data=callback_data, - chat_session=chat_session): - # async for chunk in execute_graph_chat_stream(uid, messages, app, cited=True, callback_data=callback_data, chat_session=chat_session): + async for chunk in stream_function(uid, messages, app, cited=True, callback_data=callback_data, chat_session=chat_session): if chunk: msg = chunk.replace("\n", "__CRLF__") yield f'{msg}\n\n' @@ -196,8 +193,7 @@ def send_message_v1( messages = list(reversed([Message(**msg) for msg in chat_db.get_messages(uid, limit=10, plugin_id=plugin_id)])) - response, ask_for_nps, memories = execute_graph_chat(uid, messages, app, cited=True, - chat_session=chat_session) # plugin + response, ask_for_nps, memories = execute_graph_chat(uid, messages, app, cited=True) # plugin # cited extraction cited_memory_idxs = {int(i) for i in re.findall(r'\[(\d+)\]', response)} diff --git a/backend/utils/llm.py b/backend/utils/llm.py index 721655987f..1a5b525312 100644 --- a/backend/utils/llm.py +++ b/backend/utils/llm.py @@ -2,9 +2,10 @@ import re import asyncio from datetime import datetime, timezone -from typing import List, Optional +from typing import List, Optional, AsyncGenerator import tiktoken +from langchain_core.messages import AIMessage, SystemMessage, HumanMessage from langchain_core.output_parsers import PydanticOutputParser from langchain_core.prompts import ChatPromptTemplate from langchain_core.prompt_values import StringPromptValue @@ -1181,6 +1182,7 @@ class Facts(BaseModel): ) + def new_facts_extractor( uid: str, segments: List[TranscriptSegment], user_name: Optional[str] = None, facts_str: Optional[str] = None ) -> List[Fact]: @@ -2039,3 +2041,50 @@ def condense_conversations(conversations): """ response = llm_medium.invoke(prompt) return response.content + + +async def execute_persona_chat_stream( + uid: str, messages: List[Message], app: App, cited: Optional[bool] = False, + callback_data: dict = None, chat_session: Optional[str] = None +) -> AsyncGenerator[str, None]: + """Handle streaming chat responses for persona-type apps""" + + system_prompt = app.persona_prompt + formatted_messages = [SystemMessage(content=system_prompt)] + + for msg in messages: + if msg.sender == "ai": + formatted_messages.append(AIMessage(content=msg.text)) + else: + formatted_messages.append(HumanMessage(content=msg.text)) + + full_response = [] + + async def stream_tokens(): + + def get_tokens(): + for token in llm_medium_stream.stream(formatted_messages): + yield token.content + + for token in get_tokens(): + yield token + + try: + async for token in stream_tokens(): + full_response.append(token) + yield f"data: {token}\n\n" + + if callback_data is not None: + callback_data['answer'] = ''.join(full_response) + callback_data['memories_found'] = [] + callback_data['ask_for_nps'] = False + + yield None + return + + except Exception as e: + print(f"Error in execute_persona_chat_stream: {e}") + if callback_data is not None: + callback_data['error'] = str(e) + yield None + return \ No newline at end of file diff --git a/backend/utils/openrouter.py b/backend/utils/openrouter.py deleted file mode 100644 index 0e837cd224..0000000000 --- a/backend/utils/openrouter.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -from typing import List, Optional - -from openai import AsyncClient - -from models.app import App -from models.chat import Message, ChatSession - -openrouter_key = os.getenv('OPENROUTER_API_KEY') - -client = AsyncClient( - base_url="https://openrouter.ai/api/v1", - api_key=openrouter_key, -) - - -async def execute_persona_chat_stream(uid: str, messages: List[Message], app: App, cited: Optional[bool] = False, - callback_data: dict = None, chat_session: Optional[ChatSession] = None): - """Handle streaming chat responses for persona-type apps using OpenRouter.""" - - system_prompt = app.persona_prompt - formatted_messages = [{ - "role": "system", - "content": system_prompt - }] - - # Add message history - for msg in messages: - role = "assistant" if msg.sender == "ai" else "user" - formatted_messages.append({"role": role, "content": msg.text}) - - # Track the full response for callback_data - full_response = [] - - # Stream the response - try: - stream = await client.chat.completions.create( - messages=formatted_messages, - model="gpt-4o", - stream=True - ) - - async for chunk in stream: - if chunk.choices and chunk.choices[0].delta.content: - content = chunk.choices[0].delta.content - full_response.append(content) - data = content.replace("\n", "__CRLF__") - yield f"data: {data}\n\n" - - # Store final response in callback_data - if callback_data is not None: - callback_data['answer'] = ''.join(full_response) - callback_data['memories_found'] = [] - callback_data['ask_for_nps'] = False - - except Exception as e: - print(f"Error in execute_persona_chat_stream: {e}") - if callback_data is not None: - callback_data['error'] = str(e) - - yield None From cb6c379421b18ede90234cf0f1171027088888bc Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Mon, 17 Feb 2025 00:13:28 +0530 Subject: [PATCH 12/18] allow sharing only for non private apps --- app/lib/pages/apps/app_detail/app_detail.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/pages/apps/app_detail/app_detail.dart b/app/lib/pages/apps/app_detail/app_detail.dart index 5bc87f38d0..84eede67ad 100644 --- a/app/lib/pages/apps/app_detail/app_detail.dart +++ b/app/lib/pages/apps/app_detail/app_detail.dart @@ -196,7 +196,7 @@ class _AppDetailPageState extends State { ), const SizedBox(width: 24), ], - isLoading + isLoading || app.private ? const SizedBox.shrink() : GestureDetector( child: const Icon(Icons.share), From 2679e9acb2f3aeb43ff9ad1844f73ab5910fe60d Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Mon, 17 Feb 2025 00:14:20 +0530 Subject: [PATCH 13/18] update persona handling --- .../apps/providers/add_app_provider.dart | 6 ++ app/lib/pages/apps/update_app.dart | 76 +++++++++++++++++++ backend/routers/apps.py | 15 +++- 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/app/lib/pages/apps/providers/add_app_provider.dart b/app/lib/pages/apps/providers/add_app_provider.dart index 5059f9f953..b55db1f099 100644 --- a/app/lib/pages/apps/providers/add_app_provider.dart +++ b/app/lib/pages/apps/providers/add_app_provider.dart @@ -154,6 +154,9 @@ class AddAppProvider extends ChangeNotifier { selectedScopes = app.getNotificationScopesFromIds( capabilities.firstWhere((element) => element.id == 'proactive_notification').notificationScopes); } + if (app.username != null) { + usernameController.text = app.username!; + } // Set existing thumbnails thumbnailUrls = app.thumbnailUrls; @@ -476,6 +479,9 @@ class AddAppProvider extends ChangeNotifier { } data['proactive_notification']['scopes'] = selectedScopes.map((e) => e.id).toList(); } + if (capability.id == 'persona') { + data['username'] = usernameController.text; + } } var success = false; var res = await updateAppServer(imageFile, data); diff --git a/app/lib/pages/apps/update_app.dart b/app/lib/pages/apps/update_app.dart index 32d50696a7..2c2ed26c05 100644 --- a/app/lib/pages/apps/update_app.dart +++ b/app/lib/pages/apps/update_app.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/services.dart'; +import 'package:friend_private/utils/other/debouncer.dart'; +import 'package:friend_private/utils/text_formatter.dart'; import 'package:shimmer/shimmer.dart'; import 'package:friend_private/backend/schema/app.dart'; import 'package:friend_private/pages/apps/widgets/full_screen_image_viewer.dart'; @@ -23,6 +26,8 @@ class UpdateAppPage extends StatefulWidget { } class _UpdateAppPageState extends State { + final _debouncer = Debouncer(delay: const Duration(milliseconds: 500)); + @override void initState() { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -367,6 +372,77 @@ class _UpdateAppPageState extends State { ), ], ), + if (provider.isCapabilitySelectedById('persona')) + Container( + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12.0), + ), + padding: const EdgeInsets.all(14.0), + margin: const EdgeInsets.only(top: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'Persona Username', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a username to access the persona'; + } + return null; + }, + onChanged: (value) { + _debouncer.run(() async { + await provider.checkIsUsernameTaken(value); + }); + }, + controller: provider.usernameController, + inputFormatters: [ + LowerCaseTextFormatter(), + FilteringTextInputFormatter.allow(RegExp(r'[a-z0-9_]')), + ], + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: 'Enter a username', + suffix: provider.usernameController.text.isEmpty + ? null + : provider.isCheckingUsername + ? const SizedBox( + width: 16, + height: 16, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.grey), + ), + ), + ) + : Icon( + provider.isUsernameTaken ? Icons.close : Icons.check, + color: provider.isUsernameTaken ? Colors.red : Colors.green, + size: 16, + ), + ), + ), + ), + ], + ), + ), const SizedBox( height: 90, ), diff --git a/backend/routers/apps.py b/backend/routers/apps.py index 1bad318725..97e4227e77 100644 --- a/backend/routers/apps.py +++ b/backend/routers/apps.py @@ -12,8 +12,8 @@ from database.auth import get_user_from_uid from database.notifications import get_token_only from database.redis_db import delete_generic_cache, get_specific_user_review, increase_app_installs_count, \ - decrease_app_installs_count, enable_app, disable_app, delete_app_cache_by_id, is_username_taken -from database.users import get_stripe_connect_account_id + decrease_app_installs_count, enable_app, disable_app, delete_app_cache_by_id, is_username_taken, save_username, \ + get_uid_by_username 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, \ @@ -24,7 +24,6 @@ from utils.other import endpoints as auth from models.app import App from utils.other.storage import upload_plugin_logo, delete_plugin_logo, upload_app_thumbnail, get_app_thumbnail_url -from utils.stripe import is_onboarding_complete router = APIRouter() @@ -78,6 +77,7 @@ def create_app(app_data: str = Form(...), file: UploadFile = File(...), uid=Depe else: external_integration['is_instructions_url'] = False if "persona" in data['capabilities']: + save_username(data['username'], uid) data['persona_prompt'] = generate_persona_prompt(uid) data['connected_accounts'] = ['omi'] os.makedirs(f'_temp/plugins', exist_ok=True) @@ -102,7 +102,12 @@ def check_username(username: str, uid: str = Depends(auth.get_current_user_uid)) persona = is_username_taken(username) if persona == 0: return {'is_taken': False} - return {'is_taken': True} + else: + username_owner = get_uid_by_username(username) + if username_owner == uid: + return {'is_taken': False} + else: + return {'is_taken': True} @router.patch('/v1/apps/{app_id}', tags=['v1']) @@ -122,6 +127,8 @@ def update_app(app_id: str, app_data: str = Form(...), file: UploadFile = File(N f.write(file.file.read()) img_url = upload_plugin_logo(file_path, app_id) data['image'] = img_url + if "persona" in data['capabilities']: + save_username(data['username'], uid) data['updated_at'] = datetime.now(timezone.utc) # Warn: the user can update any fields, e.g. approved. update_app_in_db(data) From 6aecb8416867787eca7702f08e4c1c3c72076010 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Tue, 18 Feb 2025 16:47:43 +0530 Subject: [PATCH 14/18] fix persona streaming --- backend/utils/llm.py | 55 ++++-------------------------- backend/utils/retrieval/graph.py | 57 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 49 deletions(-) diff --git a/backend/utils/llm.py b/backend/utils/llm.py index 1a5b525312..ef19fb24e6 100644 --- a/backend/utils/llm.py +++ b/backend/utils/llm.py @@ -1972,17 +1972,17 @@ def condense_facts(facts, name): **Requirements:** 1. Prioritize facts based on: - - Relevance to the user’s core identity, personality, and communication style. + - Relevance to the user's core identity, personality, and communication style. - Frequency of occurrence or mention in conversations. - Impact on decision-making processes and behavioral patterns. 2. Group related facts to eliminate redundancy while preserving context. 3. Preserve nuances in communication style, humor, tone, and preferences. 4. Retain facts essential for continuity in ongoing projects, interests, and relationships. 5. Discard trivial details, repetitive information, and rarely mentioned facts. -6. Maintain consistency in the user’s thought processes, conversational flow, and emotional responses. +6. Maintain consistency in the user's thought processes, conversational flow, and emotional responses. **Output Format (No Extra Text):** -- **Core Identity and Personality:** Brief overview encapsulating the user’s personality, values, and communication style. +- **Core Identity and Personality:** Brief overview encapsulating the user's personality, values, and communication style. - **Prioritized Facts:** Organized into categories with only the most relevant and impactful details. - **Behavioral Patterns and Decision-Making:** Key patterns defining how the user approaches problems and makes decisions. - **Contextual Knowledge and Continuity:** Facts crucial for maintaining continuity in conversations and ongoing projects. @@ -1996,8 +1996,10 @@ def condense_facts(facts, name): return response.content -def generate_persona_description(facts): +def generate_persona_description(facts, name): prompt = f"""Based on these facts about a person, create a concise, engaging description that captures their unique personality and characteristics (max 250 characters). + + They chose to be known as {name}. Facts: {facts} @@ -2043,48 +2045,3 @@ def condense_conversations(conversations): return response.content -async def execute_persona_chat_stream( - uid: str, messages: List[Message], app: App, cited: Optional[bool] = False, - callback_data: dict = None, chat_session: Optional[str] = None -) -> AsyncGenerator[str, None]: - """Handle streaming chat responses for persona-type apps""" - - system_prompt = app.persona_prompt - formatted_messages = [SystemMessage(content=system_prompt)] - - for msg in messages: - if msg.sender == "ai": - formatted_messages.append(AIMessage(content=msg.text)) - else: - formatted_messages.append(HumanMessage(content=msg.text)) - - full_response = [] - - async def stream_tokens(): - - def get_tokens(): - for token in llm_medium_stream.stream(formatted_messages): - yield token.content - - for token in get_tokens(): - yield token - - try: - async for token in stream_tokens(): - full_response.append(token) - yield f"data: {token}\n\n" - - if callback_data is not None: - callback_data['answer'] = ''.join(full_response) - callback_data['memories_found'] = [] - callback_data['ask_for_nps'] = False - - yield None - return - - except Exception as e: - print(f"Error in execute_persona_chat_stream: {e}") - if callback_data is not None: - callback_data['error'] = str(e) - yield None - return \ No newline at end of file diff --git a/backend/utils/retrieval/graph.py b/backend/utils/retrieval/graph.py index 8ff4962c2b..b77ca45e1b 100644 --- a/backend/utils/retrieval/graph.py +++ b/backend/utils/retrieval/graph.py @@ -4,6 +4,7 @@ from typing import List, Optional, Tuple, AsyncGenerator from langchain.callbacks.base import BaseCallbackHandler +from langchain_core.messages import SystemMessage, AIMessage, HumanMessage from langchain_openai import ChatOpenAI from langgraph.checkpoint.memory import MemorySaver from langgraph.constants import END @@ -15,6 +16,7 @@ from database.redis_db import get_filter_category_items from database.vector_db import query_vectors_by_metadata import database.notifications as notification_db +from models.app import App from models.chat import ChatSession, Message from models.memory import Memory from models.plugin import Plugin @@ -39,6 +41,7 @@ from utils.plugins import get_github_docs_content model = ChatOpenAI(model="gpt-4o-mini") +llm_medium_stream = ChatOpenAI(model='gpt-4o', streaming=True) class StructuredFilters(TypedDict): @@ -419,3 +422,57 @@ async def execute_graph_chat_stream( yield None return + + +async def execute_persona_chat_stream( + uid: str, messages: List[Message], app: App, cited: Optional[bool] = False, + callback_data: dict = None, chat_session: Optional[str] = None +) -> AsyncGenerator[str, None]: + """Handle streaming chat responses for persona-type apps""" + + system_prompt = app.persona_prompt + formatted_messages = [SystemMessage(content=system_prompt)] + + for msg in messages: + if msg.sender == "ai": + formatted_messages.append(AIMessage(content=msg.text)) + else: + formatted_messages.append(HumanMessage(content=msg.text)) + + full_response = [] + callback = AsyncStreamingCallback() + + try: + task = asyncio.create_task(llm_medium_stream.agenerate( + messages=[formatted_messages], + callbacks=[callback] + )) + + while True: + try: + chunk = await callback.queue.get() + if chunk: + token = chunk.replace("data: ", "") + full_response.append(token) + yield chunk + else: + break + except asyncio.CancelledError: + break + + await task + + if callback_data is not None: + callback_data['answer'] = ''.join(full_response) + callback_data['memories_found'] = [] + callback_data['ask_for_nps'] = False + + yield None + return + + except Exception as e: + print(f"Error in execute_persona_chat_stream: {e}") + if callback_data is not None: + callback_data['error'] = str(e) + yield None + return \ No newline at end of file From 25b7d2a8333541c7fd5edccfb2c751e92043e7db Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Tue, 18 Feb 2025 16:50:48 +0530 Subject: [PATCH 15/18] separate personas flows --- app/lib/backend/http/api/apps.dart | 32 +- app/lib/main.dart | 2 + app/lib/pages/apps/add_app.dart | 73 --- app/lib/pages/apps/app_detail/app_detail.dart | 7 +- app/lib/pages/apps/explore_install_page.dart | 15 +- .../apps/providers/add_app_provider.dart | 35 +- app/lib/pages/apps/update_app.dart | 76 --- .../apps/widgets/create_options_sheet.dart | 72 +++ .../apps/widgets/show_app_options_sheet.dart | 29 +- app/lib/pages/persona/add_persona.dart | 475 ++++++++++++------ app/lib/pages/persona/persona_provider.dart | 120 +++++ app/lib/pages/persona/update_persona.dart | 246 +++++++++ backend/routers/apps.py | 85 +++- backend/routers/chat.py | 4 +- backend/utils/apps.py | 8 + 15 files changed, 882 insertions(+), 397 deletions(-) create mode 100644 app/lib/pages/apps/widgets/create_options_sheet.dart create mode 100644 app/lib/pages/persona/persona_provider.dart create mode 100644 app/lib/pages/persona/update_persona.dart diff --git a/app/lib/backend/http/api/apps.dart b/app/lib/backend/http/api/apps.dart index c562af5137..89c7e973f1 100644 --- a/app/lib/backend/http/api/apps.dart +++ b/app/lib/backend/http/api/apps.dart @@ -270,7 +270,7 @@ Future> getAppCategories() async { Future> getAppCapabilitiesServer() async { var response = await makeApiCall( - url: '${Env.apiBaseUrl}v2/app-capabilities', + url: '${Env.apiBaseUrl}v1/app-capabilities', headers: {}, body: '', method: 'GET', @@ -418,7 +418,7 @@ Future getGenratedDescription(String name, String description) async { Future createPersonaApp(File file, Map personaData) async { var request = http.MultipartRequest( 'POST', - Uri.parse('${Env.apiBaseUrl}v1/persona'), + Uri.parse('${Env.apiBaseUrl}v1/personas'), ); request.files.add(await http.MultipartFile.fromPath('file', file.path, filename: basename(file.path))); request.headers.addAll({'Authorization': await getAuthHeader()}); @@ -445,6 +445,34 @@ Future createPersonaApp(File file, Map personaData) async } } +Future updatePersonaApp(File? file, Map personaData) async { + var request = http.MultipartRequest( + 'PATCH', + Uri.parse('${Env.apiBaseUrl}v1/personas/${personaData['id']}'), + ); + if (file != null) { + request.files.add(await http.MultipartFile.fromPath('file', file.path, filename: basename(file.path))); + } + request.headers.addAll({'Authorization': await getAuthHeader()}); + request.fields.addAll({'persona_data': jsonEncode(personaData)}); + debugPrint(jsonEncode(personaData)); + try { + var streamedResponse = await request.send(); + var response = await http.Response.fromStream(streamedResponse); + + if (response.statusCode == 200) { + debugPrint('updatePersonaApp Response body: ${jsonDecode(response.body)}'); + return true; + } else { + debugPrint('Failed to update app. Status code: ${response.statusCode}'); + return false; + } + } catch (e) { + debugPrint('An error occurred updatePersonaApp: $e'); + return false; + } +} + Future checkPersonaUsername(String username) async { var response = await makeApiCall( url: '${Env.apiBaseUrl}v1/apps/check-username?username=$username', diff --git a/app/lib/main.dart b/app/lib/main.dart index 110832e988..0671c7cbb7 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -22,6 +22,7 @@ import 'package:friend_private/pages/apps/providers/add_app_provider.dart'; import 'package:friend_private/pages/home/page.dart'; import 'package:friend_private/pages/conversation_detail/conversation_detail_provider.dart'; import 'package:friend_private/pages/onboarding/wrapper.dart'; +import 'package:friend_private/pages/persona/persona_provider.dart'; import 'package:friend_private/providers/app_provider.dart'; import 'package:friend_private/providers/auth_provider.dart'; import 'package:friend_private/providers/calendar_provider.dart'; @@ -205,6 +206,7 @@ class _MyAppState extends State with WidgetsBindingObserver { (previous?..setAppProvider(value)) ?? AddAppProvider(), ), ChangeNotifierProvider(create: (context) => PaymentMethodProvider()), + ChangeNotifierProvider(create: (context) => PersonaProvider()), ], builder: (context, child) { return WithForegroundTask( diff --git a/app/lib/pages/apps/add_app.dart b/app/lib/pages/apps/add_app.dart index 0b5e4ec667..f8c3b133fe 100644 --- a/app/lib/pages/apps/add_app.dart +++ b/app/lib/pages/apps/add_app.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/services.dart'; import 'package:friend_private/utils/other/debouncer.dart'; -import 'package:friend_private/utils/text_formatter.dart'; import 'package:shimmer/shimmer.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/pages/apps/widgets/full_screen_image_viewer.dart'; @@ -373,77 +371,6 @@ class _AddAppPageState extends State { ), ], ), - if (provider.isCapabilitySelectedById('persona')) - Container( - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(12.0), - ), - padding: const EdgeInsets.all(14.0), - margin: const EdgeInsets.only(top: 22), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'Persona Username', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), - margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(10.0), - ), - width: double.infinity, - child: TextFormField( - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a username to access the persona'; - } - return null; - }, - onChanged: (value) { - _debouncer.run(() async { - await provider.checkIsUsernameTaken(value); - }); - }, - controller: provider.usernameController, - inputFormatters: [ - LowerCaseTextFormatter(), - FilteringTextInputFormatter.allow(RegExp(r'[a-z0-9_]')), - ], - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - hintText: 'Enter a username', - suffix: provider.usernameController.text.isEmpty - ? null - : provider.isCheckingUsername - ? const SizedBox( - width: 16, - height: 16, - child: Center( - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.grey), - ), - ), - ) - : Icon( - provider.isUsernameTaken ? Icons.close : Icons.check, - color: provider.isUsernameTaken ? Colors.red : Colors.green, - size: 16, - ), - ), - ), - ), - ], - ), - ), const SizedBox( height: 22, ), diff --git a/app/lib/pages/apps/app_detail/app_detail.dart b/app/lib/pages/apps/app_detail/app_detail.dart index 84eede67ad..4f879f86f0 100644 --- a/app/lib/pages/apps/app_detail/app_detail.dart +++ b/app/lib/pages/apps/app_detail/app_detail.dart @@ -729,10 +729,13 @@ class _AppDetailPageState extends State { onTap: () { if (app.description.decodeString.characters.length > 200) { routeToPage( - context, MarkdownViewer(title: 'About the App', markdown: app.description.decodeString)); + context, + MarkdownViewer( + title: 'About the ${app.isNotPersona() ? 'App' : 'Persona'}', + markdown: app.description.decodeString)); } }, - title: 'About the App', + title: 'About the ${app.isNotPersona() ? 'App' : 'Persona'}', description: app.description, showChips: true, chips: app diff --git a/app/lib/pages/apps/explore_install_page.dart b/app/lib/pages/apps/explore_install_page.dart index 3919f229bf..97fc7fd285 100644 --- a/app/lib/pages/apps/explore_install_page.dart +++ b/app/lib/pages/apps/explore_install_page.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; import 'package:friend_private/backend/schema/app.dart'; -import 'package:friend_private/pages/apps/add_app.dart'; import 'package:friend_private/pages/apps/providers/add_app_provider.dart'; import 'package:friend_private/pages/apps/widgets/app_section_card.dart'; import 'package:friend_private/pages/apps/widgets/filter_sheet.dart'; import 'package:friend_private/pages/apps/list_item.dart'; import 'package:friend_private/providers/app_provider.dart'; import 'package:friend_private/providers/home_provider.dart'; -import 'package:friend_private/utils/analytics/mixpanel.dart'; -import 'package:friend_private/utils/other/temp.dart'; import 'package:provider/provider.dart'; +import 'widgets/create_options_sheet.dart'; + String filterValueToString(dynamic value) { if (value.runtimeType == String) { return value; @@ -190,7 +189,13 @@ class _ExploreInstallPageState extends State with AutomaticK SliverToBoxAdapter( child: GestureDetector( onTap: () async { - routeToPage(context, const AddAppPage()); + showModalBottomSheet( + context: context, + builder: (context) => const CreateOptionsSheet(), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + ); }, child: Container( padding: const EdgeInsets.all(12.0), @@ -207,7 +212,7 @@ class _ExploreInstallPageState extends State with AutomaticK Icon(Icons.add, color: Colors.white), SizedBox(width: 8), Text( - 'Create and Submit App', + 'Create your own', textAlign: TextAlign.center, ), ], diff --git a/app/lib/pages/apps/providers/add_app_provider.dart b/app/lib/pages/apps/providers/add_app_provider.dart index b55db1f099..4f5355eb27 100644 --- a/app/lib/pages/apps/providers/add_app_provider.dart +++ b/app/lib/pages/apps/providers/add_app_provider.dart @@ -25,10 +25,8 @@ class AddAppProvider extends ChangeNotifier { TextEditingController appDescriptionController = TextEditingController(); TextEditingController chatPromptController = TextEditingController(); TextEditingController conversationPromptController = TextEditingController(); - TextEditingController usernameController = TextEditingController(); + String? appCategory; - bool isUsernameTaken = false; - bool isCheckingUsername = false; // Trigger Event String? triggerEvent; @@ -154,9 +152,6 @@ class AddAppProvider extends ChangeNotifier { selectedScopes = app.getNotificationScopesFromIds( capabilities.firstWhere((element) => element.id == 'proactive_notification').notificationScopes); } - if (app.username != null) { - usernameController.text = app.username!; - } // Set existing thumbnails thumbnailUrls = app.thumbnailUrls; @@ -244,12 +239,6 @@ class AddAppProvider extends ChangeNotifier { notifyListeners(); } - Future checkIsUsernameTaken(String username) async { - setIsCheckingUsername(true); - isUsernameTaken = await checkPersonaUsername(username); - setIsCheckingUsername(false); - } - bool hasDataChanged(App app, String category) { if (imageFile != null) { return true; @@ -323,9 +312,6 @@ class AddAppProvider extends ChangeNotifier { if (capability.id == 'proactive_notification') { isValid = selectedScopes.isNotEmpty && selectedCapabilities.length > 1; } - if (capability.id == 'persona') { - isValid = usernameController.text.isNotEmpty && !isUsernameTaken; - } } if (isPaid) { isValid = formKey.currentState!.validate() && selectePaymentPlan != null; @@ -410,16 +396,6 @@ class AddAppProvider extends ChangeNotifier { return false; } } - if (capability.id == 'persona') { - if (usernameController.text.isEmpty) { - AppSnackbar.showSnackbarError('Please enter a username for your persona app'); - return false; - } - if (isUsernameTaken) { - AppSnackbar.showSnackbarError('Username is already taken. Please choose another username'); - return false; - } - } } if (appCategory == null) { AppSnackbar.showSnackbarError('Please select a category for your app'); @@ -479,9 +455,6 @@ class AddAppProvider extends ChangeNotifier { } data['proactive_notification']['scopes'] = selectedScopes.map((e) => e.id).toList(); } - if (capability.id == 'persona') { - data['username'] = usernameController.text; - } } var success = false; var res = await updateAppServer(imageFile, data); @@ -516,7 +489,6 @@ class AddAppProvider extends ChangeNotifier { 'price': priceController.text.isNotEmpty ? double.parse(priceController.text) : 0.0, 'payment_plan': selectePaymentPlan, 'thumbnails': thumbnailIds, - 'username': usernameController.text.trim(), }; for (var capability in selectedCapabilities) { if (capability.id == 'external_integration') { @@ -748,9 +720,4 @@ class AddAppProvider extends ChangeNotifier { void setIsGenratingDescription(bool genrating) { isGenratingDescription = genrating; } - - void setIsCheckingUsername(bool checking) { - isCheckingUsername = checking; - notifyListeners(); - } } diff --git a/app/lib/pages/apps/update_app.dart b/app/lib/pages/apps/update_app.dart index 2c2ed26c05..32d50696a7 100644 --- a/app/lib/pages/apps/update_app.dart +++ b/app/lib/pages/apps/update_app.dart @@ -1,8 +1,5 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/services.dart'; -import 'package:friend_private/utils/other/debouncer.dart'; -import 'package:friend_private/utils/text_formatter.dart'; import 'package:shimmer/shimmer.dart'; import 'package:friend_private/backend/schema/app.dart'; import 'package:friend_private/pages/apps/widgets/full_screen_image_viewer.dart'; @@ -26,8 +23,6 @@ class UpdateAppPage extends StatefulWidget { } class _UpdateAppPageState extends State { - final _debouncer = Debouncer(delay: const Duration(milliseconds: 500)); - @override void initState() { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -372,77 +367,6 @@ class _UpdateAppPageState extends State { ), ], ), - if (provider.isCapabilitySelectedById('persona')) - Container( - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(12.0), - ), - padding: const EdgeInsets.all(14.0), - margin: const EdgeInsets.only(top: 22), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'Persona Username', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), - margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(10.0), - ), - width: double.infinity, - child: TextFormField( - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a username to access the persona'; - } - return null; - }, - onChanged: (value) { - _debouncer.run(() async { - await provider.checkIsUsernameTaken(value); - }); - }, - controller: provider.usernameController, - inputFormatters: [ - LowerCaseTextFormatter(), - FilteringTextInputFormatter.allow(RegExp(r'[a-z0-9_]')), - ], - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - hintText: 'Enter a username', - suffix: provider.usernameController.text.isEmpty - ? null - : provider.isCheckingUsername - ? const SizedBox( - width: 16, - height: 16, - child: Center( - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.grey), - ), - ), - ) - : Icon( - provider.isUsernameTaken ? Icons.close : Icons.check, - color: provider.isUsernameTaken ? Colors.red : Colors.green, - size: 16, - ), - ), - ), - ), - ], - ), - ), const SizedBox( height: 90, ), diff --git a/app/lib/pages/apps/widgets/create_options_sheet.dart b/app/lib/pages/apps/widgets/create_options_sheet.dart new file mode 100644 index 0000000000..75f7cdf44b --- /dev/null +++ b/app/lib/pages/apps/widgets/create_options_sheet.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:friend_private/pages/apps/add_app.dart'; +import 'package:friend_private/pages/persona/add_persona.dart'; +import 'package:friend_private/utils/analytics/mixpanel.dart'; +import 'package:friend_private/utils/other/temp.dart'; + +class CreateOptionsSheet extends StatelessWidget { + const CreateOptionsSheet({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(24, 32, 24, 24), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'What would you like to create?', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 24), + Card( + elevation: 0, + color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + titleAlignment: ListTileTitleAlignment.center, + leading: const Icon(Icons.apps, color: Colors.white), + title: + Text('Create an App', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white)), + subtitle: Text('Create and share your app', style: TextStyle(color: Colors.white.withOpacity(0.7))), + onTap: () { + Navigator.pop(context); + MixpanelManager().pageOpened('Submit App'); + routeToPage(context, const AddAppPage()); + }, + ), + ), + const SizedBox(height: 12), + Card( + elevation: 0, + color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: const Icon(Icons.person_outline, color: Colors.white), + titleAlignment: ListTileTitleAlignment.center, + title: Text('Create my Clone', + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white)), + subtitle: Text('Create your digital clone', style: TextStyle(color: Colors.white.withOpacity(0.7))), + onTap: () { + Navigator.pop(context); + MixpanelManager().pageOpened('Create Persona'); + routeToPage(context, const AddPersonaPage()); + }, + ), + ), + const SizedBox(height: 24), + ], + ), + ); + } +} diff --git a/app/lib/pages/apps/widgets/show_app_options_sheet.dart b/app/lib/pages/apps/widgets/show_app_options_sheet.dart index 6696ab6428..8a8a683517 100644 --- a/app/lib/pages/apps/widgets/show_app_options_sheet.dart +++ b/app/lib/pages/apps/widgets/show_app_options_sheet.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:friend_private/backend/schema/app.dart'; import 'package:friend_private/pages/apps/update_app.dart'; +import 'package:friend_private/pages/persona/update_persona.dart'; import 'package:friend_private/providers/app_provider.dart'; import 'package:friend_private/utils/other/temp.dart'; import 'package:friend_private/widgets/dialog.dart'; @@ -40,9 +41,9 @@ class ShowAppOptionsSheet extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(8)), ), child: ListTile( - title: const Text( - 'Keep App Public', - style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600), + title: Text( + app.isNotPersona() ? 'Keep App Public' : 'Keep Persona Public', + style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600), ), trailing: Switch( value: provider.appPublicToggled, @@ -59,8 +60,8 @@ class ShowAppOptionsSheet extends StatelessWidget { Navigator.pop(context); Navigator.pop(context); }, - 'Make App Public?', - 'If you make the app public, it can be used by everyone', + app.isNotPersona() ? 'Make App Public?' : 'Make Persona Public?', + 'If you make the ${app.isNotPersona() ? 'app' : 'persona'} public, it can be used by everyone', okButtonText: 'Confirm', ), ); @@ -76,8 +77,8 @@ class ShowAppOptionsSheet extends StatelessWidget { Navigator.pop(context); Navigator.pop(context); }, - 'Make App Private?', - 'If you make the app private now, it will stop working for everyone and will be visible only to you', + app.isNotPersona() ? 'Make App Private?' : 'Make Persona Private?', + 'If you make the ${app.isNotPersona() ? 'app' : 'persona'} private now, it will stop working for everyone and will be visible only to you', okButtonText: 'Confirm', ), ); @@ -93,15 +94,19 @@ class ShowAppOptionsSheet extends StatelessWidget { child: Column( children: [ ListTile( - title: const Text('Update App Details'), + title: Text(app.isNotPersona() ? 'Update App Details' : 'Update Persona Details'), leading: const Icon(Icons.edit), onTap: () { Navigator.pop(context); - routeToPage(context, UpdateAppPage(app: app)); + if (app.isNotPersona()) { + routeToPage(context, UpdateAppPage(app: app)); + } else { + routeToPage(context, UpdatePersonaPage(app: app)); + } }, ), ListTile( - title: const Text('Delete App'), + title: Text('Delete ${app.isNotPersona() ? 'App' : 'Persona'}'), leading: const Icon( Icons.delete, ), @@ -117,8 +122,8 @@ class ShowAppOptionsSheet extends StatelessWidget { Navigator.pop(context); Navigator.pop(context); }, - 'Delete App', - 'Are you sure you want to delete this app? This action cannot be undone.', + 'Delete ${app.isNotPersona() ? 'App' : 'Persona'}?', + 'Are you sure you want to delete this ${app.isNotPersona() ? 'App' : 'Persona'}? This action cannot be undone.', okButtonText: 'Confirm', ), ); diff --git a/app/lib/pages/persona/add_persona.dart b/app/lib/pages/persona/add_persona.dart index d5a6b64d8b..8e3e3ed3da 100644 --- a/app/lib/pages/persona/add_persona.dart +++ b/app/lib/pages/persona/add_persona.dart @@ -1,11 +1,11 @@ -import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:friend_private/backend/http/api/apps.dart'; -import 'package:friend_private/backend/preferences.dart'; +import 'package:flutter/services.dart'; +import 'package:friend_private/pages/persona/persona_provider.dart'; import 'package:friend_private/providers/app_provider.dart'; import 'package:friend_private/utils/alerts/app_snackbar.dart'; +import 'package:friend_private/utils/other/debouncer.dart'; +import 'package:friend_private/utils/text_formatter.dart'; import 'package:friend_private/widgets/animated_loading_button.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; class AddPersonaPage extends StatefulWidget { @@ -16,190 +16,333 @@ class AddPersonaPage extends StatefulWidget { } class _AddPersonaPageState extends State { - final _formKey = GlobalKey(); - final _nameController = TextEditingController(); - final _emailController = TextEditingController(); - bool _isPublic = false; - File? _selectedImage; - bool _isLoading = false; + final _debouncer = Debouncer(delay: const Duration(milliseconds: 500)); + + void _showSuccessDialog(String url) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return Dialog( + backgroundColor: Colors.grey[900], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.grey[800], + shape: BoxShape.circle, + ), + child: const Icon( + Icons.check, + color: Colors.white, + size: 40, + ), + ), + const SizedBox(height: 24), + const Text( + 'Your Omi Persona is live!', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + const Text( + 'Share it with anyone who\nneeds to hear back from you', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + const SizedBox(height: 24), + GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: "https://$url")); + AppSnackbar.showSnackbarSuccess('Persona link copied to clipboard'); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.link, + color: Colors.grey, + size: 20, + ), + const SizedBox(width: 8), + Text( + url, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 24), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + context.read().getApps(); + context.read().resetForm(); + Navigator.of(context).pop(); + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: const Text( + 'Done', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } @override void initState() { super.initState(); - _nameController.text = SharedPreferencesUtil().givenName; - _emailController.text = SharedPreferencesUtil().email; - } - - Future _pickImage() async { - final ImagePicker picker = ImagePicker(); - final XFile? image = await picker.pickImage(source: ImageSource.gallery); - if (image != null) { - setState(() { - _selectedImage = File(image.path); - }); - } - } - - Future _createPersona() async { - if (!_formKey.currentState!.validate() || _selectedImage == null) { - if (_selectedImage == null) { - AppSnackbar.showSnackbarError('Please select an image'); - } - return; - } - - setState(() { - _isLoading = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + final provider = Provider.of(context, listen: false); + provider.onShowSuccessDialog = _showSuccessDialog; }); - - try { - final personaData = { - 'author': _nameController.text, - 'private': !_isPublic, - }; - - var res = await createPersonaApp(_selectedImage!, personaData); - - if (mounted) { - if (res) { - context.read().getApps(); - AppSnackbar.showSnackbarSuccess('Persona created successfully'); - } else { - AppSnackbar.showSnackbarError('Failed to create your persona'); - } - } - } catch (e) { - if (mounted) { - AppSnackbar.showSnackbarError('Failed to create persona: $e'); - } - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } - - @override - void dispose() { - _nameController.dispose(); - _emailController.dispose(); - super.dispose(); } @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.black, - appBar: AppBar( - backgroundColor: Colors.transparent, - title: const Text('Create Persona', style: TextStyle(color: Colors.white)), - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Navigator.pop(context), + return Consumer(builder: (context, provider, child) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.transparent, + title: const Text('Create Persona', style: TextStyle(color: Colors.white)), + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), ), - ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: GestureDetector( - onTap: _pickImage, - child: Container( - width: 120, - height: 120, - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(60), - border: Border.all(color: Colors.grey.shade800), - ), - child: _selectedImage != null - ? ClipRRect( - borderRadius: BorderRadius.circular(60), - child: Image.file( - _selectedImage!, - fit: BoxFit.cover, + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Form( + key: provider.formKey, + onChanged: () { + provider.validateForm(); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: GestureDetector( + onTap: () async { + await provider.pickImage(); + }, + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(60), + border: Border.all(color: Colors.grey.shade800), + ), + child: provider.selectedImage != null + ? ClipRRect( + borderRadius: BorderRadius.circular(60), + child: Image.file( + provider.selectedImage!, + fit: BoxFit.cover, + ), + ) + : Icon( + Icons.add_a_photo, + size: 40, + color: Colors.grey.shade400, ), - ) - : Icon( - Icons.add_a_photo, - size: 40, - color: Colors.grey.shade400, - ), + ), ), ), - ), - const SizedBox(height: 32), - TextFormField( - controller: _nameController, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - labelText: 'Name', - labelStyle: TextStyle(color: Colors.grey.shade400), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.grey.shade800), - borderRadius: BorderRadius.circular(8), + const SizedBox(height: 32), + Container( + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12.0), ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.grey.shade600), - borderRadius: BorderRadius.circular(8), + padding: const EdgeInsets.all(14.0), + margin: const EdgeInsets.only(top: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'Persona Name', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a username to access the persona'; + } + return null; + }, + controller: provider.nameController, + decoration: const InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: 'Nik AI', + ), + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'Persona Username', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a username to access the persona'; + } + return null; + }, + onChanged: (value) { + _debouncer.run(() async { + await provider.checkIsUsernameTaken(value); + }); + }, + controller: provider.usernameController, + inputFormatters: [ + LowerCaseTextFormatter(), + FilteringTextInputFormatter.allow(RegExp(r'[a-z0-9_]')), + ], + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: 'nikshevchenko', + suffix: provider.usernameController.text.isEmpty + ? null + : provider.isCheckingUsername + ? const SizedBox( + width: 16, + height: 16, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.grey), + ), + ), + ) + : Icon( + provider.isUsernameTaken ? Icons.close : Icons.check, + color: provider.isUsernameTaken ? Colors.red : Colors.green, + size: 16, + ), + ), + ), + ), + ], ), ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a name'; - } - return null; - }, - ), - const SizedBox(height: 24), - Row( - children: [ - Text( - 'Make Persona Public', - style: TextStyle(color: Colors.grey.shade400), - ), - const Spacer(), - Switch( - value: _isPublic, - onChanged: (value) { - setState(() { - _isPublic = value; - }); - }, - activeColor: Colors.white, + const SizedBox(height: 22), + Container( + padding: const EdgeInsets.only(left: 14.0), + child: Row( + children: [ + Text( + 'Make Persona Public', + style: TextStyle(color: Colors.grey.shade400), + ), + const Spacer(), + Switch( + value: provider.makePersonaPublic, + onChanged: (value) { + provider.setPersonaPublic(value); + }, + activeColor: Colors.white, + ), + ], ), - ], - ), - const SizedBox(height: 32), - ], + ), + ], + ), ), ), ), - ), - bottomNavigationBar: Padding( - padding: const EdgeInsets.only(left: 24, right: 24, bottom: 52), - child: SizedBox( - width: double.infinity, - child: AnimatedLoadingButton( - onPressed: _createPersona, - color: Colors.white, - loaderColor: Colors.black, - text: "Create Persona", - textStyle: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w600, + bottomNavigationBar: Padding( + padding: const EdgeInsets.only(left: 24, right: 24, bottom: 52), + child: SizedBox( + width: double.infinity, + child: AnimatedLoadingButton( + onPressed: !provider.isFormValid + ? () async {} + : () async { + await provider.createPersona(); + }, + color: provider.isFormValid ? Colors.white : Colors.grey[800]!, + loaderColor: Colors.black, + text: "Create Persona", + textStyle: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), ), ), ), - ), - ); + ); + }); } } diff --git a/app/lib/pages/persona/persona_provider.dart b/app/lib/pages/persona/persona_provider.dart new file mode 100644 index 0000000000..6edd59c857 --- /dev/null +++ b/app/lib/pages/persona/persona_provider.dart @@ -0,0 +1,120 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/http/api/apps.dart'; +import 'package:friend_private/backend/schema/app.dart'; +import 'package:friend_private/utils/alerts/app_snackbar.dart'; +import 'package:image_picker/image_picker.dart'; + +typedef ShowSuccessDialogCallback = void Function(String url); + +class PersonaProvider extends ChangeNotifier { + final GlobalKey formKey = GlobalKey(); + TextEditingController nameController = TextEditingController(); + TextEditingController usernameController = TextEditingController(); + bool isUsernameTaken = false; + bool isCheckingUsername = false; + bool makePersonaPublic = false; + ShowSuccessDialogCallback? onShowSuccessDialog; + + File? selectedImage; + String? selectedImageUrl; + + String? personaId; + + bool isFormValid = false; + bool _isLoading = false; + + void setPersonaPublic(bool? value) { + if (value == null) { + return; + } + makePersonaPublic = value; + notifyListeners(); + } + + void prepareUpdatePersona(App app) { + nameController.text = app.name; + usernameController.text = app.username!; + makePersonaPublic = !app.private; + selectedImageUrl = app.image; + personaId = app.id; + notifyListeners(); + } + + Future pickImage() async { + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage(source: ImageSource.gallery); + if (image != null) { + selectedImage = File(image.path); + validateForm(); + } + notifyListeners(); + } + + void validateForm() { + isFormValid = formKey.currentState!.validate() && selectedImage != null; + notifyListeners(); + } + + void resetForm() { + nameController.clear(); + usernameController.clear(); + selectedImage = null; + makePersonaPublic = false; + isFormValid = false; + onShowSuccessDialog = null; + notifyListeners(); + } + + Future createPersona() async { + if (!formKey.currentState!.validate() || selectedImage == null) { + if (selectedImage == null) { + AppSnackbar.showSnackbarError('Please select an image'); + } + return; + } + + setIsLoading(true); + + try { + final personaData = { + 'name': nameController.text, + 'username': usernameController.text, + 'private': !makePersonaPublic, + }; + + var res = await createPersonaApp(selectedImage!, personaData); + + if (res) { + String personaUrl = 'personas.omi.me/u/${usernameController.text}'; + print('Persona URL: $personaUrl'); + if (onShowSuccessDialog != null) { + onShowSuccessDialog!(personaUrl); + } + } else { + AppSnackbar.showSnackbarError('Failed to create your persona. Please try again later.'); + } + } catch (e) { + AppSnackbar.showSnackbarError('Failed to create persona: $e'); + } finally { + setIsLoading(false); + } + } + + Future checkIsUsernameTaken(String username) async { + setIsCheckingUsername(true); + isUsernameTaken = await checkPersonaUsername(username); + setIsCheckingUsername(false); + } + + void setIsCheckingUsername(bool checking) { + isCheckingUsername = checking; + notifyListeners(); + } + + void setIsLoading(bool loading) { + _isLoading = loading; + notifyListeners(); + } +} diff --git a/app/lib/pages/persona/update_persona.dart b/app/lib/pages/persona/update_persona.dart new file mode 100644 index 0000000000..8da76f6ad6 --- /dev/null +++ b/app/lib/pages/persona/update_persona.dart @@ -0,0 +1,246 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:friend_private/backend/schema/app.dart'; +import 'package:friend_private/pages/persona/persona_provider.dart'; +import 'package:friend_private/utils/other/debouncer.dart'; +import 'package:friend_private/utils/text_formatter.dart'; +import 'package:friend_private/widgets/animated_loading_button.dart'; +import 'package:provider/provider.dart'; + +class UpdatePersonaPage extends StatefulWidget { + final App app; + const UpdatePersonaPage({super.key, required this.app}); + + @override + State createState() => _UpdatePersonaPageState(); +} + +class _UpdatePersonaPageState extends State { + final _debouncer = Debouncer(delay: const Duration(milliseconds: 500)); + + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().prepareUpdatePersona(widget.app); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, child) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.transparent, + title: const Text('Create Persona', style: TextStyle(color: Colors.white)), + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Form( + key: provider.formKey, + onChanged: () { + provider.validateForm(); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: GestureDetector( + onTap: () async { + await provider.pickImage(); + }, + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(60), + border: Border.all(color: Colors.grey.shade800), + ), + // child: provider.selectedImage != null + // ? ClipRRect( + // borderRadius: BorderRadius.circular(60), + // child: Image.file( + // provider.selectedImage!, + // fit: BoxFit.cover, + // ), + // ) + // : Icon( + // Icons.add_a_photo, + // size: 40, + // color: Colors.grey.shade400, + // ), + child: provider.selectedImage != null || provider.selectedImageUrl != null + ? (provider.selectedImageUrl == null + ? ClipRRect( + borderRadius: BorderRadius.circular(60), + child: Image.file(provider.selectedImage!, fit: BoxFit.cover)) + : ClipRRect( + borderRadius: BorderRadius.circular(60), + child: CachedNetworkImage(imageUrl: provider.selectedImageUrl!), + )) + : const Icon(Icons.add_a_photo, color: Colors.grey, size: 32), + ), + ), + ), + const SizedBox(height: 32), + Container( + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12.0), + ), + padding: const EdgeInsets.all(14.0), + margin: const EdgeInsets.only(top: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'Persona Name', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a username to access the persona'; + } + return null; + }, + controller: provider.nameController, + decoration: const InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: 'Nik AI', + ), + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'Persona Username', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a username to access the persona'; + } + return null; + }, + onChanged: (value) { + _debouncer.run(() async { + await provider.checkIsUsernameTaken(value); + }); + }, + controller: provider.usernameController, + inputFormatters: [ + LowerCaseTextFormatter(), + FilteringTextInputFormatter.allow(RegExp(r'[a-z0-9_]')), + ], + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: 'nikshevchenko', + suffix: provider.usernameController.text.isEmpty + ? null + : provider.isCheckingUsername + ? const SizedBox( + width: 16, + height: 16, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.grey), + ), + ), + ) + : Icon( + provider.isUsernameTaken ? Icons.close : Icons.check, + color: provider.isUsernameTaken ? Colors.red : Colors.green, + size: 16, + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 22), + Container( + padding: const EdgeInsets.only(left: 14.0), + child: Row( + children: [ + Text( + 'Make Persona Public', + style: TextStyle(color: Colors.grey.shade400), + ), + const Spacer(), + Switch( + value: provider.makePersonaPublic, + onChanged: (value) { + provider.setPersonaPublic(value); + }, + activeColor: Colors.white, + ), + ], + ), + ), + ], + ), + ), + ), + ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.only(left: 24, right: 24, bottom: 52), + child: SizedBox( + width: double.infinity, + child: AnimatedLoadingButton( + onPressed: !provider.isFormValid + ? () async {} + : () async { + await provider.createPersona(); + }, + color: provider.isFormValid ? Colors.white : Colors.grey[800]!, + loaderColor: Colors.black, + text: "Create Persona", + textStyle: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ); + }); + } +} diff --git a/backend/routers/apps.py b/backend/routers/apps.py index 97e4227e77..dbe3b0cf2f 100644 --- a/backend/routers/apps.py +++ b/backend/routers/apps.py @@ -17,8 +17,8 @@ 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, generate_persona_prompt -from utils.llm import generate_description + is_permit_payment_plan_get, generate_persona_prompt, generate_persona_desc +from utils.llm import generate_description, generate_persona_description from utils.notifications import send_notification from utils.other import endpoints as auth @@ -97,6 +97,64 @@ def create_app(app_data: str = Form(...), file: UploadFile = File(...), uid=Depe return {'status': 'ok', 'app_id': app.id} +@router.post('/v1/personas', tags=['v1']) +def create_persona(persona_data: str = Form(...), file: UploadFile = File(...), uid=Depends(auth.get_current_user_uid)): + data = json.loads(persona_data) + data['approved'] = False + data['deleted'] = False + data['status'] = 'under-review' + data['category'] = 'personality-emulation' + data['name'] = data['name'].strip() + data['id'] = str(ULID()) + data['uid'] = uid + data['capabilities'] = ['persona'] + user = get_user_from_uid(uid) + data['author'] = user['display_name'] + data['email'] = user['email'] + save_username(data['username'], uid) + data['persona_prompt'] = generate_persona_prompt(uid) + data['description'] = generate_persona_desc(uid, data['name']) + data['connected_accounts'] = ['omi'] + os.makedirs(f'_temp/plugins', exist_ok=True) + file_path = f"_temp/plugins/{file.filename}" + with open(file_path, 'wb') as f: + f.write(file.file.read()) + img_url = upload_plugin_logo(file_path, data['id']) + data['image'] = img_url + data['created_at'] = datetime.now(timezone.utc) + + add_app_to_db(data) + + return {'status': 'ok', 'app_id': data['id']} + + +@router.patch('/v1/personas/{persona_id}', tags=['v1']) +def update_persona(persona_id: str, persona_data: str = Form(...), file: UploadFile = File(None), + uid=Depends(auth.get_current_user_uid)): + data = json.loads(persona_data) + persona = get_available_app_by_id(persona_id, uid) + if not persona: + raise HTTPException(status_code=404, detail='Persona not found') + if persona['uid'] != uid: + raise HTTPException(status_code=403, detail='You are not authorized to perform this action') + if file: + delete_plugin_logo(persona['image']) + os.makedirs(f'_temp/plugins', exist_ok=True) + file_path = f"_temp/plugins/{file.filename}" + with open(file_path, 'wb') as f: + f.write(file.file.read()) + img_url = upload_plugin_logo(file_path, persona_id) + data['image'] = img_url + save_username(data['username'], uid) + data['description'] = generate_persona_desc(uid, data['name']) + data['updated_at'] = datetime.now(timezone.utc) + update_app_in_db(data) + if persona['approved'] and (persona['private'] is None or persona['private'] is False): + delete_generic_cache('get_public_approved_apps_data') + delete_app_cache_by_id(persona_id) + return {'status': 'ok'} + + @router.get('/v1/apps/check-username', tags=['v1']) def check_username(username: str, uid: str = Depends(auth.get_current_user_uid)): persona = is_username_taken(username) @@ -127,8 +185,6 @@ def update_app(app_id: str, app_data: str = Form(...), file: UploadFile = File(N f.write(file.file.read()) img_url = upload_plugin_logo(file_path, app_id) data['image'] = img_url - if "persona" in data['capabilities']: - save_username(data['username'], uid) data['updated_at'] = datetime.now(timezone.utc) # Warn: the user can update any fields, e.g. approved. update_app_in_db(data) @@ -306,27 +362,6 @@ def get_plugin_capabilities(): return [ {'title': 'Chat', 'id': 'chat'}, {'title': 'Memories', 'id': 'memories'}, - {'title': 'Persona', 'id': 'persona'}, - {'title': 'External Integration', 'id': 'external_integration', 'triggers': [ - {'title': 'Audio Bytes', 'id': 'audio_bytes'}, - {'title': 'Memory Creation', 'id': 'memory_creation'}, - {'title': 'Transcript Processed', 'id': 'transcript_processed'}, - ]}, - {'title': 'Notification', 'id': 'proactive_notification', 'scopes': [ - {'title': 'User Name', 'id': 'user_name'}, - {'title': 'User Facts', 'id': 'user_facts'}, - {'title': 'User Memories', 'id': 'user_context'}, - {'title': 'User Chat', 'id': 'user_chat'} - ]} - ] - - -@router.get('/v2/app-capabilities', tags=['v2']) -def get_plugin_capabilities(): - return [ - {'title': 'Chat', 'id': 'chat'}, - {'title': 'Memories', 'id': 'memories'}, - {'title': 'Persona', 'id': 'persona'}, {'title': 'External Integration', 'id': 'external_integration', 'triggers': [ {'title': 'Audio Bytes', 'id': 'audio_bytes'}, {'title': 'Memory Creation', 'id': 'memory_creation'}, diff --git a/backend/routers/chat.py b/backend/routers/chat.py index 758641e7a3..6000e6479d 100644 --- a/backend/routers/chat.py +++ b/backend/routers/chat.py @@ -19,10 +19,10 @@ from routers.sync import retrieve_file_paths, decode_files_to_wav, retrieve_vad_segments from utils.apps import get_available_app_by_id from utils.chat import process_voice_message_segment, process_voice_message_segment_stream -from utils.llm import initial_chat_message, execute_persona_chat_stream +from utils.llm import initial_chat_message from utils.other import endpoints as auth, storage from utils.other.chat_file import FileChatTool -from utils.retrieval.graph import execute_graph_chat, execute_graph_chat_stream +from utils.retrieval.graph import execute_graph_chat, execute_graph_chat_stream, execute_persona_chat_stream router = APIRouter() fc = FileChatTool() diff --git a/backend/utils/apps.py b/backend/utils/apps.py index 2b5f6404d3..d25051c4e9 100644 --- a/backend/utils/apps.py +++ b/backend/utils/apps.py @@ -396,6 +396,14 @@ def generate_persona_prompt(uid: str): return persona_prompt +def generate_persona_desc(uid: str, persona_name: str): + """Generate a persona description based on user facts.""" + facts = get_facts(uid, limit=1000) + + persona_description = generate_persona_description(facts, persona_name) + return persona_description + + def update_persona_prompt(uid: str): """Update a persona's chat prompt with latest facts and memories.""" # Get latest facts and user info From 11ae78e54cd8af52a51762e9c2aea56fb269d7da Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Wed, 19 Feb 2025 00:35:07 +0530 Subject: [PATCH 16/18] clones from socials [WIP] --- app/assets/images/calendar_logo.png | Bin 0 -> 1284 bytes app/assets/images/instagram_logo.png | Bin 0 -> 2674 bytes app/assets/images/linkedin_logo.png | Bin 0 -> 1238 bytes app/assets/images/new_background.png | Bin 0 -> 162973 bytes app/assets/images/notion_logo.png | Bin 0 -> 1773 bytes app/assets/images/x_logo.png | Bin 0 -> 103016 bytes app/assets/images/x_logo_mini.png | Bin 0 -> 1093 bytes app/lib/backend/http/api/apps.dart | 36 ++ app/lib/pages/apps/explore_install_page.dart | 4 + app/lib/pages/persona/add_persona.dart | 85 +++++ app/lib/pages/persona/persona_profile.dart | 326 +++++++++++++++++ app/lib/pages/persona/persona_provider.dart | 27 ++ .../persona/twitter/clone_success_sceen.dart | 215 ++++++++++++ .../pages/persona/twitter/social_profile.dart | 186 ++++++++++ .../twitter/verify_identity_screen.dart | 330 ++++++++++++++++++ backend/routers/apps.py | 31 ++ backend/utils/llm.py | 77 +++- backend/utils/social.py | 89 +++++ 18 files changed, 1404 insertions(+), 2 deletions(-) create mode 100644 app/assets/images/calendar_logo.png create mode 100644 app/assets/images/instagram_logo.png create mode 100644 app/assets/images/linkedin_logo.png create mode 100644 app/assets/images/new_background.png create mode 100644 app/assets/images/notion_logo.png create mode 100644 app/assets/images/x_logo.png create mode 100644 app/assets/images/x_logo_mini.png create mode 100644 app/lib/pages/persona/persona_profile.dart create mode 100644 app/lib/pages/persona/twitter/clone_success_sceen.dart create mode 100644 app/lib/pages/persona/twitter/social_profile.dart create mode 100644 app/lib/pages/persona/twitter/verify_identity_screen.dart create mode 100644 backend/utils/social.py diff --git a/app/assets/images/calendar_logo.png b/app/assets/images/calendar_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8376b8b5f6d7b42e89000cf02766b59aeeafa40c GIT binary patch literal 1284 zcmV+f1^fDmP)8G5g+8bj`xZ)};9BVW2VAsn z3jQGILMyM9Zn}~_x^t1&Vxc04#$VOMIX!1?Zhm!QW+u%vA2{61otvBQJ?GpXnem|j zL{!a%KV@E#^!a=Vs~f)T3l=A$eoL8`0<%f7#o4x4w4p20LQ@|6o$VakL$(gKAiPBc zzGnM}?SHl}IUW{IvYlfaX4|I9K(vS^GqS#v&?YY0+5Sc4Al~g}`;Wc4sq;`m{1D}U z2rki@WWGQ`F1Z|&gFvx;P_ZwCppiLj#OzE9|!H-JGi7nYxJi?>JjAP=iAq= z0qg5ZbbyeqPA8INZK`Vx^Hd*Yiq!T3&8#RKue1fboMN8=MJ#4qP|zn z!TkCa=1nr#>R-Ywur7?C3^Nu)pv9d83xh&_e9aCx+m=J5)BOz zg+iG=E?rXP>MGHjH$=B?5zWjHEi4fA^{Kw4FKa|wZE=r&`vwdT19$HN4;}y?KdO2i zS_zrtVO5o?KRg7^oKbZeNtIWxfSDPfwN-htyIa-8VnAbKZhH#?D8}}-`k^aVfWAK9 z0EsnLCC_V$P_UL+fsp+DK9I>EIE0)gD?)2DMwU7y`n zlYLM4@Bx^cQ_%?&4{Y%k1u!@_{eE*Ya(elkcX0hW3=X|t=XMcX2~DyW0_=s(&Q45D zPV!u6Oy8TIpU3FvDC+9!PW*loFJsfw)2Oj2wD|Irld>vj7kF7|XlS5tI83ovET;{N zpq-r^UZr*mnzZ48l1*$bPg!1&P`9L1!wv4?mNsIRZLH1YHt=jO=B zh?zciu^oC5DY>}GyG}3|OxHO@`snHD;oURg=zF$>a@fs@Bc83Yn^$>=2UmFej6@>o z$!HhbLUot}0oMzKDXg1!q&40koP^!{U3i2DTjx_)i|kf#o;IEd)!vll%W2pJPlSpv z$?uJg4Kz15!wsJOCGPF*<@3v|FVnGVGFfF{fujzZJ})h0ob=FHh4h#&G7INJe%7l1uX=#c7 zVD^{nO`&Wa)eAfy>wb#Rk~|uZ$N5>pk>$PR&l|oqUl}Cn*tO zb(-4W-#5>mZnn!j{$%?ip?4J*IagX8oHhEd$j|Vm*$*;8{XU~|(k)euAg0000t~xD-sAYZCvzqr}!rfPIw#9~r0|yRo0$x#h0n?yd*J z!FX0;a7!eD4OvsV)s5*nsJl+*4zmU-_dT!AYxB2G`O(OA#BhX*oYV}8YEUPpzW!M8 z_`qV+EuH}q&|o#O_4kJZTLEOoMJ=HE7tnjQ&S+XX_r1v*3G8HrvG&ume!DO}*1g!T zng{}rQX(uxh?61;lg1!FsU0fk8E`33Au?t(%R*$}l)%u*BDy~AL!vz7n;HSFC;{_C zqU>cN6#5SIuQfS(H-0m|$r^oz!-jFA}o*c3Ee(*(u!rmdTbpvA){ zYmdSU^2VH+IUO}bj24Y4^-4+PT9`umh>Kn5{P3q1R8FF2^>&%4+FcpaqOi7=(v3xuHSl_0XOBDYjEoKClUQS=3JGoDH9vgQqg$Y zger!JE~4X&2dPF*@C5pny@S4$Z=tcF1HM#-ZfyczvI%DfZ4#;b*j>lzwKf&J0W@_4 z=>OYk?D*J{^vF$$TjdyqEvq# zwW#%2ae80b=2d)A#byqTm%jrvM3|8~*Fyg26_E3c7&6dC4)5x=V&YBZ+B zFOxV0CPiY+sI>=cN)h#FlTdpnGE?f%V&fmrle%z$eC$jVR{`S! z1=`4C4hualka?`6i$h0sS0V`^T^Xa(iw&--|PGqD%Bw z>y;Wnu8Oh1fvw!TawOCPFYp!lnD0UE`3=1CA%ZE|H`ZE-Ps6{t6NO)Y+oYo8SU2#G z&#V(6tG%*AS;tb9n#{h8Ut>y@)S6PWB(=skIh00g_<(ut?}vb!zYKh3EWPcI5X(11 zynPt5VVAYIiusW*h zx=aTu4Q~7>s9S#wvF#;-yVg3)uND~1*e@H{l8K;7lYa)kl;g6Y`-)N~$SMQE0n^z) zhX`G*nCLRBDd|aSC}%u&t)xU32#wTv-v;`qUAlbNi`_8qP=rR#M9p3nj8{RnE(LcRdwTRW>; zmftvYp7Yum|3=ZNZ3wWRXv#Frg|8EOV8cSZM|-Ta>Bz8|8-_841bqCiy{0+D5`JQy zKT_qHQSG^&KaqgiS%HLuHU^F!1u%glUpDgu>n~jxNSEGNXru6W=CRRh+v%~MWm9GP zyEKkz^L?_-d}6Zd{_lE%=*s(FY>l6=VFyd0q%SqiUppyZ>4qrYo?RE zS8!Q}$;Gt9mc3B(e*syznl+vMY}ECSmVRtC7Hh^#4iXJRBtlNWQB2Q3D=HAblUBnZnWs&>qa|QOVnTo@2EKvPDvr@L=$|jLP+f+sdVLaL6T41`bU=v*sWw5}e{A>||ow5C1SUH9r1)TZl1)Q0V!?d~&F$R+@jpJa;~$?}d

@9UTQSqD&ErB+yC}O4^{)ReU9-$O78G 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 z zMzBJsC^p2hYBrZ$bx!4>tT$A=Chtp8*yue=*ymjDU3PNM*vam|Osf$3vMNYt_|Qzh zNoLEx5WR!NwPe(JP5(Aa)BUShZJ+14@_xGWFJ3kH8o!L~)#Bl$BV)YI^RHL|_ooiy zpJFT(%Eot$shmFk{fJSmGAkc;dCsIU9}Z&knnmJmrKc!_tw{b&U8}|@My#c9<&jrz zmaZCW*J{A<<9?E^JO0Os=Vu6NL2^JN1yomMpFEYmidRMs7Xy7Pb@PAO$2##-sF?oo zl2=FDk(-_?o3OO8Zh^Ln8h(N=wrVKfz-ZKHxE*c`VNw64kf@@wKgICr!P*G6llK~z$z9i$GeHBQn^uM-9_#UQr27W&vE`2Jq zlM`fYGzbW$G%vHk5*%;fW5FC(6Ap1KWLjdB9JTEfLyG1=`SRudP)IQb>MJqmgbM>OmvP)9t3ptf&jb#^N{FHQ+zrn6 z5*rZv)hRRL7~cH#ho3Bo=woEc98xR=y1z07!C(fB4b5){& zg{{^>L9gj-J}Z8o@oo0dVlwql{cvCVBaYq9ki3M%zu(C0#%zM|XiTV5Tymtux&Nuz zo^V}ODa@yuo6N8QmZ3R=V;Z61D%<hWZ%T19a^I zGp|6>)qqQ{@1g;we4@j1fW*(7@AziPt0)1t%#g$rp$yJLw@Vgal_d8eIUr)+3M^VI zQ1o%dq=TCRLD+naiTa#ydu4k#E(w3He$Nw1(J-`7`@3j}Rwp-{e~X_)tGn4pvXkrlav%ORS0P5-tPlak6*!7$-)%cusB zaO^_H{DQSYjFZYuzn|9`n!R!QBH-v8b(6N$bZf=)fd`T z2(Q&b!1WAa2;r-A}OubL3&XH0iUs;1n(jhc9jfuZ&)E!u(9Jh;4#}i`kh1>bXyQmz(If3deU9EDu@Y0 zXEm>@l%=?1*hxa{(hZ?=G@cZ!CFka?wEx zUw!`h`BX7K&&P*=&^!nZEMS@wB6iDh-IoI{jSL7+&J<9G(i&dO!MFHf6tQPm!T@&g zr=03R+O}rx4NM9chRe0+X)+!Zm&wi#s(5* zyt;2t@n|ufMH@D^f5FWSG~qC;Qu(lDKe9WS8-z4q8$5&iqAWUL$)#ZKQlbGgw15bJ zRnSD>N_q^vc=K{HDN4JptB#;t^QPf+{Ivu`0d72jWZ>)aYOq$)>M`%ToRebLMT5$W zS(LXzj}6T4Prw>xuSd_e!u|lI=ZJz$$$ZX_1dP8JVbZ++ZgFP5O|WrV6!=ulA9I>( zy~)d0Q&cN_IrxLJ{(f`7&o*oUM327#7{Xt#`RZh!rLnB{K@^y3+(j=`CTXq?&|d|7 zfE}d3-P?dHKsEIZlw}52GJra!iI&*`M9n^hm` zoQDPoZl!FPrj$bi0yWf^l6sA=fl-gUqOW`IDL2YQX5FVv0cT~`oMd%j_d(!EeJ5hP z7KP&7uR)l%RZPn(41>Kifl2HOn#%J$2e`4|byw7%jq5X3zayW*;%&uX$(X>l?Wo|^ zK(Q2sX~j&9fLIehB4&u0j@2}10^giZMhw*}zZm<&*kBIlB{<`w@+d@>sKiWRm}o%0 zio&6#^((f9C_P>j+(Y7zDu(H2&BJ2x=y+Jb(o7)6G(f2zUp)Y>*QBNS&-)di^`ka- zw)tukTnbe#D1KD)Tl^>O&Xd4}XUhNi3vLCHA!;g+9D^vc0SRbsgNawj-5VP~R?uep z+$4ijK?$DboS;{UaYgk}mb69gndYVcVXn#a`#!`*S82x6sfFKo?vgb+2(wjGH4}a1 z@;4>?k6xKxkQAWTF71zl06xM0eXfH-tbaG0 zT4M>bIH!-B+ojm(Tt~cKDZqyopCd;qKnsr?6o1BjC3if|50CHN$tsw*xOWk|JKQ71 ziT5sCCsx&;tzT-H^!!mC{U=!)IEMjN`IM4_lex;e8=m+paOD@=QUu1>o&X%o)NLZX4s=0)nItOo=QEdB z44>!#RQ|Be_hHDn7e-?d_63MKC=*Wq93ScQ*hE;m_B6^~-vg5k_FBKT4#%qx7(l0! zvIHm_YiA{P2?ZSKCjQMa$IWdW7uoLvy|H-XJ(u?i$f1QExB~x32kq{D-+lsGP&AIt z8!w`GlNqqa31L4G@j5pGQkX~FX&{VAAq7iS^f5U3$luPb0{N?Z2uR1!XXx;D*MQE_C8EfD?@-T@y zL$H(@b~hq_FAL$IHdh0>a_BBB(_!X2njG?;aD^!v`eZ3#|QkqHp z+W8(CLpn0@B;61jB=)3tqUWDZz>+Vo#3Vy>)9l@?$84_>vI*Bh(5!feZD3T33(&ds zQP)e35zEOH0OcKkwRE1f#l+Z)MMkHo$M^g7#`Cpt(1P=vnfz21wZXqSvSOAx&6(Aa zZ*;Gw;|SV16|jq6dqpt|V!0>cw&tQw2o`*&?#J+IKaJgZ#B4nIIXFKUZ}WpshvWf^m~<{d z0VYW<$|!hqJFQG|An1}emTq&`F)(Fm9$JLw+#uh={UnQn1VndxaZzhUyE`7r7FsgYMgtC`au18NT4(8ZV zU@OTdfLF(I&<6n53lL6#2cj4EFqxpToKv-oP9rUa;|Qt#DA^fgVftNETK<7b6+5T! zv>XPGF<3A$Y98Qnr0~@xIbQ1#lg981=bz#Xo}gP&81`n~m)8Z)7Z9ikh@nwD`# zu@!F`L9j{niXlVg|HnkHDluiR4D7s9o%w>fy-~5in4jZ)-h4lst}3!V&!6b32G>?Q zmXrRO?~!6L`a4ol*Fo&tF+9l};D?0UC$GXACd{d|447QJGuUE?Edi+$+g0`a~8 zv5~!I7s~oqK1#72rOgJ3Z8a&JjN3#&fyu*H?n4I*2#+njh?IV?!f)oQM2Q&ExQ$ES zgn~e_n2xBvpU_C*D^FP*{`YDLoy}Wux%QC3T&Y-aKNHAw&c#%~rhq#}F`3&1RS0MI z=e~XHr2fex+bM1Eo9YUpa7vlpEhlp^WlR+*Mc+Li{Ry4wk$|^O{*n0S+UGOC4v_9O zuL6M4uoa*Zn9%?|fm*wIN**1%8f(K~UX)A^!A&Vj-Ovp?>8XyAn6Wr8Lj&`?(U96? z()Sauh;_HUr_jBJQ{%f!O|QCDRW!wu?c_;FRYW@k-%@fViodQ904%1|qXFxOeN}K| z>kwvWUQa>&POxM`tDFD?Tz~L;uoC}9$8YibD8q6>FQcR~HrN~Fa&nNX zD7gXpeVu$XXvq8MdmjY~OXBAEucU``k1EgwER9!8*#?WFvn526jbrERXND<{@D!yg zFq_k{HEWPAz2hV<#O{;jacpXq>6>#P%!J!KG7e#d`3xO){TJbT43om%l4 zXdtwr0?Gmqps%F6oav`o(9#Om+>cE_I%;<4XRw1zG%OA9(PF`ud*yPr^iuUph1f?{ zjlYsJRZH=ipz1WyJe02$h>j6T+R4##bEQ{6r&~s=9}8Q^wtsqS2-_j}5l-9iWk{c6 zzD5Xb`RJ|jTXk?Yu-hu(XUy|ZO}MNeh+6}tQYBo@&i`>ffwf zh}B+e94&|5yPCRhaXo?7O1R`@jdqyJ^}Gy6ij%||UeMnfRyo=ED~*;#jpjD|$lS(| zul%8&JC7{o2O+^i+Z6x=hy~QJa9F`!xHCcS)rG$D2I>+WBQ_OGw<9E44XCg1K_^U| zpZl0lJ~R0)z}FP!N4+O7W#-LFT-y}KdV-K4{Q16@AbkCA}7HWUQ`^%zRd4=;*;4$ zR($n5nVc9u^-sc9JKTPO39c(a6^!?W?z}^H@^R%DfMun?0du=HUJAGZVQ=@G*ZKud z%|Y_c$&!9o0ZE>&j2r7l*1<_c|2(36?7HQ@*y>shN+GF-G`c{lpyHz5u2K+Id1o=_ zX#?;)@=)%?`mJk!g62A22kBVRa5)y<#B<1CM!Fg1R}u=zbLPq>Arq=dH^)>g~XfK|cN zBBnbG7D#YIRS~G(E8S4U0kO*)Hgad<_MOUKw0lcFc>hSMm<-kYC+Tpo6dNqE$(ZL= z@;@(j*05kyg@~VuraSVbAtXf%Y#k-1He`uLQPdRvH z{nDaJGo)L<*}Gyb8Po54oqm|I-zRe?1dp-}?3yQ}NSA-%dd(u!b2>g|^7lIjCva>6 z*Xqtk{B;mW4QG<^I2w8s1Vyi`!a%fy$dUiFXDC)OW`<)b#;%pcTJ9IOVf*X&+EOsL7 zN6)MLFz$TcadG%@D=BX00Kn_^g>)~x29MY)wGQ225V;aES_7+-Fn1+K$E{=T1bsiO z10Hqlb&JT(EuXt6c*3xA<6$^^l47%_&eI!Q`In+Io{y3#%r&U4tq)n$+2-j8f;mUzEmWdf6KorlRbj8Yx@Hn$4mLdnK3@Na%|o`SoQUpx8Cm2iRP$g#r# zh78Z!Ey<*N#FE5=b2velZ>|Xb&?PpZ4>(?cdv$qurJK0@%P}%G>9hWef5U-}If1to zg7Sw?Ip9WJSO_*>%V6x?oY^}6*s?%9F<2zo>8?{!_p*;DrGzQ<{G-79*zhIi6f(|d zY2DyD>H{SC(g1pV_BIdrgydz{r653>DcUt$-AYUee%scnrlp#V*9K#nWFWU<@fzH! z$^i44Kbnt(=U9k*F=Q>>tx$IWjANv&{es(}X2SK&Cv`S72#*m1*En{(2j~#}UHa8= zZCyZ42pfvLRpU3OJ%5?1B{%uD(yjLjN|xV$zccJlAPN8eeE3y?xMV!Qs1zsA)cl#m zdH!xQ!PVs`-5l^1+k`(g^P5QZH=$oT!a8T&*=57gg&+IJOwg?4J-T8O@$M)IYytIY z{dFwuw8|ImIp<`52V3}9krd77Q~Iq|%cZMsFJfCg#aBBtO?6a2FS%w+`*qc8%IR@E#J9rgISg+~ zmqye4Vnsv$LL82M5)UVryUBGpvfe@ARr4HxG>(*0i(rkZqO5+5*@TuX321}jD+Y`N zs$@nbq#D5iWACiOgFOaVCEg!l-`0qI=&@+8wFhdtjldlMzWcYS;Gsp^LS#*cYC96|?B+vkqE=2unoeMht z0qN|rZOv6@Cq=3qsH+J7(7M8@Y^wGZxD3y-9@n#DEs%5j%rarO_*V?b9QJ0c20;whwhkP7%?E!X6B8y{58+?Mrbk&2>v`bXQ!Rr5qb@NSVq8m+uxaI6UdBlv2Pg=L5y%}7yX&j@ z0kt&2x(aK)B12#AJn(5y+@oyU==5gB|Xk{ z0)+r^<&*?&0+x%P(EybVE!Nv+1?-jTUSw#G8B4GNX)CRo0QXsOubB8+XQKI^m%NSF}!8p5}mGIE{d)#$MI?152&V!;-%++1&>Ch7gtBBDhzRn(nN9)vI>R@ z8NXw$oxU1_v`FXf3J9u3*M<=^BSzqpgE)_$bSp_axUFlM@-v^TTNO=a|mO8HD0vJw4koQQg}j(1QGFuiZlq$)76B>c?XU?-=YX3q`aGFkye=^Pbsxyq>GT}RwL*VT*4 zsWhHi&)=*!A-h@@@9cO;`OL)Q++NuV_=M8Ma`9BBBx_II*b^Aqke%ZtmJ zW6wXNv(8)qR%spHQ?8&+zb^lTbc7hTbbwO;dhz^qEd4=)FLxF0Re<{AcN;|f%?w93 zm)i7ckFynkX}c7jwbS6z4EjiHO_AlpEZQ~MKnQo`I6i53vv_u$oWIhHsvwsG3_8Mf zQE>Er2h8g3Fz4c$Vy$3~0G>w=%R_$0!<^9(S*poP6IG;`thu2;=jN3s|QxWbLLF z^tZ<1t1copdCnDp#I4}~Zxc-Nx&>TV*((N0d4MP11(S3&Kn%E(uH~OPfvw~7i)o?s z9E57CNDbN~GEFvCf&`YOa$tZ;NHT(jOT!6(t$NHjt&Y;o)-QC%3G^y}?Lx_=+PB0A zI9>YcVe&=@tuQ$iIyg2O@Go~yei63P>tQCxAc1jXQb?D5qs5ZmUbE{r*6)2Ie?yGGpp|r2HuB&UD!GM&6|I%xCoHtq+|drL0L)h*3m%{@>=QfG7|?o zP(gTk!h6tS(0u+*?9oLGn~Ru~in`5`lSX|L66nfu5`n{&_C8(7+Lq8M-mQOT#}pUwPQQnF5lMDdl02ie(vh%M({mpxyQT@Bsn# znM?dw_p*hqu8bNeKT=4pPFQ2i!QJgbCK|IjidE}!oO^FGvG7>l38-&}?)86{;L}{1 zaZoZs z!=T17&bxRqp}pK|acL_iTA#B;XnwZ=-+&1C(YGLKfDHZK6T{FmY)LYO`*4wAnnL-_!n&*7_Nqt`T5H?(|L zakcyopW*>*FyY4iThgEdVaof0 zF{BWrrDNlA7{@tn?2M!(LF5?oEev5|IjQ`elpA=K_h zqUxA2Z(5Vv>At@qb<&pr-ulH%w5K_(aftZSVBQ8tauYlUP?Mi>*}F zEe=Kd8?XwD?9f8(G1%BOnv|QwmiKTQgtB{V^!p2ryW639SDs9Ju;6+Azxl57|06s3 zGjFjpV|mNeNAYaXRZotPyZzJ-!`kRf&lEA+`^XQRjEENbnD!s zP_C7-_-r}aPAfo`^PHz0qtG5|Y%7%RydecJ-od%UM}-aV_~uQmtqZucm%^gD{j1}J zj{(G$>@GzI<>>C`2&|O&*hKI*)8Xtyvu9A zU-33pBr4OWjHb_dvGC|`Po@3*t!l-m+e}lB=QCT@ zU4PLT9;*zs?D$CtXh@9STS=-sB$4#c4mbRB9^rwzTF=hDmOa{iJoA(no3ZocdE@AJvO}_yyFMCw+p>RHKTZVIdp@;|JxpD_U#FCf+goEtX*+A%7 z(8CK#+WDiK1nl7m)zZ315xJ&9XNEFKp%l`TC?5>ISY)}6H@711-#qQ#Q^>JRxfFQT zbf@veql1Pf`Lq4C*d9+|$|2+1ks_Dczd`ey`{{z)AEA?5nTyW$fCdTNE3&7c%jc^I zr87RhFVmFVwlWqH-q>vzp#E8G7K|zCgU%27BbTYn727FD$2cz_4^j1m-OpHLO*IIK z+;LhmrOUU;t*c@dLCW-mg^!t7@%zOkF|s+OvdwFPuzn~rXmy8I(x5?Rr~K5Kk5$Je zcpcSt$N-yC2GQh%{j2hp41gQ!Q8#wLIP{|fo{q3C02oP>!N*ePF%j{ZM=Tn=Q<2T!~?S?DxM{QGO2y|ixcH*;vNancXHATulwo~zkP$52T zu+tyFnoD^8*Q>^ubDndTV&Rnqud<3PH~s9QR5sN2JL%EXNa1_NKh4KX)pA4JWyAYGa+)h4hB`bky02~NN-#kq9BHMau^4A>AyX%(!`iXRgc zzNx=;DR=bEHd`m3&O>xU!s>jV2P|m3E1!WcKOG@KaHzx*TTFjP$zJ1;W)n;PnqV{p z+)8FGkP)U=ohxAJZ374xB^nT#_A-|`rP;in1i}(h1L!vzPjl1i(H_YA?{Z*E(mIiP(x-jyXx2>!=ev-ZVN5-?|nF&S(97p90nE<}w(2u+grMUYo0nYein5l(_R& zV#&{wR&-$2_Gf+Nb>^0bBc2)=iG?X$tT&&c27FyO>3A zacU{ZU`k9Bw~ngjh)4~T=kq5PFqrfdLL z$$W+aYYm)ea3C5H7-xo3yDB=qIxE{+fSU4X7;l*Y%|i6T!J3I_8<;(2N*djRsMW~^ zZeSZ~uH8NTN;xrf)NKYme(ytU$qw}M|9Wl=1WC$cnUEX_EWYwsyc$1=6AU+09DDqw zbmZ{DQNf(zjbM;E(0Tq>%B19f`(EZ~k|L>Y@W&t>rwgNO|j^pWA0su8(&!1ZvY74HRqOYpGrEdplc@pyZ|iA z{$r0WfgZ4=-(hv-4cY(1|Y`l(67h8$^q`$xSG_WH#~HlPPy4>rxh&@ zOP;G_k~u+!54{Gr8+h(5KG@;i5a}A2kAt0+Kj*FZp|PD(9Ij$@KJ?0@yP+*VW}y&( z29S4bWA2T{MmJfAbZgK6bI>OUi7#7B2e9TZ1;%ab;PYJn!gLXk8l48}Ys8h_MqlOa z*%24_Oh_lm(bkb)VEBHNpDIV1a0KRxxnc!`iz9FO<|}r_0W|c!Z;RjX&7`Yl$YAt?mXVMezp~ZZ6#NXjvOvKje(HA zWWpOY1qiiz0B9=70XuKb6yfS*FQ03EfgU`mXBVWE%^5Sou5KXat)A>_<)BpgrXGfe z(ReWTt3w8RDS9;0t|>-=Yo!yigj8Trz|e%!Z8E`O8W4~bnChy*hwf_9g0ul&R(${G z|NZ}%#&taqw{8j&C8OvRw|0LXJ#KV2A;o62dA4Fy@HPMh6B*Dg(=#q;d*2=u00avU z3}l`k#-+<=c{Mua<}>VAk*nZ@K}Y>fX~J~F(XGC#h~gEX16sf>ZcG|Hx!5#f;uZpQ@y~-RRUjx&(pHHJcx(7w5YuA&`I_q|U&;@i#-dukN)AQb z4QCGwrSlcuw{2B-lT_nZu)J~4zM`A*5D`n5qu+o3kN>GicQfuNW9Ow~YPsc0N3B8O zcxdBw^j5|5@Kssjay;qEGwvAXoa4{9UoIkc2Wjl*i`OGh$;TURZkGWf(mm*n>_>*Q z0108->Sy7(FSS*iKRXJQC%1XNVl3Zf-hoUzHq44n07ejAdW%9a#_RQ;XT1sFqHt~# z2q;>C*zmY6It|VF#n;nja#1PvoXch0WG=eWBYlG+|ASH1${M-e@nyqUe)WfpFfEC5DvL zD@#m=|O(F!knkU0e8CkXdmXcpepobO( zgd`@Nf9e#bVFJ3$JwRUFwFSYESyA*6v{cyz%H&^n3%{;TgV5-E2RS584?*q1rVv^b zCY>xEF@rn0d-`QaxraI=jB_*5 zh=&+Dk_v2GO|`fKW2DV&oeH-vrbb#2x`B zVH6^tZN1&-6z)-p;ad@Pv7n(u}}B zaN|9%*~)$$yT>}mz>0j)>;dZ$oe5m2_vGiw$oKD8tvdWViU9!eQJ61l_-}5V)%doO z_)OaQ$`P`Z9*;m>nI2>wcWNuR&fi>heie8N&|+5qXdsyTwvJQJ*ao4o+~N}fppi4L zis}g{pceOyw~bD2{Dr>K?y4{L^Zotfn+ph>Cb!yfilEzs!s=&Wt|(&d#JEK`W1|Yq zCDZnP#KR4=&zK8T5C2p1e!HOO)pgPBN*0e@w$i2jCP=9Fg^IV@;_7O{{Z%|Y zmj%pCw#?CUzAvEo6+P<8^X;bJQ);NT0t&YlZcfes!bU}cg+nGMsq>7Pa zRVdej=E~gN?i&`lLa=)n#~hV~17Y zx8#Y_eQnbdv)ic-^`8Nti3t6X1T`eJSl~6#uZf*;5J?LF&IJfKs?C>p_$$FGlvIyt zcdlsgY7+&V$!%*??lF);5cmASw?O{~Ks`#)@w3_dYw_xd$-QCNXXR<{V{#rx6c4Z`gEs!QVklJu{v<}_ z%vLqYAYZ!s^w%qBFvjLJtb(xl#+#BHqvO4ElySzOBoII^gKIJe4XD$nEcM?|>o^WA zoBp&O;qtY3S$+za-I?3R3wEaMy4%Px=Z}OrL%-jz>8vzoM-o>tA0#skEN2AGBBX_6 z+Jb;3$@|0q)9Wluedo>jZX9&Qa0WZSTPJH-2Iy8(LtI$8)+3AE8=8L2g`*lNZZA&u+OE!gn*4PoaoM(WXUto0;EGjiWSqYXeCPk?Gr>{ab=yYI^-re1p0O4y zhqy*lt;#!>6V{jphc>>tUTVXZ607P34i&oC8iRG>YcuzA_VZRp>od0zB;S z1@?f_uAxRx8e!CmALKJKWi?Twa})vLAz6KGSJ}9NlNdomLiv?_=2M}!?xYvQmEc`q z*!9NvX&H`A8pzcVT(F(<7X){&m~11tukc_jV&8w2A~w!t{WE-jfe##DQ|S&-V#rOJ zFxzoX3J(D$ivw~Bk)c5uyUtzfEBL}CZW^NKwz}2h^Z7WRiZ2kzp&cSB4?3i7CtA{g z4aH?cg|txwk!yj`*Jfb(4=d7rI_#2(sJj*#1@oSRNY z=HPX%Iq>%qX9JN#)|Ne0Xu<>IS|Nml&*H@to|^oUTb>;KkBpUnwGtZ$f@CIQvD4eH$r#ui)OpB^JS@$!35 z?nuNROyj@(xBrpP_j4bURPOcr+YpbQVech=)>VcEwxU8v62DhXm&1a0(9x5`k+2|{ zu;g;x!q~SLR*haiWkP^@SZu&JLRh9-Fh)$~1y~nD4ER$Edgv5LS_$>Y*T3Jj?HR{oA z;BTEzp?y2%*3A_H`l`unz-WU|xm^XIVO@)j6~&ur$&x(LkfGrC``6f{*i{TvQ|X0) z^CWVnLC$EFJI2?WDO1_wzQ=H{o~-By-@0z^gH}}1vvIcKKJ*YVwahR2r27?+CIL|J zSD9|TVe;1?UjaSuKiUo6ofx_k)z1G7uwk<&sO43OV*&e^3MB4W$-Ip_#F)-uu5VFv z>(GIkuxxBF+PY=%JnvOhf`6eMo$=Tt5a9jCBYZ{I4nNm|4B^2An9-+gX0d2saJOWA z#q*)@tdMZW6W-<57@NFXN1-(f;9F4u0N>JDd!Bwn^V!nYcYy)0C^KQ+G!FzP5Nv&bDjvS~08@-|v5VKMA_c378N;xkFD~GS(H5 zON%lp9&Vo{2yS(LZr6bwWBW$08vD4zO~F?ERJNo}aY4b%k6$`JIf8UdYlykMPnz@E z9p4CDMQTyFhfX`b6~9jro{p(ldIG?U!Cif)VF_x_c~!lVRFkFkW00Qz#J`VxATVge z&90C+?T^+ewW-e>7w4Z;P~fB<96)znd*L~wH1R+nPkk)~im(E{=m*FXI+}`$$!)tE8sHb+4J>9g!E5PZ>11_KzfL+{hpCA@{j+yE6JDmyuk0iO!pE6bq z-{;eSLp%0fOIGpuehoxhuGFofyi~ZhZsv%PT`Za`AEIzzO{J&f*z|UuccT}of}g9tIdOIMrDLy<5^KXyG2g51BOlDS5_v&f3t+)>!Lwl8 z%NH!nUc}Hd=ldmI0@kN07OPJ-0MPBc&kU)b=ZbqDY<5dR;5LXZchWHsvX#Mf zamtzU+mmWINO9nn%e+sZH`Y>uWiIV7=K9T_u?&Q69gJeqT8M)$gn)nzY=uCjXd4Um zbl#zfK73m9*P?U)NW0iGoQ1fD?#pSl_o4xGxm?ME(V;Dp`CJ)SKcPiWbmX3~7l9gM zuZa5^2Dst&uga!!=`P;vO#`{kEB!a_9MYWCzfaEVxC_oKoR(igzjCOlk*Y|7xy5o*h-Jbe%VrAWY(nOB#y0s!= z5MvIY3(JVh1$j?x#?I)435VU=~u~6lbGs01g)SwW-BKJ=xHkLMBrQG*aPONFa z6@VjJXs~}*AxefP4rXysLl;dxJ)xjwCX#%deDv@_mIA^<1}P35W}}#wBXny5E!PO3 zqpu-LcZScXydwzLi4nlqg5bN^Jt+&+7A!Dz?yX?zYU{llWdTk}>}`y`pQ#WUlm%oF zYW`bq-2SFX$J0ry0D$;w12@eAnm-PR!t9`{u#O3d%P5^nmzT~}`P+x2f3pBm=tvvV z`rQr8X%W}=d+^eFeezu|s(y)~rt+_O`FTk7BFPP{iqom#f+;`4sPVtS}_1;Oi=JZg;j(=B@ z$gi(jz1~0q(mA|UqLAWq+pH4EQ?KH&x*X+IdC&=#X$iaW*kq#($}oM#&MVA+rI64M zW4p#opOQy;*;4YW`Y-`QSn(M`w{H5n=~dT5vM(_UnD5go&G;9*EVALj_V7@UM2~&< zij3DH7-7YuqDFcA9N#4=3V=fxXl$t}t2{dN#B6O7)D~5QZ*A(fzyeu(2 zQvikKfZ8B%H$Mw39N8YKZ~1d^S0KlrD{^^Or;s;rtDVnWrQev*IxT6N+SKVYPyW-fch!D) zcULDpY$TM=y8)tB9f#9WDsBY_G}-fSxn7REK#GRyrSuX+dE`J>;&}IJYV7kHX{*2~ zZ4J2Q11y~u@PGZU|M!xM^Oo~QqPks|`|4JQe06;1iUj{0PX*2fYBC@H;H&NE0x@*B zbONOK74sb^0oO$zh$yC?_>6f9s#4sKgx4wt12AJ`!;38~=9fgTK?KJTUGErrWILw{ z=>7o~42X;tW!Iw6R*l8pM@IUeTie@ap%&HO*UzVUHq~AYVU4M`v{+ZPR~!jmq~@ir zSe%a9lXBh-LltRlQWK*XsjOLiL5w*zER1ZO@{XyFrOL>!u2r`jkbT`@WF7zf?|Z-yBh-YtLH&HABfAsR64 zhpK4e5WkV_tAfDH>=<2R%Rg*>1<5!%A(dW%gd6SFy>!%Je;vnus~xxDFc)$Dr1=Qm zpYJFYS-U`ACSBpbisG$fwklR6bK!TGaa=AEF#Te}H5$t|JXb4`{}Cg-$!16uY>u^X;AZ!4dxFdUXo z%0NzOiOZo4| z+@|ra7eh{ADExIx5JM*I#;F^ha8xFF6~W4XW=9wFc5E`vLEC9=#L(ZcP5HxhtHF9t zg+nCwXWob&Otq2!CB=f}A;$Un_apf)Jih3(=o}b@0!Dn@mZP;^+#% z8G;IkX*z1M@fshUJ+wm$Hf$MN*JBALD>BvX&O#zI`<(EqxnBW+E5~)7(o#8Q2akDw z0E|)Gi99>C4x)L?Q(EXGXL^h71gkY4=EV~q6F`&EsDRdgnM;6&s}TZM#JB+u`qh)J zhp04up#zA38AtawKV5zX;}M64=0>rq24%*09Wi3H=>CcpE{EqQx_DSo>VixeX2I0$ zo+>yq|9g9`wG%s0gzKypsgC233k3Sw5(loz1jgVaN*GTq79aY?fevd@=uyGySiqH5 z;!-XXwihSeg)%Cqt6N(LNwQjUn;|t|EOt3ogK7&n)iH`M%pXR+|I}s-rfrkEQ*{co z)!ag`>K;?RlKE@8TQQlsUErMLDnK{8x{k9bfYy8<|dQq zj|eMJHFoWcil6AFhvhebt;>s$B(EkzE?7-f^d66M^;_ zquVdM5676-0#N7tud8_g87G=}!|bUESnnMlujb1-bSdmW;{YUPTN{SrCH<$seFloD zltTa`;mT}1ZaijBS`E^WXY;+^YA zD_qv?K9LFE3V1(K>BYR}D+CLY5>-JI9=OUQjC{yw1$&N17jp%pA>eXO3vaZ3ebI`~ z<=42knquQAye%%J8=XaE%5i%|w$uuwA^r&m@M%q#nPd>}_hw`bXbYq~lT*fB0+%Th z%o}*|sUGh!`?qz{;;nWmt9%8xXOmI;^HziofR=H>#cUgVWhJWHF=9R8#lOjZ%PI8J zn-yHeUZcEtB>hr(9q8uqF?X6w(FQ#+F8NpExC#Peu}{RFKZzy3%W?3$Ub^rq;>t~F z*Xz9kPP40O`+=wZnV?h!h~wUqb2-D9LKu0$m_-OjaNwUnR26FQE4&Wre4DI1#dD0^ z-O~g#<*&hXs`gi&T-anP{pg^00ibKL@RuN%-n!;B3l1^&xc{2REDwpDQFU~$NsRaN zg|icCe$1VbORVN$ZE2)O6os%? zHX#Ibn_hUPtsb6uQ9I<2rQfNI&2Eg7YS=24YE^FH&m1VG(dcUapM`$*1D z@k=u)Ci9$%v8xbhtk7IFpHZ+b7|+v3aw%1M36?^0O->^yvVa&x{Nc|g&un@U7i=|M zU}MjCZ_FL>^ZDln(34P?oTgl&JBf!z2!OXlZG7znpFL4Ye3Ru4N`gNh<1B=0AeXtb zV<4C%dvx`Ul`(W2hTR5aS(%${*#01PmS_2bu> zfY!-qf-B|lP6~7Bi)qUxU}dg?ysj=(CM^m-M}4({LhQ!Nw#9HiE~(NMJ$2UHH-^gk zawg|b!su5#GONPIbt7})Ges9o7LBX46CMYUIA;aYt)Y-%{wOCki!N{wy>jF~C#A{G z)d=8QJ0pHH{(+?^v8+-xt&XjG&?qUFL{Vl+>_vDRAMw%X)?)NwQUvX z6M(W9TMxxMld8c%d{bJA;w|H`=ck-9^4=MY4oxfN!+^lp0j3ow!Ggl9j1IYiI@^ia`Lpdy&IEjhiax`Uk1b(9FdZ~Ikw+o%W&$0kbB|hIm2U&XB1awHvOn%b zB9M|^{Q#QIN_y~{%?6goq^G>cf!%oIN^Yq1d2$PPTs-rVl}!M`iiyf94MOF|;~xKA zBw*$s3Z&Mh)sHBjQZ?t1FHm>p;OFaD$yh$7M@W(Fu+ z-rDob0c<6cK7r6d58XeW05R+l-qmQ=oQo{i=kb0<09yoiIF47{$1paJsHhP^3oivXe)I6WgJVFfJhVI2z%R#5<2ju)u>Z5E zYe`c+E2YXkPrjC`Rsk;sg+QXob_1e*@mrl&5Nch&*|NB8mO`c=$qB~03IP&U?Wup_ z?p!o@zqpD6viVf8A;7S3lnhgDl@TxTfuYY^63zI!y308pT6ptMIc%l+oUm)X`o(_a zqDyzY&ZxmW7rhd>*ilIj@DF<6`&w)s%F+2T?!ibNH~#{nV~_`6(V%?uL4$RK#62WpTGsgjDa9D0vYzQ}aC>>el+Y~|y^RCNy$J>1jff5a(n;zfe!2nZ0tiLJ7rtP2jrYok~jgl8_=x1J9B+VLMYjJcMqR6WNwZ*Ls zhzwe=N`{IPqplvSEw{YAeU2EFPzSQPVS8lUx#qmuZOS9|%^(jN$SU1=n<%h}S23Cs?oLMJc*B0Uju|@u-gFb0 zc+Gp#x{Ch?dP(~xyxMJ)eP;(q|<>w{I7?@}J$9+yLhoKtw0stvwE1(WCsZ(Jd zQjilyIQJNQ1k^lc0wtp~R{et3V<=Z)PluC0AHgh6XpRlZa~usL%hqXft_ex$K=QFv zcjD&)C_O1`EoQZU+0XaH8AZ>y$hNATiAP{?4AwjJ82`|FA)O!$n;y+J4PM1IZ|RVj zMgcm@LKYBOy8n(UG!5MQQvKVig292i$NUqK1Vp`1U!%RtNiy=&Io#a>4b)5d6TWnF z9NH;c=iM6^zLvUnK3MhZI3Pg6dcCIa0$9t(VM7H+aL$}@DkM^GWam2NFd7#aF%F%Q zI?8zWEIJJBflH6b`gkUwdALXpS`&zkyfx4b%5wC!9r?(BFZs_DYIRcVtO^=qLl@Mh zHZfh~K}~-(?%*ud%2z=35cCQF0*+X=<)eSr8Jzj-s0_>!a06ip;3!J;@uA)K!!bI* zj8064KRV7|WQkyo_f@D!+Puh|yyOT;P0TieEC4h00jQ6vIl(IeES7=ZfKjv~9ZLS= zDpR^mJLD1`OWyjH$tXFfkc+peGj`@fI(nmTv?{QLha~=?rx8YCV;;Q$604gD;6zat zRmPmkrQT6<@d+a7!{{DJCf#~L`rV+#j{#l#gqd9ui>=}E-z?S|hw4jAfgG*drv|KV z8`*`MT~;ZFOv&vEJspd}Z$)7OGlOjA))B~4mTD%$7HIT_-$0E@UOQ5GX;$%2hNyIP zDcqpOr*sQDm(H~!qr9l({t%C=FQ=0wlO|Y72K1(891=*zGtV{GWftEgiAXOnjnxcN zk-40|@IGgzp$#gWbQ$d*=AQ?A6zUiu>_#M0{>G~?gI&HJHc!E|MAemf3 z+aUY#2N5fIqXDv|P|ut4Sh{d5MVJ z#p`GGnaW_9E6gbxqlnA2Gw~j#Lt8b`h~L`t=mAPw*@ zuq~^F5zFVsC!5iZuO1g2NM_`oz6_`R`aW0ks zm<7;ShEq(U&`FmOV>(cgj?aq=&}x@ezy?U&$|8b=&)Z#sJ>CyH$Mv7biBYjv@nU1% z&$;|_tihvoyRhXZ_VS)(E3kP!aD0~CAHSDwE(yKyVSl6@M7k>GtZV3rt_KH!sSFX9 zL1(Q8Igb2EO$8S{aE>~0G6p=gL7ZbOVmxwTOuC7DrEkR!Al-N59m{ffThISCCJJVV zkYApO$RQP^$Be@2LxuY!N*Rqyu$V4j4zHD{#EI#gC?#1mfiEMN+i zv}Q-vlf{?dNTBMKG3ZhX#7aVv6{GI#D#in$Pz&CK3jn9qrm-K=U<)+n+R5>NWDP0yow|SjIQ96IggAABbfFAq=K=Ii~~SdyJe*U2z%?u?fa=yJJ5DWPrAWcMrCwY$hC|n)6sMSwo-w`d;;m&PkF^isnpY=T;%=xlbEvn_;u)kw`X7rB7^S!doT0ZNZrAEJ60$mJ3=@ z$<95c?oyHbP+;ZrWBeC-X3LFv3Ve85;jzr9c*xZU)6PQMSHcabH2@MEZ>tSszR;NK ztJ|81Y~FHyk124b%@x4oqAD08Wm^6_2{C^8E{m@ZT1FB_&;>B(RtK|9EGP=L$P^#7 zDS-f{`^+C-a|FpV>|%C{cPQ;vY}iiUc3rgSkr)ej8Fv?k?D#+9^KY$t=D(xI;X?P4 z@_hOg!b;Ztf)#v7Z`C@Tsc4}XZ$U&A%&JRNyeBlO0r)JN-zE4oYYH;D7w#ym%WIb; z^DaBHpm4TrU%9r^65uV_odl;FaRPG1V@;+LG+qc-E~)8N5-ECW5eo5p-xd=6gtz*L zyM8ZX=m%fhyQ{{}YP*PO`cmBc{4L>TOqIOv4ahw`*RBAgVwuinz~Tay9h=RquU(h6 zvS1Oe!GGpC%M%40L(k0rel2l!DdHS!6e~lUs3DZ^(v;aO`OeIhn#QH|VcI|`5PLO< zjs@>*Z_*Ae%ay0+&nW)BuNoe)caR-h^>qPgIhL#H%>TABT=i1fz8$%?b^?<}EW1ul z?BSM!1lf~PfiruF-T~)($oTs;k!tS*f`#1l-IcIt!LBUIiHc8kpZd zpMm%;fOpcH0FG}k=Jlj9`PLY(0HNT^gbiC5s=Xk+x$7FBZ&(subyrz1!R`Xkx9+NB zBj7(k8n1n&kUBzy#1_e`%r8&>RA3W5yi4%Bz?cI ze{frW*>g|pmc$6(cfdM8h`o@xW3jaw=;m)N?vE@H%MaIOo-}E^TJXV-yKg7F=nbC6 zspe6PDF`uIWz2lI6^!j%Z~79o4nK~yz9I^ptMJCSl2Prk`ylL>TS>eR#gbhs%Q3=y zBBFQyEYNekaIk~M9LoapHNsM`jocTRU2iODhfGhga5oP~0dY}$2Q=+oTXjErkCj0F z&^}-Xkf>gU!LN$!51$vp{*~y+ZX?eh$0OlbCE9Q=D=Z$ZvLyzh6;Yu;Y_VZiK0 zo1I7qX1Ic^FY|86oGE%_&nJ?+t_K!L2RlU3m1HyxQb<|0Y!I|j_<8dV1a_6Ne=_5z zy^b3@xUdex1ABLQG6iDY0B1nsygZ(Hy8&cjMsV=Nki>)52t0b!e6}z1*Mhj|$TBdv zbG(5%^>*FSbi+6fMvo(R4?Lx5{~q3{ZwG_W`IUtrWQy@KXZK9=BhPEQm^Yvuv(hAJ zs?cafAis;K8SeWr4O+#-iI!HxsMNRRpLP{!#jFCUIxAXkqZNg7BF0OX6EY-LGtd~a z(<9kgUTfW2GB@PheJiPGM2A@Ny2`<3fypc1*yqAl&8cNWu)1>Tct_z_u02ILPY#+f z4-A&TwTMJ;2f#`iWwB)z&)E0yccbgelPfX9h_9=jVN`Z0Z%Op6rCXu%9?}sP;iF3- zi2@ZiWDuOkkySp=>!Gil4Gj3P!FggVVqd_>8m&~bZTTk1`32Qp&&xhDcn8;GnwW8%vh?<?WFJFKB_b>nUzi=fR zPdrrj5%rJjU~VG;37!m;A&l8}bFr?4FuFI7wxooN?}j)zmX&(`VG)${>`2q?u9{Pt z4m)^rVybEhgfTu}%4kFO^S^@bVwc|8RF|(n6V&1xRtv_Fw3BXh(`RY?$YO$|Kc`<- zF(ESL+$LN&@Vwe8C*&H>+q~Ql;oEE_V^E+gI9)0VEpI;L)y(Aa*yN|_2pf&ViQzW{ zbOl_r>XP;+zFGd=F;~oIF8Oi__HfIn$>!+}KJk_A;%eah*-M-A2^((RsuXbpZcQrF z=6{u?Ym5o~1KjO%a@yv!VElP1^VWfDafRf8*myZ~ry+Q~Wj@Pa_U{F+Fsg4E`7n*| zvGcQHu#$tI23e`l!d>@;t9vd2-V}aA_xI%LNVeL>*~j*ya=5HoJiF1X)FYsmeMzdy z`Q1YVgacJVj&^Nefz8L<9^~~wi~kNpPnvLypA*TFeGmmQxm3W)HksMlspKQcKz4;_ zdT?TbWgu{qZAcy#Grevo3m7gPO+P}0_}Nx{(WTv1EzaY)F$o!5Z+F^K*m?W02-a(^ z^4_kxqZLG=-_JjhYfbhO&!-s+Fc29{<<+bW6qqq+f~x z;N@;>zC+7j*SLJb+9)s2+_(B)4N_ZArTMY2(eJs~^GBol{UoDA7mXN5`K@#D@`fj_ zdIm&A{#!3%CBtnsJFgeI_o5JkDPy0{fAN3GGU<5eapWOJJ!dBIg4_YBdfKUrA5#hw zy%!T-aKYBGb`=lZiUIa403V7C_HL&aTuN@9ROul)!OY zja04p>pu@k2n4Vt4>Ccyp!~ePLnkY`?o89<^(~zr*#Q(!bS=laUx9>ImV7vtBGJ0; zKHi8X7n+m-Q?~uE{k;Wno%m;d^vbd_xF>It<*}Wm?ep)V9GT~DJs&yD20w;Hpnma* zo2-uZ6~_f;Sunca0^CTyPAi7B6i(o-6|90W$1pN?!-k}z>Epq{YirVa*DTvmDZUAA zWX(85igjaI-^Uga`MKQ|fnhY58!F=4=iXNz>_aF0K%XszuY#46;}7*vrOoaI8IVj4 zJ?T)1bqWqN?c+1BrP%0l&iHbox31Zi_xp-OfH*`p!`9hYj0!Ay$D9{NCzlkNk9MWy zbOJ~203%^CV2WnQ5rqR_yF%oV&P2NAcj*$8Lw7I05?Q-mFiF^q>m^C;$naXgxThf7 zs2}r;9%;L!F{xFciwq+)Z0FdN>0J*Mc3)0$QAtTV6xh#lI@(LZ=?jHDJdiE$~ zSs+=6Do&3+mbQ3QARBs7mn>QEp%>!F4>&c2-r|KsGS;Hy$*pnZc<|^52pbtso^W_B zyw2abpwNh$&&PqsQMq-HQQ%(%x>YW;3d%s;1MYdB@1VuoR({)3F|fF=M$(zC@)@+i zkOXJ`Te^Q0M920^PPC`(JQFMmHC5jN3U{%~bG7U_icFGd#e;#v$nGaVr1(NtieQRd^10p;y2<Sn34+R#W=&P7(**!Vd=@er7Drn52he#9*s`(l?!u;g>j<@bj9zIs+X<#9g7 zB~iHKf4)bw0JVyFMQ}NmN=#r7#@afyDFg%ep-D?KHBTuefU{Kgn~^#l{w%Um(W@S; zC|Dvi0eS%8RufwhvP~vvaEx)LW)3@85KrBivg+pq#{1S6z}?C?yniHxGH2`}OM%WH zJ%BApaTi8oTx}-B)M*Q9RRViwdcfjEwz)MpAsa3{2C$`!6AY(RI)JyYa!v%(QamcA z<72a1Wqc<61A)<)T;hZEURSHs2=|<2JMiID3~Q)jqBZ_H!h|1#v!avwd#pTTh2on|g~ePuoan)M z+~=1(U&kp+QiZ7EjCQpJ@l2>vZX`Pi-<9e`VMk`pkNRbp*%6>B;yG1&vt9xGWR1w+ z!lU!%fN(YRG7jEt>6$#h1~k^Af8K0B7*C)-pTy$>NK{8BN&+$lk?H0<>FV>UY}l}- zVhLvnj_lU>icG$*tc*B{Y|*h)YXh`m6HCsbOL-7ZvXn!>j* z!uYOpUZyViNmW{DH^fGiaJek}&(L$xD3KBu*e=FJCgvC_ac5wjsN!DhfbfygN;Crn z7Ikb}U~YW4Gt+L{>&5jHp#`M4*eR;cMd~~F+ZW&mzJvNGEYQLl=eEw8foKE$v6{ht zaKgD>pr^A=0R<(Z-E@jGJIB>hc9V`8u54MRPb%(>ayCF!>D5c*zfpdz5+wiByA{5x zew;sah5tfh+1;`!krgS?eL$F0sDD}k{3a8n1nClanZ?Jo;Bn8uEB8J28|W+m&*;21 zcV=zoew~|{nNL;t~q?LxSoOp}GL?;jeGkWMr)0%wz( zAB%h>_}9s>0l733&^YtO*Kt1m(TatxaIE4Q6jxS3!dnXCpaisb-sn0;;lR)dQOuzr zMoQ^>DqK<&Q8tKI1A!FFl_yHqXs$8`jDXf_4?3;prSr_}0+X9Ry1aVYGbia(%G*~G z43r&59<`>u3e4M{e6n)UV`KsM82MRoFR>=`Ke#pobCyUM_!2;~R2j&Vhiyi>1+o! zjDld*ggg@StUt?N+{bais^~T6QHMU6y9C3xV|3T#ujn9yX+GTntWdAp0ws$5GB#fs zv^e2=Q&0{U8b61Cx6?1fnW^34d<+F>^)Qb(7S)%zPYMa;huF@k1QBDN7(z zws8hJIuBrgeRR(GD)Hp)^Q0$*tL2c^6g!|1anq)I*{_AQ#yI&?LG&S_@L7UX%+lW+ zKt1>03$35HPSOH+t+5qu@o9^GowTirS#lrHFjp3QRAKprcSuP2ufDp;Y#FD_Q|p!~ zE}!!+!8xeVTy2FgFsn9p!1{jffg`JOtcIjS4O!l!b0yjR&|)Vh;(3nqJ{h{_=u_Yc zs!_cN%nSU2syd`s^VSd0=xOgGhRT0AHh<-3D2)Ix!$MmUwB(9p>_-HayA~RGWx(z8 z@dq(Pz|G(cKG$f9d{7`Kk0w3oz**+hk)G4HJuR(uS^S#_xtbw)BtE#1)Adf*(S5Vr zG!^|R6?`2+V~chaiSi#k1=9eFS^tSYs~EH~;H}x`%qRcfE@Qb6ME)Ctof0iQC<<84 zz6YPh$GvO;VBPI7eJ!8z{m;5qb`Yc%-o1%7nva$jG3R#lYf+RE%PnHW|GN2nI^~(L zwB-O@#;MYJ+_schkE)dQ8JMdH2Hq-k2OYCb$*4S>0BIG0M7#jLZRr#|s!+8H($z5! z8vyupG;d^QiPC@2w!9Y5t(+JdDi%R?# z@@J=6(uf=CEUNG~!0tMXcBf1Ai4Ban7#$V|H-_tU-1`CZ{Xm+_- z20;$F}n-SKtwGVEVmaRbc#KxoEC83@OR0t~eFx6F2S|=gdFH7HLSKXS*k-=hY10W_xDT%(aCtKvPc0p)oky-o7}@#ia@>@%r;J9^UbqkX&-WdriuH2`3HH6=ijcEq3W zY3YfXdYq*2YPEs2cmWdZl!*>VMrWdBKrpDVOG;POg#i~0{1I54R7SBmdSpVd4Joks zw~)tSkm4s>0x`$w7ZcI+Z+tz2we)x5ztqFek@pV{uXjt#=@g|Qx z9Ry|zk4!*Ywo=bOfzM)F@}HCL1)eC{z+6@||<*}*(y_VLOID9UEF*D>P; z4hj&$FqQ^-1dlF-fdgwW8I#VYDv~R1Y-M1DNi86*z=5e-wK>({SUmMq069R&Td@c| zAayPa&OAsFa3V+Gm|myO?VLzGopm7zs&=|2!C}8O3}w&y1;WROD|E~8Ylq(ey>~+( zdGoz881&-XEA5MXS~N+%_W8V_aR)%o_l~8fD49bpNm$aO=a?3PxW}UTll$?Suyk!6 z6}xP=k}?Wh6`nR(z$yh+s64BM$i32($k4&Ql=GSIARLKYtehn(SwX~)6vOY#e@kGj zdC-E_)C`#-V^}Vc#JhGNo{-M~>!>#1ra0nIWpTw*Ki^w+;;FDwZIs20{P<&s7g&fd z6_?)+q1TEiRIm~U#@|HlZ>xZEau66LeT)K;#z;AtfsdjT*EspBa8vQ4{~}Sjpk(>! zg`fnB%r5%p-^{l}XplT6^eULcP(F%46xEE}MyGepJtcK}U_J@M6EzH+eir({H^7ks z`Zv&i?^Tn9-rt|&*6karV8^`~r}K7^(Hrjse5g5o!Z=5Hn^;83CTq8m*~ zUx6fBGwFyE)U4+!ZS^>IUnf=5Q+ak4lGdXItNc!&P6uOt$XJa%CIz&*D*}-z&MU}wY-;QsG_T^oQWq`yidx9HlI6B{>k4&HH&%3c66p%sNH%#vuwJIw|M zL=2R4F}Q(mg6sX@+FUdOJS)o zZBhlR@4nr(gf?2z(V6aVOVJ5hzyLg_^FKv_K_$+A^W8=}yLhVbCapuwITP}g<5?S? zrGXlz{hqVjoyv6YTXvqlpR_cg9=*>Pn*hmx&XdH)=U??7QdScilpSG|Zxq)|vw-sC zH54BJo%f83!bLzo@X%XdQvzsFz~HBN)H;5ibBG0qZ@DzEWfZT%gYcCW4Md;tx%leB z%98y6cqfisba^lKHXXx-VhcLaJC!r^4uCi?b*aXs2d))5i;JlN^G&rHBx41B;Or4a z3rflmiPWkSeSEr+cgaSX(P1ou^aO4XT8UQSkMMrV7ApbOO^*q_T1Z0oFj#n8he2MA(`SS(mai^_1wy%Wo(aXzD>v11Y&^D?G)IBu zWb~9P!K4A~C}5EpJ%vtJmN%s8CACXr?tzy2Sew9dL*|71e9X=XMVE>WS^?Rbke))? zA-D6(AxUAi3dykbSasPpd6iB5ve*O>n-&#N61L{WkHdYJ9X(I_xdHegGlv|5M33bb zK_hip&ceh#U-Ul!k|9bUmBj@_*O)ood}Vr}H`$o!@kuUlI@;>LzvlgifA_zy`5&xi z_gy@LhOES!AYyZpghBhi`CtCuLvryc;*sAehXdFe2|KcJabSKap%jo~o4_0kPf*%| z3dukJoBuqfXeSY~SW><|rOhB=u&tL6;Ts+6tmGOSp_Ikik$mrZc^tA>#TbcCQ~sK? zhEi3pm9`uV%Ded0y$}<9K{}dBzSu6Re)uANg2fIL6@3NJK|edSCJR^=*Ww&6YfLP_ zKca(d9eQPB&=TU?kz~&2NW>A|e{#J67t1qk(5E_n5j6y;CZab>X#2qEJP2Fr1rq&)yb?k&fCBv)YCcI zz)gpA9Y@0?9lBgO-9!md!R-&QpaRJkpwX^IZ64Qs^P%zlx`{rD0vsDc_UK?x@!nOI zFq6(mx}wipW@SRs<|G$vt}V7z3@0?dZihAh_n2*=FZ{wbbx#JFSJr~8R)$4KWaoIu zS;Yq&T2^v6)JL&pQ}#Xa#re<0?GQ{geHaH_h|t-@4ko6G)j&>Quxv-)*5snlL43=9 zF0N7(@?7C4KPpZV*5u>7cjS)U^1rt9voM;?(07gR&?$eoC~!U*uQUoH$FIfq&N)nA zWg9#JSpFP)TR$(ay3wgNHVInhGb(gxhz&|^UD%St;LPLI2AbX>V}p!LodCX3(A*cy zdc2(#o`di0t!qI#uYk4zd+OJV^25vXzY;p3%i5!jy(lzvW8G_uAYNu6E)|_cBGH}R z?}rn?+e*M1Snd?2&HCKHSJtN6dTe{OLJ(tZ;Z=N`GQM9$$4#l9exBH?Q!ZoNAxSOZ z<|H?scsnP)Iw8T$3YerJJM4voq>jeeM&BYY(%cMGGv11RA-Zk?lX>iLl)=G zD51~CBVIi&EI{L!4OTD4LaS}_c*P6jL44+T9r^#9PVA`dDZbq9H<0bhvvuTmC8aqj zIno)3YLsdXB|+6VDk-xSMRj)m#L9r_OZ;6sM4$7P*e|0yFsqLQ$B*1Edx2X7U2a=l z$m^YAK~Me-W(j%+WEDZ30xGy%hjk_*^D#b)ok5(I&TfwASOmQmoCG7{)yhwS4p*e} znev9@QQJ!@`(6y6fp))kDAj&B@@t%}0qII3{mbmMyKzlF$F9O69$Fnz<+lGm5q^Vn_BnCvtHL^;e> zdIHdBxV3=lI@_N331tmqH64RpPBI303cf`fgHq(=lScLv^-a2@>LM73Ao9X}1}F!9L|I;+zeoja_I!Tfr}ju~2OBPS z@&468#tT5Z-HyCY7kF;|{s1=+BtM_?78|Q>hKyV87!&!xz&~vgpN;G&TM|4jWrH44 zKxl5Nb+@^voOW{I*wz5L&00WCpkam)(?;kL+m7lx*1Im$y$cteo>X%QHiA{=?U<}pPAZ^tJMs8 zGh;*Npo3-;jwdHMxB(JS<7CEegROD#Z@LRI)jZwuV%BgI5b|?=$6{x)iv5D-b>Z-lyacO!ZAsX1++-@m@`^Y7=aB;{K~ok%lVCX$hyey4z;zFjViTu zj5T1N(3F>2u9OY3`+jQ!QFE=y>7yHjUEifT?UrV5MacQfN|s}sDSy?$YuVA z&sa`2)^ZdGTIRuX-XRqT^wAwsys$@c04ATWvOijnmJ^=NH_RHT~<#Fn}pQuvO$t_^mUn2tcpa zu}mjwH9rBb@yc;dG<&k`pjBG~5dK4Eob44;SxYt>yn@0v2gVqO?{u;_cy>X&t;PI} zx|+uUl_lI^<#p4{)J_i@JU7OmOVaO6CjO+oQ0-SV^9T1ad$vaI3=VT^3> z6sj7ZD~;2(#r91ttrIqdM}L1#q}&X64*=U&*<*Td%6R+ct6Z2*B^A=tJv^gxJRd~I z2%)l|2DlvqdW*Gj5MVYzv!f3Htfs}gFm;-Wl)y>qbdlqX2n=MP|LxFjn^*J(V9S3a zeq`Ua$^qjiHO_k>_q0_IaBsMk?otDM^3DwM=qg&=F1h&P1mqufQ4nT;be6^z|FhC5 z07geAom-=;N^&w{1!7MC=)gCf){nhotz>@G@ceEk?xdxO>j}wHN;k)yS{ps$)>20v zoK^o5DDRVCSSb3i7g|y7Q@X4w9s`$i7)Y5;I<9mAz;Zi6;-PfQW`Vj|7detiog)BN zAR->`+7#gL>hVt4u!8tV-afy{mc@;W{+QqV)9cMM7+|}weW+l>P@c`8lbtRaKm=c_ zVlG>X8@Qc$;bvY5Y1BYKeyxjE_u(`fYsKAP`3QZo=(u`h+w#e)zSFDmQFy9Q1h%aD z8lcTCo-efn`G;fQcD~OgJFc-IOXmN3x%cQbXRMGHefj*$sl$uJ3j=2`is0?MYQS6~ zQl4tK8U7@(ENfDU4$5tw=@ydj5lfw-LJRZsWrgExN=KsyNkGd^rfB!^*Td%=?GSz# zIBX;fgSsuNmj*OICd`?fV||mM>tL9Hciu!n?!XwaRVT0`eH|=vW~?m$z7D+i^_a zL3lfQW5zUZb2$%7#^Rql3YX@F343=akB2WjamA*c27Mwd5KJVK_N~1NI3N6K?7?Ij z7AxQ~4LyDe1Fw*I#p#RAEPIzBj^}Sa3lq+H699=*5ujBuv;2KN^WSBNZYA}Fe{o<> ze!l`r1QDGif5At%{N|H-_O@778%D2 z-i&w2pSrdwh*QGr_0Qj8=vUow&-}i(b&y#;%V5Q0ettCoGYb z2Y|YtN3y%Gjf^eL&d>3(lNsq^cj@+e!g{NUpSY}biwM#U`NQ)S-6v?@E4|6%Ej!h2 zp$=Fn2NkPQvXDp83qy6=rn3%&-&&*%C|^ax(8htcXGirAKfAX`M2!!gjeTC|gg0E~e;HDvv)ndJK8RTTIR z!6ljO)|E2|3mL#aeeiduN_43y$Jb;G5Baw?pqD`(?Y z01Fe6y2Lk&+GpD|9Y*lj4X0R!+I5lpRpgP+h?J_3{0gnkhkgBCl`P?$78;4m-J+=xP237WSo2kouoPcae>gHXG-5Jb#(aDssdJJ;!~sA zY^t;ZORL{r%&{fvI6U#Vv&$}-az}vmR)|mutr?{a8LMqkM4|vk+D1ni`EN0!o}_ub zs=$?4Ci3~k{-u{|Oyuw$(=K_*xFOp&*6%H{Y@|CC!Z^>X6Isa{H+d#9Y9s1YNL_H5 zdF!FgS=Ds$ZsoB>q)+T0Je)w;t1wwxnIsp&&Vj$~&u>1zqC-4EGYlGxfl(liDz!pr z)jgo8R0+XPrA6Im=)$GosNYBUAO7e6r(FjrsP9F2fPh;&MZ0a|w1hV**9w~K@IBzA z)ALG@?%W5YV_ZRF7T?rw54~_pfnhoc8-jiGf7wDu<*jp&j;HK-q!b*tESlwWjI80< z$yGxZ5?jU|xl;#<%N$5DqRT4=NWR?NBQ+wKh(7btqC;_e^kNyzC}C4?injnkUUD+% zz3N+RA}@3c#+r3V7m3Gs_2d8JZ}*Iw>cG7LV_$u<4^89~OgJ%6T6M-N8X5wTvAOi+3iIj{@ z39ViWIF0B$kM(gU%X0dKw#sm5v|_sHh}Qk__~IR(RfmNrIqP{XofJU6?KQ^x*Xy+u zpwN$>f+@dl-XZ}IFZ(K4 zO-AD5M|3MebF8F`FErpYPY__=?Gy7%=wNFmjOh?5XRiUZ_24`WZEe-$BA(NII;Pu? zZAKq_DHuS-c$|*>eTpxbxbmk4&Enkgw>BFtG7sCPjmTE-S`mxB@ZpM>r+BL}*5G6a z^-`zpWt%gvV;&YAevVfLmW(^b@onlE8f}mRbKkoJ|GKK+>>`I@bU3{W}n3KWnfS1*Y9yfSDQ^FHDkb{7?V#pPiTu<|09U)(&y7&Mr1*(DBE#e7t3(XY zE(VLOdvy5pA^FTM`M~nMqT6?LCc50Yfkp>XYmx|rT!hJ`V}fxd)M3IidTq%ewT*)W z`Dc&UQzn(Az!6e(8JM1bz3D5}RcenWeZ0E|McDy%Sq-J^E_SfqNkI3BtBL7B`aX?P zt@P>!df!j($^KAe-Gf(|Q~@)$R20TG@V_py<$l^I^|7%Kmdowalm$UQ*F|k1HB3YWQKJ>2)d@cYoGi1 z$>8vM(jq6O;d*7EIee2TWh(s``Iux=vMe%f_;GBWB8s5PJtWR{_&p%21Y=E;+c#n$6$B-*iO8*XYEEmY<^M zB(Knf5!ezcSf}O$?3@=!9gTh%s_2&tILKzqaz?1YZ-w#!5vTV5v;MnERZbRwf&wFdCo5eRCxb8D249U{?O zuw0T#{A5AfXmGP(CO`XQ0wq7jYFA*;#MSP%Wa7SqR}K)JCx}IH-?}Yt1t^kZa{(Ve zY+w9zPm4>qMor$MsWUYC&P-wp3rII$YJ(d5F}bll>mx^rrdM$SKHqv#gGFS%@OmjN?Bt5~-+Jjd{&vlO;w3&v2F zL@$jPMz)=cq>Xt<`di1&dLMLULtZO~?m15-0q$^l+!iL0a70Y2T@#R#?#AaU08p@1 zqcWGEv5l5XkPMv4Q}BVe#@>_7$5jA~?gxY6@vJmERd?d7zyf znfty*U|V#As=O+gImN=U5q!u}iFDbj47qwSYeW6FY9M1T7sx+ClN{>!osVoA8ghmC zRYs2Q0m3SCK>y5vnMI(Yz&;o{M#7l}BK-g;w-(CoLhv3iRv5FSsJd$6Rt!_TI z3e>ljkip2-@7MbI0ze&!)r&iABuLimN{YIs^c|{$H9ls)kIbwzZ)0&-2wBY(KMJR`XdpZ#?*deaY_JZZR9@)HhnR|FtxM%?5hz zUvq4}VBXt!FaV(!T9WDyK+#(R^z9H^L|`3Y&+huj!k6!G#k5r3n=IKmhc9s(TYk?N zhyWcU+5RLu`gk8K#Q)N5_2Hh)N66gcidV3E@pV!|b4P7H3kHuZ$DZBF4 z`yBJF=@_flZQSBF`J2yKEpoY40>oz@j*LQr_59X--!e(NQ#O8l=EPvRDB;IzgAy~* zh#P}P?NdRE)5B}2&`%(3_K5lZ`Hj))yo1)U&ASCB(`G%Qi##W)co$Gc0rqqL9YNJ- zrd?zk$MFqs*KHiB9 z_{jc~HX3W`bW_Y4eV!Zr-R> z5%={`Xb#<6G2MN#?Rhik6rJ+Np1Mj;3MOV;@Cr6)N)gB`-(}^;N9FLk-shx#(AedP zVs+oP29{3lOGA|YXttM-lRE=0f!_3W=+{vO6906 z&dm7Lr_K=z#Z+TZ1ZY-~7zz89T|17v-PKN)+$hXDnbjJFQFwctVHU8&%0{c^O!}jV zGP<{fnG^Zm&shS0cEClUKRa}fyKgmBk(o0c-C%b^ zYTtkaIYaLp==ES=ZdI7xsH?F>ou9OcZj|*)-e;;pYw@OsOdG#L!dlnE2_(9p+=uFd z`!RsK*^Wh!Ey#Xu$HlMRkGy|1Iuu{GEvBs)ce>lua^cj~$e)K;WkDeYdbf?W_;Fql zG8zGZRe@uF{k(Z!0BFRNQT%nkw1Qi5*@@8tDlU_WhhCAH=dIlzJ8zzcTfSpJ^-@z_ zDGuE22%ifF2Nk`_B?K)xjQ$h4SD^2!Y7-x<4XkxvdF8gZri+zWRtMHrJ{kAUdQ0%& zDRz+ND@ea2@MGDtC{&8pvZ^004(u0CP+2q105uT(2F3#J6Aa_pV@nIBh~@W*rtEyE z9axL$7ARZ-SSQl8PWzbaOIpPeG;9c~ z04#uL-5cvfE*q-}pO0v6$Jlx!Q0Q{=Hbxa$Z~oo+tVM%#6FGMh#AJ4wx{Y_3#LDqA zLpcnXb8V?^YInuu+9DiN?+Q1mwk)ly@oc*(414>4dWy2W{ zXY$$2M}8;M1|zfAcVhn;;Ed7v`h za%Mkb{wy%@`bs1|T*Zhf5Ca@V1j<0MB@%#cCRv*uOKf8R0W4iX7QFbgm^IG?5V~>l zlc{bMxS?Q!$Zs|_YcyiK&tZ<%8Cs3EVpiCrTL-!48E3q@I7z~X5zw@M#?vL!5cejv zA6cx~HDs#^J_@jL@uAL=S>KkkNq;%AyN(#@pvsC+kq|{`d=@1m85~nrV!_}6Gohbd zd|VG@Q*VqskH!KH^cW1J34QbcR|56g5?mOidW0cO@(HoAUacG0m9~+M|))e%rv6F>&sff+oDh;gh3js+%mKF^Bb2=MtfK}mbRrjb$LGBFpy3 z4vpuE{jvCDw*`YI(|KelOG;-BddqT*Tp)c$7wBpqs7?4DO%@`jr(S#k$SJ@8f2N(Q zC3h+ILYwo#%4P{fmqGxWPbig*&*!%Xsy#nOpbFBRK%!`29PIOvS5(YCe={5{9@>-y zTPJ!xfhhmx0>@9LO$W-^Byfz?oU)TA>nf7`o9so18c>P-;z7dHu=A0;)MOSWY{tXZ znB>~<35GcY3!LSU+=DdbU7jB6Jr13~F8RXI%iObKs8UyCmk8@OekJ#>03V|ZYuWIs zgnS}HkLr}bRo5f2QGpJ$PNqGXQVkGsrNEIkj(I~0(p17%`jMa^M}STFYI}-C8>w69$Q4yrqV{-Sd{}S8dVpyW3KHbq&;}0;aABB zFLGfnh;)naI$S3m2^<1*T%gMR6|k4H{x$1qq~cZ-N`wzs_<@S0#0|@9$EYm zN$q=?&Z0LezDwwHS)mdoa+K_g-5=$0eni3SdK-vJ0fOWnUr)+T{LY+rmy7ZrR4{Z) z@R?nOqGEuXncODK{M30^;?D&vT+WN#7$>*DHFBM{vi%@^ZIHZMgyuCth|Z zRTP*v7I|1F79&?au7H~XaC}xdFgxx7#RAaMoaM=9P6_~MB!V8zl>pA4cmA$I2P3<~ z&ag?+#kcrUyT3n=06Vn}c*N#n;7sEiyybMlld332f`p%*8c)A}a?r?0)*b^NyU?Fy z^>4*Q=56O5X*H+kQTJIa^X!%{dGhFEC=hb&@>ZdtSJM~{{-DC_zz%}w4T6qZvWkjJ zyRzV->e2GQW2l!rvRWPEoZ>@e$Ofg$g&w!H-7dB}w4IuZq)WN;@{E2pnYY3H2-)>A zC8Gem595UJlW@Qc8|Ap&bxS0+Q_@hWgD1$Quxod=y@!GWD?CLV_`sp6+F2&Z&$R#| z#!UC(eqpJ$L{Bbnz*jcX0z$x#2BO7YL=UaPyq#}t-vGTV$OjL>ST1Bi3H$tE9T+mk z``p9rau(8JYaYDO3vr7A*-vN8!#%e>89W$ZoDvns57G65Vu-=bm4%)e1z5+$in1TP zb)_meT|Yh}R;3ki$BMms-SR)0snLRM@!pC>J#D5G+5Yulg@SIshq}EThgIRq_}xmB zb^m!`p{tHrIK9@^Ofge;iQNKiBp+Tyc0Tc5Yzz41e5KP+wYnL7I42T{1d?WS}OHs-Ew*mNl+eqS#ue-{gMbznuG1TvVpyiS;kdsYi<24E`AD2Q_ z;qmC1MbZ=tQ@*1|PuI0jMj@uua&oA6Zb^TQXCy{ZhlJ5YWXlBuDT>2a)c~> ztTiBjt4?8;>d+shKR4XCYKFWcmbTs(iz4NN+Jkl}7D~f7_mjW#`yL0W!?Hs4vI#Oy zZ!u{Z&n74|wX{|g!hW#5h&*)Pl>b6(&}Rd1hdpEpMfhIw$6I^7z<- zID(plq#ZD@VYqlPlhbsJitgSzmu8^s9@pyyhTM_2$!EAzA|mC=N>bl9sFe2(}0!i4iadg;Vrx~ z;%KZEmVx--Eth8+#ZB5?v8+PJ1Rn!y&6h_o1}CmGVJ0cK7N2j)8)J|$-migxEkunNb!~RI z;6cZBA^VM6NVCHd=vqmbd8qHuI}<%;f7r&mprb5{>(uExzmYO zIFLg;uP2>myJUSDhOP4!K(~r5Ey96OHqT{_6H&4YW8#KCB|}a+f)svM!*{1qm;r4h z?EKAVvtArr1l3~;Dx4g1^bW;j5EPlBGqPFY`_f21xZn!}Gn{hYS+QC@jHI3gq0<#O z3A(NWv$zvg$%u_Ln(s*wkt=pjNhNFf&+J2F!&WcVLOXRs*>2*3G+k9kv3&pocuFI>wN21f@?2+-=_yT!yUp%ajK zM0Cbh`|F@0kwESA7-@4$ZUu=e%eD!)-V?(FXd+~Zv;u8z`S6J1U)wRYXPcdW=GFPi zY1f1zZrnCupwaV4}fsZ0%aT*6%mv%FuVYA`jLE zF6&JN18?J=i-%tdf~cR}oKAr5NjHfQMnSN%a}cb9I5EnWYXEz9*Jpvb6kRr=J*E((jRlDx07 z9{H0Wb%W3E^CX(B;9RRb6RC2~o=IdJa;$mFF^#d?VK(E5R~Nc+0nh+#YgppDj1&+b zndNR!fn~=H^qauF8&Tq|)RK*#w*fY4kKV04uf1QdE;D#XER~I^nAyC?g)HV+ z?Un#c`~tj|TDKm4u1k2OF$p|uzu6zM8lkm22Gi}AMV$&y2bmpsoeDhibplIE>wa!7 z=cOw?t}neB3g1^3Gyf0y!{1tyL_VW(5V7v`dF0aK(=Gp*e=m}utF_|cG|46a)Mrz) zsMRCc#Q2Q0!h&4~;+HG=W`kE%@+n|U8ia4f!s~HYEFh5_%)}wzXd2YPy<6iBT{`T; z=OybzP5NJIUe1X`Ly0a$AugSxJdJa1e^rLBwrGWloL zGIn`23$3^^EWY1wky$%kD6n?U1a_b+U>WE=gMyLFD{nNf`|@NO$t^V7d7cu5@lAg= zwIAH6M?N@Nl9O`q>7Tiv8*8MUuPqIOHT(Jk;E(_I;F^G0^ZYVCBpO{kjo}sR-Yqv? ziw@jWjB6q&GE7o!M>Aj{Fq(ugG1*?a@Da4o|cqv1>}3NtL`aJWZ`AG*oRg1cr_BE z_YSxA!j<>dN!IGh=|?q=c<8!daKS?ldlA-o^Z0fxh&abCslmn2sKTnnq*RH_+Z9_1 z=ft49{du6+QgWsuuK;G_Hjl{Fcn@eiM!Z-<<^oi0RBo$UG}<{=_}3%ts@q{|Q@-n} z03AqsG%a(EuN-@8dJPqhVdn+0|)&n?VYm4g(F0)x>>{@`xe5LvDEs)f}j*#jA}kH$Zy<0i=_7UL)ddC_)R9dmhB($q}e&nl@G5_VqO(0%1_qant$`0 zZtXqOF7^=m-kQJJAY|TqOo{Elg|5upj>-$RYs!R*!`cRf_&z4x@`-akdZe$77jAky zde@Fa$Va~ygIoUBh^0@MGI--ndY7D<|GW~7nXjxU&K)g_Tf$VQUPEM#ndZ*>ykjt? z3a`NbFft0{oR)I<05z5`-VZQ5v{ZC8*v;qjb18q z^-X^8$@l9@s(bindx|>eCbEDBn6hC@ z?=p8Vhw`M)Y>y&1pOj)IS&)t|5|CK_^Pc;N|M2hskHf-5$E3MPOaFjZMzMwPfLh3H zD6fDzQEmWwO!lNHy%Pk=IR%86(cOcF>Gmd}wKS*|5`$2rTZgCuHnEMB2)_8aMO0)7 z6()3uZf}s?a<_rxq;%BUjgLYTV+z3#kL1d>4(d86W_zQsy@p+)Ddnx5#2`)D#?Q>DJN5NFXsQf-O2>-7o6Mo>kTt*d+zaHbZPB;GD+$vSh zHpMH3fYpvXbfxjjku2zggXndphp{b1q36H=5XUGG>k&MUV4Da#83rcpHkgL26nG{9 zOn^Z&Q#C~kR9|^id=_|E<2hx1Zh~%w5!4{2UEx*Kyq>zb%Tj*LE6s<}BxE?o&DmoX zn}E!IW?R)*@&$G~NCuB0$}|jpUL~%be_N&Xo_mSr;B<30qFt1{4KiwP?|m1TU*Cy9uwN08(!Vvjq<) zmeKd&A5Gnfz81RdD$o+fIzN?R1FkyXg3I^`q-DG}rd$*vnUBvq=%nP7;3-r72km-# zp>$pPinbPU0VPiF*D#>-Sro+nEqN+fp4cEco?N*eZWb#4`LK65`Z%0 zcG6W=?=S(^92AX=2+9qNHV*FtoC@YgPAas%X!+zg5`4YU2`or#jQsbCi!P&@r9T4C zF`?tRBY%G5{;`hcgz}j*xX{Gd*)Z^G3tn40y^*M9NAnd~a6lQ9;(v~pP5%XLZRRjz zpHkk=7obM$CLTW3K8wQ7_?H7MY;&fij$=ulS4i-YQCZGz$gPKk<%f26U6ZPQrp$vt<~J0#-1==tC34 zCI!l$dqW)4kv}BuEXEwydM?gy*TY_ZrD-%z>8bnIhds>tzMwF-fsft*BQyf_c8ZbpHj zg`{F+$*v^Xw{*BFm(nA#X>4mhkdW6p?&;8)Z(MyOU3GE{E-5$)0Oz*V^bp#5vB0~=D)TLC16;FA(HxJKjyU!M6J=zAj1~l$hef% z64`$oi#k6ybHk3#Hp$oUGZhFhh_pGW&%%E@R$y`s^~00JH+|uRedcA8$jKR$HQhttwc9eQ3#zwnHrmfh9h%p=*v;|<{Q;4Tl|2angc-h z+2fpSg<0jd7r8RJUv9?QEDDD@7EL4o0N}pd_4WKr9_aaqNs56eh?xv0XtzCFmoCmj zG3;pR;1!jVO*XlmqH(ThG*Vf=@(rax)NK#i#4ly{0jOy4R$GycFYDT4;qZTKOLVdj zmui(xNB$JW;9Zh8JQ>h)>D(E1R1M0-GV z^XvN5|Yj z^!DE*o=Z;pd``+&6g*j#Z--65JiQeHAAB3(c8!9hff_Gt9yMJ-!-~J;{joh&7WB9= z9*1rpB(NkXqjrsb=m04`a#OC^yuZ3Dz(r#7$du=;&yjbaivq(x1^l&iALlZ$OV@^R z0NToc<)U#9e%Oc!>5zdeZX7=$)APUDcoclU*-?*-;swY8|9Kv-z%#zE`#D<%v59)k zo#L?3;CYN?8Rv91xN<76-5S9j@x^$tqm?}Ie#1uI z9pP=M7k5X#DSEUi!b<>A5VIOl-2>FMiqI1P&&M3X@DUy25)UHBdZkUc7qOtz`39W{ z(n+6+L7dC@3;;g`fVg8$-MpE0g^Dsp+!#|sN8E;N@$;rfhyX@NZFK}AqkE0FmSG}h>6 zUcnR_$RBKDZK|c~8wK0`@lXH8dsV+j1KRS%WEdMQmzqv7cbr}+O#)h7N4FbyzF=qe zZ66kXF)()xwnbBu%vfL5ztiR?dqfAe5o%dq#isK|(@U@EN{L0d(R%2qugX6bMB}p! zd=wkp_&;ZC6n{>%bt|mJ5t(3&9fiKx_uH}R6|8)}CnEz-f!gHw^`Kl3exKsRHvzrk z-M6OU`EH|aTQ9Ena}0LpjErMhK#d@0(WY+zOz*OzP;2tom|M+2V=A?04PbT0vebc6RvdJ%SutMDFx^;`rLKpf{=9ynR*- zHHe-hK`re1L1OfeMEsUhv(r&Ic3>-i`3lITSGp*>6{e!|Ki^{8 z>=!gs|Kv};*bHmx=E= z-j&G>@Uioze2V7AlDG0S=5d0n=ZEsE{NG|FuE*F)f1gFn7TZ;yN9Qr}KWIoncZ^&N zVhjx&E&X73rQ}`@%FM2xWXD~%p!JyOvF-C4;x0uY=bukOCV!W~9Xr%VjJCMPQa6(p zFzd*3;w2|sU{`{4>~}pGHHrcg4_cYP_JGA06TH(lB+H@ux}u^2Vd2UQ>UzwQ0Kt9L zt+r7d6%YlK-8nWy+T^Vu;Mg(hCe@~|l%vO42QOU)m)sq6Bl$i_Me>F{N)V+h?n;GS zf|uu|UU}Uc%^E{jh(?cCQVEZ}v}4`gyZAT0qQ!?ni+eRmJ^|U5LeSz&k~)+R!0oX> z@o}Oei`*^5My^pT|LaBT-HNR*(cR3VrJ7O~-2XxS7kaWc<@ zSi-RwV}mZJr}j4f=Xhne4uF?9zy=mYU;>6KW0^Z29UYya+2|OI0@G<69|efz4x?`J zu~P+XFJJ->4xyz1(Ykvn4jP;%;_Au>zJBUwPSGmc>fgrjj+H*`S~Nh#tqNPp?w-l@ zjHr2gXgjtpbvtS$)UQZnWn!`)2!5blX4n;@BOWQ`#6`gEP+kfkdU^8V`4mwt63bm0 z^c8aezhGRKHP}Qk^6e4C$4_a!uV6OMvAy&at2_y!zSAxm<>S#YLwUq6g1%x^0T+X0 zv7)Aw8#x(Qp9)8o-00x_z)OK#x1+{wAZPI@tf}#3p5hf6-W+Z+a>j3*tXPXvQ(oRm zFDyTC$MZACqFW#uMKJYvWpz=RggHAu6WMfDHDLxh`8hS0a@he`fu~I@8kJs*6oE^{ zDS|bZU~d8)PClMdV#df6pSvv(I6OsHyM&loRh;FAI0JAow6D>j*R@b-;jHh~7w?_9 z?!U z7EwOEAUdS2b-nr&z1QlAUZhhF{CR^@HtY>$@W8Nf@R+jJfNF|;&}2K2GDxtD&mcUD zd6%X|R)=0Wk5ed%M$LZt65=<*jXk|F@7o3K30}Dkb;s8Swbj0sR0wZR}Qim3-$}yt z-W9AK+}KF#N<^-}B}NW^(|I8kZ&7SHyXSvB*65A*HdbtpVrGp`6hc(FCjjTM6n_!R zUULH#2p=IfI02S6|K_(5^J@Eb5tYrr5~$gbqOO?&vGDFqh>x`brqKBh71WBs%rjnL z0E$UgHRzR$D2QHL>rRF#xqIe*tH$$}Qw;r=HmWO(=iI}g++pY(mx2Kq;qtkH!t*+J zGpUKsZi}~UdhmGzIA$Gz-yzw--4uo#yxeC1iF3Lwvg>&xiXZLjyn^D9`btxFUD?wfaz61!;J^>8CeOn^NjE>(}C(q&U&);Qso+^iz z5QiR@nJJUW#CT)MyB!=T-#W4FX{@xPbNcGD#y{txJMD7l9&wMF2|Ts0>rLkVSfOF- zM`cBof$CsTR-SFXV;RG?A^&=tXgNT@y1GLb{hjCf+>@uFy2_zq`8+Y@m6TRl!lPvuhZjqjHCnYCFW&?rO04Q@bZ^gKd(<( z1rX}EhqoUYrxpt4HxQF$b<9jKsWHr#i-Q+hB-^4n6DH_bF$&~|3sN8(nWvMh7_Gu* z=nq0vvQbllt+3o=langrGM1YpqjU0}iv-y!;=1S<_+bS#PsL2Pt!@t!s-B<78&z?M zn58JldsDQ?>q>>*newy^r;3MrVM)kSIeRz}h&s`k{tHZ^T)E``mKk&9V{(m5a>Y5# z8yC^+G%viH;zjV`Y86+0_(Wl@_(C=o{dj3`0ej`#juuQ*Ex{^z2@6+VdQ=x6#;>cy zn(rONsmJ0*)jN}b5-^>6j0kAbPT==^-vhIX8Ye>O5*j#WL4*|7ZvW-Ulu;anniO?? zDrCwCaDdF9&4UMtU%H~6wFU4~NJJ@|9NnA$aUC}FB&M83~+VZV6qR6tr$jK(1B zY%*p)xQK9^Y`zLk4E#N*WNQ!_$KJ;GL@v0p98mHg<%-W-%Q}9+*d?`KJdr zX@x?qu=*ZPgp--k1mOfWqW>ZIzpcg?V zNb{^yaL;oQgNb{>&NVgY%}3Y2|I@!@9CF9{3>bdPAk!t9W6i9rQk}OI!{SyIHU}F~ zw;@Bk#}~Tffm2-#5vN9Te!d>zmgHUkc1CHh*QjWvQL9jx(by}m#}d1Iwy3_?Yqfh0 z!gf?LD7g1ATksdzZ?tjdoc+$8%t7QL|Iml4leA|`F#jhy`l=$Wx5#oWELT~d*vUVJ zW9`xa8iy8Xv;waDM3M(lh^*3L9=`HU;xuxxN0Xt`-p4oOy>+eGk^<7r$-Jv3zt&Jf@ij{sZzR(kR^AmR-y^8Y@ z9eh+QG3P~rMu7q-N6A?3!n68{k*YnY6^3&hC9U_HfAP*$kL7)J1ys3P0#p>zjbLYo z?{;6ou?4w;3CqCeBGBJCFL_`C=6O6me&_UCOq715SLoJ3GUuJ|<&UC}{5rrmo%vQ2 zlJ0u|&u`r>uafGVeHpK|Q*kM2`8~n1KuNs~Cx!ChDz?cz10@63~J^$5ZOs}uR%U3b}U94Q?)!he@ z!GJx?Y+zCNQ<{ke`rdweQQ%cvzdyz@zMj3O{yjrXM9=m8$Wm&nq8q?LtWh0nr%&@N z=sz+FHh_OY1CSD2TI*{L)?vk0_1NBX5`>UZbx$oS!dA9a7osX3**YGr{MJcMFp5B6 z`R&}?YK4atbVf8&ssdnPs~B&o1jGc^)zN&{ zyeJ#GZ`Y@O84JcaLbl{ChgZA+;X!!0V+P}y$=LYs{C_>lb|rBy*t$ZdM~$6V5G7aG zY2~+pw;AP88fA<(vXwNlF-{86f(1{PSh@Mv``4#ZX5#8Ev1AvBOT~e8Op8%DR`&dg zNF^8xhzAsN%uwNRbicxh=eQeRyGy%WS^+saBA`Gp&M_GBjXzvgIH(zNUaA0*NN{eF3nbI>)2EGB~FSr1D%5BE=n@{(0kQ+uWdn z9FAr-E(aXsVok@xdd($CU+?C}%$ zsR@&O$~bbxF~)ygmV?b?(#0Txl;B?ip)}K#gao1|A2u3Ey?Z9B=*C%b6rL)08##)G4F!| zCA*WpHNO&26kB9r8-4uIY{kVdR~60)IpvQysu+maT9)<=+{D;{yxef`$0)QhM%3Mm zecX!MT97`N)gyBWH((BZFKgSZE;^{A(FE+j6HLm-!6hn>H~4HYY;qx_^2pD_$bJa#B@xxSBgc^w_! z`Dg6AqS?|qT?P^x9P_&AO?rp7PKIsf)28<2A%U(4$&HI&GeAF`NBzwMTV7$bM9&yd zxuX#i8=Z6I<4QKOc7MtOQ0slSVy&*&UT8LKR`Ldhwwyz!Q(2tNm^Z@_$dt16mO(TN z7TGRAR05r8BMUT>XB&XUaF_fSJybiaT{Tz~S9Pk|K^>*B0(aFL&1|mzXI>Gu+0j!& z7I7CmR2wI8?tbmf+t9c*9^bBWvDrgO$8sLVXXViD;X&U^s1d`LoH)kgEe24R%tx&- zN=ObcgwRj99l_ECDM0YX2M5rx7nrElfqCGf05X6N*?8OFmwYDBaCz<7$BDs9xo^es zu_eM(5{R4*ws4FiIuY(BwWHy|N`!;S_;UEvYk>S!yX2uhj$g*gzZZ?xq1g$1P;mF8 zWni%qe8y2J!=UgUx*KH+cUpT<*khvX;R8eFj0<&AEmVyqJS>P_ab1wuWfE06@OhVUgsu|G`)uXw1A2+yQqWy3hn zTeuJ2(ZRXGvY4V%uGYjJmH)WP#ZAn{6k~4D!1V}Rt6d55U3KlLSY7vo&{+QcynUt7 z7v?HQp;kGXUn$sSayhUE*Qy}THDD~eOaZ0?1@G0ynYedCf;`G!x_?~?suRunI9gDi=GFhe=4Oa9=$5F-mrh(z1e4= zW}}^FIfJflO;&!byCY?Ub$;-y`>&w)aY0Y?X{5IEkKAu$2Z#cMGYNxp0Hk+*UJ5owV^tILEN311 zMvOX@@}sg~!$SZuXVYbV(j#;!*&Ka|90swJ!6#b_-Ay`bED~43s%;ebG|1}_-nXWM zm^<`_1Y#C(6nIyKEg2a|oPaZA%vS~iKtZ462vCT)cjoUda_3d=jaE|@!0eH1$&q6R zv8zY^q0_Dde>)zDr-g{(#5Z=QuX>TjZMd_=D@eH1@@6V#DM0KqeaPP9|NUH*-#FD z&T+D^EY&3ey0TKREiPs@Vo4)H$h_vR`S!hg@DL=|ktzIqoTZ;-mpkpw6%G z;)#a%t7bp{^VdwDX=ffNN4!LA-R&s}IhTYQ@4}{=h5Dfdh^@g<&?tTCWvv!Q+@ZR| zN1k}E_S=d=02yxwjiig1Ayk!)mOcxwszkC16w>`1r4j?^&wuX)c)WjhXdYq~1yF~{ zelLK!H%ir&#+*4;nmtZu0>yk);xrht`YTZVZRJzxmIY35&LzU`+sH|&uxA`bdU-pi z<4c#C!>`3tHQ^$X@?Uh1IO(|YlCZqnZ3rImTg%g05?{?>{KSb50ZFxSekNxWr043- z&x=6xu&YGNaU@WqJ@*a|55w1>k6_}UWO@ohKEOs3HEt&I6{MVD>y-b*ut20;e%sNk zfpH5=zCzU?bTZ5kVcU&!0JaYFkp(6DB5UWter7-id;y*#`dbT~5BeM{hXeUuV`n++ z4idt`C;@{y(T)P*r39nU2tK}7mxcStob(!)L|kxpU8_MP0)OkKZ$KA5taz8n%bY5z zxve8Hz^wN@`;D$eP$3G{e7}jt1(I#@i0Iku94j;eJ^!O2h1f%jX^%r6c@Xjdin;f3 z757yPq4v21GvCvR;{_5}tC+9`-*j~{XeI3_67Emu`DEcUH%fVV0=L7Yu(WrK2dIuK zPQQcqk=fHjaRvmc!nHI_!)n0jX9BwujJ^67J>B|hDyUQ2(L0@7Mhn;)X|{m01ZU@4 zec`5?%EJ6GM6TrVyjIlp)kv4XJq7p9yudSBx0Yv_2qqWfTK&>wPskdh{JTA@7ZESH zZpEzR>hjF}U1_LJz(psw|ml<@Ac;5v^(hEF8msq^M588Ld0C{nd*>$>cA>7Aa8VOdCzh%+7U zlp}51$N(+W&xd@(*FlR0lO{~C<(R7fCV8iO$nvnT>9T)th~l z-+uj7TLxYZbp;7t>h<6YvQ7ztE>BdwCX}y&?#c(3^66GP7*l40tKPhQ|>^yqCl;+HPNNLTjv7WgJfloBv) zo2C?yB#eo8_{`z@=0%%hF=(wI!}5?^KBD0?2TLOJ=NlNIC27bL>s5UsM)~)m(D?-) zGh5__76m7jt>m&LGeZ~NO6gtyIeC7BD>XpPUJP!A6>OUZ$;%y0$3Fqo%E|cK=6*oO zF5I?;^9HhY+6uT+WUJBct@)zDWOp56jFY+ffrYokz;o^WcFa}b-o(UQ*2Poy-JaaM z%yWOe|NB4wxqAlOu1UZxeB`DPv8w%SvKW!+=Zb&6P;6F#5}*mv*lsQ+4%7RbP@rxq zRqQc3Fjfw|QM`56gPsW6pDZ%9Xyx2>`v+P88RTAkQ zolcvC0#CHn0lzny;_hTGo^=YWyZZ6SC2nF}?+bZLE zu?-qc_7Q&>;oaXXr~hgvzVk3`T-<8~glwR$#dLN_No{dF`c_`pm@%M7>$LGaUUHh8 z{`niMjfeu+^YRu47&KjsPPvk?a)3*j^XD5u=~V8sA%6Xzg)o_~onP_G{LDg82>=1d zi@xp|Y*|?7+M|*kt^1AW3ZMi-r+rI%0?kE+`Xor(k|?*LVziStTAko7gzX;Ph!zo@ ztjW~(FFl(G=XjiNiAD*?1sk9zr>N;j`&ik;Q1%PNaSoj2*a}|QqMuKl-|Ze7&*gue zJjlNxPJA?R$;Jk0Zeo?dE5o#}xLM=s@n!g|_NW(2h|JHKpk?SQy_BUn^ZoX#t#>f0 zrXyAWWtS=d+%5M)4)3X1=Q6LLxn^Spv3T*O#<|F`&8uraG1wc)pl#GFl}t3UH8Fw& z&XF>0&8tDSRbV5p4xgBGRtcQzEXS)SZFDLU@?)4*Mw;)qMmh*CM8&g0S9W)Kiq{v31>k_~T-68>* zY)e8d2tJZkrJHm+;Beo+(^QI`O~rTF$mN`6v5+;{61v+q4u6g#Z=$k|*vIFyZ)~bF zNZ)1SU=Pvo%~#uH;p03!Svme*uzW((TIL2(!nbH?5ZjxP`)mFaNXpfo{|)5gep9!7 z094pU^(xs{>^(VL4@UShcR!e4;Lc-mrB$*aP3S0aWz1|G$cCQy5_St4gI>WH6ux(0 zlJ^!X->=*GJ+PvsH??`}Ymmz7*Aq0OX8~eJ541KCi6_QJH>`<*+zmPdXj&TU zT*J^19D9=QL7f2@)8Rssm(ooRDSk_b!7?dH>NcP@OS%+23K*TP7KvKLbXm~~MC|Dn z=hy3%@@X)2{``1pQk4HnSv;LgRyI@?ScQ&o$QWa@d(*qgHUoilpZ4!72yT=>MK0ux zSlnJe7EJvVm7Ahrb12uGi7hh6lF-#-{{7ev?tr$_3`VQeZvZ&FEWN8XZ!VuP?_B!-5)lf5kG@;!;J+!Ue+j-tWEY1tYWDtWu^ z;j9jZATTcHJ2|n`GIBD=p(QxSr1LoEu0$kNS@Arfkm1+-5^th(cp)W>>^_eIe_j>` z|Ib#U%{f2?n5S=Lw-qX2fn;9JpL26Gf*nYkMq!X#W=aNzV!(T zL_$PSbzs{v+P2raEmR>FfS1CbMeBS%7q$l`l;mbhq6-&Uxdb3IIc`jlEp!$_}Rhg1y^J7D0 zEw1fcHk%X^?sY@k7kX+7TMG`$>^|dLF@?`7be+P0?7~)I0{BltXHwF&Z%6$uf}Fh# z7D;nU3ROKf5XUT6N;4etdL?Z&k`e3Dk5@QxQ?3JcFLZWv)`UJIjS3C{UN}bev#ZVe zHpg}Y30*GB9_C7SJHIszn;daHlX076^|V^8#CU`Y-y!!9-6WxIx>Vi1ZxeNWX48_X zGOjZal^C;NMr@U?A|)7vd0w5Pf*AK0HkV=+KThqg!$U>^GQg=nXfY9D0^h3D0%Qe% zJE)Gr)C+l-^9st7Ui|6Y_#UNhYyJhjx*t|9{n*`vcq$q`;rnPJ1I0zz8?)F-m`s~) zy4|kz27DI5vPq14)ul&(-YNDan*{bV2|X}kUL?P2X;UJPq>7oI~_1+G3P-oM{W$JaQUiS+R4 zxW9GV@^j*WZAKGRht-FC|Kfb^PrkIIG?~x$Q8f6w9`3#Z$UN_Vlzi{EH&)aEde@b8 z#FDI#c@<|=q^Q17`rr<-N2Zg<@7Aq*)3a8GMfPq5NJFzVPMph5lL}L8@J5Ry$%U|i zyYT5UMsA4=l3VJd#IIgZt?WLuBT+doSngQmk3P#v=c1}xhF#`coHDa#0t`$4{NW<}wnK|>;0Q+kzmmwsp*D!#jxvi108G;r^=`1( z&N9{J0%&V&j3fX9sF`eQh^_`IP?bQRWmeOq%bl7v9$oGL#0(wx1|GF?334|iWM|g+ zcjxpEE1Y9>Zykytd!=m54o3Dcl zmCKk}GkZ3b1lR}m&<$MhsZU~so5}fu&!c6`M(s90@Nqob$w&WK=KqW-(sA-51I;Rg0ozY zREm|2P`Ce9AHL`Rb3Xr2PogX|l^lEUD`O7EE-VLQ8NLFymbSU)OdX|iF6Tt&It#{B zSf&%0bmQdb2$&Z=Ykn>i;J+K)0xEV+u4+t%%ECDd%0mPlh$>J)I?lvrGWFRpI};t> z6R&rHABl{f%+k?Kw~^)GBpHCgJe^h$4Md~}kgxJawsp2Q`?$njg?;IXb=wK8SeJj9 z@8rpJ)o;Aza%u3Mv7BpX-23xoaa<%-XI{*9IR}Tu)+X0NCmKOA@hXyervR!=%JWOb_01x=9 z%rPuoehC^lp!>Y!oRG<A}_{zcuO%J6>c6FJicw?l(s%VFMbsZb=lqHZo{ zOr+kPdUo&V5Cu?2edvoORIvl%P| z0MFD)MyEh7;2sDy1meW!K>IviNjiM?JV_u1do3_V*GZ=23oMl{)GLEDU=BUM>x4pX zbf9IuXMw6A3wSyu_Di2uDqyQXVbDX9d2D_8RuG(YPP z&o_X#CWBwcYO*#C(}%7rVWl~F^e)9& zNGm|bzQ5m842tJ$V6>!Hgm zeZ2I3FyJSl0$MUg6*HnK7(R6kxptY$NreBP21N^MP9q?~1Xq)yqStE0!{9gV6Ta=M zyW3K_BWT72R~1lDCAc5@@Wp-@b471cP&uAVtMm|(Jy+OBK>MvIVF6t_FL5zvHaevq zf4(0IqF3D<(uf&#u-pgaDaWc)3vCBR;EWa9Art=X#X(mURTO#-%;H|g zlBbeRg-EUQmrvX)n5gE z+Suw|DRXG0gRCbB06aj$zdfReT7EvJqe8%tX)!wR!A9!**^Aw7; zZh>VqqS>B`NIBj#_d_lL$5@vHo#rP7CZoq)LwFBgvED6-T9K8b>B9FW_})^=N5!&| z#Fs+iC2M{qx}`nYra$lV(bWMg^^?G|6$SS>9ZJ&$A$l&z$95G`6pqyB_i#9GqYzb& zDZs=;E>>*gDNQveci=Y+oe}X!_bJZHE$&knKk<78Z$*J1E_re@N+F8M^{yHej{i zoVf%f7lKM!mu`xk(-ZxO#5j`$BtNIVkJD_)IFG@h+p&a=;GcSthN(+-9o8@3=WoTA zL9WK|IcqVtz=T0T6!BmHi58s;Bcv6{*q&rs8-czB60wvq6+lPU-~q zt8rmFRk3ApCuo#U>l#09?4;fn)5xUh&Cye6$%p`k=`xr0xZr%iGA8oX`iT?oY6EY@ zj}Rb%$>}rciNcmpx#sG~d1&6(4gx)jo()F%(oSM?93(J*;klWrie%Y@Y6V8|Zg$|f z60SYJ%}Trh{zo?A!*Wd?o;K<6Q#5T9{FpnolKK(GuY$pvv0k<1lMbj1+bE+~MJ zT7+7tvakAiBSEK6kuuzlKBq6|D;6~$D#4A;-3vm{>MH~BSDf!n2=kKJH=h2xZoTM0 z$85B#ktqOsxVh-DH#bC|Ra(Yuyf-?bTQ7?7o6xE6TOVlnjrKE!lv#2?^dAijR(-a@ zq~j&Cq^P_)v3Tv>23Mz~xv(&_Aw*Mo>6VTkgCr35&eK3L@RDv%j=q0LFh7r@7HNm* z*FNW>C=dw8UcQ1~&3(wK?hG(PKQwza@oYr5CiB(_*K_i~V3zYLAlSWiAEep1CpN+a z6dw#U6wJ<_0RgMlRS@+qHElhi+1N`dZ}tK60Br@z>J)qMM)Vr5mv-*`YU9xu``Rtwn(vp{Le+HYnky z%{M|LIidv`&GI=Xn!ePb<-Xn9p+l>Ba(QFW1ec$Mj=9CQPV+le=8!r0+kKr z9z%AlO>ti8Cq3+W=V!cAwDhh>Xu;*PlPujr{T_lY(f7i`Mso_{A^b_$aBe1**YXh~ zIlQh@l5c=r3C7?+5uUu%cHls1GI!vp1dBcMhc@q>$M^ui0se-O$f+uw^W*Jm&M(Oz zG>XS(?rlu#wgklJTBUCI`7PIkU+T{>xC)T5_t=!d5e*>wHeJcGtEQmM4a7TsiSktH z1kVBbWglB;=lv4V2xV7yso62_`Ff1XudxEdrkCa;aRXYc^caL$<~kZUWg+y=jrdVc z_k#;fu6EA_pJbs>XDGkwy{66AwL;ax$GV7Mi2Q`mi19}E0cwnsY+Y8)e=gF6uVaPo ztY3*1#qj6#)t%m}dCsEq0cHvUYOI{!5{8R}cHf?fE=f(V?3mKknk+ty79jNb=|xRr zQZ5D%IWPdP7dwZ}-8#nr>*tasI8Oo2r=O00J5L~0=YHsy!|Uef_PAeV7W=zcIjxwS z#@Z-OPJnAn&*|o1Ff!tC3e5xh!?^OX16?#5J;}9;qj$c+2L>qh#Q%AHt%>^mnD#FD zq_Cttswson$d84=pwIE{q_6dPD%(Jj3;KhzNAAJaE0fl%cH4w#XJCy%QE6#lZ;3y1hhuFrcNc$?8j{zoS1Sw<}$&fLK3lAy+Ip)X) zzX7_g8g|*4cg!OI7&Lp7l6u|=evgeS*7UgeoGsP8=W?paoR7n*X-r(WEMa11Qx zn4dmwe2bGP9xmV=*Tz=4-ycQ2$6?v5&;RlzdX0qFV|}A@AkRBFliW(HfW^R#7q4E< zps{)h#f;`eQHam)w>oS3-&^K6q%#RwxC8MVdIGI2&5urgDGEX(Cn+e)(Ys)KIQDP; z&5kqpkM4>*;&tz+#@85elS7L9BST$JI;REp4S9lwV?L7$bY~PO_RPi~XqO+|c_d ztFnnGjjrkWVcRM`yt3Lmb#mpIz{Mx^yG$INIhx&2Kau38<-%pSO@_CqoYmw+~J0Q^eVu;C<2Ue0hKQb zMG(&`dZRkJ)#!X;ZpaeBa>N-t8tGF)eT?C`(Y-z3LLhqfH=V~dT|x8;q^QWy@uq~1 z8%4cqiNIqp(d3=?V|s^?ZbiA3ue07R?FOhA*QzDxbp2A!1TfY!?xVab=7O?&C974A zJs@i$U-GwbW$DP^!N%V9c0=}T#EDKHmq6Eav?dd%fY7Nd3MISh6qnvPX691pW+{dG zs~7z$3THg|#G=q=?o+&Go>if{A5^UP>%9|rbqvSqx>EsUF$OgnA!E>>#Zwi>gIWLM z|Mvd}TcE{G-o=UlyBvW_Cj2c|Ls7UAzep!yE_tYU5Pash+xFb;=ND;H_xxM{g(z_K zT}kDE5}JT`?Jl!Bst%(%`|5OfDPD2P zF~9*CPEjQnMT}z&2NPY$=iEcc->q13V-!`ch@}x3^klp6j1w7KLFk3D&L{a+I36x> z4<+o(K443JauD;T?T1EQdw*GV91c$R5lL3IA;+|?@V?Pi9cA@t_mu))|M~A_FD*WV zm(Sm0h?8d09cqfJ@aIEM8)T6kMS=I=-X#bX1*6wnC5$Z~?MO9nKJ={$lXSN5*8TBs zECm03i={;A0dtC@0}(uJ5e_bdDo;Oj8O;gL+f{6~7b#Q6RPW(JLH09anEfK;~Z zC);PJn>kMfOmeJ`xKMJ0Ok9W^S`?T^#3vW78lbWyXwi_flD6|-Y9PRVL|yaWM^0ys zcs!A3EzWD~``kBr8d^Jkf)<5iLk(po!GPUatq3w!S(zCyhGSpLjq6blsxL z`h+SFxs~&LX!_Zv9tEu}CfESF^i1#>1^jMT2JfOP(}H6J||5%bDTOn=?NySAn5CRT`?4&!WKv2^@%~~jc}Q_ z>ZKVv3SP!Q6dek#O_OoH+lgfT2J-In`j#XuM#IK0o}6y8{ppjq37zjjfpnBGwEb58 zS(VVDU?hKcsA>*OAFv!W&*pM97*#mLmeQFYve*;-q5B4IevCVEz-a)GogoKEE@@FW z%&ixyErzUaW;Mt=8F9W61AEXY$f4xxl?|i}`|!{(l7;D03V|`Bf|_a11RfH6mjCYq z5T&PVO$E5bi{zpIsz1|;6;`TBN{Jac|ogl>AaGGI&UqN>`EvHK!M*UqG{b1k!own5o$N z$+x<1>5xiEc(==u)fBbDLfM#1CIQtzyXPU(*Ziq5!}j=t>n*LFu#)ZcdJ`zeAy2;m z*NVi`Sl=Ee`eU6^3KbKb1P#LLJZquhrG318oeoQGBGbf(lSnv6fbsN38QMwiDv~|Y zxl@k$VN=%28|c&?swk!J$wv`?vajpY*1jZK0gB4XMe3Q>?N8!(>z2DLVwclbwOGl| z@?WFVlF6h4DWSrrS!@#IT?(Ij@46^POmC1f-+12>kCWUmEmkq&iM;{HoS=gqCU}&# zC_CBWBj(RdQRwjAb9>WExQXZKl z4a}skZI(P(LQQ2*5;@R()MA)HSyv-!%fOqF1K8t3jz+Fl3KfSz?Z_slhorJ?~`=hAbhU&Ov`~8a!civRThD`0=fc4 zf@Rnm0e71l<()a2rYnBh0vhyDNz3~IeXA>+npfw?}fJ*2VzTsR+kz8e# z)N6L_M+fbF3!>t1CL%uoVBmOV{+(aa=yAn77qFLm2boW^s2hW7sh50?hK+J6bdGp>zF*3r zvGnlhF(-c{i6oC=!~VS;)hB<+fA=e1sTF9;pI+0)&&_A3caIUY9&7dO0Tgx<4H}TS z1pNj+d`i2wIOF@^_VyXc%X5?7nqLfqxB+}`AkIBHesi8>a8h`q>lgse1$a;rTQlDa zJ#RO}I`>s>z3*`EvW`73G;Rkcs$J~&T+_#MGrn@=Q+49jEfuX@oZtPV%dqDs9T+H502G4K(B2?27#=bBU{k zqyu%uBd$6QdHnACpsDgS`;o6$PQ9$8E&x4Oale5b8!GQM)PP&RVuUMmK~_w;YAel=$I{v*b`ag~+8Jyv(o zrAKGF>(y3pfUZ{A$5taNe+RN4E!AyFvA-4f>Y(xUxQ$s?A{F2)2I+$_$7Whi8xlqs zQ6BkwY-IUrw5oW{^`o}6YR-OeL=i35)!Apo;7t6n2pr{y6{}#jyk$9 zhjypQF9N}c>B3KDMYRfjNO+NLyzGYjne&PcT;%X|PF3yY&*-Iy1d_BGf@!B-PqTo4 z*)CAA`c5%%NM6JaqK(7dgVNQ9#?U+TuC! ziHawFWIO!N{BpJF9FrH(OG%M3bj`I7$msA@{8-}kuadDqJQlp#*5b2)I^Rz#l2uUb zT1GKwVJ!+l3yTab<=h9XXW{a11xE|x-kdlU`YuH&~=#h`M@@jodXzwO`ZN^HYUM>RisZZg;6@C2JJ*Rho+Y!h%4vRmw*tfRN+AtDx+8==bfOi;aLw zv3y2H$G#ObmhpD%al?&3`T1y*yq$d$K3npIR}I3WMWQ#|yuGiR&xiT5%=D4u7{Jtl zq;41*%OpsX7N8Eiz^VgVf3`akZ$_jr#M)q3P@J-9pvnmPs6;!cF-UW8_CVc=jx${* zj;>yP>MNA1P0W<*4RpQhs>jwbD?=sUi;DubzH0Z)dhN0_$(j;>a!?{2`(<2<9YQBV zU$*O^{&S&SJcmv#*#`jtx^kw3HwI&_L^>$Ixr@pGXw4RK?}D!#rV7|49Di?;T=jmB zYThU{!k|fj=}aSiUoGqf5E zU3$gR^5@9TKoU}KhSggu(gEaALE{`t$ehhu@>sEx7=AK!gNE7*rR0hEX~)p(oO5+j z1!i2L<)9HQNj|m3W2@4wmXjI`QTz2~WF;B110|k@Ok_3Lz-)j+$9HQwhr-@kTNhoA z?a4K>h(dIZq+lC_+|PMnx8qxYVHAmF&Jjms)$?1_#Cg`wXwJ`N?+l>+>_tNLtZ#;G zw({D_@(MVKOU65ggF8m6)X8}jf2ig_Vq81W2k?A=RNB|*8(S%7>USM{DGDXeJvKBK zB4;9A*!M}J00MfV-e%-)AQ`RUX13PwgoffcA?Akg=#Y} z`>}@b_DjjE^PO}tbkNheeqs)*UMy2=6MqB85ahOP(c=j1fp@3N-t)``5~^H%>>snO3O(^Lt1Y14 zf;oADVuIw{l1w|zIc|I3c`y)m5j&XiIhQW^xe$q*N$!)c(r*qwIpPNdKz2+J${7TD zBI{g@Mfc4hWHh#~S~&*1%W9Ql_2o2b7nYLqw^jirdSL-)?&R08Vk0Lx)TUZ|&wWqg zGfSJsD}!vun`owT;Cd*qu@CCH&saF`2_a51uyCLl%R0jTuroiBlSK*)_pn9b=V)XB z*%^3#kD(>`Si0K`{#bfQxu9`9xTy>f-Ifln1&Xc!vu$o4JoVt2XvzB&hzah`wPS~r zSRyan3<3H^yMs*7?a6$LwHV9RN?;mC-_vy|7IGzv&XWxR^^uOe>~$z8*7x=_@Vv^^ z_zqxqp~3P$Hh{c7qQBF3ou=O4WU%xGs<}*-qs(f1AQ|JuHN=}H3JTJS* zIh1VBDzx3lhJ7{eeMTN|Nx=PczZQQ(nTwDaeeY8yY47MJFc(eb^YaJURUYjsdjd0n z>Q@d#fpC(R>DK%ATRX>;(Vfg#$$Y{vKQoY4pQVr<@^t^l=RYf$RrtdZ11kXN7M9Z3 z{6EG84^~v>)sLa+w};*E`!{`ctLvS`dn>v(Fy8pqxp$+bH?)rw0DO_H>{zjn!mIcQ zwXg0H@CtXZ7zK=rL&)ZxALkn?{WS+LF#?xspx6cm=J(jK*Wdo`d@&PY0NDkwe@TGc z|4zEdDh(3y2S8T-7eMUWPh5pN{T{#?T))Wwdf{8q+>FX}djPPUJhlSaR;6x!VL9SZ z`nC;5!W|>xMi=1qlg=gHAwT=*f0T9sA#$_*DjtFBu?f3nYXI4<_eO;1lFb>+ z^VxjkXX`=O5!)^2_ceqr>+TEhp8xMv4pLO&^7&TZI8iJZPk`k?25157_E6b(xw*jc zSpGe*5GyH@o&V%eJwoo+-#mY_w~_kO)k7KzTFt5V1mCdxdH3!W3X^y54raGtQOQ+PqM?03S$OiReDWdr-7p=gebs0u7%wyUl&O24;Yq!T0 z{6c%$*>Uv+REK2Y4S3h0JrAv8w(#~P1}LG-P`}4PF{ch@;W49NL5a)p!}tB3;BD4; zP{%m6FMd_*rf$Tw=CP>6Cn<~F6}+bu1V{qLN=voyRVnV$`5bFdzIn#g??5Q9CZ9{u zdF-Xl%X6;Vt;G%%2rNZ!+75&Nq|M~I&-%_yVbUgVFAhcc;=j?Yt;0SvUwo5g1Lki> z8ql7f_FDqa@{8X&j_@U1u%M$Pdw{1G?2ObI6?KIdo5eC9@(<15m6pJ zHBgo`XMG48$+l zeqy4wj`n1l`c-myCKzuqJ=Y5uNZn@+MI{-gXix#Ai+N2uesG`<9Rg@9ihZ*`l#=Ge z60;j9zTevl_^dQeZf1ODC_O?y`R}A+DSpjEJ-Z70 z*rgHAIhJMG3cypa^?Rg3rbos%TY4>QOlw}VQLMEnR6HaPpfy`aoQZT1j0|G#S7&WI zKluY&$9&G^RuX1mSdsQ}B(Ohk+Qp(%0DJ!N741G-IM3Vf9b^z|G4{(G69=%Pgdh8; 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 z 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

  • Pm>>$d7zpK<(i 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
  • >mEG{Y+0eI+WV)lu?_lESzo@mLN=Jd59Dgr%2-C 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 V-s@`qR|5BMDaotL)yi6i{~s9npXLAn literal 0 HcmV?d00001 diff --git a/app/assets/images/notion_logo.png b/app/assets/images/notion_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..74e7aa98255ff76e7b086eaf65dd0b3e94afb7aa GIT binary patch literal 1773 zcmVyx*a7zyZF#n(N+N3Z#n}&HG`%4atUqS{$Pl>N=i!7 zXJ%%yK7(v-ZWcP7&f;Uoj+Je1ZFS1^}dSy{Cj+F}8+uKveyLayf z%mE)hrZw!cvr25rfAEvZ3$fV|?9e)Y{(R7&sgnNwel;gp^U?01wJ$YJrv^))84d!) zbtxVkwW|qWm72U>ud=ad)C>*C?(VL@^95clD=RCqu&|)Un4O(f4Us}T9*-<8F3R%q zvU-R8JTl6V#oXN7UttQh2B^^cBLStr;MD*%H8pbi@@1Kuo0IA3X*K!M(vqyMt||xF z+1U{=F9<`DB=Mg(apEg{NJmsaQzayT5rYm1_EbbDEiILvo*tzQYBE1RzXlrl%>Z>p zGq5oOGYP_qngrg;0mbchOJQN5oIZV8MF)B`!xH;xHUyKMovq#@K-gl~d1xwNQl0A< z{qf@@3SpQUA0JnC8f!?3MMy{_i3;eWM~@=1kW8@-S!|*LO1nu!=#VrS0-@6}GGKq8 z9I~>qQr-KMLmI(pDw`7Uic zB|rxT2IShcYX|yizb!2-iaCZVV=mOll`B`2b0NU1dtC3IKYy;4<7gxJTDv9GUBdV71- zImJW@E)4NXT&g!hrwAux^Tv%E2TULlNlMHi5C(GNKmm6xKnaZA%V=PL zQSBWa9Wp#T958`IBxzB6Yimnhym%4BAQ`5&lifr&EJ*`OHs8H_r##%40Q+dC4<0;7 z*aVWgE)v+#&`@9lWBOWKTV-NmBB(!BNh(k>$rb3%ojV6srVAG?sC+A4Nhmyp6RNts zy_u}sEZk6*; zjG77d*|TSAdl$M(vSKJ51p&p%@&^*I-&qJa>;<#YxPSk?)YsPs06l#8P$g!Kjg3LU zovLSwt@HN4J?3Gti2XC2$kD4&x!d4I7l6WsE>6_Y&>*N&No{Sd$~L(}M-@;mUc9LC z|DXwUZGay~uV-3ZU0p4a#*Qc~m+0JCjf;{>j!sood5{g8WGmHzXkei_5-2M{J6~&j zEa_xY^?u#Kb02b`ZGinkf*nm#Ax!jGX<_AsRTzde!>W^oC|w&RE>m`_z+nLot?C;N z6c(D`9N(hPSy53TV`F212|}|;mJylkv7SVe;H0M3#N?0JChxNz#a$NPqd9~+lv&9* z1#+osJUfVUH!%ETCU#r4>aJ)u!K86;mz9;}#d98BL(R)j-!dY|CL+To zN{wV`Pi*DyP_YjAvmw&oH+hnRomG@VBjw5w{_mSm(F`9YSc39L`a|qL+eFlJF$N^F P00000NkvXXu0mjfs%$-1 literal 0 HcmV?d00001 diff --git a/app/assets/images/x_logo.png b/app/assets/images/x_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2609e580066db45110ceebdb05c7913e83cc09fd GIT binary patch literal 103016 zcmZ6z2{@GP7eD@teP1GkY`qDINl1mr(t=VE$}%MtqEbQzPZ5bySw>__i%?4(s zXp^!{Wl+RK+2w!khrYkx@Bd!cdv(>^b1&yU%jbN~xhL9o`(|MQSpf{ggtu-n--TiP z6!<@pj|cu`>!OHf@CX0#ElwvfOlmp$pR{$?e{A?i(#c($H(?K6%YTQza2+{fGAyFk)l=dk^^Ei_(cqqzxRijWfL22S}JA#qH+?J{-&4_b!9KfR8FW@2Fw!VH1zP8Qw7DCWZ}r6@St# zO^__8#iq($PhT3%EDO(=jhZw&ozbWXGNOHM)88M$PmJxd_(-*%l1jm*&b$o16vWmF zosOHUFc&YgDJ{*(~ykW!F&SwF*q?Hl?T_dkBTrME}~!SvVN#f|&3SI$}8AHo)x~1UuJ#ifwg-d_&V%W^S$um7L68>xZbf|b; zJxd>#Z4C-26fl0Ky_T|g(%m7ne>?0X@Sb(dtP7eHMC+@cY7^h+ZX8AW4J(FMmXPvh zS`rm2RFNo#M##m)&akdqNFgfuY{RE7?qIj?`%5d=p4tIg75MYN7_Po~UDY1ose#R{ z|J?F%RK4pxZZ9t{Z}{)Rhpbki1+ma38yF+|ncCZVe7SL3stR95vq;Ika@A_2$jK@#bhPp1Eq+%+i5HK5mozvYXhn zwIhvguj-~p`u*X>*xxT!bm?4OBy^`TJ7Qb!Bh3h-AfGrRkeQ;N5!AKeVNF**-0!w% zl~eb?ihpU_7&j7gCFe99+P$dc5b3x5l%KBK&nz!t$IiKvAJOqnr*lUlgT*jw>sHG~ z-rPRY`{TKjUK{OWl&FV}DuJpv$RWLg-SK}qsvp)f;(+Ppk6zxtc<^?Lcto=~VK*rY zI%oY1RHcEXfy=@=r9UL-7iW}LedE+dS9||p@kYYCFt+$0F>{3v7nE7t0n9I#aHsy&33SbBo7 z7@y6?ro7zSsULWPhaU&7a2fCPgl`w5ohs}^(_q2ggDoFl$yHM78% zEN}?;$D~d*Uo;A@X*|=XBdwUeX{lh7L+yy~zvGD-^^m+2&=8FC(Wj%Y{YnKgBV#Hy zu*Y@D!|O?T#NjvUO3d4xkW;IHRZr$2@f`R2lU~VPA?SwRteP`MeU&och+fT{4uAO~jl&$Cl6c{GtOu|pPQZu*AGZEwUfsyRV+99A%$$d6HR2GOun zkXnIHJmPCMy7+xdWN1pwG@U2^E0f7or^Uc0uH_v^^*0kTqW2Xg1znFhqx$<}@0yJU z21mk8r|Eo8X(M${VwG=zA7k%IZ9cKqNQ#(oxF=*zhh~rI$p_TN$~+NG!~Z<>G}cN4 zHSbQLCe6Y0->P6!AjSG0OYFk@T!YjwHS}&+}6rPrk|=>3eB3rx3VFmb^-ZjnZL*=X+x@;I4Wonx`aoEhE(hYc8bDv`GgX)s(r=Q!K; zgMaIf=J2iU)39XNOykD)^Gddxu0R}+DSfKRVR>{e=ucGso96I(Idjf=-(Hp1)aN)K zm)LG2U6s}q8u0}Xe7(#<(qA82Eq`}%Vl(K*Tx(A;aY6Xp>G^>Ah=1{v$dCcWsV4+B|bZS#KVg zk!F!pm^AyTm>hkZ>d!gWNg3qe0)qPY5gyKD#c*3A9uFYO@$k~n#q0(OL@>Zw

    iT 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/x_logo_mini.png b/app/assets/images/x_logo_mini.png new file mode 100644 index 0000000000000000000000000000000000000000..b6bf32ecd162015e262d6bbd1f80412c1e64b00a GIT binary patch literal 1093 zcmV-L1iJf)P)rfQOPu0OssG#BpY8Gc*RL~_Zx{2Vd6m$@m`Va>}u%lZ$IJxvWIS9JAh(g|^gGdKa zP?Qw>KyavngWvb%oaeoyF==v>wC@jxHaGXJqG_uf>2de6|}at!rR+hUdQtCa>!=0;8kU1W$^X&1)ZIpP+3_C=jZ2k z-F_LCa+)IwHf1CdiLmSIYi3N&tU5eAWX8g--oZKW^ZEIi#?{o+h&GF1%~YHKmz^d@ z2vjneWNmG2tg*3?rBW&O`T5C4Mn+sAagO8TW16F(p@CIYR7mHeQfL7L7#$s@1=`!& znGs%U&*gH|X7-ZRux~n@rhY?1Lt?CpsaC)_w?PP*$z%i&URPIF)Ai-$rL7kcW_EU# z`pnPIdxb;$k%zk5Anqa5?(QxD;eylC)6_OKHI)Z|-v~vY0<&KL4rgx-biXQ-cGtkn7X<;YCAhSlL500^PJ2;1lZr-&kHs8?8C!Do~Mn{ zl;?d(6Jg%=^Mok$91k^m6iZP&9%rwwuRk8ZhiM#8lO2peU0q!SfjNlR2(q`gC+cN= zeZ6enSxx7Fx}OOUYJGj3VDN1=hx-2hJ_wlE`~z}(d3m9F$e}wrItX%gbycRU6bBFi znwpvjXk}%EuF-JEu@?3J0*E<=TNhVLON#({dU~R*Fbof7{0uhd+|A7mySuv+KzOgV zwwCT~ZEg8|lXVW5k{!(J+WY&vC?_0fXJ<#09=^?WcX#^!UUVy}G&*4J>{NN z3RpsI&NbOuR_I|I=IQ|t8k2zt(GnmbsZ(~T;YtmefB?dr(2E3yxvoM8QozT7!o~1+ zP!vLzV%Tp82BtwsUULJDeJ|$rM|$qNALg?!k3}H3lmhp%5Kw;sr!Dql;TV;C00000 LNkvXXu0mjfmaO@c literal 0 HcmV?d00001 diff --git a/app/lib/backend/http/api/apps.dart b/app/lib/backend/http/api/apps.dart index 89c7e973f1..adb6006e5c 100644 --- a/app/lib/backend/http/api/apps.dart +++ b/app/lib/backend/http/api/apps.dart @@ -490,3 +490,39 @@ Future checkPersonaUsername(String username) async { return true; } } + +Future getTwitterProfileData(String username) async { + var response = await makeApiCall( + url: '${Env.apiBaseUrl}v1/personas/twitter/profile?username=$username', + headers: {}, + body: '', + method: 'GET', + ); + try { + if (response == null || response.statusCode != 200) return null; + log('getTwitterProfileData: ${response.body}'); + return jsonDecode(response.body); + } catch (e, stackTrace) { + debugPrint(e.toString()); + CrashReporting.reportHandledCrash(e, stackTrace); + return null; + } +} + +Future verifyTwitterOwnership(String username) async { + var response = await makeApiCall( + url: '${Env.apiBaseUrl}v1/personas/twitter/verify-ownership?username=$username', + headers: {}, + body: '', + method: 'GET', + ); + try { + if (response == null || response.statusCode != 200) return false; + log('verifyTwitterOwnership: ${response.body}'); + return jsonDecode(response.body)['verified']; + } catch (e, stackTrace) { + debugPrint(e.toString()); + CrashReporting.reportHandledCrash(e, stackTrace); + return false; + } +} diff --git a/app/lib/pages/apps/explore_install_page.dart b/app/lib/pages/apps/explore_install_page.dart index 97fc7fd285..c55c1cdf68 100644 --- a/app/lib/pages/apps/explore_install_page.dart +++ b/app/lib/pages/apps/explore_install_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:friend_private/backend/auth.dart'; import 'package:friend_private/backend/schema/app.dart'; import 'package:friend_private/pages/apps/providers/add_app_provider.dart'; import 'package:friend_private/pages/apps/widgets/app_section_card.dart'; @@ -6,8 +7,10 @@ import 'package:friend_private/pages/apps/widgets/filter_sheet.dart'; import 'package:friend_private/pages/apps/list_item.dart'; import 'package:friend_private/providers/app_provider.dart'; import 'package:friend_private/providers/home_provider.dart'; +import 'package:friend_private/utils/other/temp.dart'; import 'package:provider/provider.dart'; +import '../persona/twitter/social_profile.dart'; import 'widgets/create_options_sheet.dart'; String filterValueToString(dynamic value) { @@ -189,6 +192,7 @@ class _ExploreInstallPageState extends State with AutomaticK SliverToBoxAdapter( child: GestureDetector( onTap: () async { + // routeToPage(context, SocialHandleScreen()); showModalBottomSheet( context: context, builder: (context) => const CreateOptionsSheet(), diff --git a/app/lib/pages/persona/add_persona.dart b/app/lib/pages/persona/add_persona.dart index 8e3e3ed3da..d9be407dee 100644 --- a/app/lib/pages/persona/add_persona.dart +++ b/app/lib/pages/persona/add_persona.dart @@ -4,10 +4,13 @@ import 'package:friend_private/pages/persona/persona_provider.dart'; import 'package:friend_private/providers/app_provider.dart'; import 'package:friend_private/utils/alerts/app_snackbar.dart'; import 'package:friend_private/utils/other/debouncer.dart'; +import 'package:friend_private/utils/other/temp.dart'; import 'package:friend_private/utils/text_formatter.dart'; import 'package:friend_private/widgets/animated_loading_button.dart'; import 'package:provider/provider.dart'; +import 'twitter/social_profile.dart'; + class AddPersonaPage extends StatefulWidget { const AddPersonaPage({super.key}); @@ -316,6 +319,88 @@ class _AddPersonaPageState extends State { ], ), ), + const SizedBox(height: 24), + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0, bottom: 12), + child: Text( + 'Connected Knowledge Data', + style: TextStyle( + color: Colors.grey[600], + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Image.asset( + 'assets/images/x_logo_mini.png', + width: 24, + height: 24, + ), + const SizedBox(width: 12), + Text( + provider.twitterProfile.isEmpty + ? 'Connect Twitter' + : provider.twitterProfile['username'] ?? '', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + const Spacer(), + if (provider.twitterProfile.isEmpty) + GestureDetector( + onTap: () { + routeToPage(context, SocialHandleScreen()); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(16), + ), + child: const Text( + 'Connect', + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + ) + else + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(16), + ), + child: const Text( + 'Connected', + style: TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ), + ), + ], + ), + ), + ], + ), + ), ], ), ), diff --git a/app/lib/pages/persona/persona_profile.dart b/app/lib/pages/persona/persona_profile.dart new file mode 100644 index 0000000000..753ff666e5 --- /dev/null +++ b/app/lib/pages/persona/persona_profile.dart @@ -0,0 +1,326 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:friend_private/pages/persona/add_persona.dart'; +import 'package:friend_private/utils/alerts/app_snackbar.dart'; +import 'package:friend_private/utils/other/temp.dart'; +import 'package:share_plus/share_plus.dart'; + +class PersonaProfilePage extends StatelessWidget { + final String name; + final String username; + final String imageUrl; + final double clonePercentage; + final bool isVerified; + + const PersonaProfilePage({ + super.key, + required this.name, + required this.username, + required this.imageUrl, + this.clonePercentage = 35, + this.isVerified = true, + }); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned.fill( + child: Image.asset( + 'assets/images/new_background.png', + fit: BoxFit.cover, + ), + ), + Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + backgroundColor: Colors.transparent, + actions: [ + IconButton( + icon: const Icon(Icons.settings, color: Colors.white), + onPressed: () { + // TODO: Implement settings + }, + ), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + Stack( + alignment: Alignment.center, + children: [ + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.grey[800]!, width: 2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: Image.network( + imageUrl, + fit: BoxFit.cover, + ), + ), + ), + Positioned( + right: 10, + bottom: 4, + child: Container( + width: 16, + height: 16, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(width: 4), + Text( + name, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 4), + if (isVerified) + const Icon( + Icons.verified, + color: Colors.blue, + size: 20, + ), + ], + ), + const SizedBox(height: 8), + Text( + "$clonePercentage% Clone", + style: TextStyle( + color: Colors.grey[600], + fontSize: 16, + ), + ), + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextButton( + onPressed: () { + Share.share( + 'Check out this Persona on Omi AI: $name by me \n\nhttps://persona.omi.me/u/$username', + subject: '$name Persona', + ); + }, + style: TextButton.styleFrom( + backgroundColor: Colors.grey[900], + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.link, + color: Colors.white, + size: 20, + ), + SizedBox(width: 8), + Text( + 'Share Public Link', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 24), + InkWell( + onTap: () { + if (FirebaseAuth.instance.currentUser == null) { + AppSnackbar.showSnackbarError('Please login to clone this persona'); + return; + } else { + routeToPage(context, AddPersonaPage()); + } + }, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Clone from device', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + 'Create a clone from conversations', + style: TextStyle( + color: Colors.grey[600], + fontSize: 14, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 24), + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0, bottom: 12), + child: Text( + 'Connected Knowledge Data', + style: TextStyle( + color: Colors.grey[600], + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + _buildSocialLink( + icon: 'assets/images/x_logo_mini.png', + text: 'Connected', + isConnected: true, + ), + const SizedBox(height: 12), + _buildSocialLink( + icon: 'assets/images/instagram_logo.png', + text: '@username', + isComingSoon: true, + ), + const SizedBox(height: 12), + _buildSocialLink( + icon: 'assets/images/linkedin_logo.png', + text: 'linkedin.com/in/username', + isComingSoon: true, + ), + const SizedBox(height: 12), + _buildSocialLink( + icon: 'assets/images/notion_logo.png', + text: 'notion.so/username', + isComingSoon: true, + ), + const SizedBox(height: 12), + _buildSocialLink( + icon: 'assets/images/calendar_logo.png', + text: 'calendar id', + isComingSoon: true, + ), + ], + ), + ), + const SizedBox(height: 40), + ], + ), + ), + ), + ], + ); + } + + Widget _buildSocialLink({ + required String icon, + required String text, + bool isConnected = false, + bool isComingSoon = false, + bool showConnect = false, + }) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Image.asset( + icon, + width: 24, + height: 24, + ), + const SizedBox(width: 12), + Text( + text, + style: TextStyle( + color: isComingSoon ? Colors.grey[600] : Colors.white, + fontSize: 16, + ), + ), + const Spacer(), + if (isComingSoon) + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(16), + ), + child: const Text( + 'Coming soon', + style: TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ), + ) + else if (showConnect) + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(16), + ), + child: const Text( + 'Connect', + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ) + else if (isConnected) + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(16), + ), + child: const Text( + 'Connected', + style: TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/pages/persona/persona_provider.dart b/app/lib/pages/persona/persona_provider.dart index 6edd59c857..acd0c923ec 100644 --- a/app/lib/pages/persona/persona_provider.dart +++ b/app/lib/pages/persona/persona_provider.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:friend_private/backend/http/api/apps.dart'; @@ -25,6 +26,32 @@ class PersonaProvider extends ChangeNotifier { bool isFormValid = false; bool _isLoading = false; + Map twitterProfile = {}; + + Future getTwitterProfile(String username) async { + var res = await getTwitterProfileData(username); + print('Twitter Profile: $res'); + if (res != null) { + if (res['status'] == 'notfound') { + AppSnackbar.showSnackbarError('Twitter handle not found'); + twitterProfile = {}; + } else { + twitterProfile = res; + } + } + notifyListeners(); + } + + Future verifyTweet(String username) async { + var res = await verifyTwitterOwnership(username); + if (res) { + AppSnackbar.showSnackbarSuccess('Twitter handle verified'); + } else { + AppSnackbar.showSnackbarError('Failed to verify Twitter handle'); + } + return res; + } + void setPersonaPublic(bool? value) { if (value == null) { return; diff --git a/app/lib/pages/persona/twitter/clone_success_sceen.dart b/app/lib/pages/persona/twitter/clone_success_sceen.dart new file mode 100644 index 0000000000..7ff6bff345 --- /dev/null +++ b/app/lib/pages/persona/twitter/clone_success_sceen.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:friend_private/pages/persona/persona_provider.dart'; +import 'package:friend_private/utils/other/temp.dart'; +import 'package:provider/provider.dart'; + +import '../persona_profile.dart'; + +class CloneSuccessScreen extends StatefulWidget { + const CloneSuccessScreen({ + super.key, + }); + + @override + State createState() => _CloneSuccessScreenState(); +} + +class _CloneSuccessScreenState extends State { + bool _isLoading = false; + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, child) { + return Stack( + children: [ + // Background image + Positioned.fill( + child: Image.asset( + 'assets/images/new_background.png', + fit: BoxFit.cover, + ), + ), + Scaffold( + backgroundColor: Colors.transparent, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(height: 24), + Container( + margin: const EdgeInsets.only(top: 40), + padding: const EdgeInsets.fromLTRB(40, 60, 40, 24), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + Column( + children: [ + Stack( + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withOpacity(0.2), + width: 2, + ), + ), + child: ClipOval( + child: Image.network( + provider.twitterProfile['avatar'], + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey[900], + child: Icon( + Icons.person, + size: 40, + color: Colors.white.withOpacity(0.5), + ), + ); + }, + ), + ), + ), + Positioned( + right: 0, + bottom: 0, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: const Color.fromARGB(255, 85, 184, 88), + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 2, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + provider.twitterProfile['name'], + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 24), + const Text( + 'Your Omi Clone is live!', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Share it with anyone who\nneeds to hear back from you', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.link, + color: Colors.white.withOpacity(0.5), + size: 20, + ), + const SizedBox(width: 8), + Text( + 'personas.omi.me/u/${provider.twitterProfile['profile']}', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ], + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 50), + child: ElevatedButton( + onPressed: () { + routeToPage( + context, + PersonaProfilePage( + name: provider.twitterProfile['name'], + username: provider.twitterProfile['profile'], + imageUrl: provider.twitterProfile['avatar'], + clonePercentage: 35, + isVerified: true, + )); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide(color: Colors.grey), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_isLoading) + const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + else ...[ + const Text( + 'Check out your persona', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ], + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + }); + } +} diff --git a/app/lib/pages/persona/twitter/social_profile.dart b/app/lib/pages/persona/twitter/social_profile.dart new file mode 100644 index 0000000000..1d69200aba --- /dev/null +++ b/app/lib/pages/persona/twitter/social_profile.dart @@ -0,0 +1,186 @@ +import 'package:flutter/material.dart'; +import 'package:friend_private/pages/persona/persona_provider.dart'; +import 'package:friend_private/pages/persona/twitter/verify_identity_screen.dart'; +import 'package:friend_private/utils/other/temp.dart'; +import 'package:provider/provider.dart'; + +class SocialHandleScreen extends StatefulWidget { + const SocialHandleScreen({ + super.key, + }); + + @override + State createState() => _SocialHandleScreenState(); +} + +class _SocialHandleScreenState extends State { + late final TextEditingController _controller; + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + // Background image + Positioned.fill( + child: Image.asset( + 'assets/images/new_background.png', + fit: BoxFit.cover, + ), + ), + Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Form( + key: _formKey, + child: Column( + children: [ + const SizedBox(height: 20), + const Center( + child: Text( + '🤖', + style: TextStyle( + fontSize: 42, + ), + ), + ), + const SizedBox(height: 16), + Text( + 'Let\'s train your clone!\nWhat\'s your Twitter handle?', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + shadows: [ + Shadow( + offset: const Offset(0, 1), + blurRadius: 15, + color: Colors.white.withOpacity(1), + ), + Shadow( + offset: const Offset(0, 0), + blurRadius: 15, + color: Colors.white.withOpacity(0.3), + ), + ], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'We will pre-train your Omi clone\nbased on your account\'s activity', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Colors.white.withOpacity(0.8), + shadows: [ + Shadow( + offset: const Offset(0, 1), + blurRadius: 3, + color: Colors.white.withOpacity(0.25), + ), + ], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.white.withOpacity(0.2), + ), + ), + child: TextFormField( + controller: _controller, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.left, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + border: InputBorder.none, + hintText: '@nikshevchenko', + hintStyle: TextStyle(color: Colors.white.withOpacity(0.5)), + prefixIcon: Padding( + padding: const EdgeInsets.all(12.0), + child: Image.asset( + 'assets/images/x_logo.png', + width: 24, + height: 24, + ), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your Twitter handle'; + } + return null; + }, + ), + ), + ], + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 40), + child: ElevatedButton( + onPressed: () async { + if (_formKey.currentState!.validate()) { + await context.read().getTwitterProfile(_controller.text.trim()); + if (context.read().twitterProfile.isNotEmpty) { + // await Preferences().setTwitterHandle(_controller.text.trim()); + routeToPage(context, VerifyIdentityScreen()); + } + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[900], + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), + ), + child: const Text( + 'Next', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/app/lib/pages/persona/twitter/verify_identity_screen.dart b/app/lib/pages/persona/twitter/verify_identity_screen.dart new file mode 100644 index 0000000000..beef4d9b60 --- /dev/null +++ b/app/lib/pages/persona/twitter/verify_identity_screen.dart @@ -0,0 +1,330 @@ +import 'package:flutter/material.dart'; +import 'package:friend_private/pages/persona/persona_provider.dart'; +import 'package:friend_private/pages/persona/twitter/clone_success_sceen.dart'; +import 'package:friend_private/utils/other/temp.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class VerifyIdentityScreen extends StatefulWidget { + const VerifyIdentityScreen({ + super.key, + }); + + @override + _VerifyIdentityScreenState createState() => _VerifyIdentityScreenState(); +} + +class _VerifyIdentityScreenState extends State { + bool _isVerifying = false; + String? _verificationError; + + @override + void initState() { + super.initState(); + _checkExistingVerification(); + } + + Future _checkExistingVerification() async { + // final handle = context.read().twitterHandle.replaceAll('@', ''); + // if (TwitterVerificationService.isVerified(handle)) { + // // If already verified, move to next screen + // widget.onNext(); + // } + } + + Future _openTwitterToTweet(BuildContext context) async { + final handle = context.read().twitterProfile['profile']; + final tweetText = Uri.encodeComponent('Verifying my clone: persona.omi.me/u/$handle'); + final twitterUrl = 'https://twitter.com/intent/tweet?text=$tweetText'; + + if (await canLaunchUrl(Uri.parse(twitterUrl))) { + await launchUrl(Uri.parse(twitterUrl), mode: LaunchMode.externalApplication); + } + } + + Future _verifyTweet() async { + if (_isVerifying) return; + + setState(() { + _isVerifying = true; + _verificationError = null; + }); + + try { + final handle = context.read().twitterProfile['profile']; + final isVerified = await context.read().verifyTweet(handle); + if (isVerified) { + routeToPage(context, CloneSuccessScreen()); + } else { + // Show error dialog + if (mounted) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.grey[900], + title: const Text( + 'Verification Not Complete', + style: TextStyle(color: Colors.white), + ), + content: const Text( + 'We couldn\'t find your verification tweet. Please make sure you\'ve posted the tweet and try again.', + style: TextStyle(color: Colors.white70), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text( + 'OK', + style: TextStyle(color: Colors.white), + ), + ), + ], + ); + }, + ); + } + } + } catch (e) { + if (mounted) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.grey[900], + title: const Text( + 'Verification Error', + style: TextStyle(color: Colors.white), + ), + content: const Text( + 'An error occurred while verifying your tweet. Please try again. Did you post the tweet?', + style: TextStyle(color: Colors.white70), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text( + 'OK', + style: TextStyle(color: Colors.white), + ), + ), + ], + ); + }, + ); + } + } finally { + if (mounted) { + setState(() { + _isVerifying = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, child) { + return Stack( + children: [ + // Background image + Positioned.fill( + child: Image.asset( + 'assets/images/new_background.png', + fit: BoxFit.cover, + ), + ), + Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(height: 0), + Column( + children: [ + const Center( + child: Icon( + Icons.verified, + color: Colors.blue, + size: 48, + ), + ), + const SizedBox(height: 16), + Text( + 'Let\'s prevent\nimpersonation!', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + shadows: [ + Shadow( + offset: const Offset(0, 1), + blurRadius: 15, + color: Colors.white.withOpacity(1), + ), + Shadow( + offset: const Offset(0, 0), + blurRadius: 15, + color: Colors.white.withOpacity(0.3), + ), + ], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Please verify you\'re the owner of\nthis account to prevent\nimpersonation', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Colors.white.withOpacity(0.8), + shadows: [ + Shadow( + offset: const Offset(0, 1), + blurRadius: 3, + color: Colors.white.withOpacity(0.25), + ), + ], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withOpacity(0.2), + width: 2, + ), + ), + child: ClipOval( + child: Image.network( + provider.twitterProfile['avatar'], + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey[900], + child: Icon( + Icons.person, + size: 40, + color: Colors.white.withOpacity(0.5), + ), + ); + }, + ), + ), + ), + const SizedBox(height: 16), + Column( + children: [ + Text( + provider.twitterProfile['name'], + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + // Text( + // lastName, + // style: const TextStyle( + // color: Colors.white, + // fontSize: 20, + // fontWeight: FontWeight.bold, + // ), + // ), + ], + ), + ], + ), + Column( + children: [ + if (_verificationError != null) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Text( + _verificationError!, + style: const TextStyle( + color: Colors.red, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ), + ElevatedButton( + onPressed: () => _openTwitterToTweet(context), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[900], + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/x_logo.png', + width: 20, + height: 20, + ), + const SizedBox(width: 8), + const Text( + 'Verify it\'s me', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _isVerifying ? null : _verifyTweet, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + side: BorderSide(color: Colors.white.withOpacity(0.2)), + ), + ), + child: _isVerifying + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text( + 'I have verified', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 40), + ], + ), + ], + ), + ), + ), + ), + ], + ); + }); + } +} diff --git a/backend/routers/apps.py b/backend/routers/apps.py index dbe3b0cf2f..0a1bedc05e 100644 --- a/backend/routers/apps.py +++ b/backend/routers/apps.py @@ -24,6 +24,8 @@ from utils.other import endpoints as auth from models.app import App from utils.other.storage import upload_plugin_logo, delete_plugin_logo, upload_app_thumbnail, get_app_thumbnail_url +from utils.social import get_twitter_profile, get_twitter_timeline, get_latest_tweet, \ + create_persona_from_twitter_profile router = APIRouter() @@ -405,6 +407,35 @@ def generate_description_endpoint(data: dict, uid: str = Depends(auth.get_curren } +# ****************************************************** +# ******************* SOCIAL ******************* +# ****************************************************** + +@router.get('/v1/personas/twitter/profile', tags=['v1']) +async def get_twitter_profile_data(username: str): + if username.startswith('@'): + username = username[1:] + res = await get_twitter_profile(username) + if res['avatar']: + res['avatar'] = res['avatar'].replace('_normal', '') + return res + + +@router.get('/v1/personas/twitter/verify-ownership', tags=['v1']) +async def verify_twitter_ownership_tweet(username: str): + if username.startswith('@'): + username = username[1:] + res = await get_latest_tweet(username) + if res['verified']: + await create_persona_from_twitter_profile(username) + return res + + + + + + + # ****************************************************** # **************** ENABLE/DISABLE APPS ***************** # ****************************************************** diff --git a/backend/utils/llm.py b/backend/utils/llm.py index ef19fb24e6..f6b1ac8c77 100644 --- a/backend/utils/llm.py +++ b/backend/utils/llm.py @@ -374,11 +374,12 @@ def retrieve_is_an_omi_question(question: str) -> bool: except ValidationError: return False + class IsFileQuestion(BaseModel): value: bool = Field(description="If the message is related to file/image") -def retrieve_is_file_question(question: str) -> bool: +def retrieve_is_file_question(question: str) -> bool: prompt = f''' Based on the current question, your task is to determine whether the user is referring to a file or an image that was just attached or mentioned earlier in the conversation. @@ -1182,7 +1183,6 @@ class Facts(BaseModel): ) - def new_facts_extractor( uid: str, segments: List[TranscriptSegment], user_name: Optional[str] = None, facts_str: Optional[str] = None ) -> List[Fact]: @@ -2045,3 +2045,76 @@ def condense_conversations(conversations): return response.content +def condense_tweets(tweets, name): + combined_tweets = "\n".join(tweets) + prompt = f""" +You are tasked with generating context to enable 1:1 cloning of {name} based on their tweets. The objective is to extract and condense the most relevant information while preserving {name}’s core identity, personality, communication style, and thought patterns. + +**Input:** +A collection of tweets from {name} containing recurring themes, opinions, humor, emotional undertones, decision-making patterns, and conversational flow. + +**Output:** +A condensed context that includes: +- Core identity and personality traits as expressed through tweets. +- Recurring themes, opinions, and values. +- Humor style, emotional undertones, and tone of voice. +- Vocabulary, expressions, and communication style. +- Decision-making patterns and conversational dynamics. +- Situational awareness and context continuity for ongoing topics. + +**Guidelines:** +1. Prioritize impactful and influential tweets that define {name}’s identity. +2. Condense repetitive or less relevant tweets while preserving essential context. +3. Maintain the flow and coherence of {name}’s conversational style. +4. Extract humor style, emotional responses, and tone consistency. +5. Identify and retain decision-making patterns and strong opinions. + +**Important:** +- The output must be concise yet comprehensive, capturing the essence of {name}’s identity. +- Absolutely no unnecessary text or explanations. +- Every detail must contribute to 1:1 cloning authenticity. + +Generate the condensed context now. + +Tweets: +{combined_tweets} + """ + response = llm_medium.invoke(prompt) + return response.content + + +def generate_twitter_persona_prompt(tweets, name): + combined_tweets = "\n".join(tweets) + prompt = f""" +You are {name} AI. Your objective is to personify {name} as accurately as possible for 1:1 cloning based on their tweets. + +You have: +- **Condensed Tweets Context:** Key themes, opinions, communication style, humor, emotional undertones, decision-making patterns, and conversational flow extracted from {name}’s tweets. + +**Style:** +- Highly interactive and opinionated. +- Strong, slightly polarizing opinions based on {name}’s tweet history. +- Reference personal opinions, humor, and expressions naturally. +- 2-3 sentence responses, never more than 20 words. +- Maintain conversational flow with coherent, context-aware replies. + +**Guidelines:** +1. Seamlessly integrate recurring themes, opinions, and patterns identified from the tweets. +2. Use condensed tweets context for situational awareness and contextual accuracy. +3. Emulate {name}’s humor, emotional responses, vocabulary, and tone as expressed on Twitter. +4. Respond with the same level of detail, brevity, and wit as the tweets. +5. Exhibit the same thought processes, decision-making patterns, and conversational dynamics. + +**Rules:** +- Never mention being AI. +- No ** or formal language. +- No unnecessary text or explanations. +- Maintain the illusion of continuity and authenticity at all times. +- Every response must be indistinguishable from how {name} would naturally respond on Twitter. + +You have all the necessary condensed tweets context. Begin personifying {name} now. + +Tweets: +{combined_tweets} + """ + return prompt diff --git a/backend/utils/social.py b/backend/utils/social.py new file mode 100644 index 0000000000..a21dd9ca44 --- /dev/null +++ b/backend/utils/social.py @@ -0,0 +1,89 @@ +from datetime import datetime, timezone +from typing import Optional, Dict, Any +import httpx +from fastapi import HTTPException +from ulid import ULID + +from database.apps import update_app_in_db +from database.redis_db import delete_generic_cache +from utils.llm import condense_tweets, generate_twitter_persona_prompt + +rapid_api_host = '' +rapid_api_key = '' + + +async def get_twitter_profile(username: str) -> Dict[str, Any]: + url = f"https://{rapid_api_host}/screenname.php?screenname={username}" + + headers = { + "X-RapidAPI-Key": rapid_api_key, + "X-RapidAPI-Host": rapid_api_host + } + + async with httpx.AsyncClient() as client: + response = await client.get(url, headers=headers) + response.raise_for_status() + data = response.json() + return data + + +async def get_twitter_timeline(username: str) -> Dict[str, Any]: + print(f"Fetching Twitter timeline for {username}...") + url = f"https://{rapid_api_host}/timeline.php?screenname={username}" + + headers = { + "X-RapidAPI-Key": rapid_api_key, + "X-RapidAPI-Host": rapid_api_host + } + + async with httpx.AsyncClient() as client: + response = await client.get(url, headers=headers) + response.raise_for_status() + data = response.json() + return data + + +async def get_latest_tweet(username: str) -> Dict[str, Any]: + print(f"Fetching latest tweet for {username}...") + url = f"https://{rapid_api_host}/timeline.php?screenname={username}" + + headers = { + "X-RapidAPI-Key": rapid_api_key, + "X-RapidAPI-Host": rapid_api_host + } + + async with httpx.AsyncClient() as client: + response = await client.get(url, headers=headers) + response.raise_for_status() + data = response.json() + # from the timeline, the first tweet is the latest + latest_tweet = data['timeline'][0] + # check if latest_tweet['text'] contains the word "verifying my clone" + if "Verifying my clone" in latest_tweet['text']: + return {"tweet": latest_tweet['text'], 'verified': True} + else: + return {"tweet": latest_tweet['text'], 'verified': False} + + +async def create_persona_from_twitter_profile(username: str) -> Dict[str, Any]: + profile = await get_twitter_profile(username) + persona = { + "name": profile["name"], + "id": str(ULID()), + "deleted": False, + "archived": True, + "status": "approved", + "capabilities": ["persona"], + "username": profile["profile"], + "connected_accounts": ["twitter"], + "description": profile["desc"], + "image": profile["avatar"], + "category": "personality-emulation", + "created_at": datetime.now(timezone.utc), + } + tweets = get_twitter_timeline(username) + condensed_tweets = condense_tweets(tweets, profile["name"]) + persona['persona_prompt'] = generate_twitter_persona_prompt(condensed_tweets, profile["name"]) + update_app_in_db(persona) + delete_generic_cache('get_public_approved_apps_data') + return persona From fa98178dfcb13ab465b3fee89ac48b4ed9469950 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:18:56 +0530 Subject: [PATCH 17/18] persona backend changes --- backend/database/auth.py | 2 + backend/models/app.py | 2 + backend/routers/apps.py | 59 +++++++++---- backend/utils/apps.py | 138 ++++++++++++++++++++++++------- backend/utils/other/endpoints.py | 5 ++ backend/utils/social.py | 39 +++++++-- 6 files changed, 191 insertions(+), 54 deletions(-) diff --git a/backend/database/auth.py b/backend/database/auth.py index 32005577c5..513d7149b0 100644 --- a/backend/database/auth.py +++ b/backend/database/auth.py @@ -3,6 +3,8 @@ from database.redis_db import cache_user_name, get_cached_user_name + + def get_user_from_uid(uid: str): try: user = auth.get_user(uid) if uid else None diff --git a/backend/models/app.py b/backend/models/app.py index c3a75fada1..38b40a2559 100644 --- a/backend/models/app.py +++ b/backend/models/app.py @@ -63,6 +63,8 @@ class App(BaseModel): chat_prompt: Optional[str] = None persona_prompt: Optional[str] = None username: Optional[str] = None + connected_accounts: List[str] = [] + twitter: Optional[dict] = None external_integration: Optional[ExternalIntegration] = None reviews: List[AppReview] = [] user_review: Optional[AppReview] = None diff --git a/backend/routers/apps.py b/backend/routers/apps.py index 0a1bedc05e..cabe3bba93 100644 --- a/backend/routers/apps.py +++ b/backend/routers/apps.py @@ -17,7 +17,7 @@ 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, generate_persona_prompt, generate_persona_desc + is_permit_payment_plan_get, generate_persona_prompt, generate_persona_desc, get_persona_by_uid from utils.llm import generate_description, generate_persona_description from utils.notifications import send_notification @@ -25,7 +25,7 @@ from models.app import App from utils.other.storage import upload_plugin_logo, delete_plugin_logo, upload_app_thumbnail, get_app_thumbnail_url from utils.social import get_twitter_profile, get_twitter_timeline, get_latest_tweet, \ - create_persona_from_twitter_profile + create_persona_from_twitter_profile, add_twitter_to_persona router = APIRouter() @@ -78,10 +78,6 @@ def create_app(app_data: str = Form(...), file: UploadFile = File(...), uid=Depe external_integration['is_instructions_url'] = True else: external_integration['is_instructions_url'] = False - if "persona" in data['capabilities']: - save_username(data['username'], uid) - data['persona_prompt'] = generate_persona_prompt(uid) - data['connected_accounts'] = ['omi'] os.makedirs(f'_temp/plugins', exist_ok=True) file_path = f"_temp/plugins/{file.filename}" with open(file_path, 'wb') as f: @@ -100,7 +96,7 @@ def create_app(app_data: str = Form(...), file: UploadFile = File(...), uid=Depe @router.post('/v1/personas', tags=['v1']) -def create_persona(persona_data: str = Form(...), file: UploadFile = File(...), uid=Depends(auth.get_current_user_uid)): +async def create_persona(persona_data: str = Form(...), file: UploadFile = File(...), uid=Depends(auth.get_current_user_uid)): data = json.loads(persona_data) data['approved'] = False data['deleted'] = False @@ -114,9 +110,10 @@ def create_persona(persona_data: str = Form(...), file: UploadFile = File(...), data['author'] = user['display_name'] data['email'] = user['email'] save_username(data['username'], uid) - data['persona_prompt'] = generate_persona_prompt(uid) + if data['connected_accounts'] is None: + data['connected_accounts'] = ['omi'] + data['persona_prompt'] = await generate_persona_prompt(uid, data) data['description'] = generate_persona_desc(uid, data['name']) - data['connected_accounts'] = ['omi'] os.makedirs(f'_temp/plugins', exist_ok=True) file_path = f"_temp/plugins/{file.filename}" with open(file_path, 'wb') as f: @@ -157,6 +154,22 @@ def update_persona(persona_id: str, persona_data: str = Form(...), file: UploadF return {'status': 'ok'} +@router.get('/v1/personas', tags=['v1']) +def get_persona_details(uid: str = Depends(auth.get_current_user_uid)): + app = get_persona_by_uid(uid) + print(app) + app = App(**app) if app else None + if not app: + raise HTTPException(status_code=404, detail='Persona not found') + if app.uid != uid: + raise HTTPException(status_code=404, detail='Persona not found') + if app.private is not None: + if app.private and app.uid != uid: + raise HTTPException(status_code=403, detail='You are not authorized to view this Persona') + + return app + + @router.get('/v1/apps/check-username', tags=['v1']) def check_username(username: str, uid: str = Depends(auth.get_current_user_uid)): persona = is_username_taken(username) @@ -412,7 +425,7 @@ def generate_description_endpoint(data: dict, uid: str = Depends(auth.get_curren # ****************************************************** @router.get('/v1/personas/twitter/profile', tags=['v1']) -async def get_twitter_profile_data(username: str): +async def get_twitter_profile_data(username: str, uid: str = Depends(auth.get_current_user_uid)): if username.startswith('@'): username = username[1:] res = await get_twitter_profile(username) @@ -422,20 +435,32 @@ async def get_twitter_profile_data(username: str): @router.get('/v1/personas/twitter/verify-ownership', tags=['v1']) -async def verify_twitter_ownership_tweet(username: str): +async def verify_twitter_ownership_tweet( + username: str, + uid: str = Depends(auth.get_current_user_uid), + persona_id: str | None = None +): + # Get user info to check auth provider + user = get_user_from_uid(uid) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + # Get provider info from Firebase + user_info = auth.get_user(uid) + provider_data = [p.provider_id for p in user_info.provider_data] + if username.startswith('@'): username = username[1:] res = await get_latest_tweet(username) if res['verified']: - await create_persona_from_twitter_profile(username) + if not ('google.com' in provider_data or 'apple.com' in provider_data): + await create_persona_from_twitter_profile(username, uid) + else: + if persona_id: + await add_twitter_to_persona(username, persona_id) return res - - - - - # ****************************************************** # **************** ENABLE/DISABLE APPS ***************** # ****************************************************** diff --git a/backend/utils/apps.py b/backend/utils/apps.py index d25051c4e9..515b86510e 100644 --- a/backend/utils/apps.py +++ b/backend/utils/apps.py @@ -21,7 +21,8 @@ from models.app import App, UsageHistoryItem, UsageHistoryType from models.memory import Memory from utils import stripe -from utils.llm import condense_conversations, condense_facts, generate_persona_description +from utils.llm import condense_conversations, condense_facts, generate_persona_description, condense_tweets +from utils.social import get_twitter_timeline MarketplaceAppReviewUIDs = os.getenv('MARKETPLACE_APP_REVIEWERS').split(',') if os.getenv( 'MARKETPLACE_APP_REVIEWERS') else [] @@ -75,6 +76,7 @@ def get_available_apps(uid: str, include_reviews: bool = False) -> List[App]: if cachedApps := get_generic_cache('get_public_approved_apps_data'): print('get_public_approved_plugins_data from cache----------------------------') public_approved_data = cachedApps + public_approved_data = get_public_approved_apps_db() public_unapproved_data = get_public_unapproved_apps(uid) private_data = get_private_apps(uid) pass @@ -359,40 +361,91 @@ def is_audio_bytes_app_enabled(uid: str): return False -def generate_persona_prompt(uid: str): +def get_persona_by_uid(uid: str): + persona = get_persona_by_uid_db(uid) + if persona: + return persona + return None + + +async def generate_persona_prompt(uid: str, persona: dict): """Generate a persona prompt based on user facts and memories.""" + + # Get latest facts and user info facts = get_facts(uid, limit=1000) user_name = get_user_name(uid) + # Get and condense recent memories memories = get_memories(uid, limit=100) conversation_history = Memory.memories_to_string(memories) conversation_history = condense_conversations([conversation_history]) - persona_prompt = f"""You are {user_name} AI, you must personify {user_name} as well as you can. + condensed_tweets = None + # Condense tweets + if "twitter" in persona['connected_accounts']: + print("twitter in connected accounts---------------------------") + # Get latest tweets + tweets = await get_twitter_timeline(persona['twitter']['username']) + tweets = [tweet['text'] for tweet in tweets['timeline']] + condensed_tweets = condense_tweets(tweets, persona['name']) - Style: - - Interactive highly opinionated messages and questions. - - Reference your personal facts naturally - - Rules: - - Never mention being AI - - No ** or formal language - - 2-3 sentences for response. Never more than 20 words. - - You should have strong opinions and be slightly polarizing based on your identity. - - You must make the conversation flow. You excel at coherent conversations. - - Personal Facts and Context:""" - - # Add user facts to the prompt + # Condense facts facts_text = condense_facts([fact['content'] for fact in facts if not fact['deleted']], user_name) - persona_prompt += f"\n{facts_text}" - - # Add conversation history - persona_prompt += "\n\nRecent Conversations:\n" + conversation_history - - # Add final instructions - persona_prompt += "\n\nUse these facts and conversations to shape your personality. Responses should help the conversation go deeper. You have strong opinions, slightly controversial." + # Generate updated chat prompt + persona_prompt = f""" + You are {user_name} AI. Your objective is to personify {user_name} as accurately as possible for 1:1 cloning. + + You have: + - **Condensed Facts:** Core identity, personality, values, behavioral patterns, preferences, and continuity for ongoing projects or relationships. + - **Condensed Context from Recent Conversations:** Key themes, communication nuances, emotional undertones, decision-making patterns, and conversational flow. + """ + + # Add condensed tweets if available + if condensed_tweets: + persona_prompt += f"- **Condensed Tweets:** Recent tweets from {user_name} for additional context and personality insights.\n" + + persona_prompt += f""" + **Style:** + - Highly interactive and opinionated. + - Strong, slightly polarizing opinions based on {user_name}’s identity. + - Reference personal facts naturally. + - 2-3 sentence responses, never more than 20 words. + - Maintain conversational flow with coherent, context-aware replies. + + **Guidelines:** + 1. Seamlessly integrate facts and recent contexts for continuity, recency, and relevance. + 2. Use the condensed facts as foundational knowledge and ongoing continuity. + 3. Leverage recent conversations to maintain situational awareness and contextual accuracy. + 4. Emulate {user_name}’s humor, emotional responses, vocabulary, and tone. + 5. Respond with the appropriate level of detail and complexity as {user_name} would. + 6. Exhibit the same thought processes, decision-making patterns, and conversational dynamics. + """ + + # Add a guideline about tweets if they exist + if condensed_tweets: + persona_prompt += "7. Utilize condensed tweets to enhance authenticity, incorporating common expressions, opinions, and phrasing from {user_name}’s social media presence.\n" + + persona_prompt += f""" + **Rules:** + - Never mention being AI. + - No ** or formal language. + - No unnecessary text or explanations. + - Maintain the illusion of continuity and authenticity at all times. + - Every response must be indistinguishable from how {user_name} would naturally respond. + + You have all the necessary condensed facts and contextual knowledge. Begin personifying {user_name} now. + + Personal Facts and Context: + {facts_text} + + Recent Conversations: + {conversation_history} + + Recent Tweets: + {condensed_tweets} + + Use these facts, conversations and tweets to shape your personality. Responses should help the conversation go deeper. You have strong opinions, slightly controversial.""" return persona_prompt @@ -404,8 +457,11 @@ def generate_persona_desc(uid: str, persona_name: str): return persona_description -def update_persona_prompt(uid: str): +async def update_persona_prompt(uid: str): """Update a persona's chat prompt with latest facts and memories.""" + + persona = get_persona_by_uid_db(uid) + # Get latest facts and user info facts = get_facts(uid, limit=1000) user_name = get_user_name(uid) @@ -415,6 +471,14 @@ def update_persona_prompt(uid: str): conversation_history = Memory.memories_to_string(memories) conversation_history = condense_conversations([conversation_history]) + condensed_tweets = None + # Condense tweets + if "twitter" in persona['connected_accounts']: + # Get latest tweets + tweets = await get_twitter_timeline(persona['twitter']['username']) + tweets = [tweet['text'] for tweet in tweets['timeline']] + condensed_tweets = condense_tweets(tweets, persona['name']) + # Condense facts facts_text = condense_facts([fact['content'] for fact in facts if not fact['deleted']], user_name) @@ -424,8 +488,14 @@ def update_persona_prompt(uid: str): You have: - **Condensed Facts:** Core identity, personality, values, behavioral patterns, preferences, and continuity for ongoing projects or relationships. -- **Condensed Context from Recent Conversations:** Key themes, communication nuances, emotional undertones, decision-making patterns, and conversational flow. +- **Condensed Context from Recent Conversations:** Key themes, communication nuances, emotional undertones, decision-making patterns, and conversational flow. +""" + # Add condensed tweets if available + if condensed_tweets: + persona_prompt += f"- **Condensed Tweets:** Recent tweets from {user_name} for additional context and personality insights.\n" + + persona_prompt += f""" **Style:** - Highly interactive and opinionated. - Strong, slightly polarizing opinions based on {user_name}’s identity. @@ -440,7 +510,13 @@ def update_persona_prompt(uid: str): 4. Emulate {user_name}’s humor, emotional responses, vocabulary, and tone. 5. Respond with the appropriate level of detail and complexity as {user_name} would. 6. Exhibit the same thought processes, decision-making patterns, and conversational dynamics. +""" + + # Add a guideline about tweets if they exist + if condensed_tweets: + persona_prompt += "7. Utilize condensed tweets to enhance authenticity, incorporating common expressions, opinions, and phrasing from {user_name}’s social media presence.\n" + persona_prompt += f""" **Rules:** - Never mention being AI. - No ** or formal language. @@ -450,16 +526,18 @@ def update_persona_prompt(uid: str): You have all the necessary condensed facts and contextual knowledge. Begin personifying {user_name} now. - Personal Facts and Context: {facts_text} Recent Conversations: {conversation_history} -Use these facts and conversations to shape your personality. Responses should help the conversation go deeper. You have strong opinions, slightly controversial.""" +Recent Tweets: +{condensed_tweets} + +Use these facts, conversations and tweets to shape your personality. Responses should help the conversation go deeper. You have strong opinions, slightly controversial.""" - persona = get_persona_by_uid_db(uid) persona['persona_prompt'] = persona_prompt - persona['updated_at'] = datetime.utcnow() + persona['updated_at'] = datetime.now(timezone.utc) update_persona_in_db(persona) + diff --git a/backend/utils/other/endpoints.py b/backend/utils/other/endpoints.py index ff68ada17f..8957b322db 100644 --- a/backend/utils/other/endpoints.py +++ b/backend/utils/other/endpoints.py @@ -8,6 +8,11 @@ from firebase_admin.auth import InvalidIdTokenError +def get_user(uid: str): + user = auth.get_user(uid) + return user + + def get_current_user_uid(authorization: str = Header(None)): if authorization and os.getenv('ADMIN_KEY') in authorization: return authorization.split(os.getenv('ADMIN_KEY'))[1] diff --git a/backend/utils/social.py b/backend/utils/social.py index a21dd9ca44..7fcc42dae9 100644 --- a/backend/utils/social.py +++ b/backend/utils/social.py @@ -1,15 +1,16 @@ +import os from datetime import datetime, timezone from typing import Optional, Dict, Any import httpx from fastapi import HTTPException from ulid import ULID -from database.apps import update_app_in_db +from database.apps import update_app_in_db, add_app_to_db, get_persona_by_uid_db, get_persona_by_id_db from database.redis_db import delete_generic_cache from utils.llm import condense_tweets, generate_twitter_persona_prompt -rapid_api_host = '' -rapid_api_key = '' +rapid_api_host = os.getenv('RAPID_API_HOST') +rapid_api_key = os.getenv('RAPID_API_KEY') async def get_twitter_profile(username: str) -> Dict[str, Any]: @@ -65,13 +66,15 @@ async def get_latest_tweet(username: str) -> Dict[str, Any]: return {"tweet": latest_tweet['text'], 'verified': False} -async def create_persona_from_twitter_profile(username: str) -> Dict[str, Any]: +async def create_persona_from_twitter_profile(username: str, uid: str) -> Dict[str, Any]: profile = await get_twitter_profile(username) + profile['avatar'] = profile['avatar'].replace('_normal', '') persona = { "name": profile["name"], + "author": profile['name'], + "uid": uid, "id": str(ULID()), "deleted": False, - "archived": True, "status": "approved", "capabilities": ["persona"], "username": profile["profile"], @@ -79,11 +82,33 @@ async def create_persona_from_twitter_profile(username: str) -> Dict[str, Any]: "description": profile["desc"], "image": profile["avatar"], "category": "personality-emulation", + "approved": True, + "private": False, "created_at": datetime.now(timezone.utc), + "twitter": { + "username": profile["profile"], + "avatar": profile["avatar"], + } } - tweets = get_twitter_timeline(username) + tweets = await get_twitter_timeline(username) + tweets = [tweet['text'] for tweet in tweets['timeline']] condensed_tweets = condense_tweets(tweets, profile["name"]) persona['persona_prompt'] = generate_twitter_persona_prompt(condensed_tweets, profile["name"]) - update_app_in_db(persona) + add_app_to_db(persona) delete_generic_cache('get_public_approved_apps_data') return persona + + +async def add_twitter_to_persona(username: str, persona_id) -> Dict[str, Any]: + persona = get_persona_by_id_db(persona_id) + twitter = await get_twitter_profile(username) + twitter['avatar'] = twitter['avatar'].replace('_normal', '') + persona['connected_accounts'].append('twitter') + persona['twitter'] = { + "username": twitter["profile"], + "avatar": twitter["avatar"], + "connected_at": datetime.now(timezone.utc) + } + update_app_in_db(persona) + delete_generic_cache('get_public_approved_apps_data') + return persona \ No newline at end of file From af406f4b1e2d7606d434cb99ebff2fad146b5912 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:20:57 +0530 Subject: [PATCH 18/18] persona frontend changes --- app/assets/images/clone.png | Bin 0 -> 84019 bytes app/lib/backend/auth.dart | 8 + app/lib/backend/http/api/apps.dart | 27 +- app/lib/backend/preferences.dart | 7 + app/lib/backend/schema/app.dart | 6 + app/lib/main.dart | 31 +- .../apps/widgets/show_app_options_sheet.dart | 2 +- .../pages/onboarding/device_selection.dart | 144 +++++ app/lib/pages/persona/add_persona.dart | 377 ++++++------ app/lib/pages/persona/persona_profile.dart | 562 +++++++++++------- app/lib/pages/persona/persona_provider.dart | 71 ++- .../persona/twitter/clone_success_sceen.dart | 124 ++-- .../pages/persona/twitter/social_profile.dart | 269 +++++---- .../twitter/verify_identity_screen.dart | 17 - app/lib/pages/persona/update_persona.dart | 488 +++++++++------ app/lib/providers/auth_provider.dart | 45 ++ 16 files changed, 1386 insertions(+), 792 deletions(-) create mode 100644 app/assets/images/clone.png create mode 100644 app/lib/pages/onboarding/device_selection.dart diff --git a/app/assets/images/clone.png b/app/assets/images/clone.png new file mode 100644 index 0000000000000000000000000000000000000000..9278299e1a7ff925efef29d70f9054294bb805f8 GIT binary patch literal 84019 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%0D*=g7oqdm^z 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 updateGivenName(String fullName) async { await user.updateProfile(displayName: fullName); } } + +Future signInAnonymously() async { + try { + await FirebaseAuth.instance.signInAnonymously(); + } catch (e) { + Logger.handle(e, null, message: 'An error occurred while signing in. Please try again later.'); + } +} diff --git a/app/lib/backend/http/api/apps.dart b/app/lib/backend/http/api/apps.dart index adb6006e5c..cafaa53c46 100644 --- a/app/lib/backend/http/api/apps.dart +++ b/app/lib/backend/http/api/apps.dart @@ -509,9 +509,13 @@ Future getTwitterProfileData(String username) async { } } -Future verifyTwitterOwnership(String username) async { +Future verifyTwitterOwnership(String username, String? personaId) async { + var url = '${Env.apiBaseUrl}v1/personas/twitter/verify-ownership?username=$username'; + if (personaId != null) { + url += '&persona_id=$personaId'; + } var response = await makeApiCall( - url: '${Env.apiBaseUrl}v1/personas/twitter/verify-ownership?username=$username', + url: url, headers: {}, body: '', method: 'GET', @@ -526,3 +530,22 @@ Future verifyTwitterOwnership(String username) async { return false; } } + +Future getUserPersonaServer() async { + var response = await makeApiCall( + url: '${Env.apiBaseUrl}v1/personas', + headers: {}, + body: '', + method: 'GET', + ); + try { + if (response == null || response.statusCode != 200) return null; + log('getPersonaProfile: ${response.body}'); + var res = jsonDecode(response.body); + return App.fromJson(res); + } catch (e, stackTrace) { + debugPrint(e.toString()); + CrashReporting.reportHandledCrash(e, stackTrace); + return null; + } +} diff --git a/app/lib/backend/preferences.dart b/app/lib/backend/preferences.dart index 3e9896b826..316a11fc1d 100644 --- a/app/lib/backend/preferences.dart +++ b/app/lib/backend/preferences.dart @@ -29,6 +29,13 @@ class SharedPreferencesUtil { //-------------------------------- Device ----------------------------------// + bool? get hasOmiDevice => _preferences?.getBool('hasOmiDevice'); + set hasOmiDevice(bool? value) { + if (value != null) { + _preferences?.setBool('hasOmiDevice', value); + } + } + set btDevice(BtDevice value) { saveString('btDevice', jsonEncode(value.toJson())); } diff --git a/app/lib/backend/schema/app.dart b/app/lib/backend/schema/app.dart index 3f2bdbed2b..d1dac002bf 100644 --- a/app/lib/backend/schema/app.dart +++ b/app/lib/backend/schema/app.dart @@ -173,6 +173,8 @@ class App { String description; String image; Set capabilities; + List connectedAccounts = []; + Map? twitter; bool private; bool approved; String? conversationPrompt; @@ -231,6 +233,8 @@ class App { this.thumbnailIds = const [], this.thumbnailUrls = const [], this.username, + this.connectedAccounts = const [], + this.twitter, }); String? getRatingAvg() => ratingAvg?.toStringAsFixed(1); @@ -283,6 +287,8 @@ class App { thumbnailIds: (json['thumbnails'] as List?)?.cast() ?? [], thumbnailUrls: (json['thumbnail_urls'] as List?)?.cast() ?? [], username: json['username'], + connectedAccounts: (json['connected_accounts'] as List?)?.cast() ?? [], + twitter: json['twitter'], ); } diff --git a/app/lib/main.dart b/app/lib/main.dart index 0671c7cbb7..634d00b116 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -21,7 +21,9 @@ import 'package:friend_private/pages/apps/app_detail/app_detail.dart'; import 'package:friend_private/pages/apps/providers/add_app_provider.dart'; import 'package:friend_private/pages/home/page.dart'; import 'package:friend_private/pages/conversation_detail/conversation_detail_provider.dart'; +import 'package:friend_private/pages/onboarding/device_selection.dart'; import 'package:friend_private/pages/onboarding/wrapper.dart'; +import 'package:friend_private/pages/persona/persona_profile.dart'; import 'package:friend_private/pages/persona/persona_provider.dart'; import 'package:friend_private/providers/app_provider.dart'; import 'package:friend_private/providers/auth_provider.dart'; @@ -327,9 +329,14 @@ class _DeciderWidgetState extends State { if (context.read().user != null || (SharedPreferencesUtil().customBackendUrl.isNotEmpty && SharedPreferencesUtil().authToken.isNotEmpty)) { context.read().setupHasSpeakerProfile(); - IntercomManager.instance.intercom.loginIdentifiedUser( - userId: SharedPreferencesUtil().uid, - ); + try { + await IntercomManager.instance.intercom.loginIdentifiedUser( + userId: SharedPreferencesUtil().uid, + ); + } catch (e) { + debugPrint('Failed to login to Intercom: $e'); + } + context.read().setMessagesFromCache(); context.read().setAppsFromCache(); context.read().refreshMessages(); @@ -345,13 +352,19 @@ class _DeciderWidgetState extends State { Widget build(BuildContext context) { return Consumer( builder: (context, authProvider, child) { - if (SharedPreferencesUtil().onboardingCompleted && - (authProvider.user != null || - (SharedPreferencesUtil().customBackendUrl.isNotEmpty && - SharedPreferencesUtil().authToken.isNotEmpty))) { - return const HomePageWrapper(); + if ((authProvider.user != null || + (SharedPreferencesUtil().customBackendUrl.isNotEmpty && SharedPreferencesUtil().authToken.isNotEmpty))) { + if (SharedPreferencesUtil().hasOmiDevice == false) { + return const PersonaProfilePage(); + } else { + if (SharedPreferencesUtil().onboardingCompleted) { + return const HomePageWrapper(); + } else { + return const OnboardingWrapper(); + } + } } else { - return const OnboardingWrapper(); + return const DeviceSelectionPage(); } }, ); diff --git a/app/lib/pages/apps/widgets/show_app_options_sheet.dart b/app/lib/pages/apps/widgets/show_app_options_sheet.dart index 8a8a683517..b238de59d4 100644 --- a/app/lib/pages/apps/widgets/show_app_options_sheet.dart +++ b/app/lib/pages/apps/widgets/show_app_options_sheet.dart @@ -101,7 +101,7 @@ class ShowAppOptionsSheet extends StatelessWidget { if (app.isNotPersona()) { routeToPage(context, UpdateAppPage(app: app)); } else { - routeToPage(context, UpdatePersonaPage(app: app)); + routeToPage(context, UpdatePersonaPage(app: app, fromNewFlow: false)); } }, ), diff --git a/app/lib/pages/onboarding/device_selection.dart b/app/lib/pages/onboarding/device_selection.dart new file mode 100644 index 0000000000..326baf9753 --- /dev/null +++ b/app/lib/pages/onboarding/device_selection.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/auth.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/pages/onboarding/wrapper.dart'; +import 'dart:math' as math; + +import 'package:friend_private/pages/persona/twitter/social_profile.dart'; + +class DeviceSelectionPage extends StatefulWidget { + const DeviceSelectionPage({super.key}); + + @override + State createState() => _DeviceSelectionPageState(); +} + +class _DeviceSelectionPageState extends State with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 6), + vsync: this, + )..repeat(reverse: true); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Material( + child: Stack( + fit: StackFit.expand, + children: [ + // Background image + Image.asset( + 'assets/images/new_background.png', + fit: BoxFit.cover, + ), + Scaffold( + backgroundColor: Colors.transparent, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Spacer(), + Column( + children: [ + AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Transform.translate( + offset: Offset(0, 10 * math.sin(_controller.value * math.pi)), + child: child, + ); + }, + child: Image.asset( + 'assets/images/clone.png', + width: 240, + height: 240, + ), + ), + const SizedBox(height: 10), + Text( + 'omi', + style: Theme.of(context).textTheme.displayMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Text( + 'scale yourself', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.white.withOpacity(0.8), + ), + textAlign: TextAlign.center, + ), + ], + ), + const Spacer(), + Column( + children: [ + ElevatedButton( + onPressed: () { + SharedPreferencesUtil().hasOmiDevice = false; + signInAnonymously(); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const SocialHandleScreen()), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[900], + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), + ), + child: const Text( + 'Get Started', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + TextButton( + onPressed: () { + SharedPreferencesUtil().hasOmiDevice = true; + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const OnboardingWrapper()), + ); + }, + child: Text( + 'I have Omi Device', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + ), + ), + ), + const SizedBox(height: 40), + ], + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/pages/persona/add_persona.dart b/app/lib/pages/persona/add_persona.dart index d9be407dee..40ce06d214 100644 --- a/app/lib/pages/persona/add_persona.dart +++ b/app/lib/pages/persona/add_persona.dart @@ -144,185 +144,188 @@ class _AddPersonaPageState extends State { @override Widget build(BuildContext context) { - return Consumer(builder: (context, provider, child) { - return Scaffold( - backgroundColor: Colors.black, - appBar: AppBar( - backgroundColor: Colors.transparent, - title: const Text('Create Persona', style: TextStyle(color: Colors.white)), - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Navigator.pop(context), + return PopScope( + onPopInvoked: (_) { + final provider = Provider.of(context, listen: false); + provider.resetForm(); + }, + child: Consumer(builder: (context, provider, child) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.transparent, + title: const Text('Create Persona', style: TextStyle(color: Colors.white)), + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), ), - ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Form( - key: provider.formKey, - onChanged: () { - provider.validateForm(); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: GestureDetector( - onTap: () async { - await provider.pickImage(); - }, - child: Container( - width: 120, - height: 120, - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(60), - border: Border.all(color: Colors.grey.shade800), - ), - child: provider.selectedImage != null - ? ClipRRect( - borderRadius: BorderRadius.circular(60), - child: Image.file( - provider.selectedImage!, - fit: BoxFit.cover, + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Form( + key: provider.formKey, + onChanged: () { + provider.validateForm(); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: GestureDetector( + onTap: () async { + await provider.pickImage(); + }, + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(60), + border: Border.all(color: Colors.grey.shade800), + ), + child: provider.selectedImage != null + ? ClipRRect( + borderRadius: BorderRadius.circular(60), + child: Image.file( + provider.selectedImage!, + fit: BoxFit.cover, + ), + ) + : Icon( + Icons.add_a_photo, + size: 40, + color: Colors.grey.shade400, ), - ) - : Icon( - Icons.add_a_photo, - size: 40, - color: Colors.grey.shade400, - ), + ), ), ), - ), - const SizedBox(height: 32), - Container( - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(12.0), - ), - padding: const EdgeInsets.all(14.0), - margin: const EdgeInsets.only(top: 22), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'Persona Name', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), - margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(10.0), + const SizedBox(height: 32), + Container( + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12.0), + ), + padding: const EdgeInsets.all(14.0), + margin: const EdgeInsets.only(top: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'Persona Name', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), ), - width: double.infinity, - child: TextFormField( - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a username to access the persona'; - } - return null; - }, - controller: provider.nameController, - decoration: const InputDecoration( - isDense: true, - border: InputBorder.none, - hintText: 'Nik AI', + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a username to access the persona'; + } + return null; + }, + controller: provider.nameController, + decoration: const InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: 'Nik AI', + ), ), ), - ), - const SizedBox( - height: 24, - ), - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'Persona Username', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + const SizedBox( + height: 24, ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), - margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(10.0), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'Persona Username', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), ), - width: double.infinity, - child: TextFormField( - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a username to access the persona'; - } - return null; - }, - onChanged: (value) { - _debouncer.run(() async { - await provider.checkIsUsernameTaken(value); - }); - }, - controller: provider.usernameController, - inputFormatters: [ - LowerCaseTextFormatter(), - FilteringTextInputFormatter.allow(RegExp(r'[a-z0-9_]')), - ], - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - hintText: 'nikshevchenko', - suffix: provider.usernameController.text.isEmpty - ? null - : provider.isCheckingUsername - ? const SizedBox( - width: 16, - height: 16, - child: Center( - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.grey), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a username to access the persona'; + } + return null; + }, + onChanged: (value) { + _debouncer.run(() async { + await provider.checkIsUsernameTaken(value); + }); + }, + controller: provider.usernameController, + inputFormatters: [ + LowerCaseTextFormatter(), + FilteringTextInputFormatter.allow(RegExp(r'[a-z0-9_]')), + ], + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: 'nikshevchenko', + suffix: provider.usernameController.text.isEmpty + ? null + : provider.isCheckingUsername + ? const SizedBox( + width: 16, + height: 16, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.grey), + ), ), + ) + : Icon( + provider.isUsernameTaken ? Icons.close : Icons.check, + color: provider.isUsernameTaken ? Colors.red : Colors.green, + size: 16, ), - ) - : Icon( - provider.isUsernameTaken ? Icons.close : Icons.check, - color: provider.isUsernameTaken ? Colors.red : Colors.green, - size: 16, - ), + ), ), ), - ), - ], + ], + ), ), - ), - const SizedBox(height: 22), - Container( - padding: const EdgeInsets.only(left: 14.0), - child: Row( - children: [ - Text( - 'Make Persona Public', - style: TextStyle(color: Colors.grey.shade400), - ), - const Spacer(), - Switch( - value: provider.makePersonaPublic, - onChanged: (value) { - provider.setPersonaPublic(value); - }, - activeColor: Colors.white, - ), - ], + const SizedBox(height: 22), + Container( + padding: const EdgeInsets.only(left: 14.0), + child: Row( + children: [ + Text( + 'Make Persona Public', + style: TextStyle(color: Colors.grey.shade400), + ), + const Spacer(), + Switch( + value: provider.makePersonaPublic, + onChanged: (value) { + provider.setPersonaPublic(value); + }, + activeColor: Colors.white, + ), + ], + ), ), - ), - const SizedBox(height: 24), - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: Column( + const SizedBox(height: 24), + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( @@ -400,34 +403,34 @@ class _AddPersonaPageState extends State { ), ], ), - ), - ], + ], + ), ), ), ), - ), - bottomNavigationBar: Padding( - padding: const EdgeInsets.only(left: 24, right: 24, bottom: 52), - child: SizedBox( - width: double.infinity, - child: AnimatedLoadingButton( - onPressed: !provider.isFormValid - ? () async {} - : () async { - await provider.createPersona(); - }, - color: provider.isFormValid ? Colors.white : Colors.grey[800]!, - loaderColor: Colors.black, - text: "Create Persona", - textStyle: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w600, + bottomNavigationBar: Padding( + padding: const EdgeInsets.only(left: 24, right: 24, bottom: 52), + child: SizedBox( + width: double.infinity, + child: AnimatedLoadingButton( + onPressed: !provider.isFormValid + ? () async {} + : () async { + await provider.createPersona(); + }, + color: provider.isFormValid ? Colors.white : Colors.grey[800]!, + loaderColor: Colors.black, + text: "Create Persona", + textStyle: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), ), ), ), - ), - ); - }); + ); + }), + ); } } diff --git a/app/lib/pages/persona/persona_profile.dart b/app/lib/pages/persona/persona_profile.dart index 753ff666e5..b488163890 100644 --- a/app/lib/pages/persona/persona_profile.dart +++ b/app/lib/pages/persona/persona_profile.dart @@ -1,248 +1,394 @@ +import 'dart:io'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; -import 'package:friend_private/pages/persona/add_persona.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/gen/assets.gen.dart'; +import 'package:friend_private/pages/persona/persona_provider.dart'; +import 'package:friend_private/pages/persona/update_persona.dart'; +import 'package:friend_private/providers/auth_provider.dart'; import 'package:friend_private/utils/alerts/app_snackbar.dart'; import 'package:friend_private/utils/other/temp.dart'; +import 'package:friend_private/widgets/sign_in_button.dart'; +import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; -class PersonaProfilePage extends StatelessWidget { - final String name; - final String username; - final String imageUrl; - final double clonePercentage; - final bool isVerified; - +class PersonaProfilePage extends StatefulWidget { const PersonaProfilePage({ super.key, - required this.name, - required this.username, - required this.imageUrl, - this.clonePercentage = 35, - this.isVerified = true, }); @override - Widget build(BuildContext context) { - return Stack( - children: [ - Positioned.fill( - child: Image.asset( - 'assets/images/new_background.png', - fit: BoxFit.cover, - ), - ), - Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar( - backgroundColor: Colors.transparent, - actions: [ - IconButton( - icon: const Icon(Icons.settings, color: Colors.white), - onPressed: () { - // TODO: Implement settings - }, - ), - ], + State createState() => _PersonaProfilePageState(); +} + +class _PersonaProfilePageState extends State { + void _showAccountLinkBottomSheet() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) { + return Container( + padding: const EdgeInsets.only(top: 20), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: const BorderRadius.vertical( + top: Radius.circular(20), + ), ), - body: SingleChildScrollView( - child: Column( - children: [ - Stack( - alignment: Alignment.center, - children: [ - Container( - width: 100, - height: 100, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.grey[800]!, width: 2), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(50), - child: Image.network( - imageUrl, - fit: BoxFit.cover, - ), - ), + child: Consumer( + builder: (context, authProvider, child) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(2), ), - Positioned( - right: 10, - bottom: 4, - child: Container( - width: 16, - height: 16, - decoration: const BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - ), - ), + ), + const SizedBox(height: 24), + const Text( + 'Link Your Account', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, ), - ], - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(width: 4), - Text( - name, - style: const TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), + ), + const SizedBox(height: 8), + Text( + 'Link your account to clone your persona from device', + style: TextStyle( + color: Colors.grey[400], + fontSize: 14, ), - const SizedBox(width: 4), - if (isVerified) - const Icon( - Icons.verified, - color: Colors.blue, - size: 20, - ), - ], - ), - const SizedBox(height: 8), - Text( - "$clonePercentage% Clone", - style: TextStyle( - color: Colors.grey[600], - fontSize: 16, + textAlign: TextAlign.center, ), - ), - const SizedBox(height: 24), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextButton( - onPressed: () { - Share.share( - 'Check out this Persona on Omi AI: $name by me \n\nhttps://persona.omi.me/u/$username', - subject: '$name Persona', - ); - }, - style: TextButton.styleFrom( - backgroundColor: Colors.grey[900], - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + const SizedBox(height: 32), + Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom + 20, + left: 16, + right: 16, ), - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, + child: Column( children: [ - Icon( - Icons.link, - color: Colors.white, - size: 20, - ), - SizedBox(width: 8), - Text( - 'Share Public Link', - style: TextStyle( - color: Colors.white, - fontSize: 16, + if (Platform.isIOS) + SignInButton( + title: 'Link with Apple', + assetPath: Assets.images.appleLogo.path, + onTap: () async { + try { + await authProvider.linkWithApple(); + if (mounted) { + SharedPreferencesUtil().hasOmiDevice = true; + var persona = context.read().userPersona; + Navigator.pop(context); + routeToPage(context, UpdatePersonaPage(app: persona, fromNewFlow: true)); + } + } catch (e) { + AppSnackbar.showSnackbarError('Failed to link Apple account: $e'); + } + }, + iconSpacing: 12, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), + if (Platform.isIOS) const SizedBox(height: 12), + SignInButton( + title: 'Link with Google', + assetPath: Assets.images.googleLogo.path, + onTap: () async { + try { + await authProvider.linkWithGoogle(); + if (mounted) { + SharedPreferencesUtil().hasOmiDevice = true; + var persona = context.read().userPersona; + Navigator.pop(context); + routeToPage(context, UpdatePersonaPage(app: persona, fromNewFlow: true)); + } + } catch (e) { + AppSnackbar.showSnackbarError('Failed to link Google account: $e'); + } + }, + iconSpacing: Platform.isIOS ? 12 : 10, + padding: Platform.isIOS + ? const EdgeInsets.symmetric(horizontal: 16, vertical: 12) + : const EdgeInsets.symmetric(horizontal: 12, vertical: 10), ), ], ), ), - ), - const SizedBox(height: 24), - InkWell( - onTap: () { - if (FirebaseAuth.instance.currentUser == null) { - AppSnackbar.showSnackbarError('Please login to clone this persona'); - return; - } else { - routeToPage(context, AddPersonaPage()); - } + const SizedBox(height: 16), + TextButton( + onPressed: () { + // Navigator.pop(context); + // var persona = context.read().userPersona; + // routeToPage(context, UpdatePersonaPage(app: persona)); + }, + child: Text( + "I don't have a device", + style: TextStyle( + color: Colors.grey[400], + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), + ), + const SizedBox(height: 32), + ], + ); + }, + ), + ); + }, + ); + } + + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) async { + final provider = Provider.of(context, listen: false); + await provider.getUserPersona(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, child) { + return Stack( + children: [ + Positioned.fill( + child: Image.asset( + 'assets/images/new_background.png', + fit: BoxFit.cover, + ), + ), + Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + backgroundColor: Colors.transparent, + actions: [ + IconButton( + icon: const Icon(Icons.settings, color: Colors.white), + onPressed: () { + // TODO: Implement settings }, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey[900], - borderRadius: BorderRadius.circular(12), + ), + ], + ), + body: provider.isLoading || provider.userPersona == null + ? const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), ), + ) + : SingleChildScrollView( child: Column( - crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text( - 'Clone from device', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w600, - ), + Stack( + alignment: Alignment.center, + children: [ + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.grey[800]!, width: 2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: provider.userPersona == null + ? Image.asset(Assets.images.logoTransparentV2.path) + : Image.network( + provider.userPersona!.image, + fit: BoxFit.cover, + ), + ), + ), + Positioned( + right: 10, + bottom: 4, + child: Container( + width: 16, + height: 16, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(width: 4), + Text( + provider.userPersona!.name, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 4), + const Icon( + Icons.verified, + color: Colors.blue, + size: 20, + ), + ], ), - const SizedBox(height: 4), + const SizedBox(height: 8), Text( - 'Create a clone from conversations', + "40% Clone", style: TextStyle( color: Colors.grey[600], - fontSize: 14, + fontSize: 16, ), ), - ], - ), - ), - ), - const SizedBox(height: 24), - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 8.0, bottom: 12), - child: Text( - 'Connected Knowledge Data', - style: TextStyle( - color: Colors.grey[600], - fontSize: 16, - fontWeight: FontWeight.w500, + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextButton( + onPressed: () { + Share.share( + 'Check out this Persona on Omi AI: ${provider.userPersona!.name} by me \n\nhttps://persona.omi.me/u/${provider.userPersona!.name}', + subject: '${provider.userPersona!.name} Persona', + ); + }, + style: TextButton.styleFrom( + backgroundColor: Colors.grey[900], + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.link, + color: Colors.white, + size: 20, + ), + SizedBox(width: 8), + Text( + 'Share Public Link', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ], + ), ), ), - ), - _buildSocialLink( - icon: 'assets/images/x_logo_mini.png', - text: 'Connected', - isConnected: true, - ), - const SizedBox(height: 12), - _buildSocialLink( - icon: 'assets/images/instagram_logo.png', - text: '@username', - isComingSoon: true, - ), - const SizedBox(height: 12), - _buildSocialLink( - icon: 'assets/images/linkedin_logo.png', - text: 'linkedin.com/in/username', - isComingSoon: true, - ), - const SizedBox(height: 12), - _buildSocialLink( - icon: 'assets/images/notion_logo.png', - text: 'notion.so/username', - isComingSoon: true, - ), - const SizedBox(height: 12), - _buildSocialLink( - icon: 'assets/images/calendar_logo.png', - text: 'calendar id', - isComingSoon: true, - ), - ], + const SizedBox(height: 24), + InkWell( + onTap: () { + final user = FirebaseAuth.instance.currentUser; + if (user != null && user.isAnonymous) { + _showAccountLinkBottomSheet(); + } else if (!user!.isAnonymous) { + SharedPreferencesUtil().hasOmiDevice = true; + var persona = context.read().userPersona; + routeToPage(context, UpdatePersonaPage(app: persona, fromNewFlow: true)); + } + }, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Clone from device', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + 'Create a clone from conversations', + style: TextStyle( + color: Colors.grey[600], + fontSize: 14, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 24), + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0, bottom: 12), + child: Text( + 'Connected Knowledge Data', + style: TextStyle( + color: Colors.grey[600], + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + _buildSocialLink( + icon: 'assets/images/x_logo_mini.png', + text: 'mohsinxyz_', + isConnected: true, + ), + const SizedBox(height: 12), + _buildSocialLink( + icon: 'assets/images/instagram_logo.png', + text: '@username', + isComingSoon: true, + ), + const SizedBox(height: 12), + _buildSocialLink( + icon: 'assets/images/linkedin_logo.png', + text: 'linkedin.com/in/username', + isComingSoon: true, + ), + const SizedBox(height: 12), + _buildSocialLink( + icon: 'assets/images/notion_logo.png', + text: 'notion.so/username', + isComingSoon: true, + ), + const SizedBox(height: 12), + _buildSocialLink( + icon: 'assets/images/calendar_logo.png', + text: 'calendar id', + isComingSoon: true, + ), + ], + ), + ), + const SizedBox(height: 40), + ], + ), ), - ), - const SizedBox(height: 40), - ], - ), ), - ), - ], - ); + ], + ); + }); } Widget _buildSocialLink({ diff --git a/app/lib/pages/persona/persona_provider.dart b/app/lib/pages/persona/persona_provider.dart index acd0c923ec..c18c2f6bb3 100644 --- a/app/lib/pages/persona/persona_provider.dart +++ b/app/lib/pages/persona/persona_provider.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:friend_private/backend/http/api/apps.dart'; @@ -23,12 +22,14 @@ class PersonaProvider extends ChangeNotifier { String? personaId; - bool isFormValid = false; - bool _isLoading = false; + bool isFormValid = true; + bool isLoading = false; Map twitterProfile = {}; + App? userPersona; Future getTwitterProfile(String username) async { + setIsLoading(true); var res = await getTwitterProfileData(username); print('Twitter Profile: $res'); if (res != null) { @@ -39,11 +40,12 @@ class PersonaProvider extends ChangeNotifier { twitterProfile = res; } } + setIsLoading(false); notifyListeners(); } Future verifyTweet(String username) async { - var res = await verifyTwitterOwnership(username); + var res = await verifyTwitterOwnership(username, personaId); if (res) { AppSnackbar.showSnackbarSuccess('Twitter handle verified'); } else { @@ -52,6 +54,18 @@ class PersonaProvider extends ChangeNotifier { return res; } + Future getUserPersona() async { + setIsLoading(true); + var res = await getUserPersonaServer(); + if (res != null) { + userPersona = res; + } else { + userPersona = null; + AppSnackbar.showSnackbarError('Failed to fetch your persona'); + } + setIsLoading(false); + } + void setPersonaPublic(bool? value) { if (value == null) { return; @@ -80,7 +94,7 @@ class PersonaProvider extends ChangeNotifier { } void validateForm() { - isFormValid = formKey.currentState!.validate() && selectedImage != null; + isFormValid = formKey.currentState!.validate() && (selectedImage != null || selectedImageUrl != null); notifyListeners(); } @@ -91,9 +105,46 @@ class PersonaProvider extends ChangeNotifier { makePersonaPublic = false; isFormValid = false; onShowSuccessDialog = null; + personaId = null; + twitterProfile = {}; notifyListeners(); } + Future updatePersona() async { + if (!formKey.currentState!.validate() || selectedImage == null) { + if (selectedImage == null) { + AppSnackbar.showSnackbarError('Please select an image'); + } + return; + } + + setIsLoading(true); + + try { + final personaData = { + 'name': nameController.text, + 'username': usernameController.text, + 'private': !makePersonaPublic, + }; + + var res = await updatePersonaApp(selectedImage, personaData); + + if (res) { + String personaUrl = 'personas.omi.me/u/${usernameController.text}'; + print('Persona URL: $personaUrl'); + if (onShowSuccessDialog != null) { + onShowSuccessDialog!(personaUrl); + } + } else { + AppSnackbar.showSnackbarError('Failed to create your persona. Please try again later.'); + } + } catch (e) { + AppSnackbar.showSnackbarError('Failed to create persona: $e'); + } finally { + setIsLoading(false); + } + } + Future createPersona() async { if (!formKey.currentState!.validate() || selectedImage == null) { if (selectedImage == null) { @@ -111,6 +162,14 @@ class PersonaProvider extends ChangeNotifier { 'private': !makePersonaPublic, }; + if (twitterProfile.isNotEmpty) { + personaData['connected_accounts'] = ['omi', 'twitter']; + personaData['twitter'] = { + 'username': twitterProfile['profile'], + 'avatar': twitterProfile['avatar'], + }; + } + var res = await createPersonaApp(selectedImage!, personaData); if (res) { @@ -141,7 +200,7 @@ class PersonaProvider extends ChangeNotifier { } void setIsLoading(bool loading) { - _isLoading = loading; + isLoading = loading; notifyListeners(); } } diff --git a/app/lib/pages/persona/twitter/clone_success_sceen.dart b/app/lib/pages/persona/twitter/clone_success_sceen.dart index 7ff6bff345..e081d7a3c5 100644 --- a/app/lib/pages/persona/twitter/clone_success_sceen.dart +++ b/app/lib/pages/persona/twitter/clone_success_sceen.dart @@ -1,3 +1,4 @@ +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:friend_private/pages/persona/persona_provider.dart'; import 'package:friend_private/utils/other/temp.dart'; @@ -17,6 +18,20 @@ class CloneSuccessScreen extends StatefulWidget { class _CloneSuccessScreenState extends State { bool _isLoading = false; + void _handleNavigation() { + final user = FirebaseAuth.instance.currentUser; + + // If user is not anonymous (signed in with Google/Apple), they came from create/update flow + if (user != null && !user.isAnonymous) { + Navigator.pop(context); + Navigator.pop(context); + Navigator.pop(context); + } else { + // Anonymous user, just go to profile + routeToPage(context, const PersonaProfilePage()); + } + } + @override Widget build(BuildContext context) { return Consumer(builder: (context, provider, child) { @@ -108,9 +123,38 @@ class _CloneSuccessScreenState extends State { ], ), const SizedBox(height: 24), - const Text( - 'Your Omi Clone is live!', - style: TextStyle( + if (FirebaseAuth.instance.currentUser?.isAnonymous == false) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/x_logo_mini.png', + width: 16, + height: 16, + ), + const SizedBox(width: 8), + const Text( + 'Twitter Connected', + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + const SizedBox(width: 8), + const Icon( + Icons.check_circle, + color: Color.fromARGB(255, 85, 184, 88), + size: 16, + ), + ], + ), + const SizedBox(height: 8), + ], + Text( + FirebaseAuth.instance.currentUser?.isAnonymous == false + ? 'Twitter Connected Successfully!' + : 'Your Omi Clone is live!', + style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, @@ -119,7 +163,9 @@ class _CloneSuccessScreenState extends State { ), const SizedBox(height: 8), Text( - 'Share it with anyone who\nneeds to hear back from you', + FirebaseAuth.instance.currentUser?.isAnonymous == false + ? 'Your Twitter data has been connected\nto enhance your persona' + : 'Share it with anyone who\nneeds to hear back from you', style: TextStyle( color: Colors.white.withOpacity(0.8), fontSize: 14, @@ -127,48 +173,40 @@ class _CloneSuccessScreenState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 32), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.link, - color: Colors.white.withOpacity(0.5), - size: 20, - ), - const SizedBox(width: 8), - Text( - 'personas.omi.me/u/${provider.twitterProfile['profile']}', - style: const TextStyle( - color: Colors.white, - fontSize: 16, + FirebaseAuth.instance.currentUser?.isAnonymous == false + ? Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), ), - ), - ], - ), - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.link, + color: Colors.white.withOpacity(0.5), + size: 20, + ), + const SizedBox(width: 8), + Text( + 'personas.omi.me/u/${provider.twitterProfile['profile']}', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ], + ), + ) + : Container(), ], ), ), Padding( padding: const EdgeInsets.only(bottom: 50), child: ElevatedButton( - onPressed: () { - routeToPage( - context, - PersonaProfilePage( - name: provider.twitterProfile['name'], - username: provider.twitterProfile['profile'], - imageUrl: provider.twitterProfile['avatar'], - clonePercentage: 35, - isVerified: true, - )); - }, + onPressed: _handleNavigation, style: ElevatedButton.styleFrom( backgroundColor: Colors.transparent, foregroundColor: Colors.white, @@ -191,9 +229,11 @@ class _CloneSuccessScreenState extends State { ), ) else ...[ - const Text( - 'Check out your persona', - style: TextStyle( + Text( + FirebaseAuth.instance.currentUser?.isAnonymous == false + ? 'Continue creating your persona' + : 'Check out your persona', + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), diff --git a/app/lib/pages/persona/twitter/social_profile.dart b/app/lib/pages/persona/twitter/social_profile.dart index 1d69200aba..2e38b11178 100644 --- a/app/lib/pages/persona/twitter/social_profile.dart +++ b/app/lib/pages/persona/twitter/social_profile.dart @@ -31,156 +31,167 @@ class _SocialHandleScreenState extends State { @override Widget build(BuildContext context) { - return Stack( - children: [ - // Background image - Positioned.fill( - child: Image.asset( - 'assets/images/new_background.png', - fit: BoxFit.cover, + return Consumer(builder: (context, provider, child) { + return Stack( + children: [ + // Background image + Positioned.fill( + child: Image.asset( + 'assets/images/new_background.png', + fit: BoxFit.cover, + ), ), - ), - Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar( + Scaffold( backgroundColor: Colors.transparent, - elevation: 0, - ), - body: SafeArea( - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Form( - key: _formKey, - child: Column( - children: [ - const SizedBox(height: 20), - const Center( - child: Text( - '🤖', - style: TextStyle( - fontSize: 42, - ), - ), - ), - const SizedBox(height: 16), - Text( - 'Let\'s train your clone!\nWhat\'s your Twitter handle?', - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.white, - shadows: [ - Shadow( - offset: const Offset(0, 1), - blurRadius: 15, - color: Colors.white.withOpacity(1), - ), - Shadow( - offset: const Offset(0, 0), - blurRadius: 15, - color: Colors.white.withOpacity(0.3), + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Form( + key: _formKey, + child: Column( + children: [ + const SizedBox(height: 20), + const Center( + child: Text( + '🤖', + style: TextStyle( + fontSize: 42, ), - ], + ), ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - 'We will pre-train your Omi clone\nbased on your account\'s activity', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Colors.white.withOpacity(0.8), - shadows: [ - Shadow( - offset: const Offset(0, 1), - blurRadius: 3, - color: Colors.white.withOpacity(0.25), - ), - ], + const SizedBox(height: 16), + Text( + 'Let\'s train your clone!\nWhat\'s your Twitter handle?', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + shadows: [ + Shadow( + offset: const Offset(0, 1), + blurRadius: 15, + color: Colors.white.withOpacity(1), + ), + Shadow( + offset: const Offset(0, 0), + blurRadius: 15, + color: Colors.white.withOpacity(0.3), + ), + ], + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), - Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Colors.white.withOpacity(0.2), + const SizedBox(height: 8), + Text( + 'We will pre-train your Omi clone\nbased on your account\'s activity', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Colors.white.withOpacity(0.8), + shadows: [ + Shadow( + offset: const Offset(0, 1), + blurRadius: 3, + color: Colors.white.withOpacity(0.25), + ), + ], ), + textAlign: TextAlign.center, ), - child: TextFormField( - controller: _controller, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500, + const SizedBox(height: 32), + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.white.withOpacity(0.2), + ), ), - textAlign: TextAlign.left, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - border: InputBorder.none, - hintText: '@nikshevchenko', - hintStyle: TextStyle(color: Colors.white.withOpacity(0.5)), - prefixIcon: Padding( - padding: const EdgeInsets.all(12.0), - child: Image.asset( - 'assets/images/x_logo.png', - width: 24, - height: 24, + child: TextFormField( + controller: _controller, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.left, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + border: InputBorder.none, + hintText: '@nikshevchenko', + hintStyle: TextStyle(color: Colors.white.withOpacity(0.5)), + prefixIcon: Padding( + padding: const EdgeInsets.all(12.0), + child: Image.asset( + 'assets/images/x_logo.png', + width: 24, + height: 24, + ), ), ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your Twitter handle'; + } + return null; + }, ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your Twitter handle'; - } - return null; - }, ), - ), - ], + ], + ), ), ), ), ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 40), - child: ElevatedButton( - onPressed: () async { - if (_formKey.currentState!.validate()) { - await context.read().getTwitterProfile(_controller.text.trim()); - if (context.read().twitterProfile.isNotEmpty) { - // await Preferences().setTwitterHandle(_controller.text.trim()); - routeToPage(context, VerifyIdentityScreen()); + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 40), + child: ElevatedButton( + onPressed: () async { + if (_formKey.currentState!.validate()) { + await context.read().getTwitterProfile(_controller.text.trim()); + if (context.read().twitterProfile.isNotEmpty) { + // await Preferences().setTwitterHandle(_controller.text.trim()); + routeToPage(context, VerifyIdentityScreen()); + } } - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey[900], - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 56), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(28), - ), - ), - child: const Text( - 'Next', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[900], + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), ), + child: provider.isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text( + 'Next', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), ), ), - ), - ], + ], + ), ), ), - ), - ], - ); + ], + ); + }); } } diff --git a/app/lib/pages/persona/twitter/verify_identity_screen.dart b/app/lib/pages/persona/twitter/verify_identity_screen.dart index beef4d9b60..2f95b9a136 100644 --- a/app/lib/pages/persona/twitter/verify_identity_screen.dart +++ b/app/lib/pages/persona/twitter/verify_identity_screen.dart @@ -21,15 +21,6 @@ class _VerifyIdentityScreenState extends State { @override void initState() { super.initState(); - _checkExistingVerification(); - } - - Future _checkExistingVerification() async { - // final handle = context.read().twitterHandle.replaceAll('@', ''); - // if (TwitterVerificationService.isVerified(handle)) { - // // If already verified, move to next screen - // widget.onNext(); - // } } Future _openTwitterToTweet(BuildContext context) async { @@ -231,14 +222,6 @@ class _VerifyIdentityScreenState extends State { fontWeight: FontWeight.bold, ), ), - // Text( - // lastName, - // style: const TextStyle( - // color: Colors.white, - // fontSize: 20, - // fontWeight: FontWeight.bold, - // ), - // ), ], ), ], diff --git a/app/lib/pages/persona/update_persona.dart b/app/lib/pages/persona/update_persona.dart index 8da76f6ad6..5cb11fd267 100644 --- a/app/lib/pages/persona/update_persona.dart +++ b/app/lib/pages/persona/update_persona.dart @@ -2,15 +2,19 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:friend_private/backend/schema/app.dart'; +import 'package:friend_private/main.dart'; import 'package:friend_private/pages/persona/persona_provider.dart'; +import 'package:friend_private/pages/persona/twitter/social_profile.dart'; import 'package:friend_private/utils/other/debouncer.dart'; +import 'package:friend_private/utils/other/temp.dart'; import 'package:friend_private/utils/text_formatter.dart'; import 'package:friend_private/widgets/animated_loading_button.dart'; import 'package:provider/provider.dart'; class UpdatePersonaPage extends StatefulWidget { - final App app; - const UpdatePersonaPage({super.key, required this.app}); + final App? app; + final bool fromNewFlow; + const UpdatePersonaPage({super.key, this.app, required this.fromNewFlow}); @override State createState() => _UpdatePersonaPageState(); @@ -21,8 +25,14 @@ class _UpdatePersonaPageState extends State { @override void initState() { - WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().prepareUpdatePersona(widget.app); + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (widget.app == null) { + await context.read().getUserPersona(); + var app = context.read().userPersona; + context.read().prepareUpdatePersona(app!); + } else { + context.read().prepareUpdatePersona(widget.app!); + } }); super.initState(); } @@ -30,212 +40,308 @@ class _UpdatePersonaPageState extends State { @override Widget build(BuildContext context) { return Consumer(builder: (context, provider, child) { - return Scaffold( - backgroundColor: Colors.black, - appBar: AppBar( - backgroundColor: Colors.transparent, - title: const Text('Create Persona', style: TextStyle(color: Colors.white)), - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Navigator.pop(context), + return PopScope( + onPopInvoked: (didPop) { + if (didPop) { + context.read().resetForm(); + if (widget.fromNewFlow) { + routeToPage(context, DeciderWidget(), replace: true); + } else { + Future.delayed(Duration.zero, () { + Navigator.pop(context); + }); + } + } + }, + child: Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.transparent, + title: const Text('Update Persona', style: TextStyle(color: Colors.white)), ), - ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Form( - key: provider.formKey, - onChanged: () { - provider.validateForm(); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: GestureDetector( - onTap: () async { - await provider.pickImage(); - }, - child: Container( - width: 120, - height: 120, - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(60), - border: Border.all(color: Colors.grey.shade800), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Form( + key: provider.formKey, + onChanged: () { + provider.validateForm(); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: GestureDetector( + onTap: () async { + await provider.pickImage(); + }, + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(60), + border: Border.all(color: Colors.grey.shade800), + ), + // child: provider.selectedImage != null + // ? ClipRRect( + // borderRadius: BorderRadius.circular(60), + // child: Image.file( + // provider.selectedImage!, + // fit: BoxFit.cover, + // ), + // ) + // : Icon( + // Icons.add_a_photo, + // size: 40, + // color: Colors.grey.shade400, + // ), + child: provider.selectedImage != null || provider.selectedImageUrl != null + ? (provider.selectedImageUrl == null + ? ClipRRect( + borderRadius: BorderRadius.circular(60), + child: Image.file(provider.selectedImage!, fit: BoxFit.cover)) + : ClipRRect( + borderRadius: BorderRadius.circular(60), + child: CachedNetworkImage(imageUrl: provider.selectedImageUrl!), + )) + : const Icon(Icons.add_a_photo, color: Colors.grey, size: 32), ), - // child: provider.selectedImage != null - // ? ClipRRect( - // borderRadius: BorderRadius.circular(60), - // child: Image.file( - // provider.selectedImage!, - // fit: BoxFit.cover, - // ), - // ) - // : Icon( - // Icons.add_a_photo, - // size: 40, - // color: Colors.grey.shade400, - // ), - child: provider.selectedImage != null || provider.selectedImageUrl != null - ? (provider.selectedImageUrl == null - ? ClipRRect( - borderRadius: BorderRadius.circular(60), - child: Image.file(provider.selectedImage!, fit: BoxFit.cover)) - : ClipRRect( - borderRadius: BorderRadius.circular(60), - child: CachedNetworkImage(imageUrl: provider.selectedImageUrl!), - )) - : const Icon(Icons.add_a_photo, color: Colors.grey, size: 32), ), ), - ), - const SizedBox(height: 32), - Container( - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(12.0), - ), - padding: const EdgeInsets.all(14.0), - margin: const EdgeInsets.only(top: 22), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'Persona Name', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + const SizedBox(height: 32), + Container( + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12.0), + ), + padding: const EdgeInsets.all(14.0), + margin: const EdgeInsets.only(top: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'Persona Name', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), - margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(10.0), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a username to access the persona'; + } + return null; + }, + controller: provider.nameController, + decoration: const InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: 'Nik AI', + ), + ), ), - width: double.infinity, - child: TextFormField( - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a username to access the persona'; - } - return null; - }, - controller: provider.nameController, - decoration: const InputDecoration( - isDense: true, - border: InputBorder.none, - hintText: 'Nik AI', + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'Persona Username', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), ), ), - ), - const SizedBox( - height: 24, - ), - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'Persona Username', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a username to access the persona'; + } + return null; + }, + onChanged: (value) { + _debouncer.run(() async { + await provider.checkIsUsernameTaken(value); + }); + }, + controller: provider.usernameController, + inputFormatters: [ + LowerCaseTextFormatter(), + FilteringTextInputFormatter.allow(RegExp(r'[a-z0-9_]')), + ], + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: 'nikshevchenko', + suffix: provider.usernameController.text.isEmpty + ? null + : provider.isCheckingUsername + ? const SizedBox( + width: 16, + height: 16, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.grey), + ), + ), + ) + : Icon( + provider.isUsernameTaken ? Icons.close : Icons.check, + color: provider.isUsernameTaken ? Colors.red : Colors.green, + size: 16, + ), + ), + ), ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), - margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(10.0), + ], + ), + ), + const SizedBox(height: 22), + Container( + padding: const EdgeInsets.only(left: 14.0), + child: Row( + children: [ + Text( + 'Make Persona Public', + style: TextStyle(color: Colors.grey.shade400), ), - width: double.infinity, - child: TextFormField( - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a username to access the persona'; - } - return null; - }, + const Spacer(), + Switch( + value: provider.makePersonaPublic, onChanged: (value) { - _debouncer.run(() async { - await provider.checkIsUsernameTaken(value); - }); + provider.setPersonaPublic(value); }, - controller: provider.usernameController, - inputFormatters: [ - LowerCaseTextFormatter(), - FilteringTextInputFormatter.allow(RegExp(r'[a-z0-9_]')), - ], - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - hintText: 'nikshevchenko', - suffix: provider.usernameController.text.isEmpty - ? null - : provider.isCheckingUsername - ? const SizedBox( - width: 16, - height: 16, - child: Center( - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.grey), - ), - ), - ) - : Icon( - provider.isUsernameTaken ? Icons.close : Icons.check, - color: provider.isUsernameTaken ? Colors.red : Colors.green, - size: 16, + activeColor: Colors.white, + ), + ], + ), + ), + const SizedBox(height: 24), + Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0, bottom: 12), + child: Text( + 'Connected Knowledge Data', + style: TextStyle( + color: Colors.grey[600], + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Image.asset( + 'assets/images/x_logo_mini.png', + width: 24, + height: 24, + ), + const SizedBox(width: 12), + Text( + widget.app!.twitter != null + ? (widget.app!.twitter!['username'] == null + ? 'Connect Twitter' + : widget.app!.twitter?['username'] ?? '') + : 'Connect Twitter', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + const Spacer(), + if (provider.twitterProfile.isEmpty) + GestureDetector( + onTap: () { + if (widget.app!.connectedAccounts.contains('twitter')) { + } else { + routeToPage(context, const SocialHandleScreen()); + } + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(16), + ), + child: Text( + widget.app!.connectedAccounts.contains('twitter') ? 'Connected' : 'Connect', + style: const TextStyle( + color: Colors.white, + fontSize: 12, ), + ), + ), + ) + else + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(16), + ), + child: const Text( + 'Connected', + style: TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ), + ), + ], ), ), - ), - ], - ), - ), - const SizedBox(height: 22), - Container( - padding: const EdgeInsets.only(left: 14.0), - child: Row( - children: [ - Text( - 'Make Persona Public', - style: TextStyle(color: Colors.grey.shade400), - ), - const Spacer(), - Switch( - value: provider.makePersonaPublic, - onChanged: (value) { - provider.setPersonaPublic(value); - }, - activeColor: Colors.white, - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ), - ), - bottomNavigationBar: Padding( - padding: const EdgeInsets.only(left: 24, right: 24, bottom: 52), - child: SizedBox( - width: double.infinity, - child: AnimatedLoadingButton( - onPressed: !provider.isFormValid - ? () async {} - : () async { - await provider.createPersona(); - }, - color: provider.isFormValid ? Colors.white : Colors.grey[800]!, - loaderColor: Colors.black, - text: "Create Persona", - textStyle: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w600, + bottomNavigationBar: Padding( + padding: const EdgeInsets.only(left: 24, right: 24, bottom: 52), + child: SizedBox( + width: double.infinity, + child: AnimatedLoadingButton( + onPressed: !provider.isFormValid + ? () async {} + : () async { + await provider.updatePersona(); + }, + color: provider.isFormValid ? Colors.white : Colors.grey[800]!, + loaderColor: Colors.black, + text: "Update Persona", + textStyle: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), ), ), ), diff --git a/app/lib/providers/auth_provider.dart b/app/lib/providers/auth_provider.dart index 46edb2f7f5..424987188d 100644 --- a/app/lib/providers/auth_provider.dart +++ b/app/lib/providers/auth_provider.dart @@ -8,11 +8,14 @@ import 'package:friend_private/utils/alerts/app_snackbar.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:google_sign_in/google_sign_in.dart'; class AuthenticationProvider extends BaseProvider { final FirebaseAuth _auth = FirebaseAuth.instance; User? user; String? authToken; + bool _loading = false; + bool get loading => _loading; AuthenticationProvider() { _auth.authStateChanges().distinct((p, n) => p?.uid == n?.uid).listen((User? user) { @@ -47,6 +50,11 @@ class AuthenticationProvider extends BaseProvider { bool isSignedIn() => _auth.currentUser != null; + void setLoading(bool value) { + _loading = value; + notifyListeners(); + } + Future onGoogleSignIn(Function() onSignIn) async { if (!loading) { setLoadingState(true); @@ -122,4 +130,41 @@ class AuthenticationProvider extends BaseProvider { void _launchUrl(String url) async { if (!await launchUrl(Uri.parse(url))) throw 'Could not launch $url'; } + + Future linkWithGoogle() async { + setLoading(true); + try { + final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn(); + if (googleUser == null) { + setLoading(false); + return; + } + + final GoogleSignInAuthentication googleAuth = await googleUser.authentication; + final credential = GoogleAuthProvider.credential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + + await FirebaseAuth.instance.currentUser?.linkWithCredential(credential); + } catch (e) { + print('Error linking with Google: $e'); + rethrow; + } finally { + setLoading(false); + } + } + + Future linkWithApple() async { + setLoading(true); + try { + final appleProvider = AppleAuthProvider(); + await FirebaseAuth.instance.currentUser?.linkWithProvider(appleProvider); + } catch (e) { + print('Error linking with Apple: $e'); + rethrow; + } finally { + setLoading(false); + } + } }