mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-05 21:50:46 +08:00
feat: separate Agent into independent module with dedicated page and route (#13420)
### What this PR does Before this PR: Agent and Assistant were mixed in a single `UnifiedList` in the home page sidebar. Users had difficulty discovering the Agent entry. The two have fundamentally different data models (Redux vs SQLite), interaction patterns (topic vs session), and component hierarchies, yet shared the same UI components and Redux state (`activeTopicOrSession`). After this PR: Agent is separated into an independent module with its own sidebar entry, route (`/agents`), page, navbar, and side panel. The home page only handles Assistants. Navigation between the two is handled via sidebar icons and routes, not Redux state. Fixes #13329 ### Why we need it and why it was done in this way The following tradeoffs were made: - `AgentSettingsTab` was moved to `pages/agents/` and is used by the `AgentChatNavbar/Tools/SettingsButton` drawer. - `Unified*` components were renamed to `Assistant*` (via `git mv`) rather than just removing agent code, to make naming accurate and improve readability. - `activeTopicOrSession` was removed from Redux runtime store entirely, replaced by route-based detection where needed, avoiding stale state issues. The following alternatives were considered: - Keeping a shared `UnifiedList` with a filter — rejected because the data models and interaction patterns are too different, leading to excessive conditional logic. - Using a tab within the home page — rejected per issue spec; agents need their own sidebar entry and dedicated page for discoverability. ### Breaking changes None. This is an internal UI refactoring. The Redux migration (v200 → v201) automatically adds the `'agents'` sidebar icon to existing user configurations. ### Special notes for your reviewer **New directory structure — `pages/agents/`:** - `AgentPage.tsx` — Top-level page with apiServer.enabled guard (early return with alert when server disabled) - `AgentChat.tsx` — Chat area with loading state management (Spin during initialization, alerts for select/create states) - `AgentNavbar.tsx` — Top navbar for left-sidebar layout mode - `AgentSidePanel.tsx` — Left panel with agent list and sessions tabs - `AgentSettingsTab.tsx` — Moved from home, used by AgentChatNavbar settings drawer - `components/AgentChatNavbar/` — Mirrors `home/components/ChatNavBar/` structure: - `index.tsx` — Sidebar toggle buttons + content wrapper - `AgentContent.tsx` — Agent → Session → Model → Workspace breadcrumb navigation - `Tools/` — Agent-specific toolbar with `SettingsButton` (uses `AgentSettingsTab`) - `OpenExternalAppButton.tsx`, `SessionWorkspaceMeta.tsx` — AgentContent-only components - `components/` — Migrated agent-only components: `AgentItem`, `AgentSessionInputbar`, `AgentSessionMessages`, `SelectAgentBaseModelButton`, `SessionItem`, `Sessions` **Hook consolidation:** - Merged two duplicate `useActiveAgent` hooks into one at `hooks/agents/useActiveAgent` — now returns both `{ agent, error, isLoading, setActiveAgentId }` - Deleted `home/Tabs/hooks/useActiveAgent.ts` **Loading state improvements:** - `AgentPage` top-level guard prevents all child components from rendering when apiServer is disabled - `AgentChat` tracks `isInitializing` covering: server starting → agents loading → agent loading → session initializing → auto-select pending - `useAgentSessionInitializer` now distinguishes `undefined` (not initialized) vs `null` (initialized, no sessions) in `activeSessionIdMap`, preventing premature "create session" alerts **Shared component extraction:** - `AddButton` moved from `home/Tabs/components/` to `renderer/src/components/` (used by both pages) ### Checklist - [x] PR: The PR description is expressive enough and will help future contributors - [x] Code: Write code that humans can understand and Keep it simple - [x] Refactor: You have left the code cleaner than you found it (Boy Scout Rule) - [x] Upgrade: Impact of this change on upgrade flows was considered and addressed if required - [x] Documentation: A user-guide update was considered and is present (link) or not required. Check this only when the PR introduces or changes a user-facing feature or behavior. - [x] Self-review: I have reviewed my own code before requesting review from others ### Release note ```release-note Added a dedicated "Agents" page accessible from the sidebar, separating Agents from the Assistants list for improved discoverability and clearer navigation. ``` --------- Signed-off-by: icarus <eurfelux@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: kangfenmao <kangfenmao@qq.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -64,6 +64,7 @@ CLAUDE.local.md
|
||||
coverage
|
||||
.vitest-cache
|
||||
vitest.config.*.timestamp-*
|
||||
.context/vitest-temp/
|
||||
|
||||
# TypeScript incremental build
|
||||
.tsbuildinfo
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ErrorBoundary } from './components/ErrorBoundary'
|
||||
import TabsContainer from './components/Tab/TabContainer'
|
||||
import NavigationHandler from './handler/NavigationHandler'
|
||||
import { useNavbarPosition } from './hooks/useSettings'
|
||||
import AgentPage from './pages/agents/AgentPage'
|
||||
import CodeToolsPage from './pages/code/CodeToolsPage'
|
||||
import FilesPage from './pages/files/FilesPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
@@ -31,6 +32,7 @@ const Router: FC = () => {
|
||||
<ErrorBoundary>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/agents" element={<AgentPage />} />
|
||||
<Route path="/store" element={<AssistantPresetsPage />} />
|
||||
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
|
||||
@@ -7,7 +7,7 @@ import { permissionModeCards } from '@renderer/config/agent'
|
||||
import { isWin } from '@renderer/config/constant'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
import SelectAgentBaseModelButton from '@renderer/pages/home/components/SelectAgentBaseModelButton'
|
||||
import SelectAgentBaseModelButton from '@renderer/pages/agents/components/SelectAgentBaseModelButton'
|
||||
import type {
|
||||
AddAgentForm,
|
||||
AgentEntity,
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
LayoutGrid,
|
||||
Monitor,
|
||||
Moon,
|
||||
MousePointerClick,
|
||||
NotepadText,
|
||||
Palette,
|
||||
Settings,
|
||||
@@ -86,9 +87,12 @@ const getTabIcon = (
|
||||
return <LayoutGrid size={14} />
|
||||
}
|
||||
|
||||
// TODO: Add TabId as type instead of string
|
||||
switch (tabId) {
|
||||
case 'home':
|
||||
return <Home size={14} />
|
||||
case 'agents':
|
||||
return <MousePointerClick size={14} />
|
||||
case 'store':
|
||||
return <Sparkle size={14} />
|
||||
case 'translate':
|
||||
@@ -240,35 +244,38 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
||||
gap={'6px'}
|
||||
onSortEnd={onSortEnd}
|
||||
className="tabs-sortable"
|
||||
renderItem={(tab) => (
|
||||
<Tab
|
||||
key={tab.id}
|
||||
active={tab.id === activeTabId}
|
||||
onClick={() => handleTabClick(tab)}
|
||||
onAuxClick={(e) => {
|
||||
if (e.button === 1 && tab.id !== 'home') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
closeTab(tab.id)
|
||||
}
|
||||
}}>
|
||||
<TabHeader>
|
||||
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps, minAppsCache)}</TabIcon>}
|
||||
<TabTitle>{getTabTitle(tab.id)}</TabTitle>
|
||||
</TabHeader>
|
||||
{tab.id !== 'home' && (
|
||||
<CloseButton
|
||||
className="close-button"
|
||||
data-no-dnd
|
||||
onClick={(e) => {
|
||||
renderItem={(tab) => {
|
||||
const isClosable = tab.id !== 'home' && tab.id !== 'agents'
|
||||
return (
|
||||
<Tab
|
||||
key={tab.id}
|
||||
active={tab.id === activeTabId}
|
||||
onClick={() => handleTabClick(tab)}
|
||||
onAuxClick={(e) => {
|
||||
if (e.button === 1 && isClosable) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
closeTab(tab.id)
|
||||
}}>
|
||||
<X size={12} />
|
||||
</CloseButton>
|
||||
)}
|
||||
</Tab>
|
||||
)}
|
||||
}
|
||||
}}>
|
||||
<TabHeader>
|
||||
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps, minAppsCache)}</TabIcon>}
|
||||
<TabTitle>{getTabTitle(tab.id)}</TabTitle>
|
||||
</TabHeader>
|
||||
{isClosable && (
|
||||
<CloseButton
|
||||
className="close-button"
|
||||
data-no-dnd
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
closeTab(tab.id)
|
||||
}}>
|
||||
<X size={12} />
|
||||
</CloseButton>
|
||||
)}
|
||||
</Tab>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<AddTabButton onClick={handleAddTab} className={classNames({ active: activeTabId === 'launchpad' })}>
|
||||
<PlusOutlined />
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
MessageSquare,
|
||||
Monitor,
|
||||
Moon,
|
||||
MousePointerClick,
|
||||
NotepadText,
|
||||
Palette,
|
||||
Settings,
|
||||
@@ -126,10 +127,11 @@ const MainMenus: FC = () => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const isRoute = (path: string): string => (pathname === path && !minappShow ? 'active' : '')
|
||||
const isRoutes = (path: string): string => (pathname.startsWith(path) && !minappShow ? 'active' : '')
|
||||
const isRoutes = (path: string): string => (pathname.startsWith(path) && path !== '/' && !minappShow ? 'active' : '')
|
||||
|
||||
const iconMap = {
|
||||
assistants: <MessageSquare size={18} className="icon" />,
|
||||
agents: <MousePointerClick size={18} className="icon" />,
|
||||
store: <Sparkle size={18} className="icon" />,
|
||||
paintings: <Palette size={18} className="icon" />,
|
||||
translate: <Languages size={18} className="icon" />,
|
||||
@@ -143,6 +145,7 @@ const MainMenus: FC = () => {
|
||||
|
||||
const pathMap = {
|
||||
assistants: '/',
|
||||
agents: '/agents',
|
||||
store: '/store',
|
||||
paintings: `/paintings/${defaultPaintingProvider}`,
|
||||
translate: '/translate',
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { SidebarIcon } from '@renderer/types'
|
||||
*/
|
||||
export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
|
||||
'assistants',
|
||||
'agents',
|
||||
'store',
|
||||
'paintings',
|
||||
'translate',
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveAgentId as setActiveAgentIdAction } from '@renderer/store/runtime'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { useRuntime } from '../useRuntime'
|
||||
import { useAgent } from './useAgent'
|
||||
import { useAgentSessionInitializer } from './useAgentSessionInitializer'
|
||||
|
||||
export const useActiveAgent = () => {
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId } = chat
|
||||
return useAgent(activeAgentId)
|
||||
const dispatch = useAppDispatch()
|
||||
const { initializeAgentSession } = useAgentSessionInitializer()
|
||||
|
||||
const setActiveAgentId = useCallback(
|
||||
async (id: string) => {
|
||||
dispatch(setActiveAgentIdAction(id))
|
||||
await initializeAgentSession(id)
|
||||
},
|
||||
[dispatch, initializeAgentSession]
|
||||
)
|
||||
|
||||
return { ...useAgent(activeAgentId), setActiveAgentId }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveSessionIdAction, setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { setActiveSessionIdAction } from '@renderer/store/runtime'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
@@ -19,6 +19,10 @@ export const useAgentSessionInitializer = () => {
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId, activeSessionIdMap } = chat
|
||||
|
||||
// Use a ref to keep the callback stable across activeSessionIdMap changes
|
||||
const activeSessionIdMapRef = useRef(activeSessionIdMap)
|
||||
activeSessionIdMapRef.current = activeSessionIdMap
|
||||
|
||||
/**
|
||||
* Initialize session for the given agent by loading its sessions
|
||||
* and setting the latest one as active
|
||||
@@ -28,11 +32,9 @@ export const useAgentSessionInitializer = () => {
|
||||
if (!agentId) return
|
||||
|
||||
try {
|
||||
// Check if this agent already has an active session
|
||||
const currentSessionId = activeSessionIdMap[agentId]
|
||||
if (currentSessionId) {
|
||||
// Session already exists, just switch to session view
|
||||
dispatch(setActiveTopicOrSessionAction('session'))
|
||||
// Check if this agent has already been initialized (key exists in map)
|
||||
if (agentId in activeSessionIdMapRef.current) {
|
||||
// Already initialized, nothing to do
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,19 +48,15 @@ export const useAgentSessionInitializer = () => {
|
||||
|
||||
// Set the latest session as active
|
||||
dispatch(setActiveSessionIdAction({ agentId, sessionId: latestSession.id }))
|
||||
dispatch(setActiveTopicOrSessionAction('session'))
|
||||
} else {
|
||||
// No sessions exist, we might want to create one
|
||||
// But for now, just switch to session view and let the Sessions component handle it
|
||||
dispatch(setActiveTopicOrSessionAction('session'))
|
||||
// Mark as initialized with no session (null vs undefined distinction)
|
||||
dispatch(setActiveSessionIdAction({ agentId, sessionId: null }))
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize agent session:', error as Error)
|
||||
// Even if loading fails, switch to session view
|
||||
dispatch(setActiveTopicOrSessionAction('session'))
|
||||
}
|
||||
},
|
||||
[client, dispatch, activeSessionIdMap]
|
||||
[client, dispatch]
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -66,13 +64,12 @@ export const useAgentSessionInitializer = () => {
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (activeAgentId) {
|
||||
// Check if we need to initialize this agent's session
|
||||
const hasActiveSession = activeSessionIdMap[activeAgentId]
|
||||
if (!hasActiveSession) {
|
||||
// Check if we need to initialize this agent's session (key not yet in map)
|
||||
if (!(activeAgentId in activeSessionIdMapRef.current)) {
|
||||
initializeAgentSession(activeAgentId)
|
||||
}
|
||||
}
|
||||
}, [activeAgentId, activeSessionIdMap, initializeAgentSession])
|
||||
}, [activeAgentId, initializeAgentSession])
|
||||
|
||||
return {
|
||||
initializeAgentSession
|
||||
|
||||
@@ -98,7 +98,7 @@ export const useAgents = () => {
|
||||
)
|
||||
|
||||
return {
|
||||
agents: data ?? [],
|
||||
agents: data,
|
||||
error,
|
||||
isLoading,
|
||||
addAgent,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { loggerService } from '@logger'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveSessionIdAction, setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import { setActiveSessionIdAction } from '@renderer/store/runtime'
|
||||
import type { CreateSessionForm } from '@renderer/types'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -36,7 +36,6 @@ export const useCreateDefaultSession = (agentId: string | null) => {
|
||||
|
||||
if (created) {
|
||||
dispatch(setActiveSessionIdAction({ agentId, sessionId: created.id }))
|
||||
dispatch(setActiveTopicOrSessionAction('session'))
|
||||
}
|
||||
|
||||
return created
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setApiServerRunningAction } from '@renderer/store/runtime'
|
||||
import { setApiServerEnabled as setApiServerEnabledAction } from '@renderer/store/settings'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -34,10 +35,17 @@ export const useApiServer = () => {
|
||||
const apiServerConfig = useAppSelector((state) => state.settings.apiServer)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// Initial state - no longer optimistic, wait for actual status
|
||||
const [apiServerRunning, setApiServerRunning] = useState(false)
|
||||
const apiServerRunning = useAppSelector((state) => state.runtime.apiServerRunning)
|
||||
// Is checking the API server status
|
||||
const [apiServerLoading, setApiServerLoading] = useState(true)
|
||||
|
||||
const setApiServerRunning = useCallback(
|
||||
(running: boolean) => {
|
||||
dispatch(setApiServerRunningAction(running))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const setApiServerEnabled = useCallback(
|
||||
(enabled: boolean) => {
|
||||
dispatch(setApiServerEnabledAction(enabled))
|
||||
@@ -59,7 +67,7 @@ export const useApiServer = () => {
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerConfig.enabled, setApiServerEnabled])
|
||||
}, [apiServerConfig.enabled, setApiServerEnabled, setApiServerLoading, setApiServerRunning])
|
||||
|
||||
const startApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
@@ -78,7 +86,7 @@ export const useApiServer = () => {
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, setApiServerEnabled, t])
|
||||
}, [apiServerLoading, setApiServerEnabled, setApiServerLoading, setApiServerRunning, t])
|
||||
|
||||
const stopApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
@@ -97,7 +105,7 @@ export const useApiServer = () => {
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, setApiServerEnabled, t])
|
||||
}, [apiServerLoading, setApiServerEnabled, setApiServerLoading, setApiServerRunning, t])
|
||||
|
||||
const restartApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
@@ -116,7 +124,7 @@ export const useApiServer = () => {
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, checkApiServerStatus, setApiServerEnabled, t])
|
||||
}, [apiServerLoading, checkApiServerStatus, setApiServerEnabled, setApiServerLoading, t])
|
||||
|
||||
useEffect(() => {
|
||||
checkApiServerStatus()
|
||||
|
||||
@@ -150,7 +150,8 @@ const titleKeyMap = {
|
||||
paintings: 'title.paintings',
|
||||
settings: 'title.settings',
|
||||
translate: 'title.translate',
|
||||
openclaw: 'openclaw.title'
|
||||
openclaw: 'openclaw.title',
|
||||
agents: 'agent.sidebar_title'
|
||||
} as const
|
||||
|
||||
export const getTitleLabel = (key: string): string => {
|
||||
@@ -181,6 +182,7 @@ export const getThemeModeLabel = (key: string): string => {
|
||||
|
||||
const sidebarIconKeyMap = {
|
||||
assistants: 'assistants.title',
|
||||
agents: 'agent.sidebar_title',
|
||||
store: 'assistants.presets.title',
|
||||
paintings: 'paintings.title',
|
||||
translate: 'translate.title',
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
"edit": {
|
||||
"title": "Edit Agent"
|
||||
},
|
||||
"empty": {
|
||||
"description": "Create an agent to handle complex tasks with AI-powered tools",
|
||||
"title": "No agents yet"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Failed to get the agent.",
|
||||
@@ -295,6 +299,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar_title": "Agents",
|
||||
"todo": {
|
||||
"panel": {
|
||||
"title": "{{completed}}/{{total}} tasks completed"
|
||||
@@ -395,7 +400,11 @@
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "Enable API Server to use agents."
|
||||
"enable_and_start": "Enable & Start",
|
||||
"enable_server": "Enable API Server to use agents.",
|
||||
"enable_server_description": "The API server must be enabled for agents to work. You can enable it directly or configure it in settings.",
|
||||
"server_not_running": "API Server is enabled but not running. Please check the server configuration.",
|
||||
"server_not_running_description": "The API server needs to be running for agents to work. You can start it directly or check the settings."
|
||||
}
|
||||
},
|
||||
"apiServer": {
|
||||
@@ -753,6 +762,7 @@
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"create_agent": "Create an agent to get started",
|
||||
"create_session": "Create a session",
|
||||
"select_agent": "Select an agent"
|
||||
},
|
||||
@@ -1389,6 +1399,7 @@
|
||||
"selected": "Selected",
|
||||
"selectedItems": "Selected {{count}} items",
|
||||
"selectedMessages": "Selected {{count}} messages",
|
||||
"sessions": "Sessions",
|
||||
"settings": "Settings",
|
||||
"sort": {
|
||||
"pinyin": {
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
"edit": {
|
||||
"title": "编辑 Agent"
|
||||
},
|
||||
"empty": {
|
||||
"description": "创建一个 Agent,让 AI 帮你处理复杂任务",
|
||||
"title": "还没有 Agent"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "获取智能体失败",
|
||||
@@ -295,6 +299,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar_title": "智能体",
|
||||
"todo": {
|
||||
"panel": {
|
||||
"title": "已完成 {{completed}}/{{total}} 个任务"
|
||||
@@ -395,7 +400,11 @@
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "请启用 API 服务器以使用智能体功能"
|
||||
"enable_and_start": "启用并启动",
|
||||
"enable_server": "请启用 API 服务器以使用智能体功能",
|
||||
"enable_server_description": "智能体功能需要启用 API 服务器。你可以直接启用,或前往设置页面进行配置。",
|
||||
"server_not_running": "API 服务器已启用但未运行,请检查服务器配置。",
|
||||
"server_not_running_description": "智能体功能需要 API 服务器运行。你可以直接启动服务器,或前往设置页面检查配置。"
|
||||
}
|
||||
},
|
||||
"apiServer": {
|
||||
@@ -753,6 +762,7 @@
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"create_agent": "请创建一个智能体以开始使用",
|
||||
"create_session": "请创建会话",
|
||||
"select_agent": "请选择智能体"
|
||||
},
|
||||
@@ -1389,6 +1399,7 @@
|
||||
"selected": "已选择",
|
||||
"selectedItems": "已选择 {{count}} 项",
|
||||
"selectedMessages": "选中 {{count}} 条消息",
|
||||
"sessions": "会话",
|
||||
"settings": "设置",
|
||||
"sort": {
|
||||
"pinyin": {
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
"edit": {
|
||||
"title": "編輯 Agent"
|
||||
},
|
||||
"empty": {
|
||||
"description": "建立 Agent 以透過 AI 驅動的工具處理複雜任務",
|
||||
"title": "尚無 Agent"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "無法取得 Agent。",
|
||||
@@ -295,6 +299,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar_title": "Agents",
|
||||
"todo": {
|
||||
"panel": {
|
||||
"title": "已完成 {{completed}}/{{total}} 個任務"
|
||||
@@ -395,7 +400,11 @@
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "啟用 API 伺服器以使用 Agent。"
|
||||
"enable_and_start": "啟用並啟動",
|
||||
"enable_server": "啟用 API 伺服器以使用 Agent。",
|
||||
"enable_server_description": "智能體功能需要啟用 API 伺服器。你可以直接啟用,或前往設定頁面進行配置。",
|
||||
"server_not_running": "API 伺服器已啟用但未執行。請檢查伺服器設定。",
|
||||
"server_not_running_description": "智能體功能需要 API 伺服器運行。你可以直接啟動伺服器,或前往設定頁面檢查配置。"
|
||||
}
|
||||
},
|
||||
"apiServer": {
|
||||
@@ -753,6 +762,7 @@
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"create_agent": "建立 Agent 以開始",
|
||||
"create_session": "建立工作階段",
|
||||
"select_agent": "選擇一個智能體"
|
||||
},
|
||||
@@ -1389,6 +1399,7 @@
|
||||
"selected": "已選擇",
|
||||
"selectedItems": "已選擇 {{count}} 項",
|
||||
"selectedMessages": "已選取 {{count}} 則訊息",
|
||||
"sessions": "會話",
|
||||
"settings": "設定",
|
||||
"sort": {
|
||||
"pinyin": {
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
"edit": {
|
||||
"title": "Agent bearbeiten"
|
||||
},
|
||||
"empty": {
|
||||
"description": "Erstellen Sie einen Agenten zur Bewältigung komplexer Aufgaben mit KI-gestützten Tools.",
|
||||
"title": "Noch keine Agenten"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Agent abrufen fehlgeschlagen",
|
||||
@@ -295,6 +299,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar_title": "Agenten",
|
||||
"todo": {
|
||||
"panel": {
|
||||
"title": "{{completed}}/{{total}} Aufgaben abgeschlossen"
|
||||
@@ -395,7 +400,11 @@
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "Bitte aktivieren Sie den API-Server, um Agent-Funktionen zu verwenden"
|
||||
"enable_and_start": "Aktivieren & Starten",
|
||||
"enable_server": "Bitte aktivieren Sie den API-Server, um Agent-Funktionen zu verwenden",
|
||||
"enable_server_description": "Der API-Server muss aktiviert sein, damit Agents funktionieren. Sie können ihn direkt aktivieren oder in den Einstellungen konfigurieren.",
|
||||
"server_not_running": "API-Server ist aktiviert, läuft aber nicht. Bitte überprüfen Sie die Serverkonfiguration.",
|
||||
"server_not_running_description": "Der API-Server muss laufen, damit die Agents funktionieren. Sie können ihn direkt starten oder die Einstellungen überprüfen."
|
||||
}
|
||||
},
|
||||
"apiServer": {
|
||||
@@ -753,6 +762,7 @@
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"create_agent": "Erstellen Sie einen Agenten, um loszulegen.",
|
||||
"create_session": "Erstelle eine Sitzung",
|
||||
"select_agent": "Wählen Sie einen Agenten"
|
||||
},
|
||||
@@ -1389,6 +1399,7 @@
|
||||
"selected": "Ausgewählt",
|
||||
"selectedItems": "{{count}} Elemente ausgewählt",
|
||||
"selectedMessages": "{{count}} Nachrichten ausgewählt",
|
||||
"sessions": "Sitzungen",
|
||||
"settings": "Einstellungen",
|
||||
"sort": {
|
||||
"pinyin": {
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
"edit": {
|
||||
"title": "Επεξεργαστής Agent"
|
||||
},
|
||||
"empty": {
|
||||
"description": "Δημιουργήστε έναν πράκτορα για τον χειρισμό σύνθετων εργασιών με εργαλεία που υποστηρίζονται από τεχνητή νοημοσύνη.",
|
||||
"title": "Δεν υπάρχουν ακόμα πράκτορες"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Αποτυχία λήψης του πράκτορα.",
|
||||
@@ -295,6 +299,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar_title": "Πράκτορες",
|
||||
"todo": {
|
||||
"panel": {
|
||||
"title": "{{completed}}/{{total}} εργασίες ολοκληρώθηκαν"
|
||||
@@ -395,7 +400,11 @@
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "Ενεργοποίηση του διακομιστή API για χρήση πρακτόρων."
|
||||
"enable_and_start": "Ενεργοποίηση & Εκκίνηση",
|
||||
"enable_server": "Ενεργοποίηση του διακομιστή API για χρήση πρακτόρων.",
|
||||
"enable_server_description": "Ο διακομιστής API πρέπει να είναι ενεργοποιημένος για να λειτουργήσουν οι πράκτορες. Μπορείτε να τον ενεργοποιήσετε απευθείας ή να τον ρυθμίσετε στις ρυθμίσεις.",
|
||||
"server_not_running": "Ο API Server είναι ενεργοποιημένος αλλά δεν εκτελείται. Παρακαλούμε ελέγξτε τη διαμόρφωση του διακομιστή.",
|
||||
"server_not_running_description": "Ο διακομιστής API πρέπει να βρίσκεται σε λειτουργία για να λειτουργήσουν οι πράκτορες. Μπορείτε να τον εκκινήσετε απευθείας ή να ελέγξετε τις ρυθμίσεις."
|
||||
}
|
||||
},
|
||||
"apiServer": {
|
||||
@@ -753,6 +762,7 @@
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"create_agent": "Δημιουργήστε έναν πράκτορα για να ξεκινήσετε.",
|
||||
"create_session": "Δημιουργία συνεδρίας",
|
||||
"select_agent": "Επιλέξτε έναν πράκτορα"
|
||||
},
|
||||
@@ -1389,6 +1399,7 @@
|
||||
"selected": "Επιλεγμένο",
|
||||
"selectedItems": "Επιλέχθηκαν {{count}} αντικείμενα",
|
||||
"selectedMessages": "Επιλέχθηκαν {{count}} μηνύματα",
|
||||
"sessions": "Συνεδρίες",
|
||||
"settings": "Ρυθμίσεις",
|
||||
"sort": {
|
||||
"pinyin": {
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
"edit": {
|
||||
"title": "Agent de edición"
|
||||
},
|
||||
"empty": {
|
||||
"description": "Crea un agente para gestionar tareas complejas con herramientas impulsadas por IA.",
|
||||
"title": "Aún no hay agentes"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "No se pudo obtener el agente.",
|
||||
@@ -295,6 +299,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar_title": "Agentes",
|
||||
"todo": {
|
||||
"panel": {
|
||||
"title": "{{completed}}/{{total}} tareas completadas"
|
||||
@@ -395,7 +400,11 @@
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "Habilitar el servidor API para usar agentes."
|
||||
"enable_and_start": "Habilitar e iniciar",
|
||||
"enable_server": "Habilitar el servidor API para usar agentes.",
|
||||
"enable_server_description": "El servidor de la API debe estar habilitado para que los agentes funcionen. Puede habilitarlo directamente o configurarlo en los ajustes.",
|
||||
"server_not_running": "El servidor API está habilitado pero no se está ejecutando. Por favor, compruebe la configuración del servidor.",
|
||||
"server_not_running_description": "El servidor de la API debe estar en ejecución para que los agentes funcionen. Puede iniciarlo directamente o comprobar los ajustes."
|
||||
}
|
||||
},
|
||||
"apiServer": {
|
||||
@@ -753,6 +762,7 @@
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"create_agent": "Crea un agente para empezar",
|
||||
"create_session": "Crear una sesión",
|
||||
"select_agent": "Selecciona un agente"
|
||||
},
|
||||
@@ -1389,6 +1399,7 @@
|
||||
"selected": "Seleccionado",
|
||||
"selectedItems": "{{count}} elementos seleccionados",
|
||||
"selectedMessages": "{{count}} mensajes seleccionados",
|
||||
"sessions": "Sesiones",
|
||||
"settings": "Configuración",
|
||||
"sort": {
|
||||
"pinyin": {
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
"edit": {
|
||||
"title": "Éditer Agent"
|
||||
},
|
||||
"empty": {
|
||||
"description": "Créez un agent pour gérer des tâches complexes avec des outils alimentés par l'IA",
|
||||
"title": "Pas encore d'agents"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Échec de l'obtention de l'agent.",
|
||||
@@ -295,6 +299,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar_title": "Agents",
|
||||
"todo": {
|
||||
"panel": {
|
||||
"title": "{{completed}}/{{total}} tâches terminées"
|
||||
@@ -395,7 +400,11 @@
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "Permettre au serveur API d'utiliser des agents."
|
||||
"enable_and_start": "Activer & Démarrer",
|
||||
"enable_server": "Permettre au serveur API d'utiliser des agents.",
|
||||
"enable_server_description": "Le serveur API doit être activé pour que les agents fonctionnent. Vous pouvez l'activer directement ou le configurer dans les paramètres.",
|
||||
"server_not_running": "Le serveur API est activé mais ne fonctionne pas. Veuillez vérifier la configuration du serveur.",
|
||||
"server_not_running_description": "Le serveur API doit être en cours d'exécution pour que les agents fonctionnent. Vous pouvez le démarrer directement ou vérifier les paramètres."
|
||||
}
|
||||
},
|
||||
"apiServer": {
|
||||
@@ -753,6 +762,7 @@
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"create_agent": "Créer un agent pour commencer",
|
||||
"create_session": "Créer une session",
|
||||
"select_agent": "Sélectionnez un agent"
|
||||
},
|
||||
@@ -1389,6 +1399,7 @@
|
||||
"selected": "Sélectionné",
|
||||
"selectedItems": "{{count}} éléments sélectionnés",
|
||||
"selectedMessages": "{{count}} messages sélectionnés",
|
||||
"sessions": "Sessions",
|
||||
"settings": "Paramètres",
|
||||
"sort": {
|
||||
"pinyin": {
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
"edit": {
|
||||
"title": "編集エージェント"
|
||||
},
|
||||
"empty": {
|
||||
"description": "AI搭載ツールで複雑なタスクを処理するエージェントを作成する",
|
||||
"title": "エージェントがまだありません"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "エージェントの取得に失敗しました。",
|
||||
@@ -295,6 +299,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar_title": "エージェント",
|
||||
"todo": {
|
||||
"panel": {
|
||||
"title": "{{completed}}/{{total}} タスク完了"
|
||||
@@ -395,7 +400,11 @@
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "APIサーバーがエージェントを使用できるようにする。"
|
||||
"enable_and_start": "有効化 & 開始",
|
||||
"enable_server": "APIサーバーがエージェントを使用できるようにする。",
|
||||
"enable_server_description": "エージェントを動作させるには、API サーバーを有効にする必要があります。直接有効にするか、設定から構成することができます。",
|
||||
"server_not_running": "APIサーバーは有効になっていますが、実行されていません。サーバー設定を確認してください。",
|
||||
"server_not_running_description": "エージェントを動作させるには、APIサーバーが稼働している必要があります。直接起動するか、設定を確認してください。"
|
||||
}
|
||||
},
|
||||
"apiServer": {
|
||||
@@ -753,6 +762,7 @@
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"create_agent": "開始するにはエージェントを作成してください",
|
||||
"create_session": "セッションを作成",
|
||||
"select_agent": "エージェントを選択してください"
|
||||
},
|
||||
@@ -1389,6 +1399,7 @@
|
||||
"selected": "選択済み",
|
||||
"selectedItems": "{{count}}件の項目を選択しました",
|
||||
"selectedMessages": "{{count}}件のメッセージを選択しました",
|
||||
"sessions": "セッション",
|
||||
"settings": "設定",
|
||||
"sort": {
|
||||
"pinyin": {
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
"edit": {
|
||||
"title": "Agent Editor"
|
||||
},
|
||||
"empty": {
|
||||
"description": "Crie um agente para lidar com tarefas complexas com ferramentas baseadas em IA",
|
||||
"title": "Ainda não há agentes"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Falha ao obter o agente.",
|
||||
@@ -295,6 +299,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar_title": "Agentes",
|
||||
"todo": {
|
||||
"panel": {
|
||||
"title": "{{completed}}/{{total}} tarefas concluídas"
|
||||
@@ -395,7 +400,11 @@
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "Ativar o Servidor de API para usar agentes."
|
||||
"enable_and_start": "Ativar & Iniciar",
|
||||
"enable_server": "Ativar o Servidor de API para usar agentes.",
|
||||
"enable_server_description": "O servidor de API deve estar habilitado para que os agentes funcionem. Você pode habilitá-lo diretamente ou configurá-lo nas configurações.",
|
||||
"server_not_running": "O Servidor API está ativado, mas não está em execução. Verifique a configuração do servidor.",
|
||||
"server_not_running_description": "O servidor da API precisa estar em execução para que os agentes funcionem. Você pode iniciá-lo diretamente ou verificar as configurações."
|
||||
}
|
||||
},
|
||||
"apiServer": {
|
||||
@@ -753,6 +762,7 @@
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"create_agent": "Crie um agente para começar",
|
||||
"create_session": "Criar uma sessão",
|
||||
"select_agent": "Selecione um agente"
|
||||
},
|
||||
@@ -1389,6 +1399,7 @@
|
||||
"selected": "Selecionado",
|
||||
"selectedItems": "{{count}} itens selecionados",
|
||||
"selectedMessages": "{{count}} mensagens selecionadas",
|
||||
"sessions": "Sessões",
|
||||
"settings": "Configurações",
|
||||
"sort": {
|
||||
"pinyin": {
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
"edit": {
|
||||
"title": "Editează agentul"
|
||||
},
|
||||
"empty": {
|
||||
"description": "Creează un agent pentru a gestiona sarcini complexe cu instrumente bazate pe IA",
|
||||
"title": "Niciun agent încă"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Nu s-a putut obține agentul.",
|
||||
@@ -295,6 +299,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar_title": "Agenți",
|
||||
"todo": {
|
||||
"panel": {
|
||||
"title": "{{completed}}/{{total}} sarcini finalizate"
|
||||
@@ -395,7 +400,11 @@
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "Activează serverul API pentru a folosi agenți."
|
||||
"enable_and_start": "Activează și pornește",
|
||||
"enable_server": "Activează serverul API pentru a folosi agenți.",
|
||||
"enable_server_description": "Serverul API trebuie să fie activat pentru ca agenții să funcționeze. Îl puteți activa direct sau îl puteți configura în setări.",
|
||||
"server_not_running": "Serverul API este activat, dar nu rulează. Vă rugăm să verificați configurația serverului.",
|
||||
"server_not_running_description": "Serverul API trebuie să ruleze pentru ca agenții să funcționeze. Îl puteți porni direct sau puteți verifica setările."
|
||||
}
|
||||
},
|
||||
"apiServer": {
|
||||
@@ -753,6 +762,7 @@
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"create_agent": "Creează un agent pentru a începe",
|
||||
"create_session": "Creează o sesiune",
|
||||
"select_agent": "Selectați un agent"
|
||||
},
|
||||
@@ -1389,6 +1399,7 @@
|
||||
"selected": "Selectat",
|
||||
"selectedItems": "{{count}} elemente selectate",
|
||||
"selectedMessages": "{{count}} mesaje selectate",
|
||||
"sessions": "Sesiuni",
|
||||
"settings": "Setări",
|
||||
"sort": {
|
||||
"pinyin": {
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
"edit": {
|
||||
"title": "Редактировать агент"
|
||||
},
|
||||
"empty": {
|
||||
"description": "Создайте агента для решения сложных задач с помощью инструментов на базе ИИ",
|
||||
"title": "Агентов пока нет"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Не удалось получить агента.",
|
||||
@@ -295,6 +299,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar_title": "Агенты",
|
||||
"todo": {
|
||||
"panel": {
|
||||
"title": "{{completed}}/{{total}} задач выполнено"
|
||||
@@ -395,7 +400,11 @@
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "Разрешить серверу API использовать агентов."
|
||||
"enable_and_start": "Включить и запустить",
|
||||
"enable_server": "Разрешить серверу API использовать агентов.",
|
||||
"enable_server_description": "Для работы агентов необходимо включить API-сервер. Вы можете включить его напрямую или в настройках.",
|
||||
"server_not_running": "Сервер API включен, но не запущен. Пожалуйста, проверьте конфигурацию сервера.",
|
||||
"server_not_running_description": "Для работы агентов необходимо, чтобы API-сервер был запущен. Вы можете запустить его напрямую или проверить настройки."
|
||||
}
|
||||
},
|
||||
"apiServer": {
|
||||
@@ -753,6 +762,7 @@
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"create_agent": "Создайте агента, чтобы начать работу",
|
||||
"create_session": "Создать сессию",
|
||||
"select_agent": "Выберите агента"
|
||||
},
|
||||
@@ -1389,6 +1399,7 @@
|
||||
"selected": "Выбрано",
|
||||
"selectedItems": "Выбрано {{count}} элементов",
|
||||
"selectedMessages": "Выбрано {{count}} сообщений",
|
||||
"sessions": "Сессии",
|
||||
"settings": "Настройки",
|
||||
"sort": {
|
||||
"pinyin": {
|
||||
|
||||
145
src/renderer/src/pages/agents/AgentChat.tsx
Normal file
145
src/renderer/src/pages/agents/AgentChat.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { QuickPanelProvider } from '@renderer/components/QuickPanel'
|
||||
import { useActiveAgent } from '@renderer/hooks/agents/useActiveAgent'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { cn } from '@renderer/utils'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { Alert, Spin } from 'antd'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { PinnedTodoPanel } from '../home/Inputbar/components/PinnedTodoPanel'
|
||||
import ChatNavigation from '../home/Messages/ChatNavigation'
|
||||
import NarrowLayout from '../home/Messages/NarrowLayout'
|
||||
import AgentChatNavbar from './components/AgentChatNavbar'
|
||||
import AgentSessionInputbar from './components/AgentSessionInputbar'
|
||||
import AgentSessionMessages from './components/AgentSessionMessages'
|
||||
import Sessions from './components/Sessions'
|
||||
|
||||
const AgentChat = () => {
|
||||
const { t } = useTranslation()
|
||||
const { messageNavigation, topicPosition } = useSettings()
|
||||
const { showTopics } = useShowTopics()
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId, activeSessionIdMap } = chat
|
||||
const activeSessionId = activeAgentId ? activeSessionIdMap[activeAgentId] : null
|
||||
// undefined = session not yet initialized, null = initialized but no sessions
|
||||
const isSessionInitialized = !activeAgentId || activeAgentId in activeSessionIdMap
|
||||
const { agent: activeAgent, isLoading: isAgentLoading } = useActiveAgent()
|
||||
const { isLoading: isAgentsLoading, agents } = useAgents()
|
||||
const { createDefaultSession } = useCreateDefaultSession(activeAgentId)
|
||||
|
||||
// Don't show select/create alerts while data is still loading
|
||||
// apiServerRunning is guaranteed by AgentPage guard
|
||||
const isInitializing =
|
||||
isAgentsLoading || isAgentLoading || !isSessionInitialized || !agents || (!activeAgentId && agents.length > 0)
|
||||
|
||||
const showRightSessions = topicPosition === 'right' && showTopics && !!activeAgentId
|
||||
|
||||
useShortcut(
|
||||
'new_topic',
|
||||
() => {
|
||||
void createDefaultSession()
|
||||
},
|
||||
{
|
||||
enabled: true,
|
||||
preventDefault: true,
|
||||
enableOnFormTags: true
|
||||
}
|
||||
)
|
||||
|
||||
if (isInitializing) {
|
||||
return (
|
||||
<Container className="flex flex-1 flex-col items-center justify-center">
|
||||
<Spin />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
// Initialized — agents.length === 0 is handled by AgentPage
|
||||
if (!activeAgentId) {
|
||||
return (
|
||||
<Container className="flex flex-1 flex-col justify-between">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Alert type="info" message={t('chat.alerts.select_agent')} style={{ margin: '5px 16px' }} />
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
if (!activeSessionId) {
|
||||
return (
|
||||
<Container className="flex flex-1 flex-col justify-between">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Alert type="warning" message={t('chat.alerts.create_session')} style={{ margin: '5px 16px' }} />
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<QuickPanelProvider>
|
||||
{/* Main Chat */}
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex h-fit w-full min-w-0">
|
||||
{activeAgent && <AgentChatNavbar className="min-w-0" activeAgent={activeAgent} />}
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="translate-z-0 relative flex w-full flex-1 flex-col justify-between overflow-y-auto">
|
||||
<AgentSessionMessages agentId={activeAgentId} sessionId={activeSessionId} />
|
||||
<div className="mt-auto px-4.5 pb-2">
|
||||
<NarrowLayout>
|
||||
<PinnedTodoPanel topicId={buildAgentSessionTopicId(activeSessionId)} />
|
||||
</NarrowLayout>
|
||||
</div>
|
||||
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
|
||||
</div>
|
||||
{/* Inputbar */}
|
||||
<AgentSessionInputbar agentId={activeAgentId} sessionId={activeSessionId} />
|
||||
</div>
|
||||
</QuickPanelProvider>
|
||||
|
||||
{/* Sessions Panel */}
|
||||
<AnimatePresence initial={false}>
|
||||
{showRightSessions && (
|
||||
<motion.div
|
||||
key="right-sessions"
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 'var(--assistants-width)', opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
className="overflow-hidden">
|
||||
<div className="flex h-full w-(--assistants-width) flex-col overflow-hidden">
|
||||
<Sessions agentId={activeAgentId!} />
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = ({ children, className }: PropsWithChildren<{ className?: string }>) => {
|
||||
const { isTopNavbar } = useNavbarPosition()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-1 overflow-hidden',
|
||||
isTopNavbar && 'rounded-tl-[10px] rounded-bl-[10px] bg-(--color-background)',
|
||||
className
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentChat
|
||||
117
src/renderer/src/pages/agents/AgentNavbar.tsx
Normal file
117
src/renderer/src/pages/agents/AgentNavbar.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import NavbarIcon from '@renderer/components/NavbarIcon'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
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 { useAppDispatch } from '@renderer/store'
|
||||
import { setNarrowMode } from '@renderer/store/settings'
|
||||
import { Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { Menu, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
|
||||
import UpdateAppButton from '../home/components/UpdateAppButton'
|
||||
import AgentSidePanelDrawer from './components/AgentSidePanelDrawer'
|
||||
|
||||
const AgentNavbar = () => {
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
const { narrowMode, topicPosition } = useSettings()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useShortcut('toggle_show_assistants', toggleShowAssistants)
|
||||
|
||||
useShortcut('search_message', () => {
|
||||
SearchPopup.show()
|
||||
})
|
||||
|
||||
const handleNarrowModeToggle = async () => {
|
||||
await modelGenerating()
|
||||
dispatch(setNarrowMode(!narrowMode))
|
||||
}
|
||||
|
||||
return (
|
||||
<Navbar className="agent-navbar">
|
||||
<AnimatePresence initial={false}>
|
||||
{showAssistants && (
|
||||
<motion.div
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 'auto', opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
style={{ overflow: 'hidden', display: 'flex', flexDirection: 'row' }}>
|
||||
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: 0 }}>
|
||||
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon onClick={toggleShowAssistants}>
|
||||
<PanelLeftClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
</NavbarLeft>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{!showAssistants && (
|
||||
<NavbarLeft
|
||||
style={{
|
||||
justifyContent: 'flex-start',
|
||||
borderRight: 'none',
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
minWidth: 'auto'
|
||||
}}>
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8} placement="right">
|
||||
<NavbarIcon onClick={() => toggleShowAssistants()}>
|
||||
<PanelRightClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
<NavbarIcon onClick={() => AgentSidePanelDrawer.show()} style={{ marginRight: 5 }}>
|
||||
<Menu size={18} />
|
||||
</NavbarIcon>
|
||||
</NavbarLeft>
|
||||
)}
|
||||
<NavbarCenter></NavbarCenter>
|
||||
<NavbarRight
|
||||
style={{
|
||||
justifyContent: 'flex-end',
|
||||
flex: 'none',
|
||||
position: 'relative',
|
||||
paddingRight: '15px',
|
||||
minWidth: 'auto'
|
||||
}}
|
||||
className="agent-navbar-right">
|
||||
<HStack alignItems="center" gap={6}>
|
||||
<UpdateAppButton />
|
||||
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon className="max-[1000px]:hidden" onClick={() => SearchPopup.show()}>
|
||||
<Search size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon className="max-[1000px]:hidden" onClick={handleNarrowModeToggle}>
|
||||
<i className="iconfont icon-icon-adaptive-width"></i>
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
{topicPosition === 'right' && !showTopics && (
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
|
||||
<NavbarIcon onClick={toggleShowTopics}>
|
||||
<PanelLeftClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{topicPosition === 'right' && showTopics && (
|
||||
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={2}>
|
||||
<NavbarIcon onClick={toggleShowTopics}>
|
||||
<PanelRightClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HStack>
|
||||
</NavbarRight>
|
||||
</Navbar>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentNavbar
|
||||
105
src/renderer/src/pages/agents/AgentPage.tsx
Normal file
105
src/renderer/src/pages/agents/AgentPage.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
||||
import { useActiveAgent } from '@renderer/hooks/agents/useActiveAgent'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { cn } from '@renderer/utils'
|
||||
import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, SECOND_MIN_WINDOW_WIDTH } from '@shared/config/constant'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import AgentChat from './AgentChat'
|
||||
import AgentNavbar from './AgentNavbar'
|
||||
import AgentSidePanel from './AgentSidePanel'
|
||||
import { AgentEmpty, AgentServerDisabled, AgentServerStopped } from './components/status'
|
||||
|
||||
const AgentPage = () => {
|
||||
const { isLeftNavbar } = useNavbarPosition()
|
||||
const { showAssistants } = useShowAssistants()
|
||||
const { showTopics } = useShowTopics()
|
||||
const { topicPosition } = useSettings()
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId } = chat
|
||||
const { agents } = useAgents()
|
||||
const { setActiveAgentId } = useActiveAgent()
|
||||
const { apiServerConfig, apiServerRunning, apiServerLoading } = useApiServer()
|
||||
|
||||
// Auto-select first agent when none is active
|
||||
useEffect(() => {
|
||||
if (!activeAgentId && agents && agents.length > 0) {
|
||||
setActiveAgentId(agents[0].id)
|
||||
}
|
||||
}, [activeAgentId, agents, setActiveAgentId])
|
||||
|
||||
useEffect(() => {
|
||||
const canMinimize = topicPosition === 'left' ? !showAssistants : !showAssistants && !showTopics
|
||||
window.api.window.setMinimumSize(canMinimize ? SECOND_MIN_WINDOW_WIDTH : MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
|
||||
return () => {
|
||||
window.api.window.resetMinimumSize()
|
||||
}
|
||||
}, [showAssistants, showTopics, topicPosition])
|
||||
|
||||
if (!apiServerConfig.enabled) {
|
||||
return (
|
||||
<Container className="bg-background">
|
||||
<AgentServerDisabled />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
if (!apiServerLoading && !apiServerRunning) {
|
||||
return (
|
||||
<Container className="bg-background">
|
||||
<AgentServerStopped />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
if (agents && agents.length === 0) {
|
||||
return (
|
||||
<Container className="bg-background">
|
||||
<AgentEmpty />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{isLeftNavbar && <AgentNavbar />}
|
||||
<div
|
||||
id={isLeftNavbar ? 'content-container' : undefined}
|
||||
className="flex min-w-0 flex-1 shrink flex-row overflow-hidden">
|
||||
<AnimatePresence initial={false}>
|
||||
{showAssistants && (
|
||||
<ErrorBoundary>
|
||||
<motion.div
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 'var(--assistants-width)', opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
style={{ overflow: 'hidden' }}>
|
||||
<AgentSidePanel />
|
||||
</motion.div>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<ErrorBoundary>
|
||||
<AgentChat />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = ({ children, className }: PropsWithChildren<{ className?: string }>) => {
|
||||
return (
|
||||
<div id="agent-page" className={cn('flex flex-1 flex-col overflow-hidden', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentPage
|
||||
@@ -1,4 +1,3 @@
|
||||
import { loggerService } from '@logger'
|
||||
import EditableNumber from '@renderer/components/EditableNumber'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import Selector from '@renderer/components/Selector'
|
||||
@@ -6,7 +5,6 @@ import { HelpTooltip } from '@renderer/components/TooltipIcons'
|
||||
import { UNKNOWN } from '@renderer/config/translate'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import useTranslate from '@renderer/hooks/useTranslate'
|
||||
import { SettingDivider, SettingRow, SettingRowTitle } from '@renderer/pages/settings'
|
||||
@@ -46,11 +44,7 @@ import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const logger = loggerService.withContext('AgentSettingsTab')
|
||||
|
||||
const AgentSettingsTab = () => {
|
||||
const { chat } = useRuntime()
|
||||
|
||||
const { messageStyle, fontSize, language } = useSettings()
|
||||
const { theme } = useTheme()
|
||||
const { themeNames } = useCodeStyle()
|
||||
@@ -116,12 +110,6 @@ const AgentSettingsTab = () => {
|
||||
[dispatch, theme, codeEditor.enabled]
|
||||
)
|
||||
|
||||
const isAgentSettings = chat.activeTopicOrSession === 'session'
|
||||
if (!isAgentSettings) {
|
||||
logger.warn('AgentSettingsTab is rendered when not session activated.')
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="settings-tab">
|
||||
<CollapsibleSettingGroup title={t('settings.messages.title')} defaultExpanded={true}>
|
||||
138
src/renderer/src/pages/agents/AgentSidePanel.tsx
Normal file
138
src/renderer/src/pages/agents/AgentSidePanel.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import AddButton from '@renderer/components/AddButton'
|
||||
import AgentModalPopup from '@renderer/components/Popups/agent/AgentModal'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useActiveAgent } from '@renderer/hooks/agents/useActiveAgent'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
|
||||
import type { AgentEntity } from '@renderer/types'
|
||||
import { cn } from '@renderer/utils'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import AgentItem from './components/AgentItem'
|
||||
import Sessions from './components/Sessions'
|
||||
|
||||
interface AgentSidePanelProps {
|
||||
onSelectItem?: () => void
|
||||
}
|
||||
|
||||
const AgentSidePanel = ({ onSelectItem }: AgentSidePanelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { agents, deleteAgent, isLoading, error } = useAgents()
|
||||
const { apiServerRunning, startApiServer } = useApiServer()
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId } = chat
|
||||
const { setActiveAgentId } = useActiveAgent()
|
||||
const { isLeftNavbar } = useNavbarPosition()
|
||||
const { topicPosition } = useSettings()
|
||||
|
||||
const sessionsOnRight = topicPosition === 'right'
|
||||
const [tab, setTab] = useState<'agents' | 'sessions'>('agents')
|
||||
|
||||
const handleAgentPress = useCallback(
|
||||
(agentId: string) => {
|
||||
setActiveAgentId(agentId)
|
||||
onSelectItem?.()
|
||||
},
|
||||
[setActiveAgentId, onSelectItem]
|
||||
)
|
||||
|
||||
const handleAddAgent = useCallback(() => {
|
||||
!apiServerRunning && startApiServer()
|
||||
AgentModalPopup.show({
|
||||
afterSubmit: (agent: AgentEntity) => {
|
||||
setActiveAgentId(agent.id)
|
||||
}
|
||||
})
|
||||
}, [apiServerRunning, startApiServer, setActiveAgentId])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col overflow-hidden"
|
||||
style={{
|
||||
width: 'var(--assistants-width)',
|
||||
height: 'calc(100vh - var(--navbar-height))',
|
||||
borderRight: isLeftNavbar ? '0.5px solid var(--color-border)' : 'none',
|
||||
backgroundColor: isLeftNavbar ? 'var(--color-background)' : undefined
|
||||
}}>
|
||||
{/* Tabs */}
|
||||
{!sessionsOnRight && (
|
||||
<div
|
||||
className="mx-3 flex border-(--color-border) border-b bg-transparent py-1.5 pt-0.5"
|
||||
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
|
||||
<TabButton active={tab === 'agents'} onClick={() => setTab('agents')}>
|
||||
{t('agent.sidebar_title')}
|
||||
</TabButton>
|
||||
<TabButton active={tab === 'sessions'} onClick={() => setTab('sessions')}>
|
||||
{t('common.sessions')}
|
||||
</TabButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
{(sessionsOnRight || tab === 'agents') && (
|
||||
<Scrollbar className="flex flex-col py-3">
|
||||
<div className="-mt-0.5 mb-1.5 px-2.5">
|
||||
<AddButton onClick={handleAddAgent}>{t('agent.sidebar_title')}</AddButton>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 px-2.5">
|
||||
{isLoading && (
|
||||
<div className="p-5 text-center text-(--color-text-secondary) text-[13px]">{t('common.loading')}</div>
|
||||
)}
|
||||
{error && <div className="p-5 text-center text-(--color-error) text-[13px]">{error.message}</div>}
|
||||
{!isLoading &&
|
||||
!error &&
|
||||
agents?.map((agent) => (
|
||||
<AgentItem
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
isActive={agent.id === activeAgentId}
|
||||
onDelete={() => deleteAgent(agent.id)}
|
||||
onPress={() => handleAgentPress(agent.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Scrollbar>
|
||||
)}
|
||||
{!sessionsOnRight && tab === 'sessions' && activeAgentId && (
|
||||
<Sessions agentId={activeAgentId} onSelectItem={onSelectItem} />
|
||||
)}
|
||||
{!sessionsOnRight && tab === 'sessions' && !activeAgentId && (
|
||||
<div className="flex flex-1 items-center justify-center p-5 text-(--color-text-secondary) text-[13px]">
|
||||
{t('chat.alerts.select_agent')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TabButton: FC<{ active: boolean; onClick: () => void; children: React.ReactNode }> = ({
|
||||
active,
|
||||
onClick,
|
||||
children
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'relative mx-0.5 flex flex-1 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-[13px]',
|
||||
'h-7.5',
|
||||
'hover:text-(--color-text)',
|
||||
'active:scale-[0.98]',
|
||||
active ? 'font-semibold text-(--color-text)' : 'font-normal text-(--color-text-secondary)',
|
||||
// Underline indicator via pseudo-element
|
||||
'after:-translate-x-1/2 after:-bottom-2 after:absolute after:left-1/2 after:h-0.75 after:rounded-sm after:transition-all after:duration-200 after:ease-in-out',
|
||||
active
|
||||
? 'after:w-7.5 after:bg-(--color-primary)'
|
||||
: 'after:w-0 after:bg-(--color-primary) hover:after:w-4 hover:after:bg-(--color-primary-soft)'
|
||||
)}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
|
||||
export default AgentSidePanel
|
||||
@@ -1,22 +1,32 @@
|
||||
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
|
||||
import NavbarIcon from '@renderer/components/NavbarIcon'
|
||||
import { useActiveSession } from '@renderer/hooks/agents/useActiveSession'
|
||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
import { useNavbarPosition } from '@renderer/hooks/useSettings'
|
||||
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||
import { AgentSettingsPopup, SessionSettingsPopup } from '@renderer/pages/settings/AgentSettings'
|
||||
import { AgentLabel, SessionLabel } from '@renderer/pages/settings/AgentSettings/shared'
|
||||
import type { AgentEntity, ApiModel } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { Menu, PanelLeftClose, PanelRightClose } from 'lucide-react'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import SelectAgentBaseModelButton from '../../SelectAgentBaseModelButton'
|
||||
import Tools from '../Tools'
|
||||
import AgentSidePanelDrawer from '../AgentSidePanelDrawer'
|
||||
import SelectAgentBaseModelButton from '../SelectAgentBaseModelButton'
|
||||
import OpenExternalAppButton from './OpenExternalAppButton'
|
||||
import SessionWorkspaceMeta from './SessionWorkspaceMeta'
|
||||
import Tools from './Tools'
|
||||
|
||||
type AgentContentProps = {
|
||||
activeAgent: AgentEntity
|
||||
}
|
||||
|
||||
const AgentContent = ({ activeAgent }: AgentContentProps) => {
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { isTopNavbar } = useNavbarPosition()
|
||||
const { session: activeSession } = useActiveSession()
|
||||
const { updateModel } = useUpdateSession(activeAgent?.id ?? null)
|
||||
|
||||
@@ -29,9 +39,36 @@ const AgentContent = ({ activeAgent }: AgentContentProps) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-w-0 shrink overflow-x-auto pr-2">
|
||||
<HorizontalScrollContainer className="ml-2 min-w-0 flex-initial">
|
||||
<div className="flex w-full justify-between pr-2">
|
||||
<div className="flex min-w-0 shrink items-center">
|
||||
{isTopNavbar && showAssistants && (
|
||||
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon onClick={toggleShowAssistants}>
|
||||
<PanelLeftClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isTopNavbar && !showAssistants && (
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8} placement="right">
|
||||
<NavbarIcon onClick={() => toggleShowAssistants()} style={{ marginRight: 8 }}>
|
||||
<PanelRightClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
<AnimatePresence initial={false}>
|
||||
{!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={() => AgentSidePanelDrawer.show()} style={{ marginRight: 5 }}>
|
||||
<Menu size={18} />
|
||||
</NavbarIcon>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<HorizontalScrollContainer className="ml-2 min-w-0 flex-initial shrink">
|
||||
<div className="flex flex-nowrap items-center gap-2">
|
||||
{/* Agent Label */}
|
||||
<div
|
||||
@@ -88,7 +125,7 @@ const AgentContent = ({ activeAgent }: AgentContentProps) => {
|
||||
)}
|
||||
<Tools />
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import NavbarIcon from '@renderer/components/NavbarIcon'
|
||||
import { Drawer, Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { Settings2 } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import AgentSettingsTab from '../../../AgentSettingsTab'
|
||||
|
||||
const SettingsButton = () => {
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon onClick={() => setSettingsOpen(true)}>
|
||||
<Settings2 size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
<Drawer
|
||||
placement="right"
|
||||
open={settingsOpen}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
width="var(--assistants-width)"
|
||||
closable={false}
|
||||
styles={{ body: { padding: 0, paddingTop: 'var(--navbar-height)' } }}>
|
||||
<AgentSettingsTab />
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsButton
|
||||
@@ -0,0 +1,69 @@
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import NavbarIcon from '@renderer/components/NavbarIcon'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setNarrowMode } from '@renderer/store/settings'
|
||||
import { Tooltip } from 'antd'
|
||||
import { PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SettingsButton from './SettingsButton'
|
||||
|
||||
const Tools = () => {
|
||||
const { t } = useTranslation()
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
const { isTopNavbar } = useNavbarPosition()
|
||||
const { topicPosition, narrowMode } = useSettings()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const handleNarrowModeToggle = async () => {
|
||||
await modelGenerating()
|
||||
dispatch(setNarrowMode(!narrowMode))
|
||||
}
|
||||
|
||||
return (
|
||||
<HStack alignItems="center" gap={8}>
|
||||
<SettingsButton />
|
||||
{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>
|
||||
)}
|
||||
{isTopNavbar && topicPosition === 'right' && showTopics && (
|
||||
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={2}>
|
||||
<NavbarIcon onClick={toggleShowTopics}>
|
||||
<PanelRightClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const NarrowIcon = styled(NavbarIcon)`
|
||||
@media (max-width: 1000px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default Tools
|
||||
@@ -0,0 +1,45 @@
|
||||
import { NavbarHeader } from '@renderer/components/app/Navbar'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { 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'
|
||||
import type { AgentEntity } from '@renderer/types'
|
||||
import { cn } from '@renderer/utils'
|
||||
|
||||
import AgentContent from './AgentContent'
|
||||
|
||||
interface Props {
|
||||
activeAgent: AgentEntity
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AgentChatNavbar = ({ activeAgent, className }: Props) => {
|
||||
const { toggleShowAssistants } = useShowAssistants()
|
||||
const { topicPosition } = useSettings()
|
||||
const { toggleShowTopics } = useShowTopics()
|
||||
|
||||
useShortcut('toggle_show_assistants', toggleShowAssistants)
|
||||
|
||||
useShortcut('toggle_show_topics', () => {
|
||||
if (topicPosition === 'right') {
|
||||
toggleShowTopics()
|
||||
} else {
|
||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||
}
|
||||
})
|
||||
|
||||
useShortcut('search_message', () => {
|
||||
SearchPopup.show()
|
||||
})
|
||||
|
||||
return (
|
||||
<NavbarHeader className={cn('agent-navbar h-(--navbar-height)', className)}>
|
||||
<div className="flex h-full min-w-0 flex-1 shrink items-center overflow-auto">
|
||||
<AgentContent activeAgent={activeAgent} />
|
||||
</div>
|
||||
</NavbarHeader>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentChatNavbar
|
||||
@@ -8,7 +8,6 @@ import { cn } from '@renderer/utils'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Dropdown, Tooltip } from 'antd'
|
||||
import { Bot, MoreVertical } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -21,7 +20,7 @@ interface AgentItemProps {
|
||||
onPress: () => void
|
||||
}
|
||||
|
||||
const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) => {
|
||||
const AgentItem = ({ agent, isActive, onDelete, onPress }: AgentItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { clickAssistantToShowTopic, topicPosition, assistantIconType } = useSettings()
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
@@ -87,7 +86,7 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) =
|
||||
trigger={['click']}
|
||||
popupRender={(menu) => <div onPointerDown={(e) => e.stopPropagation()}>{menu}</div>}>
|
||||
<MenuButton onClick={handleMenuButtonClick}>
|
||||
<MoreVertical size={14} className="text-[var(--color-text-secondary)]" />
|
||||
<MoreVertical size={14} className="text-(--color-text-secondary)" />
|
||||
</MenuButton>
|
||||
</Dropdown>
|
||||
)}
|
||||
@@ -105,9 +104,9 @@ export const Container: React.FC<{ isActive?: boolean } & React.HTMLAttributes<H
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-[37px] w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-[var(--list-item-border-radius)] border border-transparent px-2',
|
||||
!isActive && 'hover:bg-[var(--color-list-item-hover)]',
|
||||
isActive && 'bg-[var(--color-list-item)] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||
'relative flex h-9.25 w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-(--list-item-border-radius) border border-transparent px-2',
|
||||
!isActive && 'hover:bg-(--color-list-item-hover)',
|
||||
isActive && 'bg-(--color-list-item) shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -116,7 +115,7 @@ export const Container: React.FC<{ isActive?: boolean } & React.HTMLAttributes<H
|
||||
|
||||
export const AssistantNameRow: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||
<div
|
||||
className={cn('flex min-w-0 flex-1 flex-row items-center gap-2 text-[13px] text-[var(--color-text)]', className)}
|
||||
className={cn('flex min-w-0 flex-1 flex-row items-center gap-2 text-(--color-text) text-[13px]', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -128,7 +127,7 @@ export const AgentNameWrapper: React.FC<React.HTMLAttributes<HTMLDivElement>> =
|
||||
export const MenuButton: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-[22px] min-h-[22px] min-w-[22px] flex-row items-center justify-center rounded-[11px] border-[0.5px] border-[var(--color-border)] bg-[var(--color-background)] px-[5px]',
|
||||
'flex h-5.5 min-h-5.5 min-w-5.5 flex-row items-center justify-center rounded-[11px] border-(--color-border) border-[0.5px] bg-(--color-background) px-1.25',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -148,7 +147,7 @@ export const BotIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ ...pro
|
||||
|
||||
export const SessionCount: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||
<div
|
||||
className={cn('flex flex-row items-center justify-center rounded-full text-[var(--color-text)] text-xs', className)}
|
||||
className={cn('flex flex-row items-center justify-center rounded-full text-(--color-text) text-xs', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -10,6 +10,17 @@ import { getModel } from '@renderer/hooks/useModel'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTextareaResize } from '@renderer/hooks/useTextareaResize'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { InputbarCore } from '@renderer/pages/home/Inputbar/components/InputbarCore'
|
||||
import {
|
||||
InputbarToolsProvider,
|
||||
useInputbarToolsDispatch,
|
||||
useInputbarToolsInternalDispatch,
|
||||
useInputbarToolsState
|
||||
} from '@renderer/pages/home/Inputbar/context/InputbarToolsProvider'
|
||||
import InputbarTools from '@renderer/pages/home/Inputbar/InputbarTools'
|
||||
import { getInputbarConfig } from '@renderer/pages/home/Inputbar/registry'
|
||||
import type { ToolContext } from '@renderer/pages/home/Inputbar/types'
|
||||
import { TopicType } from '@renderer/pages/home/Inputbar/types'
|
||||
import { CacheService } from '@renderer/services/CacheService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { pauseTrace } from '@renderer/services/SpanManagerService'
|
||||
@@ -32,18 +43,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import { InputbarCore } from './components/InputbarCore'
|
||||
import {
|
||||
InputbarToolsProvider,
|
||||
useInputbarToolsDispatch,
|
||||
useInputbarToolsInternalDispatch,
|
||||
useInputbarToolsState
|
||||
} from './context/InputbarToolsProvider'
|
||||
import InputbarTools from './InputbarTools'
|
||||
import { getInputbarConfig } from './registry'
|
||||
import type { ToolContext } from './types'
|
||||
import { TopicType } from './types'
|
||||
|
||||
const logger = loggerService.withContext('AgentSessionInputbar')
|
||||
|
||||
const DRAFT_CACHE_TTL = 24 * 60 * 60 * 1000 // 24 hours
|
||||
@@ -55,7 +54,7 @@ type Props = {
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
const AgentSessionInputbar = ({ agentId, sessionId }: Props) => {
|
||||
const { session } = useSession(agentId, sessionId)
|
||||
// FIXME: 不应该使用ref将action传到context提供给tool,权宜之计
|
||||
const actionsRef = useRef({
|
||||
@@ -4,19 +4,17 @@ import { useSession } from '@renderer/hooks/agents/useSession'
|
||||
import { useTopicMessages } from '@renderer/hooks/useMessageOperations'
|
||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import MessageAnchorLine from '@renderer/pages/home/Messages/MessageAnchorLine'
|
||||
import MessageGroup from '@renderer/pages/home/Messages/MessageGroup'
|
||||
import NarrowLayout from '@renderer/pages/home/Messages/NarrowLayout'
|
||||
import PermissionModeDisplay from '@renderer/pages/home/Messages/PermissionModeDisplay'
|
||||
import { MessagesContainer, ScrollContainer } from '@renderer/pages/home/Messages/shared'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { getGroupedMessages } from '@renderer/services/MessagesService'
|
||||
import { type Topic, TopicType } from '@renderer/types'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { Spin } from 'antd'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import MessageAnchorLine from './MessageAnchorLine'
|
||||
import MessageGroup from './MessageGroup'
|
||||
import NarrowLayout from './NarrowLayout'
|
||||
import PermissionModeDisplay from './PermissionModeDisplay'
|
||||
import { MessagesContainer, ScrollContainer } from './shared'
|
||||
|
||||
const logger = loggerService.withContext('AgentSessionMessages')
|
||||
|
||||
@@ -25,7 +23,7 @@ type Props = {
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
const AgentSessionMessages: React.FC<Props> = ({ agentId, sessionId }) => {
|
||||
const AgentSessionMessages = ({ agentId, sessionId }: Props) => {
|
||||
const { session } = useSession(agentId, sessionId)
|
||||
const sessionTopicId = useMemo(() => buildAgentSessionTopicId(sessionId), [sessionId])
|
||||
// Use the same hook as Messages.tsx for consistent behavior
|
||||
@@ -101,9 +99,9 @@ const AgentSessionMessages: React.FC<Props> = ({ agentId, sessionId }) => {
|
||||
) : session ? (
|
||||
<PermissionModeDisplay session={session} agentId={agentId} />
|
||||
) : (
|
||||
<LoadingState>
|
||||
<div className="flex items-center justify-center py-5">
|
||||
<Spin size="small" />
|
||||
</LoadingState>
|
||||
</div>
|
||||
)}
|
||||
</ScrollContainer>
|
||||
</ContextMenu>
|
||||
@@ -113,13 +111,6 @@ const AgentSessionMessages: React.FC<Props> = ({ agentId, sessionId }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const LoadingState = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
`
|
||||
|
||||
const FALLBACK_TIMESTAMP = '1970-01-01T00:00:00.000Z'
|
||||
|
||||
export default memo(AgentSessionMessages)
|
||||
@@ -0,0 +1,71 @@
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { Drawer } from 'antd'
|
||||
import { useState } from 'react'
|
||||
|
||||
import AgentSidePanel from '../AgentSidePanel'
|
||||
|
||||
interface Props {
|
||||
resolve: () => void
|
||||
}
|
||||
|
||||
const PopupContainer = ({ resolve }: Props) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
const onClose = () => {
|
||||
setOpen(false)
|
||||
setTimeoutTimer('onClose', resolve, 300)
|
||||
}
|
||||
|
||||
AgentSidePanelDrawer.hide = onClose
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={null}
|
||||
height="100vh"
|
||||
placement="left"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
style={{ width: 'var(--assistants-width)' }}
|
||||
styles={{
|
||||
header: { display: 'none' },
|
||||
body: {
|
||||
display: 'flex',
|
||||
padding: 0,
|
||||
paddingTop: isMac ? 'var(--navbar-height)' : 0,
|
||||
height: 'calc(100vh - var(--navbar-height))',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'var(--color-background-opacity)'
|
||||
},
|
||||
wrapper: {
|
||||
width: 'var(--assistants-width)'
|
||||
}
|
||||
}}>
|
||||
<AgentSidePanel onSelectItem={onClose} />
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'AgentSidePanelDrawer'
|
||||
|
||||
export default class AgentSidePanelDrawer {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show() {
|
||||
return new Promise<void>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
resolve={() => {
|
||||
resolve()
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { apiModelAdapter } from '@renderer/utils/model'
|
||||
import type { ButtonProps } from 'antd'
|
||||
import { Button } from 'antd'
|
||||
import { ChevronsUpDown } from 'lucide-react'
|
||||
import type { CSSProperties, FC } from 'react'
|
||||
import type { CSSProperties } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
@@ -34,7 +34,7 @@ interface Props {
|
||||
containerClassName?: string
|
||||
}
|
||||
|
||||
const SelectAgentBaseModelButton: FC<Props> = ({
|
||||
const SelectAgentBaseModelButton = ({
|
||||
agentBase: agent,
|
||||
onSelect,
|
||||
isDisabled,
|
||||
@@ -45,7 +45,7 @@ const SelectAgentBaseModelButton: FC<Props> = ({
|
||||
fontSize = 12,
|
||||
iconSize = 14,
|
||||
containerClassName
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const model = useApiModel({ id: agent?.model })
|
||||
|
||||
@@ -89,7 +89,7 @@ const SelectAgentBaseModelButton: FC<Props> = ({
|
||||
<div className={containerClassName || 'flex w-full items-center gap-1.5'}>
|
||||
<div className="flex flex-1 items-center gap-1.5 overflow-x-hidden">
|
||||
<ModelAvatar model={model ? apiModelAdapter(model) : undefined} size={avatarSize} />
|
||||
<span className="truncate text-[var(--color-text)]">
|
||||
<span className="truncate text-(--color-text)">
|
||||
{model ? model.name : t('button.select_model')} {providerName ? ' | ' + providerName : ''}
|
||||
</span>
|
||||
</div>
|
||||
@@ -16,7 +16,6 @@ import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Dropdown, Tooltip } from 'antd'
|
||||
import { MenuIcon, Sparkles, XIcon } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import React, { memo, startTransition, useDeferredValue, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -31,7 +30,7 @@ interface SessionItemProps {
|
||||
onPress: () => void
|
||||
}
|
||||
|
||||
const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress }) => {
|
||||
const SessionItem = ({ session, agentId, onDelete, onPress }: SessionItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { chat } = useRuntime()
|
||||
const { updateSession } = useUpdateSession(agentId)
|
||||
@@ -1,10 +1,11 @@
|
||||
import AddButton from '@renderer/components/AddButton'
|
||||
import { DynamicVirtualList, type DynamicVirtualListRef } from '@renderer/components/VirtualList'
|
||||
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { newMessagesActions } from '@renderer/store/newMessage'
|
||||
import { setActiveSessionIdAction, setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import { setActiveSessionIdAction } from '@renderer/store/runtime'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { Alert, Button, Spin } from 'antd'
|
||||
@@ -14,17 +15,17 @@ import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AddButton from './AddButton'
|
||||
import SessionItem from './SessionItem'
|
||||
|
||||
interface SessionsProps {
|
||||
agentId: string
|
||||
onSelectItem?: () => void
|
||||
}
|
||||
|
||||
const LOAD_MORE_THRESHOLD = 100
|
||||
const SCROLL_THROTTLE_DELAY = 150
|
||||
|
||||
const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
const Sessions = ({ agentId, onSelectItem }: SessionsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { sessions, isLoading, error, deleteSession, hasMore, loadMore, isLoadingMore, isValidating, reload } =
|
||||
useSessions(agentId)
|
||||
@@ -77,7 +78,6 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
const setActiveSessionId = useCallback(
|
||||
(agentId: string, sessionId: string | null) => {
|
||||
dispatch(setActiveSessionIdAction({ agentId, sessionId }))
|
||||
dispatch(setActiveTopicOrSessionAction('session'))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
@@ -172,7 +172,10 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
session={session}
|
||||
agentId={agentId}
|
||||
onDelete={() => handleDeleteSession(session.id)}
|
||||
onPress={() => setActiveSessionId(agentId, session.id)}
|
||||
onPress={() => {
|
||||
setActiveSessionId(agentId, session.id)
|
||||
onSelectItem?.()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</StyledVirtualList>
|
||||
@@ -0,0 +1,41 @@
|
||||
import AgentModalPopup from '@renderer/components/Popups/agent/AgentModal'
|
||||
import { useActiveAgent } from '@renderer/hooks/agents/useActiveAgent'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import type { AgentEntity } from '@renderer/types'
|
||||
import { Button } from 'antd'
|
||||
import { Bot, Plus } from 'lucide-react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import AgentStatusScreen from './AgentStatusScreen'
|
||||
|
||||
const AgentEmpty = () => {
|
||||
const { t } = useTranslation()
|
||||
const { apiServerRunning, startApiServer } = useApiServer()
|
||||
const { setActiveAgentId } = useActiveAgent()
|
||||
|
||||
const handleAddAgent = useCallback(() => {
|
||||
!apiServerRunning && startApiServer()
|
||||
AgentModalPopup.show({
|
||||
afterSubmit: (agent: AgentEntity) => {
|
||||
setActiveAgentId(agent.id)
|
||||
}
|
||||
})
|
||||
}, [apiServerRunning, startApiServer, setActiveAgentId])
|
||||
|
||||
return (
|
||||
<AgentStatusScreen
|
||||
icon={Bot}
|
||||
iconClassName="text-(--color-text-secondary)"
|
||||
title={t('agent.empty.title')}
|
||||
description={t('agent.empty.description')}
|
||||
actions={
|
||||
<Button type="default" icon={<Plus size={16} />} onClick={handleAddAgent}>
|
||||
{t('agent.add.title')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentEmpty
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import { Button } from 'antd'
|
||||
import { ServerOff, Settings } from 'lucide-react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import AgentStatusScreen from './AgentStatusScreen'
|
||||
|
||||
const AgentServerDisabled = () => {
|
||||
const { t } = useTranslation()
|
||||
const { startApiServer } = useApiServer()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleGoToSettings = useCallback(() => {
|
||||
navigate('/settings/api-server')
|
||||
}, [navigate])
|
||||
|
||||
return (
|
||||
<AgentStatusScreen
|
||||
icon={ServerOff}
|
||||
iconClassName="text-(--color-status-warning)"
|
||||
title={t('agent.warning.enable_server')}
|
||||
description={t('agent.warning.enable_server_description')}
|
||||
actions={
|
||||
<>
|
||||
<Button type="primary" onClick={startApiServer}>
|
||||
{t('agent.warning.enable_and_start')}
|
||||
</Button>
|
||||
<Button type="default" icon={<Settings size={16} />} onClick={handleGoToSettings}>
|
||||
{t('common.go_to_settings')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentServerDisabled
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import { Button } from 'antd'
|
||||
import { ServerCrash, Settings } from 'lucide-react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import AgentStatusScreen from './AgentStatusScreen'
|
||||
|
||||
const AgentServerStopped = () => {
|
||||
const { t } = useTranslation()
|
||||
const { startApiServer } = useApiServer()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleGoToSettings = useCallback(() => {
|
||||
navigate('/settings/api-server')
|
||||
}, [navigate])
|
||||
|
||||
return (
|
||||
<AgentStatusScreen
|
||||
icon={ServerCrash}
|
||||
iconClassName="text-(--color-error)"
|
||||
title={t('agent.warning.server_not_running')}
|
||||
description={t('agent.warning.server_not_running_description')}
|
||||
actions={
|
||||
<>
|
||||
<Button type="primary" onClick={startApiServer}>
|
||||
{t('apiServer.actions.start')}
|
||||
</Button>
|
||||
<Button type="default" icon={<Settings size={16} />} onClick={handleGoToSettings}>
|
||||
{t('common.go_to_settings')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentServerStopped
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { motion } from 'motion/react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface AgentStatusScreenProps {
|
||||
icon: LucideIcon
|
||||
iconClassName: string
|
||||
title: string
|
||||
description: string
|
||||
actions: ReactNode
|
||||
}
|
||||
|
||||
const AgentStatusScreen = ({ icon: Icon, iconClassName, title, description, actions }: AgentStatusScreenProps) => {
|
||||
return (
|
||||
<motion.div
|
||||
className="flex h-full w-full flex-col items-center justify-center gap-4"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3, ease: 'easeOut' }}>
|
||||
<Icon size={56} strokeWidth={1.2} className={iconClassName} />
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<h3 className="m-0 font-medium text-(--color-text) text-base">{title}</h3>
|
||||
<p className="m-0 max-w-xs text-center text-(--color-text-secondary) text-sm">{description}</p>
|
||||
</div>
|
||||
<div className="flex gap-3">{actions}</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentStatusScreen
|
||||
3
src/renderer/src/pages/agents/components/status/index.ts
Normal file
3
src/renderer/src/pages/agents/components/status/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as AgentEmpty } from './AgentEmpty'
|
||||
export { default as AgentServerDisabled } from './AgentServerDisabled'
|
||||
export { default as AgentServerStopped } from './AgentServerStopped'
|
||||
@@ -7,10 +7,8 @@ import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { SelectModelPopup } from '@renderer/components/Popups/SelectModelPopup'
|
||||
import { QuickPanelProvider } from '@renderer/components/QuickPanel'
|
||||
import { isEmbeddingModel, isRerankModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
@@ -18,24 +16,19 @@ import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import type { Assistant, Model, Topic } from '@renderer/types'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { Alert, Flex } from 'antd'
|
||||
import { Flex } from 'antd'
|
||||
import { debounce } from 'lodash'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ChatNavbar from './components/ChatNavBar'
|
||||
import AgentSessionInputbar from './Inputbar/AgentSessionInputbar'
|
||||
import { PinnedTodoPanel } from './Inputbar/components/PinnedTodoPanel'
|
||||
import Inputbar from './Inputbar/Inputbar'
|
||||
import AgentSessionMessages from './Messages/AgentSessionMessages'
|
||||
import ChatNavigation from './Messages/ChatNavigation'
|
||||
import Messages from './Messages/Messages'
|
||||
import NarrowLayout from './Messages/NarrowLayout'
|
||||
import Tabs from './Tabs'
|
||||
|
||||
const logger = loggerService.withContext('Chat')
|
||||
@@ -54,12 +47,6 @@ const Chat: FC<Props> = (props) => {
|
||||
const { showTopics } = useShowTopics()
|
||||
const { isMultiSelectMode } = useChatContext(props.activeTopic)
|
||||
const { isTopNavbar } = useNavbarPosition()
|
||||
const { chat } = useRuntime()
|
||||
const { activeTopicOrSession, activeAgentId, activeSessionIdMap } = chat
|
||||
const activeSessionId = activeAgentId ? activeSessionIdMap[activeAgentId] : null
|
||||
const { apiServer } = useSettings()
|
||||
const sessionAgentId = activeTopicOrSession === 'session' ? activeAgentId : null
|
||||
const { createDefaultSession } = useCreateDefaultSession(sessionAgentId)
|
||||
|
||||
const mainRef = React.useRef<HTMLDivElement>(null)
|
||||
const contentSearchRef = React.useRef<ContentSearchRef>(null)
|
||||
@@ -98,21 +85,6 @@ const Chat: FC<Props> = (props) => {
|
||||
}
|
||||
})
|
||||
|
||||
useShortcut(
|
||||
'new_topic',
|
||||
() => {
|
||||
if (activeTopicOrSession !== 'session' || !activeAgentId) {
|
||||
return
|
||||
}
|
||||
void createDefaultSession()
|
||||
},
|
||||
{
|
||||
enabled: activeTopicOrSession === 'session',
|
||||
preventDefault: true,
|
||||
enableOnFormTags: true
|
||||
}
|
||||
)
|
||||
|
||||
useShortcut('select_model', async () => {
|
||||
const modelFilter = (m: Model) => !isEmbeddingModel(m) && !isRerankModel(m)
|
||||
const selectedModel = await SelectModelPopup.show({ model: assistant?.model, filter: modelFilter })
|
||||
@@ -178,20 +150,6 @@ const Chat: FC<Props> = (props) => {
|
||||
|
||||
const mainHeight = isTopNavbar ? 'calc(100vh - var(--navbar-height) - 6px)' : 'calc(100vh - var(--navbar-height))'
|
||||
|
||||
// TODO: more info
|
||||
const AgentInvalid = useCallback(() => {
|
||||
return <Alert type="warning" message={t('chat.alerts.select_agent')} style={{ margin: '5px 16px' }} />
|
||||
}, [t])
|
||||
|
||||
// TODO: more info
|
||||
const SessionInvalid = useCallback(() => {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Alert type="warning" message={t('chat.alerts.create_session')} style={{ margin: '5px 16px' }} />
|
||||
</div>
|
||||
)
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<Container id="chat" className={classNames([messageStyle, { 'multi-select-mode': isMultiSelectMode }])}>
|
||||
<HStack>
|
||||
@@ -217,47 +175,23 @@ const Chat: FC<Props> = (props) => {
|
||||
<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 && (
|
||||
<>
|
||||
{!apiServer.enabled ? (
|
||||
<Alert type="warning" message={t('agent.warning.enable_server')} style={{ margin: '5px 16px' }} />
|
||||
) : (
|
||||
<>
|
||||
<AgentSessionMessages agentId={activeAgentId} sessionId={activeSessionId} />
|
||||
<PinnedTodoPanelWrapper>
|
||||
<NarrowLayout>
|
||||
<PinnedTodoPanel topicId={buildAgentSessionTopicId(activeSessionId)} />
|
||||
</NarrowLayout>
|
||||
</PinnedTodoPanelWrapper>
|
||||
</>
|
||||
)}
|
||||
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
|
||||
<AgentSessionInputbar agentId={activeAgentId} sessionId={activeSessionId} />
|
||||
</>
|
||||
)}
|
||||
<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} />
|
||||
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
|
||||
</div>
|
||||
</QuickPanelProvider>
|
||||
@@ -311,9 +245,4 @@ const Main = styled(Flex)`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const PinnedTodoPanelWrapper = styled.div`
|
||||
margin-top: auto;
|
||||
padding: 0 18px 8px 18px;
|
||||
`
|
||||
|
||||
export default Chat
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
||||
import { useAgentSessionInitializer } from '@renderer/hooks/agents/useAgentSessionInitializer'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
import { newMessagesActions } from '@renderer/store/newMessage'
|
||||
import { setActiveAgentId, setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, SECOND_MIN_WINDOW_WIDTH } from '@shared/config/constant'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
@@ -27,9 +24,6 @@ const HomePage: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { isLeftNavbar } = useNavbarPosition()
|
||||
|
||||
// Initialize agent session hook
|
||||
useAgentSessionInitializer()
|
||||
|
||||
const location = useLocation()
|
||||
const state = location.state
|
||||
|
||||
@@ -39,26 +33,20 @@ const HomePage: FC = () => {
|
||||
const { activeTopic, setActiveTopic: _setActiveTopic } = useActiveTopic(activeAssistant?.id ?? '', state?.topic)
|
||||
const { showAssistants, showTopics, topicPosition } = useSettings()
|
||||
const dispatch = useDispatch()
|
||||
const { chat } = useRuntime()
|
||||
const { activeTopicOrSession } = chat
|
||||
|
||||
_activeAssistant = activeAssistant
|
||||
|
||||
const setActiveAssistant = useCallback(
|
||||
// TODO: allow to set it as null.
|
||||
(newAssistant: Assistant) => {
|
||||
if (newAssistant.id === activeAssistant?.id) return
|
||||
startTransition(() => {
|
||||
_setActiveAssistant(newAssistant)
|
||||
if (newAssistant.id !== 'fake') {
|
||||
dispatch(setActiveAgentId(null))
|
||||
}
|
||||
// 同步更新 active topic,避免不必要的重新渲染
|
||||
const newTopic = newAssistant.topics[0]
|
||||
_setActiveTopic((prev) => (newTopic?.id === prev.id ? prev : newTopic))
|
||||
})
|
||||
},
|
||||
[_setActiveTopic, activeAssistant?.id, dispatch]
|
||||
[_setActiveTopic, activeAssistant?.id]
|
||||
)
|
||||
|
||||
const setActiveTopic = useCallback(
|
||||
@@ -66,7 +54,6 @@ const HomePage: FC = () => {
|
||||
startTransition(() => {
|
||||
_setActiveTopic((prev) => (newTopic?.id === prev.id ? prev : newTopic))
|
||||
dispatch(newMessagesActions.setTopicFulfilled({ topicId: newTopic.id, fulfilled: false }))
|
||||
dispatch(setActiveTopicOrSessionAction('topic'))
|
||||
})
|
||||
},
|
||||
[_setActiveTopic, dispatch]
|
||||
@@ -100,7 +87,6 @@ const HomePage: FC = () => {
|
||||
setActiveTopic={setActiveTopic}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
position="left"
|
||||
activeTopicOrSession={activeTopicOrSession}
|
||||
/>
|
||||
)}
|
||||
<ContentContainer id={isLeftNavbar ? 'content-container' : undefined}>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Sparkle } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { memo, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import MessageTokens from './MessageTokens'
|
||||
@@ -43,9 +44,10 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, topic, isGro
|
||||
const { theme } = useTheme()
|
||||
const { userName, sidebarIcons } = useSettings()
|
||||
const { chat } = useRuntime()
|
||||
const { activeTopicOrSession, activeAgentId } = chat
|
||||
const { activeAgentId } = chat
|
||||
const { agent } = useAgent(activeAgentId)
|
||||
const isAgentView = activeTopicOrSession === 'session'
|
||||
const { pathname } = useLocation()
|
||||
const isAgentView = pathname.startsWith('/agents')
|
||||
const { t } = useTranslation()
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
const { openMinappById } = useMinappPopup()
|
||||
|
||||
@@ -26,16 +26,9 @@ interface Props {
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
setActiveAssistant: (assistant: Assistant) => void
|
||||
position: 'left' | 'right'
|
||||
activeTopicOrSession?: 'topic' | 'session'
|
||||
}
|
||||
|
||||
const HeaderNavbar: FC<Props> = ({
|
||||
activeAssistant,
|
||||
setActiveAssistant,
|
||||
activeTopic,
|
||||
setActiveTopic,
|
||||
activeTopicOrSession
|
||||
}) => {
|
||||
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => {
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { topicPosition, narrowMode } = useSettings()
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
@@ -121,10 +114,9 @@ const HeaderNavbar: FC<Props> = ({
|
||||
<NavbarRight
|
||||
style={{
|
||||
justifyContent: 'flex-end',
|
||||
flex: activeTopicOrSession === 'topic' ? 1 : 'none',
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
paddingRight: '15px',
|
||||
minWidth: activeTopicOrSession === 'topic' ? '' : 'auto'
|
||||
paddingRight: '15px'
|
||||
}}
|
||||
className="home-navbar-right">
|
||||
<HStack alignItems="center" gap={6}>
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
|
||||
import { useTags } from '@renderer/hooks/useTags'
|
||||
import type { Assistant, AssistantsSortType, Topic } from '@renderer/types'
|
||||
import type { RootState } from '@renderer/store'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import type { Assistant, AssistantsSortType } from '@renderer/types'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import * as tinyPinyin from 'tiny-pinyin'
|
||||
|
||||
import UnifiedAddButton from './components/UnifiedAddButton'
|
||||
import { UnifiedList } from './components/UnifiedList'
|
||||
import { UnifiedTagGroups } from './components/UnifiedTagGroups'
|
||||
import { useActiveAgent } from './hooks/useActiveAgent'
|
||||
import { useUnifiedGrouping } from './hooks/useUnifiedGrouping'
|
||||
import { useUnifiedItems } from './hooks/useUnifiedItems'
|
||||
import { useUnifiedSorting } from './hooks/useUnifiedSorting'
|
||||
import AssistantAddButton from './components/AssistantAddButton'
|
||||
import { AssistantList } from './components/AssistantList'
|
||||
import { AssistantTagGroups } from './components/AssistantTagGroups'
|
||||
|
||||
interface AssistantsTabProps {
|
||||
activeAssistant: Assistant
|
||||
@@ -27,52 +24,89 @@ interface AssistantsTabProps {
|
||||
onCreateDefaultAssistant: () => void
|
||||
}
|
||||
|
||||
const selectTagsOrder = createSelector(
|
||||
[(state: RootState) => state.assistants],
|
||||
(assistants) => assistants.tagsOrder ?? []
|
||||
)
|
||||
|
||||
const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
const { activeAssistant, setActiveAssistant, onCreateAssistant, onCreateDefaultAssistant } = props
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { apiServerConfig } = useApiServer()
|
||||
const apiServerEnabled = apiServerConfig.enabled
|
||||
const { chat } = useRuntime()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Agent related hooks
|
||||
const { agents, deleteAgent, isLoading: agentsLoading, error: agentsError } = useAgents()
|
||||
const { activeAgentId } = chat
|
||||
const { setActiveAgentId } = useActiveAgent()
|
||||
|
||||
// Assistant related hooks
|
||||
const { assistants, removeAssistant, copyAssistant, updateAssistants } = useAssistants()
|
||||
const { addAssistantPreset } = useAssistantPresets()
|
||||
const { collapsedTags, toggleTagCollapse } = useTags()
|
||||
const { assistantsTabSortType = 'list', setAssistantsTabSortType } = useAssistantsTabSortType()
|
||||
const [dragging, setDragging] = useState(false)
|
||||
|
||||
// Unified items management
|
||||
const { unifiedItems, handleUnifiedListReorder } = useUnifiedItems({
|
||||
agents,
|
||||
assistants,
|
||||
apiServerEnabled,
|
||||
agentsLoading,
|
||||
agentsError,
|
||||
updateAssistants
|
||||
})
|
||||
const savedTagsOrder = useAppSelector(selectTagsOrder)
|
||||
|
||||
// Sorting
|
||||
const { sortByPinyinAsc, sortByPinyinDesc } = useUnifiedSorting({
|
||||
unifiedItems,
|
||||
updateAssistants
|
||||
})
|
||||
const sortByPinyin = useCallback(
|
||||
(isAscending: boolean) => {
|
||||
const sorted = [...assistants].sort((a, b) => {
|
||||
const pinyinA = tinyPinyin.convertToPinyin(a.name, '', true)
|
||||
const pinyinB = tinyPinyin.convertToPinyin(b.name, '', true)
|
||||
return isAscending ? pinyinA.localeCompare(pinyinB) : pinyinB.localeCompare(pinyinA)
|
||||
})
|
||||
updateAssistants(sorted)
|
||||
},
|
||||
[assistants, updateAssistants]
|
||||
)
|
||||
|
||||
const sortByPinyinAsc = useCallback(() => sortByPinyin(true), [sortByPinyin])
|
||||
const sortByPinyinDesc = useCallback(() => sortByPinyin(false), [sortByPinyin])
|
||||
|
||||
// Grouping
|
||||
const { groupedUnifiedItems, handleUnifiedGroupReorder } = useUnifiedGrouping({
|
||||
unifiedItems,
|
||||
assistants,
|
||||
agents,
|
||||
apiServerEnabled,
|
||||
agentsLoading,
|
||||
agentsError,
|
||||
updateAssistants
|
||||
})
|
||||
const groupedAssistantItems = useMemo(() => {
|
||||
const groups = new Map<string, Assistant[]>()
|
||||
|
||||
assistants.forEach((assistant) => {
|
||||
const tags = assistant.tags?.length ? assistant.tags : [t('assistants.tags.untagged')]
|
||||
tags.forEach((tag) => {
|
||||
if (!groups.has(tag)) {
|
||||
groups.set(tag, [])
|
||||
}
|
||||
groups.get(tag)!.push(assistant)
|
||||
})
|
||||
})
|
||||
|
||||
const untaggedKey = t('assistants.tags.untagged')
|
||||
const sortedGroups = Array.from(groups.entries()).sort(([tagA], [tagB]) => {
|
||||
if (tagA === untaggedKey) return -1
|
||||
if (tagB === untaggedKey) return 1
|
||||
|
||||
if (savedTagsOrder.length > 0) {
|
||||
const indexA = savedTagsOrder.indexOf(tagA)
|
||||
const indexB = savedTagsOrder.indexOf(tagB)
|
||||
if (indexA !== -1 && indexB !== -1) return indexA - indexB
|
||||
if (indexA !== -1) return -1
|
||||
if (indexB !== -1) return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
return sortedGroups.map(([tag, items]) => ({ tag, items }))
|
||||
}, [assistants, t, savedTagsOrder])
|
||||
|
||||
const handleAssistantGroupReorder = useCallback(
|
||||
(tag: string, newGroupList: Assistant[]) => {
|
||||
let insertIndex = 0
|
||||
const updatedAssistants = 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 || a
|
||||
}
|
||||
return a
|
||||
})
|
||||
updateAssistants(updatedAssistants)
|
||||
},
|
||||
[assistants, t, updateAssistants]
|
||||
)
|
||||
|
||||
const onDeleteAssistant = useCallback(
|
||||
(assistant: Assistant) => {
|
||||
@@ -98,53 +132,22 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
[setAssistantsTabSortType]
|
||||
)
|
||||
|
||||
const handleAgentPress = useCallback(
|
||||
(agentId: string) => {
|
||||
setActiveAgentId(agentId)
|
||||
// TODO: should allow it to be null
|
||||
setActiveAssistant({
|
||||
id: 'fake',
|
||||
name: '',
|
||||
prompt: '',
|
||||
topics: [
|
||||
{
|
||||
id: 'fake',
|
||||
assistantId: 'fake',
|
||||
name: 'fake',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
messages: []
|
||||
} as unknown as Topic
|
||||
],
|
||||
type: 'chat'
|
||||
})
|
||||
},
|
||||
[setActiveAgentId, setActiveAssistant]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container className="assistants-tab" ref={containerRef}>
|
||||
<UnifiedAddButton
|
||||
onCreateAssistant={onCreateAssistant}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
setActiveAgentId={setActiveAgentId}
|
||||
/>
|
||||
<AssistantAddButton onCreateAssistant={onCreateAssistant} />
|
||||
|
||||
{assistantsTabSortType === 'tags' ? (
|
||||
<UnifiedTagGroups
|
||||
groupedItems={groupedUnifiedItems}
|
||||
<AssistantTagGroups
|
||||
groupedItems={groupedAssistantItems}
|
||||
activeAssistantId={activeAssistant.id}
|
||||
activeAgentId={activeAgentId}
|
||||
sortBy={assistantsTabSortType}
|
||||
collapsedTags={collapsedTags}
|
||||
onGroupReorder={handleUnifiedGroupReorder}
|
||||
onGroupReorder={handleAssistantGroupReorder}
|
||||
onDragStart={() => setDragging(true)}
|
||||
onDragEnd={() => setDragging(false)}
|
||||
onToggleTagCollapse={toggleTagCollapse}
|
||||
onAssistantSwitch={setActiveAssistant}
|
||||
onAssistantDelete={onDeleteAssistant}
|
||||
onAgentDelete={deleteAgent}
|
||||
onAgentPress={handleAgentPress}
|
||||
addPreset={addAssistantPreset}
|
||||
copyAssistant={copyAssistant}
|
||||
onCreateDefaultAssistant={onCreateDefaultAssistant}
|
||||
@@ -153,18 +156,15 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
sortByPinyinDesc={sortByPinyinDesc}
|
||||
/>
|
||||
) : (
|
||||
<UnifiedList
|
||||
items={unifiedItems}
|
||||
<AssistantList
|
||||
items={assistants}
|
||||
activeAssistantId={activeAssistant.id}
|
||||
activeAgentId={activeAgentId}
|
||||
sortBy={assistantsTabSortType}
|
||||
onReorder={handleUnifiedListReorder}
|
||||
onReorder={updateAssistants}
|
||||
onDragStart={() => setDragging(true)}
|
||||
onDragEnd={() => setDragging(false)}
|
||||
onAssistantSwitch={setActiveAssistant}
|
||||
onAssistantDelete={onDeleteAssistant}
|
||||
onAgentDelete={deleteAgent}
|
||||
onAgentPress={handleAgentPress}
|
||||
addPreset={addAssistantPreset}
|
||||
copyAssistant={copyAssistant}
|
||||
onCreateDefaultAssistant={onCreateDefaultAssistant}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { cn } from '@renderer/utils'
|
||||
import { Alert } from 'antd'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import Sessions from './components/Sessions'
|
||||
|
||||
interface SessionsTabProps {}
|
||||
|
||||
const SessionsTab: FC<SessionsTabProps> = () => {
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId } = chat
|
||||
const { t } = useTranslation()
|
||||
const { apiServer } = useSettings()
|
||||
|
||||
if (!apiServer.enabled) {
|
||||
return <Alert type="warning" message={t('agent.warning.enable_server')} style={{ margin: 10 }} />
|
||||
}
|
||||
|
||||
if (!activeAgentId) {
|
||||
return <Alert type="warning" message={'Select an agent'} style={{ margin: 10 }} />
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div className={cn('overflow-hidden', 'h-full')}>
|
||||
<Sessions agentId={activeAgentId} />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(SessionsTab)
|
||||
@@ -1,11 +1,7 @@
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import type { FC } from 'react'
|
||||
|
||||
import { Topics } from './components/Topics'
|
||||
import SessionsTab from './SessionsTab'
|
||||
|
||||
// const logger = loggerService.withContext('TopicsTab')
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@@ -15,15 +11,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const TopicsTab: FC<Props> = (props) => {
|
||||
const { chat } = useRuntime()
|
||||
const { activeTopicOrSession } = chat
|
||||
if (activeTopicOrSession === 'topic') {
|
||||
return <Topics {...props} />
|
||||
}
|
||||
if (activeTopicOrSession === 'session') {
|
||||
return <SessionsTab />
|
||||
}
|
||||
return 'Not a valid state.'
|
||||
return <Topics {...props} />
|
||||
}
|
||||
|
||||
export default TopicsTab
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import AddButton from '@renderer/components/AddButton'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface AssistantAddButtonProps {
|
||||
onCreateAssistant: () => void
|
||||
}
|
||||
|
||||
const AssistantAddButton: FC<AssistantAddButtonProps> = ({ onCreateAssistant }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="-mt-0.5 mb-1.5">
|
||||
<AddButton onClick={onCreateAssistant}>{t('chat.add.assistant.title')}</AddButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AssistantAddButton
|
||||
@@ -6,8 +6,6 @@ import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTags } from '@renderer/hooks/useTags'
|
||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import type { Assistant, AssistantsSortType } from '@renderer/types'
|
||||
import { cn, uuid } from '@renderer/utils'
|
||||
import { hasTopicPendingRequests } from '@renderer/utils/queue'
|
||||
@@ -70,7 +68,6 @@ const AssistantItem: FC<AssistantItemProps> = ({
|
||||
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
@@ -140,8 +137,7 @@ const AssistantItem: FC<AssistantItemProps> = ({
|
||||
}
|
||||
}
|
||||
onSwitch(assistant)
|
||||
dispatch(setActiveTopicOrSessionAction('topic'))
|
||||
}, [clickAssistantToShowTopic, onSwitch, assistant, dispatch, topicPosition])
|
||||
}, [clickAssistantToShowTopic, onSwitch, assistant, topicPosition])
|
||||
|
||||
const assistantName = useMemo(() => assistant.name || t('chat.default.name'), [assistant.name, t])
|
||||
const fullAssistantName = useMemo(
|
||||
@@ -177,7 +173,7 @@ const AssistantItem: FC<AssistantItemProps> = ({
|
||||
trigger={['click']}
|
||||
popupRender={(menu) => <div onPointerDown={(e) => e.stopPropagation()}>{menu}</div>}>
|
||||
<MenuButton onClick={handleMenuButtonClick}>
|
||||
<MoreVertical size={14} className="text-[var(--color-text-secondary)]" />
|
||||
<MoreVertical size={14} className="text-(--color-text-secondary)" />
|
||||
</MenuButton>
|
||||
</Dropdown>
|
||||
)}
|
||||
@@ -402,9 +398,9 @@ const Container = ({
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'relative flex h-[37px] w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-[var(--list-item-border-radius)] border-[0.5px] border-transparent px-2',
|
||||
!isActive && 'hover:bg-[var(--color-list-item-hover)]',
|
||||
isActive && 'bg-[var(--color-list-item)] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||
'relative flex h-9.25 w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-(--list-item-border-radius) border-[0.5px] border-transparent px-2',
|
||||
!isActive && 'hover:bg-(--color-list-item-hover)',
|
||||
isActive && 'bg-(--color-list-item) shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||
className
|
||||
)}>
|
||||
{children}
|
||||
@@ -418,7 +414,7 @@ const AssistantNameRow = ({
|
||||
}: PropsWithChildren<{} & React.HTMLAttributes<HTMLDivElement>>) => (
|
||||
<div
|
||||
{...props}
|
||||
className={cn('flex min-w-0 flex-1 flex-row items-center gap-2 text-[13px] text-[var(--color-text)]', className)}>
|
||||
className={cn('flex min-w-0 flex-1 flex-row items-center gap-2 text-(--color-text) text-[13px]', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@@ -443,7 +439,7 @@ const MenuButton = ({
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'absolute top-[6px] right-[9px] flex h-[22px] min-h-[22px] min-w-[22px] flex-row items-center justify-center rounded-[11px] border-[0.5px] border-[var(--color-border)] bg-[var(--color-background)] px-[5px]',
|
||||
'absolute top-1.5 right-2.25 flex h-5.5 min-h-5.5 min-w-5.5 flex-row items-center justify-center rounded-[11px] border-(--color-border) border-[0.5px] bg-(--color-background) px-1.25',
|
||||
className
|
||||
)}>
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { DraggableList } from '@renderer/components/DraggableList'
|
||||
import type { Assistant, AssistantsSortType } from '@renderer/types'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import AssistantItem from './AssistantItem'
|
||||
|
||||
interface AssistantListProps {
|
||||
items: Assistant[]
|
||||
activeAssistantId: string
|
||||
sortBy: AssistantsSortType
|
||||
onReorder: (newList: Assistant[]) => void
|
||||
onDragStart: () => void
|
||||
onDragEnd: () => void
|
||||
onAssistantSwitch: (assistant: Assistant) => void
|
||||
onAssistantDelete: (assistant: Assistant) => void
|
||||
addPreset: (assistant: Assistant) => void
|
||||
copyAssistant: (assistant: Assistant) => void
|
||||
onCreateDefaultAssistant: () => void
|
||||
handleSortByChange: (sortType: AssistantsSortType) => void
|
||||
sortByPinyinAsc: () => void
|
||||
sortByPinyinDesc: () => void
|
||||
}
|
||||
|
||||
export const AssistantList: FC<AssistantListProps> = (props) => {
|
||||
const {
|
||||
items,
|
||||
activeAssistantId,
|
||||
sortBy,
|
||||
onReorder,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onAssistantSwitch,
|
||||
onAssistantDelete,
|
||||
addPreset,
|
||||
copyAssistant,
|
||||
onCreateDefaultAssistant,
|
||||
handleSortByChange,
|
||||
sortByPinyinAsc,
|
||||
sortByPinyinDesc
|
||||
} = props
|
||||
|
||||
const renderAssistantItem = useCallback(
|
||||
(assistant: Assistant) => {
|
||||
return (
|
||||
<AssistantItem
|
||||
key={`assistant-${assistant.id}`}
|
||||
assistant={assistant}
|
||||
isActive={assistant.id === activeAssistantId}
|
||||
sortBy={sortBy}
|
||||
onSwitch={onAssistantSwitch}
|
||||
onDelete={onAssistantDelete}
|
||||
addPreset={addPreset}
|
||||
copyAssistant={copyAssistant}
|
||||
onCreateDefaultAssistant={onCreateDefaultAssistant}
|
||||
handleSortByChange={handleSortByChange}
|
||||
sortByPinyinAsc={sortByPinyinAsc}
|
||||
sortByPinyinDesc={sortByPinyinDesc}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[
|
||||
activeAssistantId,
|
||||
sortBy,
|
||||
onAssistantSwitch,
|
||||
onAssistantDelete,
|
||||
addPreset,
|
||||
copyAssistant,
|
||||
onCreateDefaultAssistant,
|
||||
handleSortByChange,
|
||||
sortByPinyinAsc,
|
||||
sortByPinyinDesc
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<DraggableList
|
||||
list={items}
|
||||
itemKey={(assistant) => `assistant-${assistant.id}`}
|
||||
onUpdate={onReorder}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}>
|
||||
{renderAssistantItem}
|
||||
</DraggableList>
|
||||
)
|
||||
}
|
||||
@@ -4,30 +4,25 @@ import type { FC } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { UnifiedItem } from '../hooks/useUnifiedItems'
|
||||
import AgentItem from './AgentItem'
|
||||
import AssistantItem from './AssistantItem'
|
||||
import { TagGroup } from './TagGroup'
|
||||
|
||||
interface GroupedItems {
|
||||
tag: string
|
||||
items: UnifiedItem[]
|
||||
items: Assistant[]
|
||||
}
|
||||
|
||||
interface UnifiedTagGroupsProps {
|
||||
interface AssistantTagGroupsProps {
|
||||
groupedItems: GroupedItems[]
|
||||
activeAssistantId: string
|
||||
activeAgentId: string | null
|
||||
sortBy: AssistantsSortType
|
||||
collapsedTags: Record<string, boolean>
|
||||
onGroupReorder: (tag: string, newList: UnifiedItem[]) => void
|
||||
onGroupReorder: (tag: string, newList: Assistant[]) => void
|
||||
onDragStart: () => void
|
||||
onDragEnd: () => void
|
||||
onToggleTagCollapse: (tag: string) => void
|
||||
onAssistantSwitch: (assistant: Assistant) => void
|
||||
onAssistantDelete: (assistant: Assistant) => void
|
||||
onAgentDelete: (agentId: string) => void
|
||||
onAgentPress: (agentId: string) => void
|
||||
addPreset: (assistant: Assistant) => void
|
||||
copyAssistant: (assistant: Assistant) => void
|
||||
onCreateDefaultAssistant: () => void
|
||||
@@ -36,11 +31,10 @@ interface UnifiedTagGroupsProps {
|
||||
sortByPinyinDesc: () => void
|
||||
}
|
||||
|
||||
export const UnifiedTagGroups: FC<UnifiedTagGroupsProps> = (props) => {
|
||||
export const AssistantTagGroups: FC<AssistantTagGroupsProps> = (props) => {
|
||||
const {
|
||||
groupedItems,
|
||||
activeAssistantId,
|
||||
activeAgentId,
|
||||
sortBy,
|
||||
collapsedTags,
|
||||
onGroupReorder,
|
||||
@@ -49,8 +43,6 @@ export const UnifiedTagGroups: FC<UnifiedTagGroupsProps> = (props) => {
|
||||
onToggleTagCollapse,
|
||||
onAssistantSwitch,
|
||||
onAssistantDelete,
|
||||
onAgentDelete,
|
||||
onAgentPress,
|
||||
addPreset,
|
||||
copyAssistant,
|
||||
onCreateDefaultAssistant,
|
||||
@@ -61,45 +53,30 @@ export const UnifiedTagGroups: FC<UnifiedTagGroupsProps> = (props) => {
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const renderUnifiedItem = useCallback(
|
||||
(item: UnifiedItem) => {
|
||||
if (item.type === 'agent') {
|
||||
return (
|
||||
<AgentItem
|
||||
key={`agent-${item.data.id}`}
|
||||
agent={item.data}
|
||||
isActive={item.data.id === activeAgentId}
|
||||
onDelete={() => onAgentDelete(item.data.id)}
|
||||
onPress={() => onAgentPress(item.data.id)}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<AssistantItem
|
||||
key={`assistant-${item.data.id}`}
|
||||
assistant={item.data}
|
||||
isActive={item.data.id === activeAssistantId}
|
||||
sortBy={sortBy}
|
||||
onSwitch={onAssistantSwitch}
|
||||
onDelete={onAssistantDelete}
|
||||
addPreset={addPreset}
|
||||
copyAssistant={copyAssistant}
|
||||
onCreateDefaultAssistant={onCreateDefaultAssistant}
|
||||
handleSortByChange={handleSortByChange}
|
||||
sortByPinyinAsc={sortByPinyinAsc}
|
||||
sortByPinyinDesc={sortByPinyinDesc}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const renderAssistantItem = useCallback(
|
||||
(assistant: Assistant) => {
|
||||
return (
|
||||
<AssistantItem
|
||||
key={`assistant-${assistant.id}`}
|
||||
assistant={assistant}
|
||||
isActive={assistant.id === activeAssistantId}
|
||||
sortBy={sortBy}
|
||||
onSwitch={onAssistantSwitch}
|
||||
onDelete={onAssistantDelete}
|
||||
addPreset={addPreset}
|
||||
copyAssistant={copyAssistant}
|
||||
onCreateDefaultAssistant={onCreateDefaultAssistant}
|
||||
handleSortByChange={handleSortByChange}
|
||||
sortByPinyinAsc={sortByPinyinAsc}
|
||||
sortByPinyinDesc={sortByPinyinDesc}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[
|
||||
activeAgentId,
|
||||
activeAssistantId,
|
||||
sortBy,
|
||||
onAssistantSwitch,
|
||||
onAssistantDelete,
|
||||
onAgentDelete,
|
||||
onAgentPress,
|
||||
addPreset,
|
||||
copyAssistant,
|
||||
onCreateDefaultAssistant,
|
||||
@@ -120,11 +97,11 @@ export const UnifiedTagGroups: FC<UnifiedTagGroupsProps> = (props) => {
|
||||
showTitle={group.tag !== t('assistants.tags.untagged')}>
|
||||
<DraggableList
|
||||
list={group.items}
|
||||
itemKey={(item) => `${item.type}-${item.data.id}`}
|
||||
itemKey={(assistant) => `assistant-${assistant.id}`}
|
||||
onUpdate={(newList) => onGroupReorder(group.tag, newList)}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}>
|
||||
{renderUnifiedItem}
|
||||
{renderAssistantItem}
|
||||
</DraggableList>
|
||||
</TagGroup>
|
||||
))}
|
||||
@@ -1,3 +1,4 @@
|
||||
import AddButton from '@renderer/components/AddButton'
|
||||
import AssistantAvatar from '@renderer/components/Avatar/AssistantAvatar'
|
||||
import type { DraggableVirtualListRef } from '@renderer/components/DraggableList'
|
||||
import { DraggableVirtualList } from '@renderer/components/DraggableList'
|
||||
@@ -59,7 +60,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AddButton from './AddButton'
|
||||
import { TopicManagePanel, useTopicManageMode } from './TopicManageMode'
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import AddAssistantOrAgentPopup from '@renderer/components/Popups/AddAssistantOrAgentPopup'
|
||||
import AgentModalPopup from '@renderer/components/Popups/agent/AgentModal'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import type { AgentEntity, Assistant, Topic } from '@renderer/types'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import AddButton from './AddButton'
|
||||
|
||||
interface UnifiedAddButtonProps {
|
||||
onCreateAssistant: () => void
|
||||
setActiveAssistant: (a: Assistant) => void
|
||||
setActiveAgentId: (id: string) => void
|
||||
}
|
||||
|
||||
const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant, setActiveAssistant, setActiveAgentId }) => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const { apiServerRunning, startApiServer } = useApiServer()
|
||||
|
||||
const afterCreate = useCallback(
|
||||
(a: AgentEntity) => {
|
||||
// TODO: should allow it to be null
|
||||
setActiveAssistant({
|
||||
id: 'fake',
|
||||
name: '',
|
||||
prompt: '',
|
||||
topics: [
|
||||
{
|
||||
id: 'fake',
|
||||
assistantId: 'fake',
|
||||
name: 'fake',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
messages: []
|
||||
} as unknown as Topic
|
||||
],
|
||||
type: 'chat'
|
||||
})
|
||||
setActiveAgentId(a.id)
|
||||
dispatch(setActiveTopicOrSessionAction('session'))
|
||||
},
|
||||
[dispatch, setActiveAgentId, setActiveAssistant]
|
||||
)
|
||||
|
||||
const handleAddButtonClick = async () => {
|
||||
AddAssistantOrAgentPopup.show({
|
||||
onSelect: (type) => {
|
||||
if (type === 'assistant') {
|
||||
onCreateAssistant()
|
||||
}
|
||||
|
||||
if (type === 'agent') {
|
||||
!apiServerRunning && startApiServer()
|
||||
AgentModalPopup.show({ afterSubmit: afterCreate })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="-mt-[2px] mb-[6px]">
|
||||
<AddButton onClick={handleAddButtonClick}>{t('chat.add.assistant.title')}</AddButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UnifiedAddButton
|
||||
@@ -1,109 +0,0 @@
|
||||
import { DraggableList } from '@renderer/components/DraggableList'
|
||||
import type { Assistant, AssistantsSortType } from '@renderer/types'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import type { UnifiedItem } from '../hooks/useUnifiedItems'
|
||||
import AgentItem from './AgentItem'
|
||||
import AssistantItem from './AssistantItem'
|
||||
|
||||
interface UnifiedListProps {
|
||||
items: UnifiedItem[]
|
||||
activeAssistantId: string
|
||||
activeAgentId: string | null
|
||||
sortBy: AssistantsSortType
|
||||
onReorder: (newList: UnifiedItem[]) => void
|
||||
onDragStart: () => void
|
||||
onDragEnd: () => void
|
||||
onAssistantSwitch: (assistant: Assistant) => void
|
||||
onAssistantDelete: (assistant: Assistant) => void
|
||||
onAgentDelete: (agentId: string) => void
|
||||
onAgentPress: (agentId: string) => void
|
||||
addPreset: (assistant: Assistant) => void
|
||||
copyAssistant: (assistant: Assistant) => void
|
||||
onCreateDefaultAssistant: () => void
|
||||
handleSortByChange: (sortType: AssistantsSortType) => void
|
||||
sortByPinyinAsc: () => void
|
||||
sortByPinyinDesc: () => void
|
||||
}
|
||||
|
||||
export const UnifiedList: FC<UnifiedListProps> = (props) => {
|
||||
const {
|
||||
items,
|
||||
activeAssistantId,
|
||||
activeAgentId,
|
||||
sortBy,
|
||||
onReorder,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onAssistantSwitch,
|
||||
onAssistantDelete,
|
||||
onAgentDelete,
|
||||
onAgentPress,
|
||||
addPreset,
|
||||
copyAssistant,
|
||||
onCreateDefaultAssistant,
|
||||
handleSortByChange,
|
||||
sortByPinyinAsc,
|
||||
sortByPinyinDesc
|
||||
} = props
|
||||
|
||||
const renderUnifiedItem = useCallback(
|
||||
(item: UnifiedItem) => {
|
||||
if (item.type === 'agent') {
|
||||
return (
|
||||
<AgentItem
|
||||
key={`agent-${item.data.id}`}
|
||||
agent={item.data}
|
||||
isActive={item.data.id === activeAgentId}
|
||||
onDelete={() => onAgentDelete(item.data.id)}
|
||||
onPress={() => onAgentPress(item.data.id)}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<AssistantItem
|
||||
key={`assistant-${item.data.id}`}
|
||||
assistant={item.data}
|
||||
isActive={item.data.id === activeAssistantId}
|
||||
sortBy={sortBy}
|
||||
onSwitch={onAssistantSwitch}
|
||||
onDelete={onAssistantDelete}
|
||||
addPreset={addPreset}
|
||||
copyAssistant={copyAssistant}
|
||||
onCreateDefaultAssistant={onCreateDefaultAssistant}
|
||||
handleSortByChange={handleSortByChange}
|
||||
sortByPinyinAsc={sortByPinyinAsc}
|
||||
sortByPinyinDesc={sortByPinyinDesc}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
[
|
||||
activeAgentId,
|
||||
activeAssistantId,
|
||||
sortBy,
|
||||
onAssistantSwitch,
|
||||
onAssistantDelete,
|
||||
onAgentDelete,
|
||||
onAgentPress,
|
||||
addPreset,
|
||||
copyAssistant,
|
||||
onCreateDefaultAssistant,
|
||||
handleSortByChange,
|
||||
sortByPinyinAsc,
|
||||
sortByPinyinDesc
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<DraggableList
|
||||
list={items}
|
||||
itemKey={(item) => `${item.type}-${item.data.id}`}
|
||||
onUpdate={onReorder}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}>
|
||||
{renderUnifiedItem}
|
||||
</DraggableList>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useAgentSessionInitializer } from '@renderer/hooks/agents/useAgentSessionInitializer'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveAgentId as setActiveAgentIdAction } from '@renderer/store/runtime'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export const useActiveAgent = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { initializeAgentSession } = useAgentSessionInitializer()
|
||||
|
||||
const setActiveAgentId = useCallback(
|
||||
async (id: string) => {
|
||||
dispatch(setActiveAgentIdAction(id))
|
||||
await initializeAgentSession(id)
|
||||
},
|
||||
[dispatch, initializeAgentSession]
|
||||
)
|
||||
|
||||
return { setActiveAgentId }
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
import type { RootState } from '@renderer/store'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setUnifiedListOrder } from '@renderer/store/assistants'
|
||||
import type { AgentEntity, Assistant } from '@renderer/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { UnifiedItem } from './useUnifiedItems'
|
||||
|
||||
interface UseUnifiedGroupingOptions {
|
||||
unifiedItems: UnifiedItem[]
|
||||
assistants: Assistant[]
|
||||
agents: AgentEntity[]
|
||||
apiServerEnabled: boolean
|
||||
agentsLoading: boolean
|
||||
agentsError: Error | null
|
||||
updateAssistants: (assistants: Assistant[]) => void
|
||||
}
|
||||
|
||||
export const useUnifiedGrouping = (options: UseUnifiedGroupingOptions) => {
|
||||
const { unifiedItems, assistants, agents, apiServerEnabled, agentsLoading, agentsError, updateAssistants } = options
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// Selector to get tagsOrder from Redux store
|
||||
const selectTagsOrder = useMemo(
|
||||
() => createSelector([(state: RootState) => state.assistants], (assistants) => assistants.tagsOrder ?? []),
|
||||
[]
|
||||
)
|
||||
const savedTagsOrder = useAppSelector(selectTagsOrder)
|
||||
|
||||
// Group unified items by tags
|
||||
const groupedUnifiedItems = useMemo(() => {
|
||||
const groups = new Map<string, UnifiedItem[]>()
|
||||
|
||||
unifiedItems.forEach((item) => {
|
||||
if (item.type === 'agent') {
|
||||
// Agents go to untagged group
|
||||
const groupKey = t('assistants.tags.untagged')
|
||||
if (!groups.has(groupKey)) {
|
||||
groups.set(groupKey, [])
|
||||
}
|
||||
groups.get(groupKey)!.push(item)
|
||||
} else {
|
||||
// Assistants use their tags
|
||||
const tags = item.data.tags?.length ? item.data.tags : [t('assistants.tags.untagged')]
|
||||
tags.forEach((tag) => {
|
||||
if (!groups.has(tag)) {
|
||||
groups.set(tag, [])
|
||||
}
|
||||
groups.get(tag)!.push(item)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Sort groups: untagged first, then by savedTagsOrder
|
||||
const untaggedKey = t('assistants.tags.untagged')
|
||||
const sortedGroups = Array.from(groups.entries()).sort(([tagA], [tagB]) => {
|
||||
if (tagA === untaggedKey) return -1
|
||||
if (tagB === untaggedKey) return 1
|
||||
|
||||
if (savedTagsOrder.length > 0) {
|
||||
const indexA = savedTagsOrder.indexOf(tagA)
|
||||
const indexB = savedTagsOrder.indexOf(tagB)
|
||||
|
||||
if (indexA !== -1 && indexB !== -1) {
|
||||
return indexA - indexB
|
||||
}
|
||||
|
||||
if (indexA !== -1) return -1
|
||||
|
||||
if (indexB !== -1) return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
return sortedGroups.map(([tag, items]) => ({ tag, items }))
|
||||
}, [unifiedItems, t, savedTagsOrder])
|
||||
|
||||
const handleUnifiedGroupReorder = useCallback(
|
||||
(tag: string, newGroupList: UnifiedItem[]) => {
|
||||
// Extract only assistants from the new list for updating
|
||||
const newAssistants = newGroupList.filter((item) => item.type === 'assistant').map((item) => item.data)
|
||||
|
||||
// Update assistants state
|
||||
let insertIndex = 0
|
||||
const updatedAssistants = assistants.map((a) => {
|
||||
const tags = a.tags?.length ? a.tags : [t('assistants.tags.untagged')]
|
||||
if (tags.includes(tag)) {
|
||||
const replaced = newAssistants[insertIndex]
|
||||
insertIndex += 1
|
||||
return replaced || a
|
||||
}
|
||||
return a
|
||||
})
|
||||
updateAssistants(updatedAssistants)
|
||||
|
||||
// Rebuild unified order and save to Redux
|
||||
const newUnifiedItems: UnifiedItem[] = []
|
||||
const availableAgents = new Map<string, AgentEntity>()
|
||||
const availableAssistants = new Map<string, Assistant>()
|
||||
|
||||
if (apiServerEnabled && !agentsLoading && !agentsError) {
|
||||
agents.forEach((agent) => availableAgents.set(agent.id, agent))
|
||||
}
|
||||
updatedAssistants.forEach((assistant) => availableAssistants.set(assistant.id, assistant))
|
||||
|
||||
// Reconstruct order based on current groupedUnifiedItems structure
|
||||
groupedUnifiedItems.forEach((group) => {
|
||||
if (group.tag === tag) {
|
||||
// Use the new group list for this tag
|
||||
newGroupList.forEach((item) => {
|
||||
newUnifiedItems.push(item)
|
||||
if (item.type === 'agent') {
|
||||
availableAgents.delete(item.data.id)
|
||||
} else {
|
||||
availableAssistants.delete(item.data.id)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Keep existing order for other tags
|
||||
group.items.forEach((item) => {
|
||||
newUnifiedItems.push(item)
|
||||
if (item.type === 'agent') {
|
||||
availableAgents.delete(item.data.id)
|
||||
} else {
|
||||
availableAssistants.delete(item.data.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Add any remaining items
|
||||
availableAgents.forEach((agent) => newUnifiedItems.push({ type: 'agent', data: agent }))
|
||||
availableAssistants.forEach((assistant) => newUnifiedItems.push({ type: 'assistant', data: assistant }))
|
||||
|
||||
// Save to Redux
|
||||
const orderToSave = newUnifiedItems.map((item) => ({
|
||||
type: item.type,
|
||||
id: item.data.id
|
||||
}))
|
||||
dispatch(setUnifiedListOrder(orderToSave))
|
||||
},
|
||||
[
|
||||
assistants,
|
||||
t,
|
||||
updateAssistants,
|
||||
apiServerEnabled,
|
||||
agentsLoading,
|
||||
agentsError,
|
||||
agents,
|
||||
groupedUnifiedItems,
|
||||
dispatch
|
||||
]
|
||||
)
|
||||
|
||||
return {
|
||||
groupedUnifiedItems,
|
||||
handleUnifiedGroupReorder
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setUnifiedListOrder } from '@renderer/store/assistants'
|
||||
import type { AgentEntity, Assistant } from '@renderer/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
export type UnifiedItem = { type: 'agent'; data: AgentEntity } | { type: 'assistant'; data: Assistant }
|
||||
|
||||
interface UseUnifiedItemsOptions {
|
||||
agents: AgentEntity[]
|
||||
assistants: Assistant[]
|
||||
apiServerEnabled: boolean
|
||||
agentsLoading: boolean
|
||||
agentsError: Error | null
|
||||
updateAssistants: (assistants: Assistant[]) => void
|
||||
}
|
||||
|
||||
export const useUnifiedItems = (options: UseUnifiedItemsOptions) => {
|
||||
const { agents, assistants, apiServerEnabled, agentsLoading, agentsError, updateAssistants } = options
|
||||
const dispatch = useAppDispatch()
|
||||
const unifiedListOrder = useAppSelector((state) => state.assistants.unifiedListOrder || [])
|
||||
|
||||
// Create unified items list (agents + assistants) with saved order
|
||||
const unifiedItems = useMemo(() => {
|
||||
const items: UnifiedItem[] = []
|
||||
|
||||
// Collect all available items
|
||||
const availableAgents = new Map<string, AgentEntity>()
|
||||
const availableAssistants = new Map<string, Assistant>()
|
||||
|
||||
if (apiServerEnabled && !agentsLoading && !agentsError) {
|
||||
agents.forEach((agent) => availableAgents.set(agent.id, agent))
|
||||
}
|
||||
assistants.forEach((assistant) => availableAssistants.set(assistant.id, assistant))
|
||||
|
||||
// Apply saved order
|
||||
unifiedListOrder.forEach((item) => {
|
||||
if (item.type === 'agent' && availableAgents.has(item.id)) {
|
||||
items.push({ type: 'agent', data: availableAgents.get(item.id)! })
|
||||
availableAgents.delete(item.id)
|
||||
} else if (item.type === 'assistant' && availableAssistants.has(item.id)) {
|
||||
items.push({ type: 'assistant', data: availableAssistants.get(item.id)! })
|
||||
availableAssistants.delete(item.id)
|
||||
}
|
||||
})
|
||||
|
||||
// Add new items (not in saved order) to the beginning
|
||||
const newItems: UnifiedItem[] = []
|
||||
availableAgents.forEach((agent) => newItems.push({ type: 'agent', data: agent }))
|
||||
availableAssistants.forEach((assistant) => newItems.push({ type: 'assistant', data: assistant }))
|
||||
items.unshift(...newItems)
|
||||
|
||||
return items
|
||||
}, [agents, assistants, apiServerEnabled, agentsLoading, agentsError, unifiedListOrder])
|
||||
|
||||
const handleUnifiedListReorder = useCallback(
|
||||
(newList: UnifiedItem[]) => {
|
||||
// Save the unified order to Redux
|
||||
const orderToSave = newList.map((item) => ({
|
||||
type: item.type,
|
||||
id: item.data.id
|
||||
}))
|
||||
dispatch(setUnifiedListOrder(orderToSave))
|
||||
|
||||
// Extract and update assistants order
|
||||
const newAssistants = newList.filter((item) => item.type === 'assistant').map((item) => item.data)
|
||||
updateAssistants(newAssistants)
|
||||
},
|
||||
[dispatch, updateAssistants]
|
||||
)
|
||||
|
||||
return {
|
||||
unifiedItems,
|
||||
handleUnifiedListReorder
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setUnifiedListOrder } from '@renderer/store/assistants'
|
||||
import type { Assistant } from '@renderer/types'
|
||||
import { useCallback } from 'react'
|
||||
import * as tinyPinyin from 'tiny-pinyin'
|
||||
|
||||
import type { UnifiedItem } from './useUnifiedItems'
|
||||
|
||||
interface UseUnifiedSortingOptions {
|
||||
unifiedItems: UnifiedItem[]
|
||||
updateAssistants: (assistants: Assistant[]) => void
|
||||
}
|
||||
|
||||
export const useUnifiedSorting = (options: UseUnifiedSortingOptions) => {
|
||||
const { unifiedItems, updateAssistants } = options
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const sortUnifiedItemsByPinyin = useCallback((items: UnifiedItem[], isAscending: boolean) => {
|
||||
return [...items].sort((a, b) => {
|
||||
const nameA = a.type === 'agent' ? a.data.name || a.data.id : a.data.name
|
||||
const nameB = b.type === 'agent' ? b.data.name || b.data.id : b.data.name
|
||||
const pinyinA = tinyPinyin.convertToPinyin(nameA, '', true)
|
||||
const pinyinB = tinyPinyin.convertToPinyin(nameB, '', true)
|
||||
return isAscending ? pinyinA.localeCompare(pinyinB) : pinyinB.localeCompare(pinyinA)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const sortByPinyinAsc = useCallback(() => {
|
||||
const sorted = sortUnifiedItemsByPinyin(unifiedItems, true)
|
||||
const orderToSave = sorted.map((item) => ({
|
||||
type: item.type,
|
||||
id: item.data.id
|
||||
}))
|
||||
dispatch(setUnifiedListOrder(orderToSave))
|
||||
// Also update assistants order
|
||||
const newAssistants = sorted.filter((item) => item.type === 'assistant').map((item) => item.data)
|
||||
updateAssistants(newAssistants)
|
||||
}, [unifiedItems, sortUnifiedItemsByPinyin, dispatch, updateAssistants])
|
||||
|
||||
const sortByPinyinDesc = useCallback(() => {
|
||||
const sorted = sortUnifiedItemsByPinyin(unifiedItems, false)
|
||||
const orderToSave = sorted.map((item) => ({
|
||||
type: item.type,
|
||||
id: item.data.id
|
||||
}))
|
||||
dispatch(setUnifiedListOrder(orderToSave))
|
||||
// Also update assistants order
|
||||
const newAssistants = sorted.filter((item) => item.type === 'assistant').map((item) => item.data)
|
||||
updateAssistants(newAssistants)
|
||||
}, [unifiedItems, sortUnifiedItemsByPinyin, dispatch, updateAssistants])
|
||||
|
||||
return {
|
||||
sortByPinyinAsc,
|
||||
sortByPinyinDesc
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant
|
||||
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveAgentId, setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import type { Tab } from '@renderer/types/chat'
|
||||
import { classNames, uuid } from '@renderer/utils'
|
||||
@@ -43,7 +41,6 @@ const HomeTabs: FC<Props> = ({
|
||||
const { toggleShowTopics } = useShowTopics()
|
||||
const { isLeftNavbar } = useNavbarPosition()
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const [tab, setTab] = useState<Tab>(position === 'left' ? _tab || 'assistants' : 'topic')
|
||||
const borderStyle = '0.5px solid var(--color-border)'
|
||||
@@ -62,8 +59,6 @@ const HomeTabs: FC<Props> = ({
|
||||
const assistant = await AddAssistantPopup.show()
|
||||
if (assistant) {
|
||||
setActiveAssistant(assistant)
|
||||
dispatch(setActiveAgentId(null))
|
||||
dispatch(setActiveTopicOrSessionAction('topic'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +66,6 @@ const HomeTabs: FC<Props> = ({
|
||||
const assistant = { ...defaultAssistant, id: uuid() }
|
||||
addAssistant(assistant)
|
||||
setActiveAssistant(assistant)
|
||||
dispatch(setActiveAgentId(null))
|
||||
dispatch(setActiveTopicOrSessionAction('topic'))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { useActiveAgent } from '@renderer/hooks/agents/useActiveAgent'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import type { Assistant } from '@renderer/types'
|
||||
import type { FC } from 'react'
|
||||
|
||||
import AgentContent from './AgentContent'
|
||||
import TopicContent from './TopicContent'
|
||||
|
||||
interface Props {
|
||||
@@ -11,14 +8,9 @@ interface Props {
|
||||
}
|
||||
|
||||
const ChatNavbarContent: FC<Props> = ({ assistant }) => {
|
||||
const { chat } = useRuntime()
|
||||
const { activeTopicOrSession } = chat
|
||||
const { agent: activeAgent } = useActiveAgent()
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-1 items-center justify-between">
|
||||
{activeTopicOrSession === 'topic' && <TopicContent assistant={assistant} />}
|
||||
{activeTopicOrSession === 'session' && activeAgent && <AgentContent activeAgent={activeAgent} />}
|
||||
<TopicContent assistant={assistant} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import type { Assistant } from '@renderer/types'
|
||||
import { Drawer, Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
@@ -7,7 +6,7 @@ import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import NavbarIcon from '../../../../../components/NavbarIcon'
|
||||
import { AgentSettingsTab, AssistantSettingsTab } from './SettingsTab'
|
||||
import { AssistantSettingsTab } from './SettingsTab'
|
||||
|
||||
interface Props {
|
||||
assistant?: Assistant
|
||||
@@ -15,10 +14,6 @@ interface Props {
|
||||
|
||||
const SettingsButton: FC<Props> = ({ assistant }) => {
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
const { chat } = useRuntime()
|
||||
|
||||
const isTopicSettings = chat.activeTopicOrSession === 'topic'
|
||||
const isAgentSettings = chat.activeTopicOrSession === 'session'
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -34,8 +29,7 @@ const SettingsButton: FC<Props> = ({ assistant }) => {
|
||||
width="var(--assistants-width)"
|
||||
closable={false}
|
||||
styles={{ body: { padding: 0, paddingTop: 'var(--navbar-height)' } }}>
|
||||
{isTopicSettings && assistant && <AssistantSettingsTab assistant={assistant} />}
|
||||
{isAgentSettings && <AgentSettingsTab />}
|
||||
{assistant && <AssistantSettingsTab assistant={assistant} />}
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { loggerService } from '@logger'
|
||||
import EditableNumber from '@renderer/components/EditableNumber'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import Selector from '@renderer/components/Selector'
|
||||
@@ -9,7 +8,6 @@ import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import useTranslate from '@renderer/hooks/useTranslate'
|
||||
import { SettingDivider, SettingRow, SettingRowTitle } from '@renderer/pages/settings'
|
||||
@@ -62,14 +60,11 @@ import styled from 'styled-components'
|
||||
import GroqSettingsGroup from './GroqSettingsGroup'
|
||||
import OpenAISettingsGroup from './OpenAISettingsGroup'
|
||||
|
||||
const logger = loggerService.withContext('AssistantSettingsTab')
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
}
|
||||
|
||||
const AssistantSettingsTab = (props: Props) => {
|
||||
const { chat } = useRuntime()
|
||||
const { assistant } = useAssistant(props.assistant.id)
|
||||
const { provider } = useProvider(assistant.model.provider)
|
||||
|
||||
@@ -150,13 +145,6 @@ const AssistantSettingsTab = (props: Props) => {
|
||||
isSupportServiceTierProvider(provider) ||
|
||||
(isSupportVerbosityModel(model) && isSupportVerbosityProvider(provider))
|
||||
|
||||
const isTopicSettings = chat.activeTopicOrSession === 'topic'
|
||||
|
||||
if (!isTopicSettings) {
|
||||
logger.warn('AssistantSettingsTab is rendered when not topic activated.')
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="settings-tab">
|
||||
{showOpenAiSettings && (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import AgentSettingsTab from './AgentSettingsTab'
|
||||
import AssistantSettingsTab from './AssistantSettingsTab'
|
||||
|
||||
export { AgentSettingsTab, AssistantSettingsTab }
|
||||
export { AssistantSettingsTab }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HelpTooltip } from '@renderer/components/TooltipIcons'
|
||||
import SelectAgentBaseModelButton from '@renderer/pages/home/components/SelectAgentBaseModelButton'
|
||||
import SelectAgentBaseModelButton from '@renderer/pages/agents/components/SelectAgentBaseModelButton'
|
||||
import type { AgentBaseWithId, ApiModel, UpdateAgentFunctionUnion } from '@renderer/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Languages,
|
||||
LayoutGrid,
|
||||
MessageSquareQuote,
|
||||
MousePointerClick,
|
||||
NotepadText,
|
||||
Palette,
|
||||
Sparkle
|
||||
@@ -117,6 +118,7 @@ const SidebarIconsManager: FC<SidebarIconsManagerProps> = ({
|
||||
() =>
|
||||
({
|
||||
assistants: <MessageSquareQuote size={16} />,
|
||||
agents: <MousePointerClick size={16} />,
|
||||
store: <Sparkle size={16} />,
|
||||
paintings: <Palette size={16} />,
|
||||
translate: <Languages size={16} />,
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface AssistantsState {
|
||||
tagsOrder: string[]
|
||||
collapsedTags: Record<string, boolean>
|
||||
presets: AssistantPreset[]
|
||||
/** @deprecated should be removed in v2 */
|
||||
unifiedListOrder: Array<{ type: 'agent' | 'assistant'; id: string }>
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 202,
|
||||
version: 203,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -3315,6 +3315,39 @@ const migrateConfig = {
|
||||
logger.error('migrate 202 error', error as Error)
|
||||
return state
|
||||
}
|
||||
},
|
||||
'203': (state: RootState) => {
|
||||
try {
|
||||
if (state.settings && state.settings.sidebarIcons) {
|
||||
// Add 'agents' to visible icons if not already present
|
||||
if (!state.settings.sidebarIcons.visible.includes('agents')) {
|
||||
// Insert after 'assistants' if present, otherwise append
|
||||
const assistantsIndex = state.settings.sidebarIcons.visible.indexOf('assistants')
|
||||
if (assistantsIndex !== -1) {
|
||||
state.settings.sidebarIcons.visible = [
|
||||
...state.settings.sidebarIcons.visible.slice(0, assistantsIndex + 1),
|
||||
'agents',
|
||||
...state.settings.sidebarIcons.visible.slice(assistantsIndex + 1)
|
||||
]
|
||||
} else {
|
||||
state.settings.sidebarIcons.visible = [...state.settings.sidebarIcons.visible, 'agents']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add 'agents' tab if not already present
|
||||
if (state.tabs && !state.tabs.tabs.some((tab: { id: string }) => tab.id === 'agents')) {
|
||||
const homeIndex = state.tabs.tabs.findIndex((tab: { id: string }) => tab.id === 'home')
|
||||
const insertIndex = homeIndex !== -1 ? homeIndex + 1 : state.tabs.tabs.length
|
||||
state.tabs.tabs.splice(insertIndex, 0, { id: 'agents', path: '/agents' })
|
||||
}
|
||||
|
||||
logger.info('migrate 203 success')
|
||||
return state
|
||||
} catch (error) {
|
||||
logger.error('migrate 203 error', error as Error)
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,8 +29,6 @@ export interface ChatState {
|
||||
/** UI state. Map agent id to active session id.
|
||||
* null represents no active session */
|
||||
activeSessionIdMap: Record<string, string | null>
|
||||
/** meanwhile active Assistants or Agents */
|
||||
activeTopicOrSession: 'topic' | 'session'
|
||||
/** topic ids that are currently being renamed */
|
||||
renamingTopics: string[]
|
||||
/** topic ids that are newly renamed */
|
||||
@@ -77,6 +75,9 @@ export interface RuntimeState {
|
||||
detectedRegion: MinAppRegion | null
|
||||
/** Query whether a task is processing or not. undefined and false share same semantics. */
|
||||
loadingMap: Record<string, boolean>
|
||||
// Migrated from useApiServer, it's global state now
|
||||
/** Is the api server running */
|
||||
apiServerRunning: boolean
|
||||
}
|
||||
|
||||
export interface ExportState {
|
||||
@@ -112,7 +113,6 @@ const initialState: RuntimeState = {
|
||||
selectedMessageIds: [],
|
||||
activeTopic: null,
|
||||
activeAgentId: null,
|
||||
activeTopicOrSession: 'topic',
|
||||
activeSessionIdMap: {},
|
||||
renamingTopics: [],
|
||||
newlyRenamedTopics: []
|
||||
@@ -121,7 +121,8 @@ const initialState: RuntimeState = {
|
||||
activeSearches: {}
|
||||
},
|
||||
detectedRegion: null,
|
||||
loadingMap: {}
|
||||
loadingMap: {},
|
||||
apiServerRunning: false
|
||||
}
|
||||
|
||||
const runtimeSlice = createSlice({
|
||||
@@ -188,9 +189,6 @@ const runtimeSlice = createSlice({
|
||||
const { agentId, sessionId } = action.payload
|
||||
state.chat.activeSessionIdMap[agentId] = sessionId
|
||||
},
|
||||
setActiveTopicOrSessionAction: (state, action: PayloadAction<'topic' | 'session'>) => {
|
||||
state.chat.activeTopicOrSession = action.payload
|
||||
},
|
||||
setRenamingTopics: (state, action: PayloadAction<string[]>) => {
|
||||
state.chat.renamingTopics = action.payload
|
||||
},
|
||||
@@ -218,6 +216,9 @@ const runtimeSlice = createSlice({
|
||||
},
|
||||
setDetectedRegion: (state, action: PayloadAction<MinAppRegion | null>) => {
|
||||
state.detectedRegion = action.payload
|
||||
},
|
||||
setApiServerRunningAction: (state, action: PayloadAction<boolean>) => {
|
||||
state.apiServerRunning = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -242,7 +243,6 @@ export const {
|
||||
setActiveTopic,
|
||||
setActiveAgentId,
|
||||
setActiveSessionIdAction,
|
||||
setActiveTopicOrSessionAction,
|
||||
setRenamingTopics,
|
||||
setNewlyRenamedTopics,
|
||||
startLoadingAction,
|
||||
@@ -251,7 +251,8 @@ export const {
|
||||
setActiveSearches,
|
||||
setWebSearchStatus,
|
||||
// Region detection
|
||||
setDetectedRegion
|
||||
setDetectedRegion,
|
||||
setApiServerRunningAction
|
||||
} = runtimeSlice.actions
|
||||
|
||||
export default runtimeSlice.reducer
|
||||
|
||||
@@ -32,6 +32,10 @@ const initialState: TabsState = {
|
||||
{
|
||||
id: 'home',
|
||||
path: '/'
|
||||
},
|
||||
{
|
||||
id: 'agents',
|
||||
path: '/agents'
|
||||
}
|
||||
],
|
||||
activeTabId: 'home'
|
||||
|
||||
@@ -657,6 +657,7 @@ export const isAutoDetectionMethod = (method: string): method is AutoDetectionMe
|
||||
|
||||
export type SidebarIcon =
|
||||
| 'assistants'
|
||||
| 'agents'
|
||||
| 'store'
|
||||
| 'paintings'
|
||||
| 'translate'
|
||||
|
||||
Reference in New Issue
Block a user