Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions app/lib/pages/apps/app_detail/app_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'package:omi/backend/http/api/apps.dart';
import 'package:omi/backend/preferences.dart';
import 'package:omi/pages/apps/app_detail/reviews_list_page.dart';
import 'package:omi/pages/apps/markdown_viewer.dart';
import 'package:omi/pages/chat/page.dart';
import 'package:omi/providers/home_provider.dart';
import 'package:omi/pages/apps/providers/add_app_provider.dart';
import 'package:omi/providers/app_provider.dart';
import 'package:omi/providers/message_provider.dart';
Expand Down Expand Up @@ -677,12 +677,8 @@ class _AppDetailPageState extends State<AppDetailPage> {

// Navigate directly to chat page
if (mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ChatPage(isPivotBottom: false),
),
);
Navigator.popUntil(context, (route) => route.isFirst);
Provider.of<HomeProvider>(context, listen: false).setIndex(2);
}
} finally {
if (mounted) {
Expand Down
110 changes: 80 additions & 30 deletions app/lib/pages/chat/page.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:math' as math;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
Expand All @@ -22,6 +23,7 @@ import 'package:omi/providers/home_provider.dart';
import 'package:omi/providers/conversation_provider.dart';
import 'package:omi/providers/message_provider.dart';
import 'package:omi/providers/app_provider.dart';
import 'package:omi/providers/device_provider.dart';
import 'package:omi/utils/alerts/app_snackbar.dart';
import 'package:omi/utils/analytics/mixpanel.dart';
import 'package:omi/utils/other/temp.dart';
Expand All @@ -36,11 +38,13 @@ import 'widgets/message_action_menu.dart';
class ChatPage extends StatefulWidget {
final bool isPivotBottom;
final String? autoMessage;
final bool isEmbeddedInTab;

const ChatPage({
super.key,
this.isPivotBottom = false,
this.autoMessage,
this.isEmbeddedInTab = false,
});

@override
Expand All @@ -61,14 +65,18 @@ class ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin,
var prefs = SharedPreferencesUtil();
late List<App> apps;

final scaffoldKey = GlobalKey<ScaffoldState>();
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();

// Track which app is pending deletion confirmation
String? _pendingDeleteAppId;

@override
bool get wantKeepAlive => true;

void openEndDrawer() {
scaffoldKey.currentState?.openEndDrawer();
}

@override
void initState() {
apps = prefs.appsList;
Expand Down Expand Up @@ -167,7 +175,7 @@ class ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin,
key: scaffoldKey,
resizeToAvoidBottomInset: false,
backgroundColor: Theme.of(context).colorScheme.primary,
appBar: _buildAppBar(context, provider),
appBar: widget.isEmbeddedInTab ? null : _buildAppBar(context, provider),
endDrawer: _buildChatAppsEndDrawer(context),
onEndDrawerChanged: (isOpened) {
if (isOpened) {
Expand Down Expand Up @@ -384,7 +392,10 @@ class ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin,
),
// Send message area - fixed at bottom
Container(
margin: EdgeInsets.only(top: 10, bottom: MediaQuery.of(context).viewInsets.bottom),
margin: EdgeInsets.only(
top: 10,
bottom: widget.isEmbeddedInTab ? 0 : MediaQuery.of(context).viewInsets.bottom,
),
decoration: const BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.only(
Expand Down Expand Up @@ -495,20 +506,32 @@ class ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin,
}
}),
// Send bar
Padding(
padding: EdgeInsets.only(
left: 8,
right: 8,
top: provider.selectedFiles.isNotEmpty ? 0 : 8,
bottom: widget.isPivotBottom ? 20 : (textFieldFocusNode.hasFocus ? 10 : 40),
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF2A2A2F),
borderRadius: BorderRadius.circular(32),
),
child: Row(
Consumer<DeviceProvider>(
builder: (context, deviceProvider, child) {
final bool hasDevice = deviceProvider.isConnected && deviceProvider.connectedDevice != null;
final double embeddedBottomPadding = hasDevice ? 100 : 130;
final double keyboardHeight = MediaQuery.of(context).viewInsets.bottom;

return AnimatedPadding(
duration: widget.isEmbeddedInTab ? Duration.zero : const Duration(milliseconds: 200),
curve: Curves.easeOut,
padding: EdgeInsets.only(
left: 8,
right: 8,
top: provider.selectedFiles.isNotEmpty ? 0 : 8,
bottom: widget.isPivotBottom
? 20
: (widget.isEmbeddedInTab
? math.max(embeddedBottomPadding, 10 + keyboardHeight)
: (keyboardHeight > 0 ? 10 : 40)),
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF2A2A2F),
borderRadius: BorderRadius.circular(32),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// Plus button
Expand Down Expand Up @@ -648,7 +671,9 @@ class ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin,
],
),
),
),
);
},
),
],
);
}),
Expand Down Expand Up @@ -708,6 +733,40 @@ class ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin,

scrollToBottom() => _moveListToBottom();

void addAutoMessage(String message) {
if (message.isEmpty || !mounted) return;

final aiMessage = ServerMessage(
const Uuid().v4(),
DateTime.now(),
message,
MessageSender.ai,
MessageType.text,
null,
false,
[],
[],
[],
askForNps: false,
);
context.read<MessageProvider>().addMessage(aiMessage);
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) scrollToBottom();
});
}

void handleAppSelection(String? val, AppProvider provider) {
_handleAppSelection(val, provider);
}

void navigateToChatAppsPage() {
_navigateToChatAppsPage();
}

Future<void> handleAppUninstall(String appId, AppProvider appProvider, MessageProvider messageProvider) async {
await _handleAppUninstall(appId, appProvider, messageProvider);
}

void _handleAppSelection(String? val, AppProvider provider) {
if (val == null || val == provider.selectedChatAppId) {
return;
Expand Down Expand Up @@ -758,7 +817,7 @@ class ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin,
await routeToPage(
context,
CapabilityAppsPage(
capability: AppCapability(id: 'chat', title: 'Chat Assistants'),
capability: AppCapability(id: 'chat', title: 'Chat Apps'),
apps: const [],
),
);
Expand Down Expand Up @@ -984,7 +1043,7 @@ class ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin,
child: FaIcon(FontAwesomeIcons.circlePlus, color: Colors.white, size: 20),
),
title: const Text(
'Enable Apps',
'Get More Chat Apps',
style: TextStyle(color: Colors.white, fontSize: 16),
),
trailing: const Padding(
Expand All @@ -1000,7 +1059,7 @@ class ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin,
const Padding(
padding: EdgeInsets.fromLTRB(16, 16, 20, 8),
child: Text(
'Select App',
'Select Chat App',
style: TextStyle(
color: Colors.white60,
fontSize: 13,
Expand Down Expand Up @@ -1037,15 +1096,6 @@ class ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin,
? () => _handleAppUninstall(app.id, appProvider, messageProvider)
: null,
)),
if (chatApps.isEmpty)
const Padding(
padding: EdgeInsets.all(20),
child: Text(
'No chat apps enabled.\nTap "Enable Apps" to add some.',
style: TextStyle(color: Colors.white38, fontSize: 14),
textAlign: TextAlign.center,
),
),
],
),
),
Expand Down
10 changes: 9 additions & 1 deletion app/lib/pages/conversations/conversations_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,15 @@ class _ConversationsPageState extends State<ConversationsPage> with AutomaticKee
const SliverToBoxAdapter(child: SearchResultHeaderWidget()),
getProcessingConversationsWidget(convoProvider.processingConversations),
// Goal tracker widget - before folders
const SliverToBoxAdapter(child: GoalTrackerWidget()),
Consumer2<HomeProvider, ConversationProvider>(
builder: (context, homeProvider, convoProvider, _) {
bool isSearching = homeProvider.showConvoSearchBar || convoProvider.previousQuery.isNotEmpty;
if (isSearching) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return const SliverToBoxAdapter(child: GoalTrackerWidget());
},
),
// Folder tabs
Consumer2<FolderProvider, ConversationProvider>(
builder: (context, folderProvider, convoProvider, _) {
Expand Down
37 changes: 24 additions & 13 deletions app/lib/pages/conversations/widgets/goal_tracker_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:omi/backend/http/api/goals.dart';
import 'package:omi/pages/chat/page.dart';
import 'package:omi/backend/schema/message.dart';
import 'package:omi/providers/home_provider.dart';
import 'package:omi/providers/message_provider.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';

/// Goal tracker widget with semicircle gauge
class GoalTrackerWidget extends StatefulWidget {
Expand All @@ -22,7 +26,6 @@ class _GoalTrackerWidgetState extends State<GoalTrackerWidget>
bool _isLoading = false; // Start as false - show cached data immediately
bool _isEditingGoal = false;
bool _isEditingValue = false;
bool _initialLoadDone = false;

static const String _goalStorageKey = 'goal_tracker_local_goal';
static const String _adviceStorageKey = 'goal_tracker_local_advice';
Expand Down Expand Up @@ -132,7 +135,6 @@ class _GoalTrackerWidgetState extends State<GoalTrackerWidget>
if (mounted) {
setState(() {
_isLoading = false;
_initialLoadDone = true;
});
}
}
Expand Down Expand Up @@ -202,17 +204,26 @@ class _GoalTrackerWidgetState extends State<GoalTrackerWidget>
void _openChatWithAdvice() {
if (_advice == null || _advice!.isEmpty) return;
HapticFeedback.lightImpact();

// Navigate to chat and send the advice as a message from Omi AI
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatPage(
isPivotBottom: false,
autoMessage: _advice,
),
),

// Add the AI message directly to MessageProvider
final aiMessage = ServerMessage(
const Uuid().v4(),
DateTime.now(),
_advice!,
MessageSender.ai,
MessageType.text,
null,
false,
[],
[],
[],
askForNps: false,
);
Provider.of<MessageProvider>(context, listen: false).addMessage(aiMessage);

// Navigate to chat tab
Navigator.popUntil(context, (route) => route.isFirst);
Provider.of<HomeProvider>(context, listen: false).setIndex(2);
}

Future<void> _createGoalFromSuggestion() async {
Expand Down
Loading