mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-03 19:30:04 +08:00
Merge branch 'feat/agents-new' into refactor/agent-assistant-unified
This commit is contained in:
commit
7e369bef00
@ -15,6 +15,11 @@ import { modelsRoutes } from './routes/models'
|
||||
const logger = loggerService.withContext('ApiServer')
|
||||
|
||||
const app = express()
|
||||
app.use(
|
||||
express.json({
|
||||
limit: '50mb'
|
||||
})
|
||||
)
|
||||
|
||||
// Global middleware
|
||||
app.use((req, res, next) => {
|
||||
@ -111,7 +116,6 @@ app.get('/', (_req, res) => {
|
||||
// Provider-specific API routes with auth (must be before /v1 to avoid conflicts)
|
||||
const providerRouter = express.Router({ mergeParams: true })
|
||||
providerRouter.use(authMiddleware)
|
||||
providerRouter.use(express.json())
|
||||
// Mount provider-specific messages route
|
||||
providerRouter.use('/v1/messages', messagesProviderRoutes)
|
||||
app.use('/:provider', providerRouter)
|
||||
@ -119,7 +123,6 @@ app.use('/:provider', providerRouter)
|
||||
// API v1 routes with auth
|
||||
const apiRouter = express.Router()
|
||||
apiRouter.use(authMiddleware)
|
||||
apiRouter.use(express.json())
|
||||
// Mount routes
|
||||
apiRouter.use('/chat', chatRoutes)
|
||||
apiRouter.use('/mcps', mcpRoutes)
|
||||
|
||||
@ -7,7 +7,7 @@ const logger = loggerService.withContext('ApiServerUtils')
|
||||
|
||||
// Cache configuration
|
||||
const PROVIDERS_CACHE_KEY = 'api-server:providers'
|
||||
const PROVIDERS_CACHE_TTL = 5 * 60 * 1000 // 5 minutes
|
||||
const PROVIDERS_CACHE_TTL = 1 * 60 * 1000 // 1 minutes
|
||||
|
||||
export async function getAvailableProviders(): Promise<Provider[]> {
|
||||
try {
|
||||
|
||||
@ -34,7 +34,7 @@ const generateMessageId = (): string => `msg_${uuidv4().replace(/-/g, '')}`
|
||||
// Main transform function
|
||||
export function transformSDKMessageToStreamParts(sdkMessage: SDKMessage): AgentStreamPart[] {
|
||||
const chunks: AgentStreamPart[] = []
|
||||
logger.debug('Transforming SDKMessage to stream parts', sdkMessage)
|
||||
logger.silly('Transforming SDKMessage to stream parts', sdkMessage)
|
||||
switch (sdkMessage.type) {
|
||||
case 'assistant':
|
||||
case 'user':
|
||||
@ -105,7 +105,6 @@ function handleUserOrAssistantMessage(message: Extract<SDKMessage, { type: 'assi
|
||||
}
|
||||
} else if (Array.isArray(message.message.content)) {
|
||||
for (const block of message.message.content) {
|
||||
logger.debug('Handling user or assistant message:', { block })
|
||||
switch (block.type) {
|
||||
case 'text':
|
||||
chunks.push(...generateTextChunks(messageId, block.text, message))
|
||||
@ -127,8 +126,7 @@ function handleUserOrAssistantMessage(message: Extract<SDKMessage, { type: 'assi
|
||||
})
|
||||
break
|
||||
case 'tool_result': {
|
||||
logger.debug('Handling tool result:', { block })
|
||||
logger.debug('contentblockState', { content: contentBlockState })
|
||||
logger.silly('Handling tool result:', { block, content: contentBlockState })
|
||||
const hasToolCall = contentBlockState.has(block.tool_use_id)
|
||||
const toolCall = contentBlockState.get(block.tool_use_id) as toolCallBlock
|
||||
chunks.push({
|
||||
@ -161,7 +159,7 @@ function handleStreamEvent(message: Extract<SDKMessage, { type: 'stream_event' }
|
||||
const chunks: AgentStreamPart[] = []
|
||||
const event = message.event
|
||||
const blockKey = `${message.uuid ?? message.session_id ?? 'session'}:${event.type}`
|
||||
logger.debug('Handling stream event:', { event })
|
||||
logger.silly('Handling stream event:', { event })
|
||||
switch (event.type) {
|
||||
case 'message_start':
|
||||
// No specific UI chunk needed for message start in this protocol
|
||||
@ -291,9 +289,6 @@ function handleStreamEvent(message: Extract<SDKMessage, { type: 'stream_event' }
|
||||
// Handle system messages
|
||||
function handleSystemMessage(message: Extract<SDKMessage, { type: 'system' }>): AgentStreamPart[] {
|
||||
const chunks: AgentStreamPart[] = []
|
||||
logger.debug('Received system message', {
|
||||
subtype: message.subtype
|
||||
})
|
||||
switch (message.subtype) {
|
||||
case 'init': {
|
||||
chunks.push({
|
||||
|
||||
@ -20,6 +20,7 @@ import { getModelLogo } from '@renderer/config/models'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
import { AgentEntity, AgentSessionEntity, BaseSessionForm, CreateSessionForm, UpdateSessionForm } from '@renderer/types'
|
||||
import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -86,7 +87,8 @@ export const SessionModal: React.FC<Props> = ({
|
||||
const { t } = useTranslation()
|
||||
const loadingRef = useRef(false)
|
||||
// const { setTimeoutTimer } = useTimer()
|
||||
const { createSession, updateSession } = useSessions(agentId)
|
||||
const { createSession } = useSessions(agentId)
|
||||
const updateSession = useUpdateSession(agentId)
|
||||
// Only support claude code for now
|
||||
const { models } = useApiModels({ providerType: 'anthropic' })
|
||||
const { agent } = useAgent(agentId)
|
||||
@ -164,7 +166,7 @@ export const SessionModal: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
if (form.accessible_paths.length === 0) {
|
||||
window.toast.error(t('agent.session.accessible_paths.required'))
|
||||
window.toast.error(t('agent.session.accessible_paths.error.at_least_one'))
|
||||
loadingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { CreateSessionForm, UpdateSessionForm } from '@renderer/types'
|
||||
import { CreateSessionForm } from '@renderer/types'
|
||||
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -46,25 +46,14 @@ export const useSessions = (agentId: string) => {
|
||||
|
||||
const deleteSession = useCallback(
|
||||
async (id: string) => {
|
||||
if (!agentId) return
|
||||
if (!agentId) return false
|
||||
try {
|
||||
await client.deleteSession(agentId, id)
|
||||
mutate((prev) => prev?.filter((session) => session.id !== id))
|
||||
return true
|
||||
} catch (error) {
|
||||
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.delete.error.failed')))
|
||||
}
|
||||
},
|
||||
[agentId, client, mutate, t]
|
||||
)
|
||||
|
||||
const updateSession = useCallback(
|
||||
async (form: UpdateSessionForm) => {
|
||||
if (!agentId) return
|
||||
try {
|
||||
const result = await client.updateSession(agentId, form)
|
||||
mutate((prev) => prev?.map((session) => (session.id === form.id ? result : session)))
|
||||
} catch (error) {
|
||||
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.update.error.failed')))
|
||||
return false
|
||||
}
|
||||
},
|
||||
[agentId, client, mutate, t]
|
||||
@ -76,7 +65,6 @@ export const useSessions = (agentId: string) => {
|
||||
isLoading,
|
||||
createSession,
|
||||
getSession,
|
||||
deleteSession,
|
||||
updateSession
|
||||
deleteSession
|
||||
}
|
||||
}
|
||||
|
||||
36
src/renderer/src/hooks/agents/useUpdateSession.ts
Normal file
36
src/renderer/src/hooks/agents/useUpdateSession.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { ListAgentSessionsResponse, UpdateSessionForm } from '@renderer/types'
|
||||
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { mutate } from 'swr'
|
||||
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
export const useUpdateSession = (agentId: string) => {
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
const paths = client.getSessionPaths(agentId)
|
||||
const listKey = paths.base
|
||||
|
||||
const updateSession = useCallback(
|
||||
async (form: UpdateSessionForm) => {
|
||||
const sessionId = form.id
|
||||
try {
|
||||
const itemKey = paths.withId(sessionId)
|
||||
// may change to optimistic update
|
||||
const result = await client.updateSession(agentId, form)
|
||||
mutate<ListAgentSessionsResponse['data']>(
|
||||
listKey,
|
||||
(prev) => prev?.map((session) => (session.id === result.id ? result : session)) ?? []
|
||||
)
|
||||
mutate(itemKey, result)
|
||||
window.toast.success(t('common.update_success'))
|
||||
} catch (error) {
|
||||
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.update.error.failed')))
|
||||
}
|
||||
},
|
||||
[agentId, client, listKey, paths, t]
|
||||
)
|
||||
|
||||
return updateSession
|
||||
}
|
||||
@ -52,7 +52,8 @@
|
||||
"delete": {
|
||||
"content": "Are you sure to delete this session?",
|
||||
"error": {
|
||||
"failed": "Failed to delete the session"
|
||||
"failed": "Failed to delete the session",
|
||||
"last": "At least one session must be kept"
|
||||
},
|
||||
"title": "Delete session"
|
||||
},
|
||||
@ -84,6 +85,9 @@
|
||||
"error": {
|
||||
"failed": "Failed to update the agent"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "Enable API Server to use agents."
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
|
||||
@ -52,7 +52,8 @@
|
||||
"delete": {
|
||||
"content": "确定要删除此会话吗?",
|
||||
"error": {
|
||||
"failed": "删除会话失败"
|
||||
"failed": "删除会话失败",
|
||||
"last": "至少需要保留一个会话"
|
||||
},
|
||||
"title": "删除会话"
|
||||
},
|
||||
@ -84,6 +85,9 @@
|
||||
"error": {
|
||||
"failed": "更新智能体失败"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "请启用 API 服务器以使用智能体功能"
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
|
||||
@ -52,7 +52,8 @@
|
||||
"delete": {
|
||||
"content": "您確定要刪除此工作階段嗎?",
|
||||
"error": {
|
||||
"failed": "無法刪除工作階段"
|
||||
"failed": "無法刪除工作階段",
|
||||
"last": "至少必須保留一個工作階段"
|
||||
},
|
||||
"title": "刪除工作階段"
|
||||
},
|
||||
@ -84,6 +85,9 @@
|
||||
"error": {
|
||||
"failed": "無法更新代理程式"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "啟用 API 伺服器以使用代理程式。"
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
|
||||
@ -52,7 +52,8 @@
|
||||
"delete": {
|
||||
"content": "Είσαι σίγουρος ότι θέλεις να διαγράψεις αυτήν τη συνεδρία;",
|
||||
"error": {
|
||||
"failed": "Αποτυχία διαγραφής της συνεδρίας"
|
||||
"failed": "Αποτυχία διαγραφής της συνεδρίας",
|
||||
"last": "Τουλάχιστον μία συνεδρία πρέπει να διατηρηθεί"
|
||||
},
|
||||
"title": "Διαγραφή συνεδρίας"
|
||||
},
|
||||
@ -84,6 +85,9 @@
|
||||
"error": {
|
||||
"failed": "Αποτυχία ενημέρωσης του πράκτορα"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "Ενεργοποίηση του διακομιστή API για χρήση πρακτόρων."
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
|
||||
@ -52,7 +52,8 @@
|
||||
"delete": {
|
||||
"content": "¿Estás seguro de eliminar esta sesión?",
|
||||
"error": {
|
||||
"failed": "Error al eliminar la sesión"
|
||||
"failed": "Error al eliminar la sesión",
|
||||
"last": "Debe mantenerse al menos una sesión"
|
||||
},
|
||||
"title": "Eliminar sesión"
|
||||
},
|
||||
@ -84,6 +85,9 @@
|
||||
"error": {
|
||||
"failed": "Error al actualizar el agente"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "Habilitar el servidor API para usar agentes."
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
|
||||
@ -52,7 +52,8 @@
|
||||
"delete": {
|
||||
"content": "Êtes-vous sûr de vouloir supprimer cette session ?",
|
||||
"error": {
|
||||
"failed": "Échec de la suppression de la session"
|
||||
"failed": "Échec de la suppression de la session",
|
||||
"last": "Au moins une session doit être conservée"
|
||||
},
|
||||
"title": "Supprimer la session"
|
||||
},
|
||||
@ -84,6 +85,9 @@
|
||||
"error": {
|
||||
"failed": "Échec de la mise à jour de l'agent"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "Permettre au serveur API d'utiliser des agents."
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
|
||||
@ -52,7 +52,8 @@
|
||||
"delete": {
|
||||
"content": "このセッションを削除してもよろしいですか?",
|
||||
"error": {
|
||||
"failed": "セッションの削除に失敗しました"
|
||||
"failed": "セッションの削除に失敗しました",
|
||||
"last": "少なくとも1つのセッションを維持する必要があります"
|
||||
},
|
||||
"title": "セッションを削除"
|
||||
},
|
||||
@ -84,6 +85,9 @@
|
||||
"error": {
|
||||
"failed": "エージェントの更新に失敗しました"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "APIサーバーがエージェントを使用できるようにする。"
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
|
||||
@ -52,7 +52,8 @@
|
||||
"delete": {
|
||||
"content": "Tem certeza de que deseja excluir esta sessão?",
|
||||
"error": {
|
||||
"failed": "Falha ao excluir a sessão"
|
||||
"failed": "Falha ao excluir a sessão",
|
||||
"last": "Pelo menos uma sessão deve ser mantida"
|
||||
},
|
||||
"title": "Excluir sessão"
|
||||
},
|
||||
@ -84,6 +85,9 @@
|
||||
"error": {
|
||||
"failed": "Falha ao atualizar o agente"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "Ativar o Servidor de API para usar agentes."
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
|
||||
@ -52,7 +52,8 @@
|
||||
"delete": {
|
||||
"content": "Вы уверены, что хотите удалить этот сеанс?",
|
||||
"error": {
|
||||
"failed": "Не удалось удалить сеанс"
|
||||
"failed": "Не удалось удалить сеанс",
|
||||
"last": "Должен быть сохранён хотя бы один сеанс"
|
||||
},
|
||||
"title": "Удалить сеанс"
|
||||
},
|
||||
@ -84,6 +85,9 @@
|
||||
"error": {
|
||||
"failed": "Не удалось обновить агента"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"enable_server": "Разрешить серверу API использовать агентов."
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Alert } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
@ -49,6 +50,7 @@ const Chat: FC<Props> = (props) => {
|
||||
const chatMaxWidth = useChatMaxWidth()
|
||||
const { chat } = useRuntime()
|
||||
const { activeTopicOrSession, activeAgentId, activeSessionId } = chat
|
||||
const { apiServer } = useSettings()
|
||||
|
||||
const mainRef = React.useRef<HTMLDivElement>(null)
|
||||
const contentSearchRef = React.useRef<ContentSearchRef>(null)
|
||||
@ -149,8 +151,15 @@ const Chat: FC<Props> = (props) => {
|
||||
if (!sessionId) {
|
||||
return () => <div> Active Session ID is invalid.</div>
|
||||
}
|
||||
if (!apiServer.enabled) {
|
||||
return () => (
|
||||
<div>
|
||||
<Alert color="warning" title={t('agent.warning.enable_server')} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return () => <AgentSessionMessages agentId={activeAgentId} sessionId={sessionId} />
|
||||
}, [activeAgentId, activeSessionId])
|
||||
}, [activeAgentId, activeSessionId, apiServer.enabled, t])
|
||||
|
||||
const SessionInputBar = useMemo(() => {
|
||||
if (activeAgentId === null) {
|
||||
|
||||
@ -1,21 +1,28 @@
|
||||
import { Tooltip } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { ActionIconButton } from '@renderer/components/Buttons'
|
||||
import { QuickPanelView } from '@renderer/components/QuickPanel'
|
||||
import { useSession } from '@renderer/hooks/agents/useSession'
|
||||
import { selectNewTopicLoading } from '@renderer/hooks/useMessageOperations'
|
||||
import { getModel } from '@renderer/hooks/useModel'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import PasteService from '@renderer/services/PasteService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { pauseTrace } from '@renderer/services/SpanManagerService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||
import { sendMessage as dispatchSendMessage } from '@renderer/store/thunk/messageThunk'
|
||||
import type { Assistant, Message, Model, Topic } from '@renderer/types'
|
||||
import { MessageBlock, MessageBlockStatus } from '@renderer/types/newMessage'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { abortCompletion } from '@renderer/utils/abortController'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input'
|
||||
import { createMainTextBlock, createMessage } from '@renderer/utils/messageUtils/create'
|
||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import { isEmpty } from 'lodash'
|
||||
import React, { CSSProperties, FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { CirclePause } from 'lucide-react'
|
||||
import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
@ -36,6 +43,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
const [text, setText] = useState(_text)
|
||||
const [inputFocus, setInputFocus] = useState(false)
|
||||
const { session } = useSession(agentId, sessionId)
|
||||
const { apiServer } = useSettings()
|
||||
|
||||
const { sendMessageShortcut, fontSize, enableSpellCheck } = useSettings()
|
||||
const textareaRef = useRef<TextAreaRef>(null)
|
||||
@ -46,12 +54,37 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
const dispatch = useAppDispatch()
|
||||
const sessionTopicId = buildAgentSessionTopicId(sessionId)
|
||||
const topicMessages = useAppSelector((state) => selectMessagesForTopic(state, sessionTopicId))
|
||||
const loading = useAppSelector((state) => selectNewTopicLoading(state, sessionTopicId))
|
||||
|
||||
const focusTextarea = useCallback(() => {
|
||||
textareaRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
const inputEmpty = isEmpty(text)
|
||||
const sendDisabled = inputEmpty || !apiServer.enabled
|
||||
|
||||
const streamingAskIds = useMemo(() => {
|
||||
if (!topicMessages) {
|
||||
return []
|
||||
}
|
||||
|
||||
const askIdSet = new Set<string>()
|
||||
for (const message of topicMessages) {
|
||||
if (!message) continue
|
||||
if (message.status === 'processing' || message.status === 'pending') {
|
||||
if (message.askId) {
|
||||
askIdSet.add(message.askId)
|
||||
} else if (message.id) {
|
||||
askIdSet.add(message.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(askIdSet)
|
||||
}, [topicMessages])
|
||||
|
||||
const canAbort = loading && streamingAskIds.length > 0
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
//to check if the SendMessage key is pressed
|
||||
@ -95,8 +128,27 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const abortAgentSession = useCallback(async () => {
|
||||
if (!streamingAskIds.length) {
|
||||
logger.debug('No active agent session streams to abort', { sessionTopicId })
|
||||
return
|
||||
}
|
||||
|
||||
logger.info('Aborting agent session message generation', {
|
||||
sessionTopicId,
|
||||
askIds: streamingAskIds
|
||||
})
|
||||
|
||||
for (const askId of streamingAskIds) {
|
||||
abortCompletion(askId)
|
||||
}
|
||||
|
||||
pauseTrace(sessionTopicId)
|
||||
dispatch(newMessagesActions.setTopicLoading({ topicId: sessionTopicId, loading: false }))
|
||||
}, [dispatch, sessionTopicId, streamingAskIds])
|
||||
|
||||
const sendMessage = useCallback(async () => {
|
||||
if (inputEmpty) {
|
||||
if (sendDisabled) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -158,7 +210,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
}, [
|
||||
agentId,
|
||||
dispatch,
|
||||
inputEmpty,
|
||||
sendDisabled,
|
||||
session?.agent_id,
|
||||
session?.instructions,
|
||||
session?.model,
|
||||
@ -231,7 +283,16 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
onBlur={() => setInputFocus(false)}
|
||||
/>
|
||||
<div className="flex justify-end px-1">
|
||||
<SendMessageButton sendMessage={sendMessage} disabled={inputEmpty} />
|
||||
<div className="flex items-center gap-1">
|
||||
<SendMessageButton sendMessage={sendMessage} disabled={sendDisabled} />
|
||||
{canAbort && (
|
||||
<Tooltip placement="top" content={t('chat.input.pause')}>
|
||||
<ActionIconButton onClick={abortAgentSession} style={{ marginRight: -2 }}>
|
||||
<CirclePause size={20} color="var(--color-error)" />
|
||||
</ActionIconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</InputBarContainer>
|
||||
</Container>
|
||||
|
||||
@ -3,7 +3,7 @@ import { Assistant } from '@renderer/types'
|
||||
import { FC, useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { Agents } from './components/Agents'
|
||||
import { AgentSection } from './components/AgentSection'
|
||||
import Assistants from './components/Assistants'
|
||||
|
||||
interface AssistantsTabProps {
|
||||
@ -17,7 +17,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
return (
|
||||
<Container className="assistants-tab" ref={containerRef}>
|
||||
<Agents />
|
||||
<AgentSection />
|
||||
<Assistants {...props} />
|
||||
</Container>
|
||||
)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Spinner } from '@heroui/react'
|
||||
import { Alert, Spinner } from '@heroui/react'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { FC, memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -12,6 +13,15 @@ const SessionsTab: FC<SessionsTabProps> = () => {
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId } = chat
|
||||
const { t } = useTranslation()
|
||||
const { apiServer } = useSettings()
|
||||
|
||||
if (!apiServer.enabled) {
|
||||
return (
|
||||
<div>
|
||||
<Alert color="warning" title={t('agent.warning.enable_server')} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
|
||||
39
src/renderer/src/pages/home/Tabs/components/AgentSection.tsx
Normal file
39
src/renderer/src/pages/home/Tabs/components/AgentSection.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Alert } from '@heroui/react'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { addIknowAction } from '@renderer/store/runtime'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Agents } from './Agents'
|
||||
import { SectionName } from './SectionName'
|
||||
|
||||
const ALERT_KEY = 'enable_api_server_to_use_agent'
|
||||
|
||||
export const AgentSection = () => {
|
||||
const { t } = useTranslation()
|
||||
const { apiServer } = useSettings()
|
||||
const { iknow } = useRuntime()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
if (!apiServer.enabled) {
|
||||
if (iknow[ALERT_KEY]) return null
|
||||
return (
|
||||
<Alert
|
||||
color="warning"
|
||||
title={t('agent.warning.enable_server')}
|
||||
isClosable
|
||||
onClose={() => {
|
||||
dispatch(addIknowAction(ALERT_KEY))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="agents-tab h-full w-full">
|
||||
<SectionName name={t('common.agent_other')} />
|
||||
<Agents />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -9,7 +9,6 @@ import { FC, useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import AgentItem from './AgentItem'
|
||||
import { SectionName } from './SectionName'
|
||||
|
||||
interface AssistantsTabProps {}
|
||||
|
||||
@ -35,8 +34,7 @@ export const Agents: FC<AssistantsTabProps> = () => {
|
||||
}, [isLoading, agents, activeAgentId, setActiveAgentId])
|
||||
|
||||
return (
|
||||
<div className="agents-tab h-full w-full">
|
||||
<SectionName name={t('common.agent_other')} />
|
||||
<>
|
||||
{isLoading && <Spinner />}
|
||||
{error && <Alert color="danger" title={t('agent.list.error.failed')} />}
|
||||
{!isLoading &&
|
||||
@ -65,6 +63,6 @@ export const Agents: FC<AssistantsTabProps> = () => {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -13,11 +13,13 @@ interface SessionItemProps {
|
||||
session: AgentSessionEntity
|
||||
// use external agentId as SSOT, instead of session.agent_id
|
||||
agentId: string
|
||||
isDisabled?: boolean
|
||||
isLoading?: boolean
|
||||
onDelete: () => void
|
||||
onPress: () => void
|
||||
}
|
||||
|
||||
const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress }) => {
|
||||
const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoading, onDelete, onPress }) => {
|
||||
const { t } = useTranslation()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const { chat } = useRuntime()
|
||||
@ -38,7 +40,11 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
|
||||
<>
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger>
|
||||
<ButtonContainer onPress={onPress} className={isActive ? 'active' : ''}>
|
||||
<ButtonContainer
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
onPress={onPress}
|
||||
className={isActive ? 'active' : ''}>
|
||||
<SessionLabelContainer className="name" title={session.name ?? session.id}>
|
||||
<SessionLabel />
|
||||
</SessionLabelContainer>
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
import { Button, Spinner } from '@heroui/react'
|
||||
import { SessionModal } from '@renderer/components/Popups/agent/SessionModal'
|
||||
import { Alert, Button, Spinner } from '@heroui/react'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveSessionIdAction, setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import {
|
||||
setActiveSessionIdAction,
|
||||
setActiveTopicOrSessionAction,
|
||||
setSessionWaitingAction
|
||||
} from '@renderer/store/runtime'
|
||||
import { CreateSessionForm } from '@renderer/types'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { memo, useCallback, useEffect } from 'react'
|
||||
@ -19,9 +25,11 @@ interface SessionsProps {
|
||||
|
||||
const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
const { t } = useTranslation()
|
||||
const { sessions, isLoading, deleteSession } = useSessions(agentId)
|
||||
const { agent } = useAgent(agentId)
|
||||
const { sessions, isLoading, error, deleteSession, createSession } = useSessions(agentId)
|
||||
const updateSession = useUpdateSession(agentId)
|
||||
const { chat } = useRuntime()
|
||||
const { activeSessionId } = chat
|
||||
const { activeSessionId, sessionWaiting } = chat
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const setActiveSessionId = useCallback(
|
||||
@ -32,6 +40,39 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const handleCreateSession = useCallback(async () => {
|
||||
if (!agent) return
|
||||
const session = {
|
||||
...agent,
|
||||
id: undefined
|
||||
} satisfies CreateSessionForm
|
||||
const created = await createSession(session)
|
||||
if (created) {
|
||||
dispatch(setActiveSessionIdAction({ agentId, sessionId: created.id }))
|
||||
}
|
||||
}, [agent, agentId, createSession, dispatch])
|
||||
|
||||
const handleDeleteSession = useCallback(
|
||||
async (id: string) => {
|
||||
if (sessions.length === 1) {
|
||||
window.toast.error(t('agent.session.delete.error.last'))
|
||||
return
|
||||
}
|
||||
dispatch(setSessionWaitingAction({ id, value: true }))
|
||||
const success = await deleteSession(id)
|
||||
if (success) {
|
||||
const newSessionId = sessions.find((s) => s.id !== id)?.id
|
||||
if (newSessionId) {
|
||||
dispatch(setActiveSessionIdAction({ agentId, sessionId: newSessionId }))
|
||||
} else {
|
||||
// may clear messages instead of forbidden deletion
|
||||
}
|
||||
}
|
||||
dispatch(setSessionWaitingAction({ id, value: false }))
|
||||
},
|
||||
[agentId, deleteSession, dispatch, sessions]
|
||||
)
|
||||
|
||||
const currentActiveSessionId = activeSessionId[agentId]
|
||||
|
||||
useEffect(() => {
|
||||
@ -52,7 +93,7 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
)
|
||||
}
|
||||
|
||||
// if (error) return
|
||||
if (error) return <Alert color="danger" content={t('agent.session.get.error.failed')} />
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@ -64,20 +105,12 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2, delay: 0.1 }}>
|
||||
<SessionModal
|
||||
agentId={agentId}
|
||||
onSessionCreated={(created) => setActiveSessionId(agentId, created.id)}
|
||||
trigger={{
|
||||
content: (
|
||||
<Button
|
||||
onPress={(e) => e.continuePropagation()}
|
||||
className="mb-2 w-full justify-start bg-transparent text-foreground-500 hover:bg-accent">
|
||||
<Plus size={16} className="mr-1 shrink-0" />
|
||||
{t('agent.session.add.title')}
|
||||
</Button>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onPress={handleCreateSession}
|
||||
className="mb-2 w-full justify-start bg-transparent text-foreground-500 hover:bg-accent">
|
||||
<Plus size={16} className="mr-1 shrink-0" />
|
||||
{t('agent.session.add.title')}
|
||||
</Button>
|
||||
</motion.div>
|
||||
<AnimatePresence>
|
||||
{sessions.map((session, index) => (
|
||||
@ -90,7 +123,9 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
<SessionItem
|
||||
session={session}
|
||||
agentId={agentId}
|
||||
onDelete={() => deleteSession(session.id)}
|
||||
isDisabled={sessionWaiting[session.id]}
|
||||
isLoading={sessionWaiting[session.id]}
|
||||
onDelete={() => handleDeleteSession(session.id)}
|
||||
onPress={() => setActiveSessionId(agentId, session.id)}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
@ -18,6 +18,8 @@ export interface ChatState {
|
||||
renamingTopics: string[]
|
||||
/** topic ids that are newly renamed */
|
||||
newlyRenamedTopics: string[]
|
||||
/** is a session waiting for updating/deleting. undefined and false share same semantics. */
|
||||
sessionWaiting: Record<string, boolean>
|
||||
}
|
||||
|
||||
export interface WebSearchState {
|
||||
@ -53,6 +55,7 @@ export interface RuntimeState {
|
||||
export: ExportState
|
||||
chat: ChatState
|
||||
websearch: WebSearchState
|
||||
iknow: Record<string, boolean>
|
||||
}
|
||||
|
||||
export interface ExportState {
|
||||
@ -89,11 +92,13 @@ const initialState: RuntimeState = {
|
||||
activeTopicOrSession: 'topic',
|
||||
activeSessionId: {},
|
||||
renamingTopics: [],
|
||||
newlyRenamedTopics: []
|
||||
newlyRenamedTopics: [],
|
||||
sessionWaiting: {}
|
||||
},
|
||||
websearch: {
|
||||
activeSearches: {}
|
||||
}
|
||||
},
|
||||
iknow: {}
|
||||
}
|
||||
|
||||
const runtimeSlice = createSlice({
|
||||
@ -179,6 +184,13 @@ const runtimeSlice = createSlice({
|
||||
delete state.websearch.activeSearches[requestId]
|
||||
}
|
||||
state.websearch.activeSearches[requestId] = status
|
||||
},
|
||||
addIknowAction: (state, action: PayloadAction<string>) => {
|
||||
state.iknow[action.payload] = true
|
||||
},
|
||||
setSessionWaitingAction: (state, action: PayloadAction<{ id: string; value: boolean }>) => {
|
||||
const { id, value } = action.payload
|
||||
state.chat.sessionWaiting[id] = value
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -197,6 +209,7 @@ export const {
|
||||
setResourcesPath,
|
||||
setUpdateState,
|
||||
setExportState,
|
||||
addIknowAction,
|
||||
// Chat related actions
|
||||
toggleMultiSelectMode,
|
||||
setSelectedMessageIds,
|
||||
@ -206,6 +219,7 @@ export const {
|
||||
setActiveTopicOrSessionAction,
|
||||
setRenamingTopics,
|
||||
setNewlyRenamedTopics,
|
||||
setSessionWaitingAction,
|
||||
// WebSearch related actions
|
||||
setActiveSearches,
|
||||
setWebSearchStatus
|
||||
|
||||
Loading…
Reference in New Issue
Block a user