Skip to content

Commit

Permalink
Persona using omi data and twitter (#1775)
Browse files Browse the repository at this point in the history
Part of #1535 

- [x] Allow users to create personas from their omi data in one click
- [x] Shareable link to chat with the persona on the web (only if
public)
- [x] Update persona whenever a new conversation is added
- [x] Chat with the persona within the app (files chatting not
supported)
- [x] Connect twitter to existing and new personas
- [x] New personas creation flow without auth
  • Loading branch information
beastoin authored Feb 19, 2025
2 parents 65bcc0b + af406f4 commit 598ed96
Show file tree
Hide file tree
Showing 47 changed files with 3,624 additions and 180 deletions.
Binary file added app/assets/images/calendar_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/clone.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/instagram_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/linkedin_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/new_background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/notion_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/x_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/x_logo_mini.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions app/lib/backend/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,11 @@ Future<void> updateGivenName(String fullName) async {
await user.updateProfile(displayName: fullName);
}
}

Future<void> signInAnonymously() async {
try {
await FirebaseAuth.instance.signInAnonymously();
} catch (e) {
Logger.handle(e, null, message: 'An error occurred while signing in. Please try again later.');
}
}
135 changes: 135 additions & 0 deletions app/lib/backend/http/api/apps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,138 @@ Future<String> getGenratedDescription(String name, String description) async {
return '';
}
}

Future<bool> createPersonaApp(File file, Map<String, dynamic> personaData) async {
var request = http.MultipartRequest(
'POST',
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()});
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;
}
}

Future<bool> updatePersonaApp(File? file, Map<String, dynamic> 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<bool> 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;
}
}

Future<Map?> 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<bool> 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: url,
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;
}
}

Future<App?> 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;
}
}
7 changes: 7 additions & 0 deletions app/lib/backend/preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
Expand Down
13 changes: 12 additions & 1 deletion app/lib/backend/schema/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ class App {
String description;
String image;
Set<String> capabilities;
List<String> connectedAccounts = [];
Map? twitter;
bool private;
bool approved;
String? conversationPrompt;
Expand All @@ -195,6 +197,7 @@ class App {
String? paymentLink;
List<String> thumbnailIds;
List<String> thumbnailUrls;
String? username;

App({
required this.id,
Expand Down Expand Up @@ -229,6 +232,9 @@ class App {
this.paymentLink,
this.thumbnailIds = const [],
this.thumbnailUrls = const [],
this.username,
this.connectedAccounts = const [],
this.twitter,
});

String? getRatingAvg() => ratingAvg?.toStringAsFixed(1);
Expand All @@ -237,7 +243,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');

Expand Down Expand Up @@ -278,6 +286,9 @@ class App {
paymentLink: json['payment_link'],
thumbnailIds: (json['thumbnails'] as List<dynamic>?)?.cast<String>() ?? [],
thumbnailUrls: (json['thumbnail_urls'] as List<dynamic>?)?.cast<String>() ?? [],
username: json['username'],
connectedAccounts: (json['connected_accounts'] as List<dynamic>?)?.cast<String>() ?? [],
twitter: json['twitter'],
);
}

Expand Down
33 changes: 24 additions & 9 deletions app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ 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';
import 'package:friend_private/providers/calendar_provider.dart';
Expand Down Expand Up @@ -205,6 +208,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
(previous?..setAppProvider(value)) ?? AddAppProvider(),
),
ChangeNotifierProvider(create: (context) => PaymentMethodProvider()),
ChangeNotifierProvider(create: (context) => PersonaProvider()),
],
builder: (context, child) {
return WithForegroundTask(
Expand Down Expand Up @@ -325,9 +329,14 @@ class _DeciderWidgetState extends State<DeciderWidget> {
if (context.read<AuthenticationProvider>().user != null ||
(SharedPreferencesUtil().customBackendUrl.isNotEmpty && SharedPreferencesUtil().authToken.isNotEmpty)) {
context.read<HomeProvider>().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<MessageProvider>().setMessagesFromCache();
context.read<AppProvider>().setAppsFromCache();
context.read<MessageProvider>().refreshMessages();
Expand All @@ -343,13 +352,19 @@ class _DeciderWidgetState extends State<DeciderWidget> {
Widget build(BuildContext context) {
return Consumer<AuthenticationProvider>(
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();
}
},
);
Expand Down
5 changes: 3 additions & 2 deletions app/lib/pages/apps/add_app.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:friend_private/utils/other/debouncer.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';
Expand Down Expand Up @@ -29,6 +30,8 @@ class AddAppPage extends StatefulWidget {

class _AddAppPageState extends State<AddAppPage> {
late bool showSubmitAppConfirmation;
final _debouncer = Debouncer(delay: const Duration(milliseconds: 500));

@override
void initState() {
showSubmitAppConfirmation = SharedPreferencesUtil().showSubmitAppConfirmation;
Expand Down Expand Up @@ -114,8 +117,6 @@ class _AddAppPageState extends State<AddAppPage> {
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,
Expand Down
24 changes: 17 additions & 7 deletions app/lib/pages/apps/app_detail/app_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -199,16 +199,23 @@ class _AppDetailPageState extends State<AppDetailPage> {
),
const SizedBox(width: 24),
],
isLoading
isLoading || app.private
? const SizedBox.shrink()
: GestureDetector(
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<AppProvider>().isAppOwner
Expand Down Expand Up @@ -725,10 +732,13 @@ class _AppDetailPageState extends State<AppDetailPage> {
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
Expand Down
20 changes: 14 additions & 6 deletions app/lib/pages/apps/explore_install_page.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
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/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 '../persona/twitter/social_profile.dart';
import 'widgets/create_options_sheet.dart';

String filterValueToString(dynamic value) {
if (value.runtimeType == String) {
return value;
Expand Down Expand Up @@ -189,9 +191,15 @@ class _ExploreInstallPageState extends State<ExploreInstallPage> with AutomaticK
)),
SliverToBoxAdapter(
child: GestureDetector(
onTap: () {
MixpanelManager().pageOpened('Submit App');
routeToPage(context, const AddAppPage());
onTap: () async {
// routeToPage(context, SocialHandleScreen());
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),
Expand All @@ -208,7 +216,7 @@ class _ExploreInstallPageState extends State<ExploreInstallPage> with AutomaticK
Icon(Icons.add, color: Colors.white),
SizedBox(width: 8),
Text(
'Create and submit a new app',
'Create your own',
textAlign: TextAlign.center,
),
],
Expand Down
Loading

0 comments on commit 598ed96

Please sign in to comment.