Skip to content
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

[AI Chat] fix issues with conversation context menu #26990

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions components/ai_chat/core/browser/ai_chat_database.cc
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,8 @@ bool AIChatDatabase::AddConversationEntry(
bool AIChatDatabase::UpdateConversationTitle(std::string_view conversation_uuid,
std::string_view title) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DVLOG(4) << __func__ << " for " << conversation_uuid << " with title "
<< title;
if (!LazyInit()) {
return false;
}
Expand Down
31 changes: 15 additions & 16 deletions components/ai_chat/core/browser/ai_chat_service.cc
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ constexpr auto kAllowedSchemes = base::MakeFixedFlatSet<std::string_view>(
{url::kHttpsScheme, url::kHttpScheme, url::kFileScheme, url::kDataScheme});

std::vector<mojom::Conversation*> FilterVisibleConversations(
std::map<std::string, mojom::ConversationPtr>& conversations_map) {
std::map<std::string, mojom::ConversationPtr, std::less<>>&
conversations_map) {
std::vector<mojom::Conversation*> conversations;
for (const auto& kv : conversations_map) {
auto& conversation = kv.second;
Expand Down Expand Up @@ -598,14 +599,7 @@ void AIChatService::DeleteConversation(const std::string& id) {

void AIChatService::RenameConversation(const std::string& id,
const std::string& new_name) {
ConversationHandler* conversation_handler =
conversation_handlers_.at(id).get();
if (!conversation_handler) {
return;
}

DVLOG(1) << "Renamed conversation " << id << " to '" << new_name << "'";
OnConversationTitleChanged(conversation_handler, new_name);
OnConversationTitleChanged(id, new_name);
}

void AIChatService::ConversationExists(const std::string& conversation_uuid,
Expand Down Expand Up @@ -814,20 +808,25 @@ void AIChatService::OnClientConnectionChanged(ConversationHandler* handler) {
MaybeUnloadConversation(handler);
}

void AIChatService::OnConversationTitleChanged(ConversationHandler* handler,
std::string title) {
auto conversation_it = conversations_.find(handler->get_conversation_uuid());
CHECK(conversation_it != conversations_.end());
auto& conversation = conversation_it->second;
conversation->title = title;
void AIChatService::OnConversationTitleChanged(
const std::string& conversation_uuid,
const std::string& new_title) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this just take a std::string so we can move the new_title into this function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed in the slack thread, doesn't seem like it's worth the optimization

auto conversation_it = conversations_.find(conversation_uuid);
if (conversation_it == conversations_.end()) {
DLOG(ERROR) << "Conversation not found for title change";
return;
}

auto& conversation_metadata = conversation_it->second;
conversation_metadata->title = new_title;

OnConversationListChanged();

// Persist the change
if (ai_chat_db_) {
ai_chat_db_
.AsyncCall(base::IgnoreResult(&AIChatDatabase::UpdateConversationTitle))
.WithArgs(handler->get_conversation_uuid(), std::move(title));
.WithArgs(conversation_uuid, new_title);
Comment on lines +820 to +829
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we just take a std::string:

Suggested change
auto& conversation_metadata = conversation_it->second;
conversation_metadata->title = new_title;
OnConversationListChanged();
// Persist the change
if (ai_chat_db_) {
ai_chat_db_
.AsyncCall(base::IgnoreResult(&AIChatDatabase::UpdateConversationTitle))
.WithArgs(handler->get_conversation_uuid(), std::move(title));
.WithArgs(conversation_uuid, new_title);
auto& conversation_metadata = conversation_it->second;
conversation_metadata->title = std::move(new_title);
OnConversationListChanged();
// Persist the change
if (ai_chat_db_) {
ai_chat_db_
.AsyncCall(base::IgnoreResult(&AIChatDatabase::UpdateConversationTitle))
.WithArgs(conversation_uuid, conversation_metadata->title);

}
}

Expand Down
8 changes: 5 additions & 3 deletions components/ai_chat/core/browser/ai_chat_service.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include <stddef.h>

#include <functional>
#include <map>
#include <memory>
#include <ostream>
Expand Down Expand Up @@ -93,8 +94,8 @@ class AIChatService : public KeyedService,
void OnConversationEntryRemoved(ConversationHandler* handler,
std::string entry_uuid) override;
void OnClientConnectionChanged(ConversationHandler* handler) override;
void OnConversationTitleChanged(ConversationHandler* handler,
std::string title) override;
void OnConversationTitleChanged(const std::string& conversation_uuid,
const std::string& title) override;
void OnAssociatedContentDestroyed(ConversationHandler* handler,
int content_id) override;

Expand Down Expand Up @@ -175,7 +176,8 @@ class AIChatService : public KeyedService,

private:
// Key is uuid
using ConversationMap = std::map<std::string, mojom::ConversationPtr>;
using ConversationMap =
std::map<std::string, mojom::ConversationPtr, std::less<>>;
fallaciousreasoning marked this conversation as resolved.
Show resolved Hide resolved
using ConversationMapCallback = base::OnceCallback<void(ConversationMap&)>;

void MaybeInitStorage();
Expand Down
4 changes: 2 additions & 2 deletions components/ai_chat/core/browser/conversation_handler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1683,9 +1683,9 @@ void ConversationHandler::OnClientConnectionChanged() {
}
}

void ConversationHandler::OnConversationTitleChanged(std::string title) {
void ConversationHandler::OnConversationTitleChanged(std::string_view title) {
for (auto& observer : observers_) {
observer.OnConversationTitleChanged(this, title);
observer.OnConversationTitleChanged(metadata_->uuid, std::string(title));
}
}

Expand Down
7 changes: 4 additions & 3 deletions components/ai_chat/core/browser/conversation_handler.h
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,9 @@ class ConversationHandler : public mojom::ConversationHandler,

// Called when a mojo client connects or disconnects
virtual void OnClientConnectionChanged(ConversationHandler* handler) {}
virtual void OnConversationTitleChanged(ConversationHandler* handler,
std::string title) {}
virtual void OnConversationTitleChanged(
const std::string& conversation_uuid,
const std::string& title) {}
virtual void OnSelectedLanguageChanged(
ConversationHandler* handler,
const std::string& selected_language) {}
Expand Down Expand Up @@ -407,7 +408,7 @@ class ConversationHandler : public mojom::ConversationHandler,
void OnSuggestedQuestionsChanged();
void OnAssociatedContentInfoChanged();
void OnClientConnectionChanged();
void OnConversationTitleChanged(std::string title);
void OnConversationTitleChanged(std::string_view title);
void OnConversationUIConnectionChanged(mojo::RemoteSetElementId id);
void OnSelectedLanguageChanged(const std::string& selected_language);
void OnAssociatedContentFaviconImageDataChanged();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,24 @@ class MockConversationHandlerObserver : public ConversationHandler::Observer {

MOCK_METHOD(void,
OnRequestInProgressChanged,
(ConversationHandler * handler, bool in_progress),
(ConversationHandler*, bool),
(override));

MOCK_METHOD(void,
OnConversationEntryAdded,
(ConversationHandler * handler,
mojom::ConversationTurnPtr& entry,
std::optional<std::string_view> associated_content_value),
(ConversationHandler*,
mojom::ConversationTurnPtr&,
std::optional<std::string_view>),
(override));

MOCK_METHOD(void,
OnConversationEntryRemoved,
(ConversationHandler * handler, std::string turn_uuid),
(ConversationHandler*, std::string),
(override));

MOCK_METHOD(void,
OnConversationEntryUpdated,
(ConversationHandler * handler, mojom::ConversationTurnPtr entry),
(ConversationHandler*, mojom::ConversationTurnPtr),
(override));

MOCK_METHOD(void,
Expand All @@ -52,7 +52,7 @@ class MockConversationHandlerObserver : public ConversationHandler::Observer {

MOCK_METHOD(void,
OnConversationTitleChanged,
(ConversationHandler * handler, std::string title),
(const std::string&, const std::string&),
(override));

private:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,46 +62,48 @@ interface DisplayTitleProps {
}

function DisplayTitle(props: DisplayTitleProps) {
const [isButtonMenuVisible, setIsButtonMenuVisible] = React.useState(false)
const [isOptionsMenuOpen, setIsOptionsMenuOpen] = React.useState(false)

const handleButtonMenuChange = React.useCallback((e: {isOpen: boolean}) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: with stuff like this where you're at a leaf node, I think its okay to not wrap things in a useCallback (or do it inline)

Up to you on this one

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wasn't sure if our web component would cause a new listener to be added (and the old one removed) on every single render. But I suppose we're doing that everywhere anyway

Copy link
Contributor

@fallaciousreasoning fallaciousreasoning Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will 😆 - but its the same with every other component that does this so 🤷

And with React compiler it will (probably) Just Work (tm)

setIsOptionsMenuOpen(e.isOpen)
}, [])

return (
<div
className={styles.displayTitle}
onMouseEnter={() => setIsButtonMenuVisible(true)}
onMouseLeave={() => setIsButtonMenuVisible(false)}
className={classnames(styles.displayTitle, isOptionsMenuOpen && styles.isOptionsMenuOpen)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a handy util taher added!

>
<div className={styles.displayTitleContent}>
<div
className={styles.text}
onDoubleClick={props.onEditTitle}
title={props.title}
>
{props.title}
</div>
<div className={styles.description}>{props.description}</div>
</div>
{isButtonMenuVisible && (
<ButtonMenu className={styles.optionsMenu}>
<div
slot='anchor-content'
className={styles.optionsButton}
>
<Icon name='more-vertical' />
<ButtonMenu
className={styles.optionsMenu}
onChange={handleButtonMenuChange}
>
<div
slot='anchor-content'
className={styles.optionsButton}
>
<Icon name='more-vertical' />
</div>
<leo-menu-item onClick={props.onEditTitle}>
<div className={styles.optionsMenuItemWithIcon}>
<Icon name='edit-pencil' />
<div>{getLocale('menuRenameConversation')}</div>
</div>
<leo-menu-item onClick={props.onEditTitle}>
<div className={styles.optionsMenuItemWithIcon}>
<Icon name='edit-pencil' />
<div>{getLocale('menuRenameConversation')}</div>
</div>
</leo-menu-item>
<leo-menu-item onClick={props.onDelete}>
<div className={styles.optionsMenuItemWithIcon}>
<Icon name='trash' />
<div>{getLocale('menuDeleteConversation')}</div>
</div>
</leo-menu-item>
</ButtonMenu>
)}
</leo-menu-item>
<leo-menu-item onClick={props.onDelete}>
<div className={styles.optionsMenuItemWithIcon}>
<Icon name='trash' />
<div>{getLocale('menuDeleteConversation')}</div>
</div>
</leo-menu-item>
</ButtonMenu>
</div>
)
}
Expand Down Expand Up @@ -140,19 +142,30 @@ export default function ConversationsList(props: ConversationsListProps) {
{aiChatContext.visibleConversations.length > 0 &&
<ol>
{aiChatContext.visibleConversations.map(item => {
const isEditing = aiChatContext.editingConversationId === item.uuid
return (
<li key={item.uuid}>
<a
className={classnames({
[styles.navItem]: true,
[styles.navItemActive]: item.uuid === conversationContext.conversationUuid
})}
onClick={() => {
onClick={(e) => {
if (isEditing) {
e.preventDefault()
}
props.setIsConversationsListOpen?.(false)
}}
onDoubleClick={() => aiChatContext.setEditingConversationId(item.uuid)}
href={`/${item.uuid}`}
>
{item.uuid === aiChatContext.editingConversationId ? (
<DisplayTitle
title={item.title || getLocale('conversationListUntitled')}
description=''
onEditTitle={() => aiChatContext.setEditingConversationId(item.uuid)}
onDelete={() => getAPI().service.deleteConversation(item.uuid)}
/>
{item.uuid === aiChatContext.editingConversationId && (
<div className={styles.editibleTitle}>
<SimpleInput
text={item.title}
Expand All @@ -163,13 +176,6 @@ export default function ConversationsList(props: ConversationsListProps) {
}}
/>
</div>
) : (
<DisplayTitle
title={item.title || getLocale('conversationListUntitled')}
description=''
onEditTitle={() => aiChatContext.setEditingConversationId(item.uuid)}
onDelete={() => getAPI().service.deleteConversation(item.uuid)}
/>
)}
</a>
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

.navItem {
all: unset;
position: relative;
border-radius: var(--leo-radius-m);
display: block;
font: var(--leo-font-default-regular);
Expand Down Expand Up @@ -65,6 +66,10 @@
transition: all 0.2s ease-out allow-discrete;
width: 100%;

&:not(.isOptionsMenuOpen,:hover) .optionsMenu {
visibility: hidden;
}

&:hover {
background: var(--leo-color-container-highlight);
box-shadow: var(--leo-effect-elevation-01);
Expand Down Expand Up @@ -92,9 +97,14 @@
}

.editibleTitle {
// Take up the full size of the parent
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
width: 100%;

form,
input {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
// Ensure navigation content always stays at full-width even when animation
// is decreasing the width of the parent
width: var(--navigation-width);
// Ensure fills the height so that there's enough space for the conversation
// item context menu
height: -webkit-fill-available;
}

.left {
Expand Down
Loading
Loading