From a3df109d868e459c30137a108757b910fc58aaf2 Mon Sep 17 00:00:00 2001 From: Pete Miller Date: Wed, 11 Dec 2024 16:33:40 -0800 Subject: [PATCH 1/3] AssistantResponse uses key for events --- .../components/assistant_response/index.tsx | 78 +++++++++++-------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/components/ai_chat/resources/untrusted_conversation_frame/components/assistant_response/index.tsx b/components/ai_chat/resources/untrusted_conversation_frame/components/assistant_response/index.tsx index f6cc88c30e8e..25eb810714ca 100644 --- a/components/ai_chat/resources/untrusted_conversation_frame/components/assistant_response/index.tsx +++ b/components/ai_chat/resources/untrusted_conversation_frame/components/assistant_response/index.tsx @@ -28,11 +28,11 @@ function SearchSummary (props: { searchQueries: string[] }) { const message = formatMessage(getLocale('searchQueries'), { placeholders: { $1: props.searchQueries.map((query, i, a) => ( - <> + " handleOpenSearchQuery(e, query)}> {query} "{(i < a.length-1) ? ', ' : null} - + )) } }) @@ -47,42 +47,52 @@ function SearchSummary (props: { searchQueries: string[] }) { ) } +function AssistantEvent(props: { event: Mojom.ConversationEntryEvent, hasCompletionStarted: boolean, isEntryInProgress: boolean }) { + if (props.event.completionEvent) { + return ( + + ) + } + if (props.event.searchStatusEvent && props.isEntryInProgress && !props.hasCompletionStarted) { + return ( +
Improving answer with Brave Search…
+ ) + } + if (props.event.pageContentRefineEvent && props.isEntryInProgress && !props.hasCompletionStarted) { + return ( +
{getLocale('pageContentRefinedInProgress')}
+ ) + } + // TODO(petemill): Consider displaying in-progress queries if the API + // timing improves (or worsens for the completion events). + // if (event.searchQueriesEvent && props.isEntryInProgress) { + // return (<> + // {event.searchQueriesEvent.searchQueries.map(query =>
Searching for {query}
)} + // ) + // } + + // Unknown events should be ignored + return null +} + export default function AssistantResponse(props: { entry: Mojom.ConversationTurn, isEntryInProgress: boolean }) { - const searchQueriesEvent = props.entry.events?.find(event => !!event.searchQueriesEvent)?.searchQueriesEvent - const hasCompletionStarted = !props.isEntryInProgress || props.entry.events?.find(event => !!event.completionEvent) + const searchQueriesEvent = props.entry.events?.find(event => event.searchQueriesEvent)?.searchQueriesEvent + const hasCompletionStarted = !props.isEntryInProgress || + (props.entry.events?.some(event => event.completionEvent) ?? false) return (<> { - props.entry.events?.map((event) => { - if (event.completionEvent) { - return ( - - ) - } - if (event.searchStatusEvent && props.isEntryInProgress && !hasCompletionStarted) { - return ( -
Improving answer with Brave Search…
- ) - } - if (event.pageContentRefineEvent && props.isEntryInProgress && !hasCompletionStarted) { - return ( -
{getLocale('pageContentRefinedInProgress')}
- ) - } - // TODO(petemill): Consider displaying in-progress queries if the API - // timing improves (or worsens for the completion events). - // if (event.searchQueriesEvent && props.isEntryInProgress) { - // return (<> - // {event.searchQueriesEvent.searchQueries.map(query =>
Searching for {query}
)} - // ) - // } - - // Unknown events should be ignored - return null - }) + props.entry.events?.map((event, i) => + + ) } { !props.isEntryInProgress && searchQueriesEvent && From 24479332f30a38dc0afc903790c2a0b785824b1d Mon Sep 17 00:00:00 2001 From: Pete Miller Date: Wed, 11 Dec 2024 16:34:00 -0800 Subject: [PATCH 2/3] AI Chat conversations list item menu and edit improvements - ButtonMenu can open without being tiny and scrolling - make sure edit isn't blocked by the link activation - storybook get edit ability - storybook menu open responsiveness matches webui - storybook context can use react hooks --- .../components/conversations_list/index.tsx | 175 ++++++----- .../conversations_list/style.module.scss | 38 ++- .../components/full_page/style.module.scss | 5 + .../page/stories/components_panel.tsx | 283 ++++++++++-------- 4 files changed, 283 insertions(+), 218 deletions(-) diff --git a/components/ai_chat/resources/page/components/conversations_list/index.tsx b/components/ai_chat/resources/page/components/conversations_list/index.tsx index 29bd4d900737..10c489996c88 100644 --- a/components/ai_chat/resources/page/components/conversations_list/index.tsx +++ b/components/ai_chat/resources/page/components/conversations_list/index.tsx @@ -8,6 +8,7 @@ import styles from './style.module.scss' import classnames from '$web-common/classnames' import Icon from '@brave/leo/react/icon' import ButtonMenu from '@brave/leo/react/buttonMenu' +import * as Mojom from '../../../common/mojom' import { useAIChat } from '../../state/ai_chat_context' import { getLocale } from '$web-common/locale' import getAPI from '../../api' @@ -54,55 +55,103 @@ function SimpleInput(props: SimpleInputProps) { ) } -interface DisplayTitleProps { - title: string - description?: string - onEditTitle?: () => void - onDelete?: () => void +interface ConversationItemProps extends ConversationsListProps { + conversation: Mojom.Conversation } -function DisplayTitle(props: DisplayTitleProps) { - const [isButtonMenuVisible, setIsButtonMenuVisible] = React.useState(false) +function ConversationItem(props: ConversationItemProps) { + const [isOptionsMenuOpen, setIsOptionsMenuOpen] = React.useState(false) + + const aiChatContext = useAIChat() + const conversationContext = useConversation() + + const { uuid } = props.conversation + const title = props.conversation.title || getLocale('conversationListUntitled') + + const handleButtonMenuChange = (e: {isOpen: boolean}) => { + setIsOptionsMenuOpen(e.isOpen) + } + + const handleEditTitle: EventListener = (e) => { + e.preventDefault() + aiChatContext.setEditingConversationId(uuid) + } + + const handleDelete: EventListener = (e) => { + e.preventDefault() + aiChatContext.service?.deleteConversation(uuid) + } + + const isEditing = aiChatContext.editingConversationId === uuid + const isActive = uuid === conversationContext.conversationUuid return ( -
setIsButtonMenuVisible(true)} - onMouseLeave={() => setIsButtonMenuVisible(false)} - > - - {isButtonMenuVisible && ( - -
- -
- -
- -
{getLocale('menuRenameConversation')}
+
+
+ {title}
- - -
- -
{getLocale('menuDeleteConversation')}
+
+ +
+
-
- - )} -
+ +
+ +
{getLocale('menuRenameConversation')}
+
+
+ +
+ +
{getLocale('menuDeleteConversation')}
+
+
+ +
+ {uuid === aiChatContext.editingConversationId && ( +
+ aiChatContext.setEditingConversationId(null)} + onSubmit={(value) => { + aiChatContext.setEditingConversationId(null) + getAPI().service.renameConversation(uuid, value) + }} + /> +
+ )} +
+ ) } @@ -112,7 +161,6 @@ interface ConversationsListProps { export default function ConversationsList(props: ConversationsListProps) { const aiChatContext = useAIChat() - const conversationContext = useConversation() return ( <> @@ -139,42 +187,13 @@ export default function ConversationsList(props: ConversationsListProps) { } {aiChatContext.visibleConversations.length > 0 &&
    - {aiChatContext.visibleConversations.map(item => { - return ( -
  1. - { - props.setIsConversationsListOpen?.(false) - }} - href={`/${item.uuid}`} - > - {item.uuid === aiChatContext.editingConversationId ? ( -
    - aiChatContext.setEditingConversationId(null)} - onSubmit={(value) => { - aiChatContext.setEditingConversationId(null) - getAPI().service.renameConversation(item.uuid, value) - }} - /> -
    - ) : ( - aiChatContext.setEditingConversationId(item.uuid)} - onDelete={() => getAPI().service.deleteConversation(item.uuid)} - /> - )} -
    -
  2. - ) - })} + {aiChatContext.visibleConversations.map(conversation => + + )}
} diff --git a/components/ai_chat/resources/page/components/conversations_list/style.module.scss b/components/ai_chat/resources/page/components/conversations_list/style.module.scss index 1799623ff804..f770191cc133 100644 --- a/components/ai_chat/resources/page/components/conversations_list/style.module.scss +++ b/components/ai_chat/resources/page/components/conversations_list/style.module.scss @@ -28,9 +28,26 @@ .navItem { all: unset; + position: relative; border-radius: var(--leo-radius-m); display: block; font: var(--leo-font-default-regular); + + // Display ButtonMenu button when menu is open or title + // is hovered. + @media (hover: hover) { + &:not(.isOptionsMenuOpen,:hover) .optionsMenu { + visibility: hidden; + } + } + + // Dispaly ButtonMenu button when item is active when + // primary input method does not have convenient hover. + @media (hover: none) { + &:not(.navItemActive) .optionsMenu { + visibility: hidden; + } + } } .navItemActive { @@ -64,8 +81,11 @@ position: relative; transition: all 0.2s ease-out allow-discrete; width: 100%; + // Take up minimum 1 line in case the title is + // somehow empty + min-height: 1lh; - &:hover { + &:hover, .isOptionsMenuOpen & { background: var(--leo-color-container-highlight); box-shadow: var(--leo-effect-elevation-01); } @@ -82,19 +102,15 @@ text-overflow: ellipsis; } -.description { - font: var(--leo-font-default-regular); - color: var(--leo-color-text-tertiary); - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - overflow: hidden; -} - .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 { diff --git a/components/ai_chat/resources/page/components/full_page/style.module.scss b/components/ai_chat/resources/page/components/full_page/style.module.scss index b1f4c5124581..1ef36987fe17 100644 --- a/components/ai_chat/resources/page/components/full_page/style.module.scss +++ b/components/ai_chat/resources/page/components/full_page/style.module.scss @@ -59,6 +59,11 @@ // 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: 100%; + display: flex; + flex-direction: column; } .left { diff --git a/components/ai_chat/resources/page/stories/components_panel.tsx b/components/ai_chat/resources/page/stories/components_panel.tsx index a9dc08ab152b..f9345dcb6fb3 100644 --- a/components/ai_chat/resources/page/stories/components_panel.tsx +++ b/components/ai_chat/resources/page/stories/components_panel.tsx @@ -4,7 +4,7 @@ * You can obtain one at https://mozilla.org/MPL/2.0/. */ import * as React from 'react' -import { useArgs, useState } from '@storybook/preview-api' +import { useArgs } from '@storybook/preview-api' import { Meta, StoryObj } from '@storybook/react' import '@brave/leo/tokens/css/variables.css' import '$web-components/app.global.scss' @@ -12,7 +12,7 @@ import { getKeysForMojomEnum } from '$web-common/mojomUtils' import { InferControlsFromArgs } from '../../../../../.storybook/utils' import * as Mojom from '../../common/mojom' import { ActiveChatContext, SelectedChatDetails } from '../state/active_chat_context' -import { AIChatContext, AIChatReactContext } from '../state/ai_chat_context' +import { AIChatContext, AIChatReactContext, useIsSmall } from '../state/ai_chat_context' import { ConversationContext, ConversationReactContext } from '../state/conversation_context' import FeedbackForm from '../components/feedback_form' import FullPage from '../components/full_page' @@ -97,7 +97,7 @@ const CONVERSATIONS: Mojom.Conversation[] = [ modelKey: undefined, }, { - title: 'Wedding speech improvements', + title: '', uuid: '3', hasContent: true, updatedTime: { internalValue: BigInt('13278618001000002') }, @@ -397,7 +397,8 @@ type CustomArgs = { model: string inputText: string hasConversation: boolean - hasConversationListItems: boolean + editingConversationId: string | null + visibleConversationListCount: number hasSuggestedQuestions: boolean hasSiteInfo: boolean isFeedbackFormVisible: boolean @@ -422,9 +423,10 @@ const args: CustomArgs = { initialized: true, inputText: `Write a Star Trek poem about Data's life on board the Enterprise`, hasConversation: true, - hasConversationListItems: true, + visibleConversationListCount: CONVERSATIONS.length, hasSuggestedQuestions: true, hasSiteInfo: true, + editingConversationId: null, isFeedbackFormVisible: false, isStorageNoticeDismissed: false, canShowPremiumPrompt: false, @@ -445,7 +447,7 @@ const args: CustomArgs = { shouldShowRefinedWarning: false, } -const preview: Meta = { +const meta: Meta = { title: 'Chat/Chat', parameters: { layout: 'centered' @@ -464,140 +466,163 @@ const preview: Meta = { options: MODELS.map(model => model.displayName), control: { type: 'select' } }, + visibleConversationListCount: { + control: { type: 'number' } + } }, args, decorators: [ (Story, options) => { const [, setArgs] = useArgs() - - const siteInfo = options.args.hasSiteInfo ? SITE_INFO : new Mojom.SiteInfo() - const suggestedQuestions = options.args.hasSuggestedQuestions - ? SAMPLE_QUESTIONS - : siteInfo - ? [SAMPLE_QUESTIONS[0]] - : [] - - const currentError = Mojom.APIError[options.args.currentErrorState] - const apiHasError = currentError !== Mojom.APIError.None - const currentModel = MODELS.find(m => m.displayName === options.args.model) - - const switchToBasicModel = () => { - const nonPremiumModel = MODELS.find(model => model.options.leoModelOptions?.access === Mojom.ModelAccess.BASIC) - setArgs({ model: nonPremiumModel }) - } - - const setInputText = (inputText: string) => { - setArgs({ inputText }) - } - - const [showSidebar, setShowSidebar] = useState(false) - const aiChatContext: AIChatContext = { - conversationEntriesComponent: StorybookConversationEntries, - initialized: options.args.initialized, - editingConversationId: null, - visibleConversations: options.args.hasConversationListItems ? CONVERSATIONS : [], - isStoragePrefEnabled: options.args.isStoragePrefEnabled, - hasAcceptedAgreement: options.args.hasAcceptedAgreement, - isPremiumStatusFetching: false, - isPremiumUser: options.args.isPremiumUser, - isPremiumUserDisconnected: options.args.isPremiumUserDisconnected, - isStorageNoticeDismissed: options.args.isStorageNoticeDismissed, - canShowPremiumPrompt: options.args.canShowPremiumPrompt, - isMobile: options.args.isMobile, - isHistoryFeatureEnabled: options.args.isHistoryEnabled, - isStandalone: options.args.isStandalone, - allActions: ACTIONS_LIST, - goPremium: () => {}, - managePremium: () => {}, - handleAgreeClick: () => {}, - enableStoragePref: () => {}, - markStorageNoticeViewed: () => {}, - dismissStorageNotice: () => {}, - dismissPremiumPrompt: () => {}, - userRefreshPremiumSession: () => {}, - setEditingConversationId: () => {}, - showSidebar: showSidebar, - toggleSidebar: () => setShowSidebar(s => !s) - } - - const activeChatContext: SelectedChatDetails = { - selectedConversationId: CONVERSATIONS[0].uuid, - updateSelectedConversationId: () => {}, - callbackRouter: undefined!, - conversationHandler: undefined!, - createNewConversation: () => {}, - isTabAssociated: options.args.isDefaultConversation - } - - const inputText = options.args.inputText - - const conversationContext: ConversationContext = { - conversationUuid: CONVERSATIONS[1].uuid, - conversationHistory: options.args.hasConversation ? HISTORY : [], - associatedContentInfo: siteInfo, - allModels: MODELS, - currentModel, - suggestedQuestions, - isGenerating: true, - suggestionStatus: Mojom.SuggestionGenerationStatus[options.args.suggestionStatus], - currentError, - apiHasError, - isFeedbackFormVisible: options.args.isFeedbackFormVisible, - shouldDisableUserInput: false, - shouldShowLongPageWarning: options.args.shouldShowLongPageWarning, - shouldShowLongConversationInfo: options.args.shouldShowLongConversationInfo, - shouldSendPageContents: siteInfo?.isContentAssociationPossible, - inputText, - actionList: ACTIONS_LIST, - selectedActionType: undefined, - isToolsMenuOpen: false, - isCurrentModelLeo: true, - isCharLimitApproaching: inputText.length > 64, - isCharLimitExceeded: inputText.length > 70, - inputTextCharCountDisplay: `${inputText.length} / 70`, - setInputText, - setCurrentModel: () => {}, - switchToBasicModel, - generateSuggestedQuestions: () => {}, - dismissLongConversationInfo: () => {}, - updateShouldSendPageContents: () => {}, - retryAPIRequest: () => {}, - handleResetError: () => {}, - submitInputTextToAPI: () => {}, - resetSelectedActionType: () => {}, - handleActionTypeClick: () => {}, - setIsToolsMenuOpen: () => {}, - handleFeedbackFormCancel: () => {}, - handleFeedbackFormSubmit: () => {} - } - - const conversationEntriesContext: UntrustedConversationContext = { - conversationHistory: conversationContext.conversationHistory, - isGenerating: conversationContext.isGenerating, - isLeoModel: conversationContext.isCurrentModelLeo, - contentUsedPercentage: (options.args.shouldShowLongPageWarning || options.args.shouldShowRefinedWarning) - ? 48 : 100, - isContentRefined: options.args.shouldShowRefinedWarning, - canSubmitUserEntries: !conversationContext.shouldDisableUserInput, - isMobile: aiChatContext.isMobile - } - return ( - - - - - - - - - + + + ) } ] } -export default preview +function StoryContext(props: React.PropsWithChildren<{args: CustomArgs, setArgs: (newArgs: Partial) => void}>) { + const isSmall = useIsSmall() + + const options = { args: props.args } + const { setArgs } = props + + const siteInfo = options.args.hasSiteInfo ? SITE_INFO : new Mojom.SiteInfo() + const suggestedQuestions = options.args.hasSuggestedQuestions + ? SAMPLE_QUESTIONS + : siteInfo + ? [SAMPLE_QUESTIONS[0]] + : [] + + const currentError = Mojom.APIError[options.args.currentErrorState] + const apiHasError = currentError !== Mojom.APIError.None + const currentModel = MODELS.find(m => m.displayName === options.args.model) + + const switchToBasicModel = () => { + const nonPremiumModel = MODELS.find(model => model.options.leoModelOptions?.access === Mojom.ModelAccess.BASIC) + setArgs({ model: nonPremiumModel?.key }) + } + + const setInputText = (inputText: string) => { + setArgs({ inputText }) + } + + const [showSidebar, setShowSidebar] = React.useState(isSmall) + + let visibleConversations: typeof CONVERSATIONS = [] + for (let i = 0; i < Math.floor(options.args.visibleConversationListCount / CONVERSATIONS.length); i++) { + visibleConversations = visibleConversations.concat(CONVERSATIONS) + } + const remainingConversationsCount = options.args.visibleConversationListCount % CONVERSATIONS.length + visibleConversations = visibleConversations.concat(CONVERSATIONS.slice(0, remainingConversationsCount)) + + const aiChatContext: AIChatContext = { + conversationEntriesComponent: StorybookConversationEntries, + initialized: options.args.initialized, + editingConversationId: options.args.editingConversationId, + visibleConversations, + isStoragePrefEnabled: options.args.isStoragePrefEnabled, + hasAcceptedAgreement: options.args.hasAcceptedAgreement, + isPremiumStatusFetching: false, + isPremiumUser: options.args.isPremiumUser, + isPremiumUserDisconnected: options.args.isPremiumUserDisconnected, + isStorageNoticeDismissed: options.args.isStorageNoticeDismissed, + canShowPremiumPrompt: options.args.canShowPremiumPrompt, + isMobile: options.args.isMobile, + isHistoryFeatureEnabled: options.args.isHistoryEnabled, + isStandalone: options.args.isStandalone, + allActions: ACTIONS_LIST, + goPremium: () => {}, + managePremium: () => {}, + handleAgreeClick: () => {}, + enableStoragePref: () => {}, + markStorageNoticeViewed: () => {}, + dismissStorageNotice: () => {}, + dismissPremiumPrompt: () => {}, + userRefreshPremiumSession: () => {}, + setEditingConversationId: (id: string | null) => setArgs({ editingConversationId: id }), + showSidebar: showSidebar, + toggleSidebar: () => setShowSidebar(s => !s) + } + + const activeChatContext: SelectedChatDetails = { + selectedConversationId: CONVERSATIONS[0].uuid, + updateSelectedConversationId: () => {}, + callbackRouter: undefined!, + conversationHandler: undefined!, + createNewConversation: () => {}, + isTabAssociated: options.args.isDefaultConversation + } + + const inputText = options.args.inputText + + const conversationContext: ConversationContext = { + conversationUuid: CONVERSATIONS[1].uuid, + conversationHistory: options.args.hasConversation ? HISTORY : [], + associatedContentInfo: siteInfo, + allModels: MODELS, + currentModel, + suggestedQuestions, + isGenerating: true, + suggestionStatus: Mojom.SuggestionGenerationStatus[options.args.suggestionStatus], + currentError, + apiHasError, + isFeedbackFormVisible: options.args.isFeedbackFormVisible, + shouldDisableUserInput: false, + shouldShowLongPageWarning: options.args.shouldShowLongPageWarning, + shouldShowLongConversationInfo: options.args.shouldShowLongConversationInfo, + shouldSendPageContents: siteInfo?.isContentAssociationPossible, + inputText, + actionList: ACTIONS_LIST, + selectedActionType: undefined, + isToolsMenuOpen: false, + isCurrentModelLeo: true, + isCharLimitApproaching: inputText.length > 64, + isCharLimitExceeded: inputText.length > 70, + inputTextCharCountDisplay: `${inputText.length} / 70`, + setInputText, + setCurrentModel: () => {}, + switchToBasicModel, + generateSuggestedQuestions: () => {}, + dismissLongConversationInfo: () => {}, + updateShouldSendPageContents: () => {}, + retryAPIRequest: () => {}, + handleResetError: () => {}, + submitInputTextToAPI: () => {}, + resetSelectedActionType: () => {}, + handleActionTypeClick: () => {}, + setIsToolsMenuOpen: () => {}, + handleFeedbackFormCancel: () => {}, + handleFeedbackFormSubmit: () => {} + } + + const conversationEntriesContext: UntrustedConversationContext = { + conversationHistory: conversationContext.conversationHistory, + isGenerating: conversationContext.isGenerating, + isLeoModel: conversationContext.isCurrentModelLeo, + contentUsedPercentage: (options.args.shouldShowLongPageWarning || options.args.shouldShowRefinedWarning) + ? 48 : 100, + isContentRefined: options.args.shouldShowRefinedWarning, + canSubmitUserEntries: !conversationContext.shouldDisableUserInput, + isMobile: aiChatContext.isMobile + } + + return ( + + + + + {props.children} + + + + + ) +} + +export default meta type Story = StoryObj From 4e4f8d9b2d84c78fdcdabfb540bc2b050edcfe62 Mon Sep 17 00:00:00 2001 From: Pete Miller Date: Wed, 11 Dec 2024 17:01:14 -0800 Subject: [PATCH 3/3] Don't crash when editing the title of a conversation not in-memory and log database title because of historical bad values due to ref expiring --- .../ai_chat/core/browser/ai_chat_database.cc | 2 ++ .../ai_chat/core/browser/ai_chat_service.cc | 31 +++++++++---------- .../ai_chat/core/browser/ai_chat_service.h | 8 +++-- .../core/browser/conversation_handler.cc | 4 +-- .../core/browser/conversation_handler.h | 7 +++-- .../mock_conversation_handler_observer.h | 14 ++++----- 6 files changed, 35 insertions(+), 31 deletions(-) diff --git a/components/ai_chat/core/browser/ai_chat_database.cc b/components/ai_chat/core/browser/ai_chat_database.cc index 941588cb8efa..6a4d2602f941 100644 --- a/components/ai_chat/core/browser/ai_chat_database.cc +++ b/components/ai_chat/core/browser/ai_chat_database.cc @@ -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; } diff --git a/components/ai_chat/core/browser/ai_chat_service.cc b/components/ai_chat/core/browser/ai_chat_service.cc index 58a3226d90f6..9e0063194098 100644 --- a/components/ai_chat/core/browser/ai_chat_service.cc +++ b/components/ai_chat/core/browser/ai_chat_service.cc @@ -61,7 +61,8 @@ constexpr auto kAllowedSchemes = base::MakeFixedFlatSet( {url::kHttpsScheme, url::kHttpScheme, url::kFileScheme, url::kDataScheme}); std::vector FilterVisibleConversations( - std::map& conversations_map) { + std::map>& + conversations_map) { std::vector conversations; for (const auto& kv : conversations_map) { auto& conversation = kv.second; @@ -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, @@ -814,12 +808,17 @@ 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) { + 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(); @@ -827,7 +826,7 @@ void AIChatService::OnConversationTitleChanged(ConversationHandler* handler, 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); } } diff --git a/components/ai_chat/core/browser/ai_chat_service.h b/components/ai_chat/core/browser/ai_chat_service.h index 80913c0985ba..3515ae66aa53 100644 --- a/components/ai_chat/core/browser/ai_chat_service.h +++ b/components/ai_chat/core/browser/ai_chat_service.h @@ -8,6 +8,7 @@ #include +#include #include #include #include @@ -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; @@ -175,7 +176,8 @@ class AIChatService : public KeyedService, private: // Key is uuid - using ConversationMap = std::map; + using ConversationMap = + std::map>; using ConversationMapCallback = base::OnceCallback; void MaybeInitStorage(); diff --git a/components/ai_chat/core/browser/conversation_handler.cc b/components/ai_chat/core/browser/conversation_handler.cc index 898b40671860..bff15e30cd1c 100644 --- a/components/ai_chat/core/browser/conversation_handler.cc +++ b/components/ai_chat/core/browser/conversation_handler.cc @@ -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)); } } diff --git a/components/ai_chat/core/browser/conversation_handler.h b/components/ai_chat/core/browser/conversation_handler.h index 8dfcd8a9d99b..d40f1c66ab0c 100644 --- a/components/ai_chat/core/browser/conversation_handler.h +++ b/components/ai_chat/core/browser/conversation_handler.h @@ -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) {} @@ -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(); diff --git a/components/ai_chat/core/browser/mock_conversation_handler_observer.h b/components/ai_chat/core/browser/mock_conversation_handler_observer.h index b944d74382f3..a33e2c360188 100644 --- a/components/ai_chat/core/browser/mock_conversation_handler_observer.h +++ b/components/ai_chat/core/browser/mock_conversation_handler_observer.h @@ -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 associated_content_value), + (ConversationHandler*, + mojom::ConversationTurnPtr&, + std::optional), (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, @@ -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: