From 41d03a3144c4949185c6431ffac91fd94ae9ffb1 Mon Sep 17 00:00:00 2001 From: Jay Harris Date: Fri, 10 Jan 2025 15:11:53 +1300 Subject: [PATCH] [AI Chat]: Add UI for Tab Informer --- components/ai_chat/core/browser/constants.cc | 4 + .../page/components/attachments/index.tsx | 56 ++++ .../components/attachments/style.module.scss | 76 ++++++ .../page/components/header/index.tsx | 13 +- .../resources/page/components/main/index.tsx | 258 +++++++++--------- .../page/components/main/style.module.scss | 21 ++ .../resources/page/state/ai_chat_context.tsx | 2 +- .../page/state/conversation_context.tsx | 26 +- components/resources/ai_chat_ui_strings.grdp | 9 + ui/webui/resources/BUILD.gn | 1 + 10 files changed, 338 insertions(+), 128 deletions(-) create mode 100644 components/ai_chat/resources/page/components/attachments/index.tsx create mode 100644 components/ai_chat/resources/page/components/attachments/style.module.scss diff --git a/components/ai_chat/core/browser/constants.cc b/components/ai_chat/core/browser/constants.cc index d41ba39177cb..305dc6025ee4 100644 --- a/components/ai_chat/core/browser/constants.cc +++ b/components/ai_chat/core/browser/constants.cc @@ -132,6 +132,10 @@ base::span GetLocalizedStrings() { {"searchQueries", IDS_CHAT_UI_SEARCH_QUERIES}, {"learnMore", IDS_CHAT_UI_LEARN_MORE}, {"closeNotice", IDS_CHAT_UI_CLOSE_NOTICE}, + {"attachmentsTitle", IDS_CHAT_UI_ATTACHMENTS_TITLE}, + {"attachmentsDescription", IDS_CHAT_UI_ATTACHMENTS_DESCRIPTION}, + {"attachmentsBrowserTabsTitle", + IDS_CHAT_UI_ATTACHMENTS_BROWSER_TABS_TITLE}, {"noticeConversationHistoryBody", IDS_CHAT_UI_NOTICE_CONVERSATION_HISTORY_BODY}, {"noticeConversationHistoryEmpty", diff --git a/components/ai_chat/resources/page/components/attachments/index.tsx b/components/ai_chat/resources/page/components/attachments/index.tsx new file mode 100644 index 000000000000..d21173c3f637 --- /dev/null +++ b/components/ai_chat/resources/page/components/attachments/index.tsx @@ -0,0 +1,56 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +import * as React from 'react' +import styles from './style.module.scss' +import Button from '@brave/leo/react/button' +import Icon from '@brave/leo/react/icon' +import Input from '@brave/leo/react/input' +import RadioButton from '@brave/leo/react/radioButton' +import Flex from '$web-common/Flex' +import { useAIChat } from '../../state/ai_chat_context' +import { Tab } from 'components/ai_chat/resources/common/mojom' +import { useConversation } from '../../state/conversation_context' +import { getLocale } from '$web-common/locale' + +function TabItem({ tab }: { tab: Tab }) { + const aiChat = useAIChat() + const { conversationUuid, associatedContentInfo } = useConversation() + return { + aiChat.uiHandler?.associateTab(tab, conversationUuid!) + }}> + {tab.title} + + +} + +export default function Attachments() { + const aiChat = useAIChat() + const conversation = useConversation() + const [search, setSearch] = React.useState('') + + const tabs = aiChat.windows.flatMap(w => w.tabs).filter(t => t.title.toLowerCase().includes(search.toLowerCase())) + return
+
+ +

{getLocale('attachmentsTitle')}

+ +
+ {getLocale('attachmentsDescription')} +
+
+ +
{getLocale('attachmentsBrowserTabsTitle')}
+
+ setSearch(e.value)}> + + +
+ {tabs.map(t => )} +
+
+
+} diff --git a/components/ai_chat/resources/page/components/attachments/style.module.scss b/components/ai_chat/resources/page/components/attachments/style.module.scss new file mode 100644 index 000000000000..126fac5c6f21 --- /dev/null +++ b/components/ai_chat/resources/page/components/attachments/style.module.scss @@ -0,0 +1,76 @@ +.root { + background: var(--leo-color-container-highlight); + padding: var(--leo-spacing-m); + margin-top: var(--leo-spacing-m); + border-radius: var(--leo-radius-l); + box-shadow: var(--leo-effect-elevation-01); + + & leo-button[fab] { + flex: 0; + } + + & h4 { + margin: 0; + color: var(--leo-color-text-secondary); + font: var(--leo-font-heading-h4); + } + + & h5 { + margin: 0; + color: var(--leo-color-text-secondary); + font: var(--leo-font-default-semibold); + } +} + +.header { + padding: var(--leo-spacing-l) var(--leo-spacing-xl); +} + +.tabSearchContainer { + background: var(--leo-color-container-background); + padding: var(--leo-spacing-l) var(--leo-spacing-xl); + border-radius: var(--leo-radius-l); + display: flex; + flex-direction: column; + gap: var(--leo-spacing-m); +} + +.tabList { + display: flex; + flex-direction: column; + gap: var(--leo-spacing-s); + + border: 1px solid var(--leo-color-divider-subtle); + border-radius: var(--leo-radius-m); + + padding: var(--leo-spacing-m); +} + +.description { + font: var(--leo-font-small-regular); + color: var(--leo-color-text-tertiary); +} + +.tabItem { + --leo-radiobutton-flex-direction: row-reverse; + + padding: var(--leo-spacing-m); + display: flex; + gap: var(--leo-spacing-m); + align-items: center; + justify-content: space-between; + + & .icon { + flex-shrink: 0; + } + + & .title { + text-wrap: nowrap; + text-overflow: ellipsis; + overflow: hidden; + flex: 1; + + color: var(--leo-color-text-primary); + font: var(--leo-font-default-regular); + } +} diff --git a/components/ai_chat/resources/page/components/header/index.tsx b/components/ai_chat/resources/page/components/header/index.tsx index c2acf2e09373..00823f20dec7 100644 --- a/components/ai_chat/resources/page/components/header/index.tsx +++ b/components/ai_chat/resources/page/components/header/index.tsx @@ -11,7 +11,7 @@ import getAPI from '../../api' import FeatureButtonMenu, { Props as FeatureButtonMenuProps } from '../feature_button_menu' import styles from './style.module.scss' import { useAIChat, useIsSmall } from '../../state/ai_chat_context' -import { useConversation } from '../../state/conversation_context' +import { useConversation, useSupportsAttachments } from '../../state/conversation_context' import { getLocale } from '$web-common/locale' import { tabAssociatedChatId, useActiveChat } from '../../state/active_chat_context' @@ -42,6 +42,8 @@ export const ConversationHeader = React.forwardRef(function (props: FeatureButto const activeConversation = aiChatContext.visibleConversations.find(c => c.uuid === conversationContext.conversationUuid) const showTitle = (!isTabAssociated || aiChatContext.isStandalone) && !isMobile const canShowFullScreenButton = aiChatContext.isHistoryFeatureEnabled && !isMobile && !aiChatContext.isStandalone && conversationContext.conversationUuid + const supportsAttachments = useSupportsAttachments() + return (
{showTitle ? ( @@ -97,6 +99,15 @@ export const ConversationHeader = React.forwardRef(function (props: FeatureButto > } + {supportsAttachments && } {!aiChatContext.isStandalone && -
+ className={classnames({ + [styles.conversationsListHeader]: true, + })} + > + + @@ -223,126 +226,131 @@ function Main() { -
- -
+
- {aiChatContext.hasAcceptedAgreement && ( - <> - + +
+ {aiChatContext.hasAcceptedAgreement && ( + <> + + + {conversationContext.associatedContentInfo?.isContentAssociationPossible && conversationContext.shouldSendPageContents && ( +
+ +
+ )} - {conversationContext.associatedContentInfo?.isContentAssociationPossible && conversationContext.shouldSendPageContents && ( -
- +
+ {!!conversationContext.conversationUuid && + + }
- )} -
- {!!conversationContext.conversationUuid && - + {conversationContext.isFeedbackFormVisible && +
+ +
} -
- - {conversationContext.isFeedbackFormVisible && -
- -
- } - {showSuggestions && ( -
-
- {conversationContext.suggestedQuestions.map((question, i) => )} - {SUGGESTION_STATUS_SHOW_BUTTON.has( - conversationContext.suggestionStatus - ) && conversationContext.shouldSendPageContents && ( - - )} -
+ {showSuggestions && ( +
+
+ {conversationContext.suggestedQuestions.map((question, i) => )} + {SUGGESTION_STATUS_SHOW_BUTTON.has( + conversationContext.suggestionStatus + ) && conversationContext.shouldSendPageContents && ( + + )} +
+
+ )} + + )} + {currentErrorElement && ( +
{currentErrorElement}
+ )} + {shouldShowStorageNotice && ( +
+
)} - - )} - {currentErrorElement && ( -
{currentErrorElement}
- )} - {shouldShowStorageNotice && ( -
- -
- )} - {shouldShowPremiumSuggestionForModel && ( -
- conversationContext.switchToBasicModel()} - > - {getLocale('switchToBasicModelButtonLabel')} - - } - /> -
- )} - {shouldShowPremiumSuggestionStandalone && ( -
- aiChatContext.dismissPremiumPrompt()} - > - {getLocale('dismissButtonLabel')} - - } - /> -
- )} - {aiChatContext.isPremiumUserDisconnected && (!conversationContext.currentModel || isLeoModel(conversationContext.currentModel)) && ( -
- -
- )} - {conversationContext.shouldShowLongConversationInfo && ( -
- + {shouldShowPremiumSuggestionForModel && ( +
+ conversationContext.switchToBasicModel()} + > + {getLocale('switchToBasicModelButtonLabel')} + + } + /> +
+ )} + {shouldShowPremiumSuggestionStandalone && ( +
+ aiChatContext.dismissPremiumPrompt()} + > + {getLocale('dismissButtonLabel')} + + } + /> +
+ )} + {aiChatContext.isPremiumUserDisconnected && (!conversationContext.currentModel || isLeoModel(conversationContext.currentModel)) && ( +
+ +
+ )} + {conversationContext.shouldShowLongConversationInfo && ( +
+ +
+ )} + {!aiChatContext.hasAcceptedAgreement && !conversationContext.conversationHistory.length && ( + + )} +
+
+ {showAttachments &&
+ +
} +
+ {showContextToggle && ( +
+
)} - {!aiChatContext.hasAcceptedAgreement && !conversationContext.conversationHistory.length && ( - - )} + + +
-
- {showContextToggle && ( -
- -
- )} - - - -
) } diff --git a/components/ai_chat/resources/page/components/main/style.module.scss b/components/ai_chat/resources/page/components/main/style.module.scss index e5d121533923..56dfe84b79c7 100644 --- a/components/ai_chat/resources/page/components/main/style.module.scss +++ b/components/ai_chat/resources/page/components/main/style.module.scss @@ -30,6 +30,27 @@ margin-top: var(--conversation-content-margin); } +.mainContent { + &:has(.attachmentsContainer) { + display: grid; + grid-template-columns: 1fr 400px; + grid-template-rows: auto min-content; + gap: var(--leo-spacing-m); + + height: 100%; + width: 100%; + } + + display: contents; +} + +.attachmentsContainer { + margin: var(--leo-spacing-m); + align-self: start; + position: sticky; + top: 0; +} + .scroller { position: relative; --scrollbar-width: 6px; diff --git a/components/ai_chat/resources/page/state/ai_chat_context.tsx b/components/ai_chat/resources/page/state/ai_chat_context.tsx index 05404df1965e..646fe64d8f16 100644 --- a/components/ai_chat/resources/page/state/ai_chat_context.tsx +++ b/components/ai_chat/resources/page/state/ai_chat_context.tsx @@ -89,7 +89,7 @@ export function AIChatContextProvider(props: React.PropsWithChildren setShowSidebar(s => !s), - conversationEntriesComponent: props.conversationEntriesComponent + conversationEntriesComponent: props.conversationEntriesComponent, } return ( diff --git a/components/ai_chat/resources/page/state/conversation_context.tsx b/components/ai_chat/resources/page/state/conversation_context.tsx index 4c290c43c87c..ebf7b07458de 100644 --- a/components/ai_chat/resources/page/state/conversation_context.tsx +++ b/components/ai_chat/resources/page/state/conversation_context.tsx @@ -59,6 +59,9 @@ export type ConversationContext = SendFeedbackState & CharCountContext & { setIsToolsMenuOpen: (isOpen: boolean) => void handleVoiceRecognition?: () => void conversationHandler?: Mojom.ConversationHandlerRemote + + showAttachments: boolean + setShowAttachments: (show: boolean) => void } export const defaultCharCountContext: CharCountContext = { @@ -96,6 +99,8 @@ const defaultContext: ConversationContext = { resetSelectedActionType: () => { }, handleActionTypeClick: () => { }, setIsToolsMenuOpen: () => { }, + showAttachments: false, + setShowAttachments: () => { }, ...defaultSendFeedbackState, ...defaultCharCountContext } @@ -153,7 +158,15 @@ export const ConversationReactContext = export function ConversationContextProvider(props: React.PropsWithChildren) { const [context, setContext] = - React.useState(defaultContext) + React.useState({ + ...defaultContext, + setShowAttachments: (showAttachments: boolean) => { + setContext((value) => ({ + ...value, + showAttachments + })) + } + }) const aiChatContext = useAIChat() const { conversationHandler, callbackRouter, selectedConversationId, updateSelectedConversationId } = useActiveChat() @@ -171,6 +184,10 @@ export function ConversationContextProvider(props: React.PropsWithChildren) { })) } + React.useEffect(() => { + context.setShowAttachments(!!aiChatContext.isStandalone && !aiChatContext.visibleConversations.some(c => c.uuid === context.conversationUuid)) + }, [context.conversationUuid]) + const getModelContext = ( currentModelKey: string, allModels: Mojom.Model[] @@ -522,3 +539,10 @@ export function ConversationContextProvider(props: React.PropsWithChildren) { export function useConversation() { return React.useContext(ConversationReactContext) } + +export function useSupportsAttachments() { + const aiChatContext = useAIChat() + const conversationContext = useConversation() + return aiChatContext.isStandalone + && !aiChatContext.visibleConversations.find(c => c.uuid === conversationContext.conversationUuid) +} diff --git a/components/resources/ai_chat_ui_strings.grdp b/components/resources/ai_chat_ui_strings.grdp index 7c7b4ec9e87c..dd915b8caa9d 100644 --- a/components/resources/ai_chat_ui_strings.grdp +++ b/components/resources/ai_chat_ui_strings.grdp @@ -279,6 +279,15 @@ Close this notice + + Attachments + + + Add your browser tabs to give Leo more context for the conversation + + + Browser tabs + Leo will now remember your conversations so you can go back to them. They are stored encrypted on your device, and you can delete them any time. diff --git a/ui/webui/resources/BUILD.gn b/ui/webui/resources/BUILD.gn index 3caa87bbaaf8..e815f3b176af 100644 --- a/ui/webui/resources/BUILD.gn +++ b/ui/webui/resources/BUILD.gn @@ -138,6 +138,7 @@ leo_icons_base_path = "../../../node_modules/@brave/leo/icons" # your needs here: https://leo.bravesoftware.com/?path=/story/icon--all-icons leo_icons = [ "activity.svg", + "attachment.svg", "appearance.svg", "arrow-diagonal-down-right.svg", "arrow-diagonal-up-right.svg",