-
Notifications
You must be signed in to change notification settings - Fork 401
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve chat initial experience #885
Changes from 10 commits
a8e4ed8
22fbf89
a280ead
eb1b0aa
9cbb312
83c747d
2c52e5b
4c4fdae
f38bd58
5250c23
f945d41
8be59e4
47e8f15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -274,8 +274,10 @@ class _DeciderWidgetState extends State<DeciderWidget> { | |
if (context.read<ConnectivityProvider>().isConnected) { | ||
NotificationService.instance.saveNotificationToken(); | ||
} | ||
|
||
if (context.read<AuthenticationProvider>().user != null) { | ||
context.read<HomeProvider>().setupHasSpeakerProfile(); | ||
context.read<MessageProvider>().setMessagesFromCache(); | ||
context.read<MessageProvider>().refreshMessages(); | ||
Comment on lines
+277
to
+280
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The - context.read<MessageProvider>().setMessagesFromCache();
- context.read<MessageProvider>().refreshMessages();
+ try {
+ await context.read<MessageProvider>().setMessagesFromCache();
+ await context.read<MessageProvider>().refreshMessages();
+ } catch (e) {
+ // Handle or log error
+ } Please note that this suggestion assumes that both |
||
} | ||
}); | ||
super.initState(); | ||
|
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,9 @@ import 'package:friend_private/backend/preferences.dart'; | |
import 'package:friend_private/backend/schema/memory.dart'; | ||
import 'package:friend_private/backend/schema/message.dart'; | ||
import 'package:friend_private/backend/schema/plugin.dart'; | ||
import 'package:friend_private/pages/memory_detail/memory_detail_provider.dart'; | ||
import 'package:friend_private/pages/memory_detail/page.dart'; | ||
import 'package:friend_private/providers/memory_provider.dart'; | ||
import 'package:friend_private/utils/analytics/mixpanel.dart'; | ||
import 'package:friend_private/providers/connectivity_provider.dart'; | ||
import 'package:friend_private/utils/other/temp.dart'; | ||
|
@@ -116,8 +118,8 @@ class _AIMessageState extends State<AIMessage> { | |
style: TextStyle(fontSize: 15.0, fontWeight: FontWeight.w500, color: Colors.grey.shade300), | ||
)), | ||
if (widget.message.id != 1) _getCopyButton(context), // RESTORE ME | ||
// if (message.id == 1 && displayOptions) const SizedBox(height: 8), | ||
// if (message.id == 1 && displayOptions) ..._getInitialOptions(context), | ||
if (widget.displayOptions) const SizedBox(height: 8), | ||
if (widget.displayOptions) ..._getInitialOptions(context), | ||
Comment on lines
+121
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
if (messageMemories.isNotEmpty) ...[ | ||
const SizedBox(height: 16), | ||
for (var data in messageMemories.indexed) ...[ | ||
|
@@ -127,30 +129,58 @@ class _AIMessageState extends State<AIMessage> { | |
onTap: () async { | ||
final connectivityProvider = Provider.of<ConnectivityProvider>(context, listen: false); | ||
if (connectivityProvider.isConnected) { | ||
if (memoryDetailLoading[data.$1]) return; | ||
setState(() => memoryDetailLoading[data.$1] = true); | ||
var memProvider = Provider.of<MemoryProvider>(context, listen: false); | ||
var idx = memProvider.memoriesWithDates.indexWhere((e) { | ||
if (e.runtimeType == ServerMemory) { | ||
return e.id == data.$2.id; | ||
} | ||
return false; | ||
}); | ||
|
||
ServerMemory? m = await getMemoryById(data.$2.id); | ||
if (m == null) return; | ||
MixpanelManager().chatMessageMemoryClicked(m); | ||
setState(() => memoryDetailLoading[data.$1] = false); | ||
await Navigator.of(context) | ||
.push(MaterialPageRoute(builder: (c) => MemoryDetailPage(memory: m))); | ||
if (SharedPreferencesUtil().modifiedMemoryDetails?.id == m.id) { | ||
ServerMemory modifiedDetails = SharedPreferencesUtil().modifiedMemoryDetails!; | ||
widget.updateMemory(SharedPreferencesUtil().modifiedMemoryDetails!); | ||
var copy = List<MessageMemory>.from(widget.message.memories); | ||
copy[data.$1] = MessageMemory( | ||
modifiedDetails.id, | ||
modifiedDetails.createdAt, | ||
MessageMemoryStructured( | ||
modifiedDetails.structured.title, | ||
modifiedDetails.structured.emoji, | ||
)); | ||
widget.message.memories.clear(); | ||
widget.message.memories.addAll(copy); | ||
SharedPreferencesUtil().modifiedMemoryDetails = null; | ||
setState(() {}); | ||
if (idx != -1) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. clean chat history should have confirmation button. |
||
context.read<MemoryDetailProvider>().updateMemory(idx); | ||
var m = memProvider.memoriesWithDates[idx]; | ||
MixpanelManager().chatMessageMemoryClicked(m); | ||
await Navigator.of(context).push( | ||
MaterialPageRoute( | ||
builder: (c) => MemoryDetailPage( | ||
memory: m, | ||
), | ||
), | ||
); | ||
} else { | ||
if (memoryDetailLoading[data.$1]) return; | ||
setState(() => memoryDetailLoading[data.$1] = true); | ||
ServerMemory? m = await getMemoryById(data.$2.id); | ||
if (m == null) return; | ||
idx = memProvider.addMemoryWithDate(m); | ||
MixpanelManager().chatMessageMemoryClicked(m); | ||
setState(() => memoryDetailLoading[data.$1] = false); | ||
context.read<MemoryDetailProvider>().updateMemory(idx); | ||
await Navigator.of(context).push( | ||
MaterialPageRoute( | ||
builder: (c) => MemoryDetailPage( | ||
memory: m, | ||
), | ||
), | ||
); | ||
//TODO: Not needed anymore I guess because memories are stored in provider and read from there only | ||
if (SharedPreferencesUtil().modifiedMemoryDetails?.id == m.id) { | ||
ServerMemory modifiedDetails = SharedPreferencesUtil().modifiedMemoryDetails!; | ||
widget.updateMemory(SharedPreferencesUtil().modifiedMemoryDetails!); | ||
var copy = List<MessageMemory>.from(widget.message.memories); | ||
copy[data.$1] = MessageMemory( | ||
modifiedDetails.id, | ||
modifiedDetails.createdAt, | ||
MessageMemoryStructured( | ||
modifiedDetails.structured.title, | ||
modifiedDetails.structured.emoji, | ||
)); | ||
widget.message.memories.clear(); | ||
widget.message.memories.addAll(copy); | ||
SharedPreferencesUtil().modifiedMemoryDetails = null; | ||
setState(() {}); | ||
} | ||
Comment on lines
+132
to
+183
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block of code has been significantly refactored to improve error handling and maintainability. It now checks if the memory already exists in the provider before fetching it from the server, which can improve performance by reducing unnecessary network calls. However, there's a potential issue with this approach: if the memory data on the server has changed since it was last fetched, the app will display outdated information. To fix this, you could add a timestamp to each memory and check if it's older than a certain threshold before deciding whether to fetch it again. + var memProvider = Provider.of<MemoryProvider>(context, listen: false);
+ var idx = memProvider.memoriesWithDates.indexWhere((e) {
+ if (e.runtimeType == ServerMemory) {
+ return e.id == data.$2.id;
+ }
+ return false;
+ });
+
+ if (idx != -1 && memProvider.memoriesWithDates[idx].timestamp < SOME_THRESHOLD) {
+ // Fetch the memory again
+ } 289: |
||
} | ||
} else { | ||
ScaffoldMessenger.of(context).showSnackBar( | ||
|
@@ -256,7 +286,7 @@ class _AIMessageState extends State<AIMessage> { | |
_getInitialOption(BuildContext context, String optionText) { | ||
return GestureDetector( | ||
child: Container( | ||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8), | ||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
width: double.maxFinite, | ||
decoration: BoxDecoration( | ||
color: Colors.grey.shade900, | ||
|
@@ -273,11 +303,11 @@ class _AIMessageState extends State<AIMessage> { | |
_getInitialOptions(BuildContext context) { | ||
return [ | ||
const SizedBox(height: 8), | ||
_getInitialOption(context, 'What tasks do I have from yesterday?'), | ||
_getInitialOption(context, 'What\'s been on my mind a lot?'), | ||
const SizedBox(height: 8), | ||
_getInitialOption(context, 'What conversations did I have with John?'), | ||
_getInitialOption(context, 'Did I forget to follow up on something?'), | ||
const SizedBox(height: 8), | ||
_getInitialOption(context, 'What advise have I received about entrepreneurship?'), | ||
_getInitialOption(context, 'What\'s the funniest thing I\'ve said lately?'), | ||
Comment on lines
+306
to
+310
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
]; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import 'package:flutter/material.dart'; | ||
|
||
class AnimatedMiniBanner extends StatelessWidget implements PreferredSizeWidget { | ||
const AnimatedMiniBanner({super.key, required this.showAppBar, required this.child}); | ||
|
||
final bool showAppBar; | ||
final Widget child; | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return AnimatedContainer( | ||
height: showAppBar ? kToolbarHeight : 0, | ||
duration: const Duration(milliseconds: 400), | ||
child: child, | ||
); | ||
} | ||
|
||
@override | ||
Size get preferredSize => const Size.fromHeight(30); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -106,8 +106,34 @@ class MemoryProvider extends ChangeNotifier { | |
|
||
void addMemory(ServerMemory memory) { | ||
memories.insert(0, memory); | ||
filterMemories(''); | ||
initFilteredMemories(); | ||
notifyListeners(); | ||
} | ||
|
||
int addMemoryWithDate(ServerMemory memory) { | ||
int idx; | ||
var date = memoriesWithDates.indexWhere((element) => | ||
element is DateTime && | ||
element.day == memory.createdAt.day && | ||
element.month == memory.createdAt.month && | ||
element.year == memory.createdAt.year); | ||
if (date != -1) { | ||
var hour = memoriesWithDates[date + 1].createdAt.hour; | ||
var newHour = memory.createdAt.hour; | ||
if (newHour > hour) { | ||
memoriesWithDates.insert(date + 1, memory); | ||
idx = date + 1; | ||
} else { | ||
memoriesWithDates.insert(date + 2, memory); | ||
idx = date + 2; | ||
} | ||
} else { | ||
memoriesWithDates.add(memory.createdAt); | ||
memoriesWithDates.add(memory); | ||
idx = memoriesWithDates.length - 1; | ||
} | ||
notifyListeners(); | ||
return idx; | ||
Comment on lines
+113
to
+136
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The To ensure correct behavior, you should sort the memories for a particular day by hour before inserting a new memory. Here's a possible way to do it: - var hour = memoriesWithDates[date + 1].createdAt.hour;
- var newHour = memory.createdAt.hour;
- if (newHour > hour) {
- memoriesWithDates.insert(date + 1, memory);
- idx = date + 1;
- } else {
- memoriesWithDates.insert(date + 2, memory);
- idx = date + 2;
- }
+ int insertIndex = date + 1;
+ while (insertIndex < memoriesWithDates.length &&
+ memoriesWithDates[insertIndex] is ServerMemory &&
+ (memoriesWithDates[insertIndex] as ServerMemory).createdAt.hour < memory.createdAt.hour) {
+ insertIndex++;
+ }
+ memoriesWithDates.insert(insertIndex, memory);
+ idx = insertIndex; This change will find the correct position to insert the new memory by comparing its hour with existing memories' hours and stop when it finds a memory with a greater hour or reaches the end of the list. |
||
} | ||
|
||
void updateMemory(ServerMemory memory, [int? index]) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,11 +10,25 @@ class MessageProvider extends ChangeNotifier { | |
List<ServerMessage> messages = []; | ||
|
||
bool isLoadingMessages = false; | ||
bool hasCachedMessages = false; | ||
bool isClearingChat = false; | ||
|
||
String firstTimeLoadingText = ''; | ||
|
||
void updatePluginProvider(PluginProvider p) { | ||
pluginProvider = p; | ||
} | ||
|
||
void setHasCachedMessages(bool value) { | ||
hasCachedMessages = value; | ||
notifyListeners(); | ||
} | ||
|
||
void setClearingChat(bool value) { | ||
isClearingChat = value; | ||
notifyListeners(); | ||
} | ||
Comment on lines
+13
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new boolean flags + bool hasCachedMessages = false;
+ bool isClearingChat = false;
+ void setHasCachedMessages(bool value) {
+ hasCachedMessages = value;
+ notifyListeners();
+ }
+ void setClearingChat(bool value) {
+ isClearingChat = value;
+ notifyListeners();
+ } |
||
|
||
void setLoadingMessages(bool value) { | ||
isLoadingMessages = value; | ||
notifyListeners(); | ||
|
@@ -32,15 +46,39 @@ class MessageProvider extends ChangeNotifier { | |
notifyListeners(); | ||
} | ||
|
||
void setMessagesFromCache() { | ||
if (SharedPreferencesUtil().cachedMessages.isNotEmpty) { | ||
setHasCachedMessages(true); | ||
messages = SharedPreferencesUtil().cachedMessages; | ||
} | ||
notifyListeners(); | ||
} | ||
|
||
Future<List<ServerMessage>> getMessagesFromServer() async { | ||
if (!hasCachedMessages) { | ||
firstTimeLoadingText = 'Reading your memories...'; | ||
notifyListeners(); | ||
} | ||
setLoadingMessages(true); | ||
var mes = await getMessagesServer(); | ||
if (!hasCachedMessages) { | ||
firstTimeLoadingText = 'Learning from your memories...'; | ||
notifyListeners(); | ||
} | ||
messages = mes; | ||
setLoadingMessages(false); | ||
notifyListeners(); | ||
return messages; | ||
} | ||
|
||
Future clearChat() async { | ||
setClearingChat(true); | ||
var mes = await clearChatServer(); | ||
messages = mes; | ||
setClearingChat(false); | ||
notifyListeners(); | ||
} | ||
Comment on lines
+49
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The methods + void setMessagesFromCache() {
+ if (SharedPreferencesUtil().cachedMessages.isNotEmpty) {
+ setHasCachedMessages(true);
+ messages = SharedPreferencesUtil().cachedMessages;
+ }
+ notifyListeners();
+ }
+ Future<List<ServerMessage>> getMessagesFromServer() async {
+ if (!hasCachedMessages) {
+ firstTimeLoadingText = 'Reading your memories...';
+ notifyListeners();
+ }
+ setLoadingMessages(true);
+ var mes = await getMessagesServer();
+ if (!hasCachedMessages) {
+ firstTimeLoadingText = 'Learning from your memories...';
+ notifyListeners();
+ }
+ messages = mes;
+ setLoadingMessages(false);
+ notifyListeners();
+ return messages;
+ }
+ Future clearChat() async {
+ setClearingChat(true);
+ var mes = await clearChatServer();
+ messages = mes;
+ setClearingChat(false);
+ notifyListeners();
+ } |
||
|
||
void addMessage(ServerMessage message) { | ||
messages.insert(0, message); | ||
notifyListeners(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
from datetime import datetime, timezone | ||
from typing import Optional | ||
|
||
from fastapi import HTTPException | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The import of - from fastapi import HTTPException |
||
from google.cloud import firestore | ||
|
||
from models.chat import Message | ||
|
@@ -82,3 +83,27 @@ def get_messages(uid: str, limit: int = 20, offset: int = 0, include_memories: b | |
] | ||
|
||
return messages | ||
|
||
|
||
async def batch_delete_messages(parent_doc_ref, batch_size=450): | ||
# batch size is 450 because firebase can perform upto 500 operations in a batch | ||
messages_ref = parent_doc_ref.collection('messages') | ||
while True: | ||
docs = messages_ref.limit(batch_size).stream() | ||
docs_list = list(docs) | ||
if not docs_list: | ||
break | ||
batch = db.batch() | ||
for doc in docs_list: | ||
batch.delete(doc.reference) | ||
batch.commit() | ||
|
||
|
||
async def clear_chat( uid: str): | ||
try: | ||
user_ref = db.collection('users').document(uid) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't use db in a router There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't .delete, check the other delete in the code, is setting a flag There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes it's in database directory only There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Got it |
||
if not user_ref.get().exists: | ||
raise HTTPException(status_code=404, detail="User not found") | ||
await batch_delete_messages(user_ref) | ||
except Exception as e: | ||
raise HTTPException(status_code=500, detail=f"Error deleting messages: {str(e)}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The In the async def batch_delete_messages(parent_doc_ref, batch_size=450):
# batch size is 450 because firebase can perform upto 500 operations in a batch
messages_ref = parent_doc_ref.collection('messages')
while True:
docs = messages_ref.limit(batch_size).stream()
docs_list = list(docs)
if not docs_list:
break
batch = db.batch()
for doc in docs_list:
batch.delete(doc.reference)
batch.commit()
async def clear_chat( uid: str):
try:
user_ref = db.collection('users').document(uid)
if not user_ref.get().exists:
raise HTTPException(status_code=404, detail="User not found")
await batch_delete_messages(user_ref)
except Exception as e:
# Differentiate between different types of errors
+ if isinstance(e, PermissionError):
+ raise HTTPException(status_code=403, detail=f"Insufficient permissions: {str(e)}")
+ elif isinstance(e, SomeOtherError):
+ raise HTTPException(status_code=XXX, detail=f"Some other error: {str(e)}")
- raise HTTPException(status_code=500, detail=f"Error deleting messages: {str(e)}")
\ No newline at end of file |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,6 +55,12 @@ def send_message( | |
ai_message.memories = memories if len(memories) < 5 else memories[:5] | ||
return ai_message | ||
|
||
@router.delete('/v1/clear-chat', tags=['chat'], response_model=Message) | ||
def clear_chat(uid: str = Depends(auth.get_current_user_uid)): | ||
chat_db.clear_chat(uid) | ||
return initial_message_util(uid) | ||
Comment on lines
+58
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new endpoint @router.delete('/v1/clear-chat', tags=['chat'], response_model=Message)
def clear_chat(uid: str = Depends(auth.get_current_user_uid)):
+ try:
chat_db.clear_chat(uid)
return initial_message_util(uid)
+ except Exception as e:
+ # Log the exception and return an appropriate HTTP response
+ logging.error(f"Error while clearing chat for user {uid}: {str(e)}")
+ raise HTTPException(status_code=500, detail="Internal server error") |
||
|
||
|
||
|
||
def initial_message_util(uid: str, plugin_id: Optional[str] = None): | ||
plugin = get_plugin_by_id(plugin_id) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Entelligence.AI
The
clearChatServer
function is making an API call to clear the chat. However, it's not passing any user identification or authentication headers in the request. This could lead to security issues as anyone can clear the chat without proper authorization. Please ensure that you are passing the necessary headers for user identification and authorization.Also, consider adding error handling for different types of HTTP status codes. Currently, if the status code is not 200, it throws a generic exception. It would be more informative to throw specific exceptions based on the status code.