Skip to content

Commit

Permalink
AI Chat conversations list item menu and edit improvements
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
petemill committed Dec 13, 2024
1 parent a3df109 commit 2447933
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 218 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 (
<div
className={styles.displayTitle}
onMouseEnter={() => setIsButtonMenuVisible(true)}
onMouseLeave={() => setIsButtonMenuVisible(false)}
>
<div className={styles.displayTitleContent}>
<li>
<a
className={classnames(
styles.navItem,
isActive && styles.navItemActive,
isOptionsMenuOpen && styles.isOptionsMenuOpen
)}
onClick={(e) => {
if (isEditing) {
e.preventDefault()
return
}
props.setIsConversationsListOpen?.(false)
}}
onDoubleClick={() => aiChatContext.setEditingConversationId(uuid)}
href={`/${uuid}`}
>
<div
className={styles.text}
onDoubleClick={props.onEditTitle}
title={props.title}
className={styles.displayTitle}
>
{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' />
</div>
<leo-menu-item onClick={props.onEditTitle}>
<div className={styles.optionsMenuItemWithIcon}>
<Icon name='edit-pencil' />
<div>{getLocale('menuRenameConversation')}</div>
<div className={styles.displayTitleContent}>
<div
className={styles.text}
title={title}
>
{title}
</div>
</leo-menu-item>
<leo-menu-item onClick={props.onDelete}>
<div className={styles.optionsMenuItemWithIcon}>
<Icon name='trash' />
<div>{getLocale('menuDeleteConversation')}</div>
</div>
<ButtonMenu
className={styles.optionsMenu}
onChange={handleButtonMenuChange}
>
<div
slot='anchor-content'
className={styles.optionsButton}
>
<Icon name='more-vertical' />
</div>
</leo-menu-item>
</ButtonMenu>
)}
</div>
<leo-menu-item onClick={handleEditTitle}>
<div className={styles.optionsMenuItemWithIcon}>
<Icon name='edit-pencil' />
<div>{getLocale('menuRenameConversation')}</div>
</div>
</leo-menu-item>
<leo-menu-item onClick={handleDelete}>
<div className={styles.optionsMenuItemWithIcon}>
<Icon name='trash' />
<div>{getLocale('menuDeleteConversation')}</div>
</div>
</leo-menu-item>
</ButtonMenu>
</div>
{uuid === aiChatContext.editingConversationId && (
<div className={styles.editibleTitle}>
<SimpleInput
text={title}
onBlur={() => aiChatContext.setEditingConversationId(null)}
onSubmit={(value) => {
aiChatContext.setEditingConversationId(null)
getAPI().service.renameConversation(uuid, value)
}}
/>
</div>
)}
</a>
</li>
)
}

Expand All @@ -112,7 +161,6 @@ interface ConversationsListProps {

export default function ConversationsList(props: ConversationsListProps) {
const aiChatContext = useAIChat()
const conversationContext = useConversation()

return (
<>
Expand All @@ -139,42 +187,13 @@ export default function ConversationsList(props: ConversationsListProps) {
}
{aiChatContext.visibleConversations.length > 0 &&
<ol>
{aiChatContext.visibleConversations.map(item => {
return (
<li key={item.uuid}>
<a
className={classnames({
[styles.navItem]: true,
[styles.navItemActive]: item.uuid === conversationContext.conversationUuid
})}
onClick={() => {
props.setIsConversationsListOpen?.(false)
}}
href={`/${item.uuid}`}
>
{item.uuid === aiChatContext.editingConversationId ? (
<div className={styles.editibleTitle}>
<SimpleInput
text={item.title}
onBlur={() => aiChatContext.setEditingConversationId(null)}
onSubmit={(value) => {
aiChatContext.setEditingConversationId(null)
getAPI().service.renameConversation(item.uuid, value)
}}
/>
</div>
) : (
<DisplayTitle
title={item.title || getLocale('conversationListUntitled')}
description=''
onEditTitle={() => aiChatContext.setEditingConversationId(item.uuid)}
onDelete={() => getAPI().service.deleteConversation(item.uuid)}
/>
)}
</a>
</li>
)
})}
{aiChatContext.visibleConversations.map(conversation =>
<ConversationItem
key={conversation.uuid}
{...props}
conversation={conversation}
/>
)}
</ol>
}
</nav>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 2447933

Please sign in to comment.