fix: adjust Navbar and Chat components for better layout and responsiveness

- Updated Navbar styles to improve margin handling for macOS.
- Refactored Chat component to streamline layout and enhance responsiveness, including adjustments to main height calculations and navbar integration.
- Cleaned up commented code and improved the structure of the ChatNavbar for better clarity and maintainability.
- Enhanced styling in various components for consistent appearance and behavior across different screen sizes.
This commit is contained in:
kangfenmao 2025-10-18 00:12:38 +08:00
parent 8470e252d6
commit d4b1db0407
18 changed files with 343 additions and 300 deletions

View File

@ -67,14 +67,14 @@ const NavbarContainer = styled.div<{ $isFullScreen: boolean }>`
flex-direction: row;
min-height: ${isMac ? 'env(titlebar-area-height)' : 'var(--navbar-height)'};
max-height: var(--navbar-height);
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1 + 2px)' : 0};
padding-left: ${({ $isFullScreen }) =>
isMac ? ($isFullScreen ? 'var(--sidebar-width)' : 'env(titlebar-area-x)') : 0};
-webkit-app-region: drag;
`
const NavbarLeftContainer = styled.div`
min-width: ${isMac ? 'calc(var(--assistants-width) - 20px)' : 'var(--assistants-width)'};
/* min-width: ${isMac ? 'calc(var(--assistants-width) - 20px)' : 'var(--assistants-width)'}; */
padding: 0 10px;
display: flex;
flex-direction: row;

View File

@ -140,9 +140,7 @@ const Chat: FC<Props> = (props) => {
firstUpdateOrNoFirstUpdateHandler()
}
const mainHeight = isTopNavbar
? 'calc(100vh - var(--navbar-height) - var(--navbar-height) - 12px)'
: 'calc(100vh - var(--navbar-height))'
const mainHeight = isTopNavbar ? 'calc(100vh - var(--navbar-height) - 6px)' : 'calc(100vh - var(--navbar-height))'
const SessionMessages = useMemo(() => {
if (activeAgentId === null) {
@ -192,66 +190,84 @@ const Chat: FC<Props> = (props) => {
</div>
)
}, [])
return (
<Container id="chat" className={classNames([messageStyle, { 'multi-select-mode': isMultiSelectMode }])}>
{isTopNavbar && (
<ChatNavbar
activeAssistant={props.assistant}
activeTopic={props.activeTopic}
setActiveTopic={props.setActiveTopic}
setActiveAssistant={props.setActiveAssistant}
position="left"
/>
)}
<HStack>
<Main
ref={mainRef}
id="chat-main"
vertical
flex={1}
justify="space-between"
style={{ maxWidth: chatMaxWidth, height: mainHeight }}>
<QuickPanelProvider>
{activeTopicOrSession === 'topic' && (
<>
<Messages
key={props.activeTopic.id}
assistant={assistant}
topic={props.activeTopic}
setActiveTopic={props.setActiveTopic}
onComponentUpdate={messagesComponentUpdateHandler}
onFirstUpdate={messagesComponentFirstUpdateHandler}
/>
<ContentSearch
ref={contentSearchRef}
searchTarget={mainRef as React.RefObject<HTMLElement>}
filter={contentSearchFilter}
includeUser={filterIncludeUser}
onIncludeUserChange={userOutlinedItemClickHandler}
/>
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
</>
)}
{activeTopicOrSession === 'session' && !activeAgentId && <AgentInvalid />}
{activeTopicOrSession === 'session' && activeAgentId && !activeSessionId && <SessionInvalid />}
{activeTopicOrSession === 'session' && activeAgentId && activeSessionId && (
<>
<SessionMessages />
<SessionInputBar />
</>
)}
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
</QuickPanelProvider>
</Main>
<motion.div
animate={{
marginRight: topicPosition === 'right' && showTopics ? 'var(--assistants-width)' : 0
}}
transition={{ duration: 0.3, ease: 'easeInOut' }}
style={{ flex: 1, display: 'flex', minWidth: 0 }}>
<Main
ref={mainRef}
id="chat-main"
vertical
flex={1}
justify="space-between"
style={{ maxWidth: chatMaxWidth, height: mainHeight }}>
<QuickPanelProvider>
<ChatNavbar
activeAssistant={props.assistant}
activeTopic={props.activeTopic}
setActiveTopic={props.setActiveTopic}
setActiveAssistant={props.setActiveAssistant}
position="left"
/>
<div
className="flex flex-1 flex-col justify-between"
style={{ height: `calc(${mainHeight} - var(--navbar-height))` }}>
{activeTopicOrSession === 'topic' && (
<>
<Messages
key={props.activeTopic.id}
assistant={assistant}
topic={props.activeTopic}
setActiveTopic={props.setActiveTopic}
onComponentUpdate={messagesComponentUpdateHandler}
onFirstUpdate={messagesComponentFirstUpdateHandler}
/>
<ContentSearch
ref={contentSearchRef}
searchTarget={mainRef as React.RefObject<HTMLElement>}
filter={contentSearchFilter}
includeUser={filterIncludeUser}
onIncludeUserChange={userOutlinedItemClickHandler}
/>
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
</>
)}
{activeTopicOrSession === 'session' && !activeAgentId && <AgentInvalid />}
{activeTopicOrSession === 'session' && activeAgentId && !activeSessionId && <SessionInvalid />}
{activeTopicOrSession === 'session' && activeAgentId && activeSessionId && (
<>
<SessionMessages />
<SessionInputBar />
</>
)}
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
</div>
</QuickPanelProvider>
</Main>
</motion.div>
<AnimatePresence initial={false}>
{topicPosition === 'right' && showTopics && (
<motion.div
initial={{ width: 0, opacity: 0 }}
animate={{ width: 'auto', opacity: 1 }}
exit={{ width: 0, opacity: 0 }}
key="right-tabs"
initial={{ x: 'var(--assistants-width)' }}
animate={{ x: 0 }}
exit={{ x: 'var(--assistants-width)' }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
style={{ overflow: 'hidden' }}>
style={{
position: 'absolute',
right: 0,
top: isTopNavbar ? 0 : 'calc(var(--navbar-height) + 1px)',
width: 'var(--assistants-width)',
height: '100%',
zIndex: 10
}}>
<Tabs
activeAssistant={assistant}
activeTopic={props.activeTopic}
@ -269,13 +285,14 @@ const Chat: FC<Props> = (props) => {
export const useChatMaxWidth = () => {
const { showTopics, topicPosition } = useSettings()
const { isLeftNavbar } = useNavbarPosition()
const { isLeftNavbar, isTopNavbar } = useNavbarPosition()
const { showAssistants } = useShowAssistants()
const showRightTopics = showTopics && topicPosition === 'right'
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
const minusBorderWidth = isTopNavbar ? (showTopics ? '- 12px' : '- 6px') : ''
const sidebarWidth = isLeftNavbar ? '- var(--sidebar-width)' : ''
return `calc(100vw ${sidebarWidth} ${minusAssistantsWidth} ${minusRightTopicsWidth})`
return `calc(100vw ${sidebarWidth} ${minusAssistantsWidth} ${minusRightTopicsWidth} ${minusBorderWidth})`
}
const Container = styled.div`

View File

@ -3,7 +3,7 @@ import { HStack } from '@renderer/components/Layout'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
@ -34,6 +34,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { topicPosition, narrowMode } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics()
const { isTopNavbar } = useNavbarPosition()
const dispatch = useAppDispatch()
useShortcut('toggle_show_assistants', toggleShowAssistants)
@ -73,16 +74,16 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
// )
return (
<NavbarHeader className="home-navbar">
<div className="flex min-w-0 flex-1 shrink items-center overflow-auto">
{showAssistants && (
<NavbarHeader className="home-navbar" style={{ height: 'var(--navbar-height)' }}>
<div className="flex h-full min-w-0 flex-1 shrink items-center overflow-auto">
{isTopNavbar && showAssistants && (
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={toggleShowAssistants}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
)}
{!showAssistants && (
{isTopNavbar && !showAssistants && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => toggleShowAssistants()} style={{ marginRight: 8 }}>
<PanelRightClose size={18} />
@ -90,13 +91,13 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
</Tooltip>
)}
<AnimatePresence initial={false}>
{!showAssistants && (
{!showAssistants && isTopNavbar && (
<motion.div
initial={{ width: 0, opacity: 0 }}
animate={{ width: 'auto', opacity: 1 }}
exit={{ width: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}>
<NavbarIcon onClick={onShowAssistantsDrawer} style={{ marginRight: 8 }}>
<NavbarIcon onClick={onShowAssistantsDrawer} style={{ marginRight: 5 }}>
<Menu size={18} />
</NavbarIcon>
</motion.div>
@ -105,25 +106,29 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
<ChatNavbarContent assistant={assistant} />
</div>
<HStack alignItems="center" gap={8}>
<UpdateAppButton />
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={handleNarrowModeToggle}>
<i className="iconfont icon-icon-adaptive-width"></i>
</NarrowIcon>
</Tooltip>
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NavbarIcon>
</Tooltip>
{topicPosition === 'right' && !showTopics && (
{isTopNavbar && <UpdateAppButton />}
{isTopNavbar && (
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={handleNarrowModeToggle}>
<i className="iconfont icon-icon-adaptive-width"></i>
</NarrowIcon>
</Tooltip>
)}
{isTopNavbar && (
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NavbarIcon>
</Tooltip>
)}
{isTopNavbar && topicPosition === 'right' && !showTopics && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={toggleShowTopics}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
)}
{topicPosition === 'right' && showTopics && (
{isTopNavbar && topicPosition === 'right' && showTopics && (
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={toggleShowTopics}>
<PanelRightClose size={18} />

View File

@ -1,14 +1,11 @@
import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isLinux, isMac, isWin } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { isLinux, isWin } from '@renderer/config/constant'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { useChatMaxWidth } from '@renderer/pages/home/Chat'
import ChatNavbarContent from '@renderer/pages/home/components/ChatNavbarContent'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import { setNarrowMode } from '@renderer/store/settings'
@ -17,11 +14,10 @@ import { Tooltip } from 'antd'
import { t } from 'i18next'
import { Menu, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
import { AnimatePresence, motion } from 'motion/react'
import React, { FC } from 'react'
import { FC } from 'react'
import styled from 'styled-components'
import AssistantsDrawer from './components/AssistantsDrawer'
import SelectModelButton from './components/SelectModelButton'
import UpdateAppButton from './components/UpdateAppButton'
interface Props {
@ -40,11 +36,9 @@ const HeaderNavbar: FC<Props> = ({
setActiveTopic,
activeTopicOrSession
}) => {
const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { topicPosition, narrowMode } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics()
const chatMaxWidth = useChatMaxWidth()
const dispatch = useAppDispatch()
useShortcut('toggle_show_assistants', toggleShowAssistants)
@ -101,7 +95,7 @@ const HeaderNavbar: FC<Props> = ({
justifyContent: 'flex-start',
borderRight: 'none',
paddingLeft: 0,
paddingRight: 10,
paddingRight: 0,
minWidth: 'auto'
}}>
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
@ -123,22 +117,7 @@ const HeaderNavbar: FC<Props> = ({
</AnimatePresence>
</NavbarLeft>
)}
<NavbarCenter>
{activeTopicOrSession === 'topic' ? (
<HStack alignItems="center" gap={6} ml={!isMac ? 16 : 0}>
<SelectModelButton assistant={assistant} />
</HStack>
) : (
<ChatNavbarContainer
style={{
maxWidth: chatMaxWidth,
marginLeft: !isMac ? 16 : 0
}}>
<ChatNavbarContent assistant={assistant} />
</ChatNavbarContainer>
)}
</NavbarCenter>
<NavbarCenter></NavbarCenter>
<NavbarRight
style={{
justifyContent: 'flex-end',
@ -220,15 +199,4 @@ const NarrowIcon = styled(NavbarIcon)`
}
`
const ChatNavbarContainer: React.FC<{ children: React.ReactNode; style?: React.CSSProperties }> = ({
children,
style
}) => {
return (
<div className="nodrag flex min-w-0 flex-1 items-center justify-start gap-1.5 overflow-hidden" style={style}>
{children}
</div>
)
}
export default HeaderNavbar

View File

@ -180,7 +180,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
const Container = styled(Scrollbar)`
display: flex;
flex-direction: column;
padding: 10px;
padding: 12px 10px;
`
export default AssistantsTab

View File

@ -30,7 +30,7 @@ const SessionSettingsTab: FC<Props> = ({ session, update }) => {
return (
<div className="w-[var(--assistants-width)] p-2 px-3 pt-4">
<EssentialSettings agentBase={session} update={update} />
<EssentialSettings agentBase={session} update={update} showModelSetting={false} />
<AdvancedSettings agentBase={session} update={update} />
<Divider className="my-2" />
<Button size="sm" fullWidth onPress={onMoreSetting}>

View File

@ -14,7 +14,6 @@ const SessionsTab: FC<SessionsTabProps> = () => {
const { activeAgentId } = chat
const { t } = useTranslation()
const { apiServer } = useSettings()
const { topicPosition, navbarPosition } = useSettings()
if (!apiServer.enabled) {
return (
@ -34,15 +33,7 @@ const SessionsTab: FC<SessionsTabProps> = () => {
return (
<AnimatePresence mode="wait">
<motion.div
initial={{ width: 0, opacity: 0 }}
animate={{ width: 'var(--assistants-width)', opacity: 1 }}
exit={{ width: 0, opacity: 0 }}
transition={{ duration: 0.5, ease: 'easeInOut' }}
className={cn(
'overflow-hidden',
topicPosition === 'right' && navbarPosition === 'top' ? 'rounded-l-2xl border-t border-b border-l' : undefined
)}>
<motion.div className={cn('overflow-hidden', 'h-full')}>
<Sessions agentId={activeAgentId} />
</motion.div>
</AnimatePresence>

View File

@ -44,9 +44,16 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) =
<AgentLabel agent={agent} />
</AgentNameWrapper>
</AssistantNameRow>
<MenuButton>
{isActive ? <SessionCount>{sessions.length}</SessionCount> : <Bot size={14} className="text-primary" />}
</MenuButton>
{isActive && (
<MenuButton>
<SessionCount>{sessions.length}</SessionCount>
</MenuButton>
)}
{!isActive && (
<BotIcon>
<Bot size={16} className="text-primary" />
</BotIcon>
)}
</Container>
</ContextMenuTrigger>
<ContextMenuContent>
@ -110,6 +117,16 @@ export const MenuButton: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ cla
/>
)
export const BotIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
<div
className={cn(
'absolute top-[8px] right-[12px] flex flex-row items-center justify-center rounded-full text-[14px] text-[var(--color-text)]',
className
)}
{...props}
/>
)
export const SessionCount: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
<div
className={cn(

View File

@ -1,4 +1,3 @@
import { Button, cn, Input, Tooltip } from '@heroui/react'
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
import { isMac } from '@renderer/config/constant'
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
@ -11,9 +10,19 @@ import { SessionLabel } from '@renderer/pages/settings/AgentSettings/shared'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { newMessagesActions } from '@renderer/store/newMessage'
import { AgentSessionEntity } from '@renderer/types'
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@renderer/ui/context-menu'
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger
} from '@renderer/ui/context-menu'
import { classNames } from '@renderer/utils'
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
import { XIcon } from 'lucide-react'
import { Tooltip } from 'antd'
import { MenuIcon, XIcon } from 'lucide-react'
import React, { FC, memo, startTransition, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -24,13 +33,11 @@ interface SessionItemProps {
session: AgentSessionEntity
// use external agentId as SSOT, instead of session.agent_id
agentId: string
isDisabled?: boolean
isLoading?: boolean
onDelete: () => void
onPress: () => void
}
const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoading, onDelete, onPress }) => {
const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress }) => {
const { t } = useTranslation()
const { chat } = useRuntime()
const { updateSession } = useUpdateSession(agentId)
@ -50,16 +57,16 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoa
const DeleteButton = () => {
return (
<Tooltip
content={t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })}
classNames={{ content: 'text-xs' }}
delay={500}
closeDelay={0}>
<div
role="button"
className={cn(
'mr-2 flex aspect-square h-6 w-6 items-center justify-center rounded-2xl',
isConfirmingDeletion ? 'hover:bg-danger-100' : 'hover:bg-foreground-300'
)}
placement="bottom"
mouseEnterDelay={0.7}
mouseLeaveDelay={0}
title={
<div style={{ fontSize: '12px', opacity: 0.8, fontStyle: 'italic' }}>
{t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })}
</div>
}>
<MenuButton
className="menu"
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
if (isConfirmingDeletion || e.ctrlKey || e.metaKey) {
@ -78,17 +85,11 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoa
}
}}>
{isConfirmingDeletion ? (
<DeleteIcon
size={14}
className="opacity-0 transition-colors-opacity group-hover:text-danger group-hover:opacity-100"
/>
<DeleteIcon size={14} color="var(--color-error)" style={{ pointerEvents: 'none' }} />
) : (
<XIcon
size={14}
className={cn(isActive ? 'opacity-100' : 'opacity-0', 'group-hover:opacity-100', 'transition-opacity')}
/>
<XIcon size={14} color="var(--color-text-3)" style={{ pointerEvents: 'none' }} />
)}
</div>
</MenuButton>
</Tooltip>
)
}
@ -106,44 +107,44 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoa
}
}, [activeSessionId, dispatch, isFulfilled, session.id, sessionTopicId])
const { topicPosition, setTopicPosition } = useSettings()
const singlealone = topicPosition === 'right'
return (
<>
<ContextMenu modal={false}>
<ContextMenuTrigger>
<ButtonContainer
isDisabled={isDisabled}
isLoading={isLoading}
onPress={onPress}
isActive={isActive}
<SessionListItem
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
onClick={isEditing ? undefined : onPress}
onDoubleClick={() => startEdit(session.name ?? '')}
className="group">
<SessionLabelContainer className="name h-full w-full pl-1" title={session.name ?? session.id}>
{isPending && !isActive && <PendingIndicator />}
{isFulfilled && !isActive && <FulfilledIndicator />}
{isEditing && (
<Input
title={session.name ?? session.id}
style={{
borderRadius: 'var(--list-item-border-radius)',
cursor: isEditing ? 'default' : 'pointer'
}}>
{isPending && !isActive && <PendingIndicator />}
{isFulfilled && !isActive && <FulfilledIndicator />}
<SessionNameContainer>
{isEditing ? (
<SessionEditInput
ref={inputRef}
variant="bordered"
value={editValue}
onValueChange={handleValueChange}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleValueChange(e.target.value)}
onKeyDown={handleKeyDown}
onClick={(e) => e.stopPropagation()}
classNames={{
base: 'h-full',
mainWrapper: 'h-full',
inputWrapper: 'h-full min-h-0 px-1.5',
input: isSaving ? 'brightness-50' : undefined
}}
onClick={(e: React.MouseEvent) => e.stopPropagation()}
style={{ opacity: isSaving ? 0.5 : 1 }}
/>
)}
{!isEditing && (
<div className="flex w-full items-center justify-between">
<SessionLabel session={session} />
) : (
<>
<SessionName>
<SessionLabel session={session} />
</SessionName>
<DeleteButton />
</div>
</>
)}
</SessionLabelContainer>
</ButtonContainer>
</SessionNameContainer>
</SessionListItem>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
@ -157,6 +158,20 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoa
<EditIcon size={14} />
{t('common.edit')}
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger className="gap-2">
<MenuIcon size={14} />
{t('settings.topic.position.label')}
</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem key="left" onClick={() => setTopicPosition('left')}>
{t('settings.topic.position.left')}
</ContextMenuItem>
<ContextMenuItem key="right" onClick={() => setTopicPosition('right')}>
{t('settings.topic.position.right')}
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem
key="delete"
className="text-danger"
@ -172,38 +187,96 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoa
)
}
const ButtonContainer: React.FC<React.ComponentProps<typeof Button> & { isActive?: boolean }> = ({
isActive,
className,
children,
...props
}) => {
const { topicPosition } = useSettings()
const activeBg = topicPosition === 'left' ? 'bg-[var(--color-list-item)]' : 'bg-foreground-100'
return (
<Button
{...props}
variant="light"
className={cn(
'relative mb-2 flex h-9 flex-row justify-between p-0',
'rounded-[var(--list-item-border-radius)]',
'border-[0.5px] border-transparent',
'w-[calc(var(--assistants-width)_-_20px)]',
'cursor-pointer',
isActive ? cn(activeBg, 'shadow-sm') : undefined,
className
)}>
{children}
</Button>
)
}
const SessionListItem = styled.div`
padding: 7px 12px;
border-radius: var(--list-item-border-radius);
font-size: 13px;
display: flex;
flex-direction: column;
justify-content: space-between;
cursor: pointer;
width: calc(var(--assistants-width) - 20px);
margin-bottom: 8px;
const SessionLabelContainer: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
<div
{...props}
className={cn('text-[13px] text-[var(--color-text)]', 'flex flex-row items-center gap-2', className)}
/>
)
.menu {
opacity: 0;
color: var(--color-text-3);
}
&:hover {
background-color: var(--color-list-item-hover);
transition: background-color 0.1s;
.menu {
opacity: 1;
}
}
&.active {
background-color: var(--color-list-item);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
.menu {
opacity: 1;
&:hover {
color: var(--color-text-2);
}
}
}
&.singlealone {
border-radius: 0 !important;
&:hover {
background-color: var(--color-background-soft);
}
&.active {
border-left: 2px solid var(--color-primary);
box-shadow: none;
}
}
`
const SessionNameContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
height: 20px;
justify-content: space-between;
`
const SessionName = styled.div`
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: 13px;
position: relative;
`
const SessionEditInput = styled.input`
background: var(--color-background);
border: none;
color: var(--color-text-1);
font-size: 13px;
font-family: inherit;
padding: 2px 6px;
width: 100%;
outline: none;
padding: 0;
`
const MenuButton = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
min-width: 20px;
min-height: 20px;
.anticon {
font-size: 12px;
}
`
const PendingIndicator = styled.div.attrs({
className: 'animation-pulse'

View File

@ -12,7 +12,7 @@ import {
} from '@renderer/store/runtime'
import { CreateSessionForm } from '@renderer/types'
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
import { AnimatePresence, motion } from 'framer-motion'
import { motion } from 'framer-motion'
import { memo, useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
@ -30,7 +30,7 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
const { agent } = useAgent(agentId)
const { sessions, isLoading, error, deleteSession, createSession } = useSessions(agentId)
const { chat } = useRuntime()
const { activeSessionIdMap, sessionWaiting } = chat
const { activeSessionIdMap } = chat
const dispatch = useAppDispatch()
const setActiveSessionId = useCallback(
@ -109,45 +109,30 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
if (error) return <Alert color="danger" content={t('agent.session.get.error.failed')} />
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
className="sessions-tab flex h-full w-full flex-col p-2">
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}>
<AddButton onPress={handleCreateSession} className="mb-2">
{t('agent.session.add.title')}
</AddButton>
</motion.div>
<AnimatePresence>
{/* h-9 */}
<DynamicVirtualList
list={sessions}
estimateSize={() => 9 * 4}
scrollerStyle={{
// FIXME: This component only supports CSSProperties
overflowX: 'hidden'
}}
autoHideScrollbar>
{(session) => (
<motion.div
key={session.id}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}>
<SessionItem
session={session}
agentId={agentId}
isDisabled={sessionWaiting[session.id]}
isLoading={sessionWaiting[session.id]}
onDelete={() => handleDeleteSession(session.id)}
onPress={() => setActiveSessionId(agentId, session.id)}
/>
</motion.div>
)}
</DynamicVirtualList>
</AnimatePresence>
</motion.div>
<div className="sessions-tab flex h-full w-full flex-col p-2">
<AddButton onPress={handleCreateSession} className="mb-2">
{t('agent.session.add.title')}
</AddButton>
{/* h-9 */}
<DynamicVirtualList
list={sessions}
estimateSize={() => 9 * 4}
scrollerStyle={{
// FIXME: This component only supports CSSProperties
overflowX: 'hidden'
}}
autoHideScrollbar>
{(session) => (
<SessionItem
key={session.id}
session={session}
agentId={agentId}
onDelete={() => handleDeleteSession(session.id)}
onPress={() => setActiveSessionId(agentId, session.id)}
/>
)}
</DynamicVirtualList>
</div>
)
}

View File

@ -189,10 +189,7 @@ const Container = styled.div`
background-color: var(--color-background);
}
[navbar-position='top'] & {
height: calc(100vh - var(--navbar-height) - 12px);
&.right {
height: calc(100vh - var(--navbar-height) - var(--navbar-height) - 12px);
}
height: calc(100vh - var(--navbar-height));
}
overflow: hidden;
.collapsed {

View File

@ -1,13 +1,13 @@
import { BreadcrumbItem, Breadcrumbs, Chip, cn } from '@heroui/react'
import { BreadcrumbItem, Breadcrumbs, cn } from '@heroui/react'
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
import { permissionModeCards } from '@renderer/constants/permissionModes'
import { useActiveAgent } from '@renderer/hooks/agents/useActiveAgent'
import { useActiveSession } from '@renderer/hooks/agents/useActiveSession'
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { AgentEntity, AgentSessionEntity, ApiModel, Assistant, PermissionMode } from '@renderer/types'
import { AgentEntity, AgentSessionEntity, ApiModel, Assistant } from '@renderer/types'
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
import { t } from 'i18next'
import { Folder } from 'lucide-react'
import { FC, ReactNode, useCallback } from 'react'
import { AgentSettingsPopup, SessionSettingsPopup } from '../../settings/AgentSettings'
@ -38,24 +38,15 @@ const ChatNavbarContent: FC<Props> = ({ assistant }) => {
<>
{activeTopicOrSession === 'topic' && <SelectModelButton assistant={assistant} />}
{activeTopicOrSession === 'session' && activeAgent && (
<HorizontalScrollContainer>
<Breadcrumbs
classNames={{
base: 'flex',
list: 'flex-nowrap'
}}>
<HorizontalScrollContainer className="ml-2 flex-initial">
<Breadcrumbs classNames={{ base: 'flex', list: 'flex-nowrap' }}>
<BreadcrumbItem
onPress={() => AgentSettingsPopup.show({ agentId: activeAgent.id })}
classNames={{
base: 'self-stretch',
item: 'h-full'
}}>
<Chip size="md" variant="light" className="h-full transition-background hover:bg-foreground-100">
<AgentLabel
agent={activeAgent}
classNames={{ name: 'max-w-40 font-bold text-xs', avatar: 'h-4.5 w-4.5', container: 'gap-1.5' }}
/>
</Chip>
classNames={{ base: 'self-stretch', item: 'h-full' }}>
<AgentLabel
agent={activeAgent}
classNames={{ name: 'max-w-40 text-xs', avatar: 'h-4.5 w-4.5', container: 'gap-1.5' }}
/>
</BreadcrumbItem>
{activeSession && (
<BreadcrumbItem
@ -65,13 +56,8 @@ const ChatNavbarContent: FC<Props> = ({ assistant }) => {
sessionId: activeSession.id
})
}
classNames={{
base: 'self-stretch',
item: 'h-full'
}}>
<Chip size="md" variant="light" className="h-full transition-background hover:bg-foreground-100">
<SessionLabel session={activeSession} className="max-w-40 font-bold text-xs" />
</Chip>
classNames={{ base: 'self-stretch', item: 'h-full' }}>
<SessionLabel session={activeSession} className="max-w-40 text-xs" />
</BreadcrumbItem>
)}
{activeSession && (
@ -97,11 +83,11 @@ const SessionWorkspaceMeta: FC<{ agent: AgentEntity; session: AgentSessionEntity
}
const firstAccessiblePath = session.accessible_paths?.[0]
const permissionMode = (session.configuration?.permission_mode ?? 'default') as PermissionMode
const permissionModeCard = permissionModeCards.find((card) => card.mode === permissionMode)
const permissionModeLabel = permissionModeCard
? t(permissionModeCard.titleKey, permissionModeCard.titleFallback)
: permissionMode
// const permissionMode = (session.configuration?.permission_mode ?? 'default') as PermissionMode
// const permissionModeCard = permissionModeCards.find((card) => card.mode === permissionMode)
// const permissionModeLabel = permissionModeCard
// ? t(permissionModeCard.titleKey, permissionModeCard.titleFallback)
// : permissionMode
const infoItems: ReactNode[] = []
@ -117,12 +103,13 @@ const SessionWorkspaceMeta: FC<{ agent: AgentEntity; session: AgentSessionEntity
}) => (
<div
className={cn(
'rounded-medium border border-default-200 px-2 py-1 text-foreground-500 text-xs dark:text-foreground-400',
'flex items-center gap-1.5 text-foreground-500 text-xs dark:text-foreground-400',
onClick !== undefined ? 'cursor-pointer' : undefined,
className
)}
title={text}
onClick={onClick}>
<Folder className="h-3.5 w-3.5 shrink-0" />
<span className="block truncate">{text}</span>
</div>
)
@ -148,7 +135,7 @@ const SessionWorkspaceMeta: FC<{ agent: AgentEntity; session: AgentSessionEntity
)
}
infoItems.push(<InfoTag key="permission-mode" text={permissionModeLabel} className="max-w-50" />)
// infoItems.push(<InfoTag key="permission-mode" text={permissionModeLabel} className="max-w-50" />)
if (infoItems.length === 0) {
return null

View File

@ -38,12 +38,12 @@ const SelectAgentBaseModelButton: FC<Props> = ({ agentBase: agent, onSelect, isD
<Button
size="sm"
variant="light"
className="nodrag rounded-2xl px-1 py-3"
className="nodrag h-[28px] rounded-2xl px-1"
onPress={onSelectModel}
isDisabled={isDisabled}>
<div className="flex items-center gap-1.5 overflow-x-hidden">
<ModelAvatar model={model ? apiModelAdapter(model) : undefined} size={20} />
<span className="truncate font-medium">
<span className="truncate text-[var(--color-text)]">
{model ? model.name : t('button.select_model')} {providerName ? ' | ' + providerName : ''}
</span>
</div>

View File

@ -87,6 +87,7 @@ const ButtonContent = styled.div`
const ModelName = styled.span`
font-weight: 500;
margin-right: -2px;
font-size: 12px;
`
export default SelectModelButton

View File

@ -104,7 +104,7 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
afterClose={afterClose}
maskClosable={false}
footer={null}
title={<AgentLabel agent={agent} classNames={{ name: 'text-lg font-extrabold' }} />}
title={<AgentLabel agent={agent} />}
transitionName="animation-move-down"
styles={{
content: {

View File

@ -20,13 +20,15 @@ type EssentialSettingsProps =
| {
agentBase: GetAgentResponse | undefined | null
update: ReturnType<typeof useUpdateAgent>['updateAgent']
showModelSetting?: boolean
}
| {
agentBase: GetAgentSessionResponse | undefined | null
update: ReturnType<typeof useUpdateSession>['updateSession']
showModelSetting?: boolean
}
const EssentialSettings: FC<EssentialSettingsProps> = ({ agentBase, update }) => {
const EssentialSettings: FC<EssentialSettingsProps> = ({ agentBase, update, showModelSetting = true }) => {
const { t } = useTranslation()
if (!agentBase) return null
@ -46,7 +48,7 @@ const EssentialSettings: FC<EssentialSettingsProps> = ({ agentBase, update }) =>
)}
{isAgent && <AvatarSetting agent={agentBase} update={update} />}
<NameSetting base={agentBase} update={update} />
<ModelSetting base={agentBase} update={update} />
{showModelSetting && <ModelSetting base={agentBase} update={update} />}
<AccessibleDirsSetting base={agentBase} update={update} />
<DescriptionSetting base={agentBase} update={update} />
</SettingsContainer>

View File

@ -106,7 +106,7 @@ const SessionSettingPopupContainer: React.FC<SessionSettingPopupParams> = ({ tab
afterClose={afterClose}
maskClosable={false}
footer={null}
title={<SessionLabel session={session} className="font-extrabold text-lg" />}
title={<SessionLabel session={session} />}
transitionName="animation-move-down"
styles={{
content: {

View File

@ -36,7 +36,7 @@ export const AgentLabel: React.FC<AgentLabelProps> = ({ agent, classNames }) =>
return (
<div className={cn('flex w-full items-center gap-2 truncate', classNames?.container)}>
<EmojiIcon emoji={emoji || '⭐️'} className={classNames?.avatar} />
<span className={cn('truncate', classNames?.name)}>
<span className={cn('truncate', 'text-[var(--color-text)]', classNames?.name)}>
{agent?.name ?? (agent?.type ? getAgentTypeLabel(agent.type) : '')}
</span>
</div>
@ -52,7 +52,7 @@ export const SessionLabel: React.FC<SessionLabelProps> = ({ session, className }
const displayName = session?.name ?? session?.id
return (
<>
<span className={cn('truncate px-2 text-sm', className)}>{displayName}</span>
<span className={cn('truncate text-[var(--color-text)] text-sm', className)}>{displayName}</span>
</>
)
}