refactor(home/Tabs): restructure tabs components and add section names

- Split AssistantsTab into separate components (Assistants and Agents)
- Add SectionName component for better UI organization
- Remove unused tabs ('agents' and 'sessions') from chat type
- Clean up imports and type definitions
This commit is contained in:
icarus 2025-09-20 19:44:20 +08:00
parent 55645a75cc
commit b43b4b581e
8 changed files with 232 additions and 204 deletions

View File

@ -7,8 +7,8 @@ import { useTimer } from '@renderer/hooks/useTimer'
import PasteService from '@renderer/services/PasteService'
import { useAppDispatch } from '@renderer/store'
import { sendMessage as dispatchSendMessage } from '@renderer/store/thunk/messageThunk'
import type { Assistant, Message, MessageBlock, Model, Topic } from '@renderer/types'
import { MessageBlockStatus } from '@renderer/types/newMessage'
import type { Assistant, Message, Model, Topic } from '@renderer/types'
import { MessageBlock, MessageBlockStatus } from '@renderer/types/newMessage'
import { classNames } from '@renderer/utils'
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input'
@ -126,7 +126,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
const userMessage: Message = createMessage('user', sessionTopicId, agentId, {
id: userMessageId,
blocks: userMessageBlocks.map((block) => block.id),
blocks: userMessageBlocks.map((block) => block?.id),
model,
modelId: model?.id
})

View File

@ -9,10 +9,11 @@ import { FC, useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import AgentItem from './components/AgentItem'
import { SectionName } from './components/SectionName'
interface AssistantsTabProps {}
export const AgentsTab: FC<AssistantsTabProps> = () => {
export const Agents: FC<AssistantsTabProps> = () => {
const { agents, deleteAgent, isLoading } = useAgents()
const { t } = useTranslation()
const { chat } = useRuntime()
@ -34,7 +35,8 @@ export const AgentsTab: FC<AssistantsTabProps> = () => {
}, [isLoading, agents, activeAgentId, setActiveAgentId])
return (
<div className="agents-tab h-full w-full p-2">
<div className="agents-tab h-full w-full">
<SectionName name={t('common.agent_other')} />
{isLoading && <Spinner />}
{!isLoading &&
agents.map((agent) => (
@ -51,8 +53,8 @@ export const AgentsTab: FC<AssistantsTabProps> = () => {
content: (
<Button
onPress={(e) => e.continuePropagation()}
startContent={<Plus size={16} className="mr-1 shrink-0 translate-x-[-2px]" />}
className="w-full justify-start bg-transparent text-foreground-500 hover:bg-accent">
<Plus size={16} className="mr-1 shrink-0" />
{t('agent.add.title')}
</Button>
)

View File

@ -0,0 +1,206 @@
import { DownOutlined, RightOutlined } from '@ant-design/icons'
import { Button } from '@heroui/react'
import { DraggableList } from '@renderer/components/DraggableList'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
import { useTags } from '@renderer/hooks/useTags'
import { Assistant, AssistantsSortType } from '@renderer/types'
import { Tooltip } from 'antd'
import { Plus } from 'lucide-react'
import { FC, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import AssistantItem from './components/AssistantItem'
import { SectionName } from './components/SectionName'
interface AssistantsProps {
activeAssistant: Assistant
setActiveAssistant: (assistant: Assistant) => void
onCreateAssistant: () => void
onCreateDefaultAssistant: () => void
}
const Assistants: FC<AssistantsProps> = ({
activeAssistant,
setActiveAssistant,
onCreateAssistant,
onCreateDefaultAssistant
}) => {
const { assistants, removeAssistant, copyAssistant, updateAssistants } = useAssistants()
const [dragging, setDragging] = useState(false)
const { addAssistantPreset } = useAssistantPresets()
const { t } = useTranslation()
const { getGroupedAssistants, collapsedTags, toggleTagCollapse } = useTags()
const { assistantsTabSortType = 'list', setAssistantsTabSortType } = useAssistantsTabSortType()
const onDelete = useCallback(
(assistant: Assistant) => {
const remaining = assistants.filter((a) => a.id !== assistant.id)
if (assistant.id === activeAssistant?.id) {
const newActive = remaining[remaining.length - 1]
newActive ? setActiveAssistant(newActive) : onCreateDefaultAssistant()
}
removeAssistant(assistant.id)
},
[activeAssistant, assistants, removeAssistant, setActiveAssistant, onCreateDefaultAssistant]
)
const handleSortByChange = useCallback(
(sortType: AssistantsSortType) => {
setAssistantsTabSortType(sortType)
},
[setAssistantsTabSortType]
)
const handleGroupReorder = useCallback(
(tag: string, newGroupList: Assistant[]) => {
let insertIndex = 0
const newGlobal = assistants.map((a) => {
const tags = a.tags?.length ? a.tags : [t('assistants.tags.untagged')]
if (tags.includes(tag)) {
const replaced = newGroupList[insertIndex]
insertIndex += 1
return replaced
}
return a
})
updateAssistants(newGlobal)
},
[assistants, t, updateAssistants]
)
const renderAddAssistantButton = useMemo(() => {
return (
<Button onPress={onCreateAssistant} className="justify-start bg-transparent text-foreground-500 hover:bg-accent">
<Plus size={16} style={{ marginRight: 4, flexShrink: 0 }} />
{t('chat.add.assistant.title')}
</Button>
)
}, [onCreateAssistant, t])
if (assistantsTabSortType === 'tags') {
return (
<>
<SectionName name={t('common.assistant_other')} />
<div style={{ marginBottom: '8px' }}>
{getGroupedAssistants.map((group) => (
<TagsContainer key={group.tag}>
{group.tag !== t('assistants.tags.untagged') && (
<GroupTitle onClick={() => toggleTagCollapse(group.tag)}>
<Tooltip title={group.tag}>
<GroupTitleName>
{collapsedTags[group.tag] ? (
<RightOutlined style={{ fontSize: '10px', marginRight: '5px' }} />
) : (
<DownOutlined style={{ fontSize: '10px', marginRight: '5px' }} />
)}
{group.tag}
</GroupTitleName>
</Tooltip>
<GroupTitleDivider />
</GroupTitle>
)}
{!collapsedTags[group.tag] && (
<div>
<DraggableList
list={group.assistants}
onUpdate={(newList) => handleGroupReorder(group.tag, newList)}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(assistant) => (
<AssistantItem
key={assistant.id}
assistant={assistant}
isActive={assistant.id === activeAssistant.id}
sortBy={assistantsTabSortType}
onSwitch={setActiveAssistant}
onDelete={onDelete}
addPreset={addAssistantPreset}
copyAssistant={copyAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange}
/>
)}
</DraggableList>
</div>
)}
</TagsContainer>
))}
</div>
{renderAddAssistantButton}
</>
)
}
return (
<div>
<SectionName name={t('common.assistant_other')} />
<DraggableList
list={assistants}
onUpdate={updateAssistants}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(assistant) => (
<AssistantItem
key={assistant.id}
assistant={assistant}
isActive={assistant.id === activeAssistant.id}
sortBy={assistantsTabSortType}
onSwitch={setActiveAssistant}
onDelete={onDelete}
addPreset={addAssistantPreset}
copyAssistant={copyAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange}
/>
)}
</DraggableList>
{!dragging && renderAddAssistantButton}
<div style={{ minHeight: 10 }}></div>
</div>
)
}
// 样式组件
const TagsContainer = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`
const GroupTitle = styled.div`
color: var(--color-text-2);
font-size: 12px;
font-weight: 500;
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 24px;
margin: 5px 0;
`
const GroupTitleName = styled.div`
max-width: 50%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
padding: 0 4px;
color: var(--color-text);
font-size: 13px;
line-height: 24px;
margin-right: 5px;
display: flex;
`
const GroupTitleDivider = styled.div`
flex: 1;
border-top: 1px solid var(--color-border);
`
export default Assistants

View File

@ -1,19 +1,10 @@
import { DownOutlined, RightOutlined } from '@ant-design/icons'
import { Button } from '@heroui/react'
import { DraggableList } from '@renderer/components/DraggableList'
import Scrollbar from '@renderer/components/Scrollbar'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
import { useTags } from '@renderer/hooks/useTags'
import { Assistant, AssistantsSortType } from '@renderer/types'
import { Tooltip } from 'antd'
import { Plus } from 'lucide-react'
import { FC, useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Assistant } from '@renderer/types'
import { FC, useRef } from 'react'
import styled from 'styled-components'
import AssistantItem from './components/AssistantItem'
import { Agents } from './Agents'
import Assistants from './Assistants'
interface AssistantsTabProps {
activeAssistant: Assistant
@ -21,147 +12,17 @@ interface AssistantsTabProps {
onCreateAssistant: () => void
onCreateDefaultAssistant: () => void
}
const Assistants: FC<AssistantsTabProps> = ({
activeAssistant,
setActiveAssistant,
onCreateAssistant,
onCreateDefaultAssistant
}) => {
const { assistants, removeAssistant, copyAssistant, updateAssistants } = useAssistants()
const [dragging, setDragging] = useState(false)
const { addAssistantPreset } = useAssistantPresets()
const { t } = useTranslation()
const { getGroupedAssistants, collapsedTags, toggleTagCollapse } = useTags()
const { assistantsTabSortType = 'list', setAssistantsTabSortType } = useAssistantsTabSortType()
const AssistantsTab: FC<AssistantsTabProps> = (props) => {
const containerRef = useRef<HTMLDivElement>(null)
const onDelete = useCallback(
(assistant: Assistant) => {
const remaining = assistants.filter((a) => a.id !== assistant.id)
if (assistant.id === activeAssistant?.id) {
const newActive = remaining[remaining.length - 1]
newActive ? setActiveAssistant(newActive) : onCreateDefaultAssistant()
}
removeAssistant(assistant.id)
},
[activeAssistant, assistants, removeAssistant, setActiveAssistant, onCreateDefaultAssistant]
)
const handleSortByChange = useCallback(
(sortType: AssistantsSortType) => {
setAssistantsTabSortType(sortType)
},
[setAssistantsTabSortType]
)
const handleGroupReorder = useCallback(
(tag: string, newGroupList: Assistant[]) => {
let insertIndex = 0
const newGlobal = assistants.map((a) => {
const tags = a.tags?.length ? a.tags : [t('assistants.tags.untagged')]
if (tags.includes(tag)) {
const replaced = newGroupList[insertIndex]
insertIndex += 1
return replaced
}
return a
})
updateAssistants(newGlobal)
},
[assistants, t, updateAssistants]
)
const renderAddAssistantButton = useMemo(() => {
return (
<Button onPress={onCreateAssistant} className="justify-start bg-transparent text-foreground-500 hover:bg-accent">
<Plus size={16} style={{ marginRight: 4, flexShrink: 0 }} />
{t('chat.add.assistant.title')}
</Button>
)
}, [onCreateAssistant, t])
if (assistantsTabSortType === 'tags') {
return (
<Container className="assistants-tab" ref={containerRef}>
<div style={{ marginBottom: '8px' }}>
{getGroupedAssistants.map((group) => (
<TagsContainer key={group.tag}>
{group.tag !== t('assistants.tags.untagged') && (
<GroupTitle onClick={() => toggleTagCollapse(group.tag)}>
<Tooltip title={group.tag}>
<GroupTitleName>
{collapsedTags[group.tag] ? (
<RightOutlined style={{ fontSize: '10px', marginRight: '5px' }} />
) : (
<DownOutlined style={{ fontSize: '10px', marginRight: '5px' }} />
)}
{group.tag}
</GroupTitleName>
</Tooltip>
<GroupTitleDivider />
</GroupTitle>
)}
{!collapsedTags[group.tag] && (
<div>
<DraggableList
list={group.assistants}
onUpdate={(newList) => handleGroupReorder(group.tag, newList)}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(assistant) => (
<AssistantItem
key={assistant.id}
assistant={assistant}
isActive={assistant.id === activeAssistant.id}
sortBy={assistantsTabSortType}
onSwitch={setActiveAssistant}
onDelete={onDelete}
addPreset={addAssistantPreset}
copyAssistant={copyAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange}
/>
)}
</DraggableList>
</div>
)}
</TagsContainer>
))}
</div>
{renderAddAssistantButton}
</Container>
)
}
return (
<Container className="assistants-tab" ref={containerRef}>
<DraggableList
list={assistants}
onUpdate={updateAssistants}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(assistant) => (
<AssistantItem
key={assistant.id}
assistant={assistant}
isActive={assistant.id === activeAssistant.id}
sortBy={assistantsTabSortType}
onSwitch={setActiveAssistant}
onDelete={onDelete}
addPreset={addAssistantPreset}
copyAssistant={copyAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange}
/>
)}
</DraggableList>
{!dragging && renderAddAssistantButton}
<div style={{ minHeight: 10 }}></div>
<Agents />
<Assistants {...props} />
</Container>
)
}
// 样式组件
const Container = styled(Scrollbar)`
display: flex;
flex-direction: column;
@ -169,42 +30,4 @@ const Container = styled(Scrollbar)`
margin-top: 3px;
`
const TagsContainer = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`
const GroupTitle = styled.div`
color: var(--color-text-2);
font-size: 12px;
font-weight: 500;
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 24px;
margin: 5px 0;
`
const GroupTitleName = styled.div`
max-width: 50%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
padding: 0 4px;
color: var(--color-text);
font-size: 13px;
line-height: 24px;
margin-right: 5px;
display: flex;
`
const GroupTitleDivider = styled.div`
flex: 1;
border-top: 1px solid var(--color-border);
`
export default Assistants
export default AssistantsTab

View File

@ -0,0 +1,7 @@
type Props = {
name: string
}
export const SectionName: React.FC<Props> = ({ name }) => {
return <div className="mb-2 text-gray-500 text-sm">{name}</div>
}

View File

@ -13,9 +13,7 @@ import { FC, useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { AgentsTab as Agents } from './AgentsTab'
import Assistants from './AssistantsTab'
import Sessions from './SessionsTab'
import Settings from './SettingsTab'
import Topics from './TopicsTab'
@ -127,15 +125,9 @@ const HomeTabs: FC<Props> = ({
<TabItem active={tab === 'assistants'} onClick={() => setTab('assistants')}>
{t('assistants.abbr')}
</TabItem>
<TabItem active={tab === 'agents'} onClick={() => setTab('agents')}>
{t('agents.title')}
</TabItem>
<TabItem active={tab === 'topic'} onClick={() => setTab('topic')}>
{t('common.topics')}
</TabItem>
<TabItem active={tab === 'sessions'} onClick={() => setTab('sessions')}>
{t('agent.session.label_other')}
</TabItem>
<TabItem active={tab === 'settings'} onClick={() => setTab('settings')}>
{t('settings.title')}
</TabItem>
@ -170,8 +162,6 @@ const HomeTabs: FC<Props> = ({
position={position}
/>
)}
{tab === 'agents' && <Agents />}
{tab === 'sessions' && <Sessions />}
{tab === 'settings' && <Settings assistant={activeAssistant} />}
</TabContent>
</Container>

View File

@ -4,7 +4,7 @@
*
* WARNING: Any null value will be converted to undefined from api.
*/
import { TextStreamPart } from 'ai'
import { ModelMessage, TextStreamPart } from 'ai'
import { z } from 'zod'
import type { Message, MessageBlock } from './newMessage'

View File

@ -1 +1 @@
export type Tab = 'assistants' | 'topic' | 'settings' | 'agents' | 'sessions'
export type Tab = 'assistants' | 'topic' | 'settings'