mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
Merge bb919e3b21 into 8ab375161d
This commit is contained in:
commit
b9405cb6c8
@ -172,10 +172,19 @@ export class AgentApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listSessions(agentId: string): Promise<ListAgentSessionsResponse> {
|
public async listSessions(agentId: string, options?: ListOptions): Promise<ListAgentSessionsResponse> {
|
||||||
const url = this.getSessionPaths(agentId).base
|
const url = this.getSessionPaths(agentId).base
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get(url)
|
const params = new URLSearchParams()
|
||||||
|
if (options?.limit !== undefined) params.append('limit', String(options.limit))
|
||||||
|
if (options?.offset !== undefined) params.append('offset', String(options.offset))
|
||||||
|
if (options?.sortBy) params.append('sortBy', options.sortBy)
|
||||||
|
if (options?.orderBy) params.append('orderBy', options.orderBy)
|
||||||
|
|
||||||
|
const queryString = params.toString()
|
||||||
|
const fullUrl = queryString ? `${url}?${queryString}` : url
|
||||||
|
|
||||||
|
const response = await this.axios.get(fullUrl)
|
||||||
const result = ListAgentSessionsResponseSchema.safeParse(response.data)
|
const result = ListAgentSessionsResponseSchema.safeParse(response.data)
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error('Not a valid Sessions array.')
|
throw new Error('Not a valid Sessions array.')
|
||||||
|
|||||||
@ -1,29 +1,72 @@
|
|||||||
import type { CreateAgentSessionResponse, CreateSessionForm, GetAgentSessionResponse } from '@renderer/types'
|
import type {
|
||||||
|
CreateAgentSessionResponse,
|
||||||
|
CreateSessionForm,
|
||||||
|
GetAgentSessionResponse,
|
||||||
|
ListAgentSessionsResponse
|
||||||
|
} from '@renderer/types'
|
||||||
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
|
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
|
||||||
import { useCallback } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import useSWR from 'swr'
|
import useSWRInfinite from 'swr/infinite'
|
||||||
|
|
||||||
import { useAgentClient } from './useAgentClient'
|
import { useAgentClient } from './useAgentClient'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
export const useSessions = (agentId: string | null) => {
|
export const useSessions = (agentId: string | null) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const client = useAgentClient()
|
const client = useAgentClient()
|
||||||
const key = agentId ? client.getSessionPaths(agentId).base : null
|
|
||||||
|
|
||||||
const fetcher = async () => {
|
const getKey = (pageIndex: number, previousPageData: ListAgentSessionsResponse | null) => {
|
||||||
if (!agentId) throw new Error('No active agent.')
|
if (!agentId) return null
|
||||||
const data = await client.listSessions(agentId)
|
if (previousPageData && previousPageData.data.length === 0) return null
|
||||||
return data.data
|
return [client.getSessionPaths(agentId).base, pageIndex]
|
||||||
}
|
}
|
||||||
const { data, error, isLoading, mutate } = useSWR(key, fetcher)
|
|
||||||
|
const fetcher = async ([, pageIndex]: [string, number]) => {
|
||||||
|
if (!agentId) throw new Error('No active agent.')
|
||||||
|
return await client.listSessions(agentId, {
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
offset: pageIndex * PAGE_SIZE
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error, isLoading, isValidating, mutate, size, setSize } = useSWRInfinite(getKey, fetcher)
|
||||||
|
|
||||||
|
const sessions = useMemo(() => {
|
||||||
|
if (!data) return []
|
||||||
|
return data.flatMap((page) => page.data)
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const total = data?.[0]?.total ?? 0
|
||||||
|
const hasMore = sessions.length < total
|
||||||
|
const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined')
|
||||||
|
|
||||||
|
const loadMore = useCallback(() => {
|
||||||
|
if (!isLoadingMore && hasMore) {
|
||||||
|
setSize(size + 1)
|
||||||
|
}
|
||||||
|
}, [isLoadingMore, hasMore, setSize, size])
|
||||||
|
|
||||||
const createSession = useCallback(
|
const createSession = useCallback(
|
||||||
async (form: CreateSessionForm): Promise<CreateAgentSessionResponse | null> => {
|
async (form: CreateSessionForm): Promise<CreateAgentSessionResponse | null> => {
|
||||||
if (!agentId) return null
|
if (!agentId) return null
|
||||||
try {
|
try {
|
||||||
const result = await client.createSession(agentId, form)
|
const result = await client.createSession(agentId, form)
|
||||||
await mutate((prev) => [result, ...(prev ?? [])], { revalidate: false })
|
await mutate(
|
||||||
|
(prev) => {
|
||||||
|
if (!prev || prev.length === 0) {
|
||||||
|
return [{ data: [result], total: 1, limit: PAGE_SIZE, offset: 0 }]
|
||||||
|
}
|
||||||
|
const newTotal = prev[0].total + 1
|
||||||
|
return prev.map((page, index) => ({
|
||||||
|
...page,
|
||||||
|
data: index === 0 ? [result, ...page.data] : page.data,
|
||||||
|
total: newTotal
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{ revalidate: false }
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.create.error.failed')))
|
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.create.error.failed')))
|
||||||
@ -38,7 +81,14 @@ export const useSessions = (agentId: string | null) => {
|
|||||||
if (!agentId) return null
|
if (!agentId) return null
|
||||||
try {
|
try {
|
||||||
const result = await client.getSession(agentId, id)
|
const result = await client.getSession(agentId, id)
|
||||||
mutate((prev) => prev?.map((session) => (session.id === result.id ? result : session)))
|
mutate(
|
||||||
|
(prev) =>
|
||||||
|
prev?.map((page) => ({
|
||||||
|
...page,
|
||||||
|
data: page.data.map((session) => (session.id === result.id ? result : session))
|
||||||
|
})),
|
||||||
|
{ revalidate: false }
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.get.error.failed')))
|
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.get.error.failed')))
|
||||||
@ -53,7 +103,18 @@ export const useSessions = (agentId: string | null) => {
|
|||||||
if (!agentId) return false
|
if (!agentId) return false
|
||||||
try {
|
try {
|
||||||
await client.deleteSession(agentId, id)
|
await client.deleteSession(agentId, id)
|
||||||
mutate((prev) => prev?.filter((session) => session.id !== id))
|
mutate(
|
||||||
|
(prev) => {
|
||||||
|
if (!prev || prev.length === 0) return prev
|
||||||
|
const newTotal = prev[0].total - 1
|
||||||
|
return prev.map((page) => ({
|
||||||
|
...page,
|
||||||
|
data: page.data.filter((session) => session.id !== id),
|
||||||
|
total: newTotal
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{ revalidate: false }
|
||||||
|
)
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.delete.error.failed')))
|
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.delete.error.failed')))
|
||||||
@ -64,9 +125,14 @@ export const useSessions = (agentId: string | null) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessions: data ?? [],
|
sessions,
|
||||||
|
total,
|
||||||
|
hasMore,
|
||||||
error,
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isLoadingMore,
|
||||||
|
isValidating,
|
||||||
|
loadMore,
|
||||||
createSession,
|
createSession,
|
||||||
getSession,
|
getSession,
|
||||||
deleteSession
|
deleteSession
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
import { DynamicVirtualList, type DynamicVirtualListRef } from '@renderer/components/VirtualList'
|
||||||
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
|
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
|
||||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
@ -12,7 +12,8 @@ import {
|
|||||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||||
import { Alert, Spin } from 'antd'
|
import { Alert, Spin } from 'antd'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { memo, useCallback, useEffect } from 'react'
|
import { throttle } from 'lodash'
|
||||||
|
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -25,11 +26,39 @@ interface SessionsProps {
|
|||||||
|
|
||||||
const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { sessions, isLoading, error, deleteSession } = useSessions(agentId)
|
const { sessions, isLoading, error, deleteSession, hasMore, loadMore, isLoadingMore } = useSessions(agentId)
|
||||||
const { chat } = useRuntime()
|
const { chat } = useRuntime()
|
||||||
const { activeSessionIdMap } = chat
|
const { activeSessionIdMap } = chat
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { createDefaultSession, creatingSession } = useCreateDefaultSession(agentId)
|
const { createDefaultSession, creatingSession } = useCreateDefaultSession(agentId)
|
||||||
|
const listRef = useRef<DynamicVirtualListRef>(null)
|
||||||
|
|
||||||
|
// Throttled scroll handler to avoid excessive calls
|
||||||
|
const handleScroll = useMemo(
|
||||||
|
() =>
|
||||||
|
throttle(() => {
|
||||||
|
const scrollElement = listRef.current?.scrollElement()
|
||||||
|
if (!scrollElement) return
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = scrollElement
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 100 && hasMore && !isLoadingMore) {
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
}, 150),
|
||||||
|
[hasMore, isLoadingMore, loadMore]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle scroll to load more
|
||||||
|
useEffect(() => {
|
||||||
|
const scrollElement = listRef.current?.scrollElement()
|
||||||
|
if (!scrollElement) return
|
||||||
|
|
||||||
|
scrollElement.addEventListener('scroll', handleScroll)
|
||||||
|
return () => {
|
||||||
|
handleScroll.cancel()
|
||||||
|
scrollElement.removeEventListener('scroll', handleScroll)
|
||||||
|
}
|
||||||
|
}, [handleScroll])
|
||||||
|
|
||||||
const setActiveSessionId = useCallback(
|
const setActiveSessionId = useCallback(
|
||||||
(agentId: string, sessionId: string | null) => {
|
(agentId: string, sessionId: string | null) => {
|
||||||
@ -97,6 +126,7 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledVirtualList
|
<StyledVirtualList
|
||||||
|
ref={listRef}
|
||||||
className="sessions-tab"
|
className="sessions-tab"
|
||||||
list={sessions}
|
list={sessions}
|
||||||
estimateSize={() => 9 * 4}
|
estimateSize={() => 9 * 4}
|
||||||
@ -108,14 +138,21 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
|||||||
{t('agent.session.add.title')}
|
{t('agent.session.add.title')}
|
||||||
</AddButton>
|
</AddButton>
|
||||||
}>
|
}>
|
||||||
{(session) => (
|
{(session, index) => (
|
||||||
<SessionItem
|
<>
|
||||||
key={session.id}
|
<SessionItem
|
||||||
session={session}
|
key={session.id}
|
||||||
agentId={agentId}
|
session={session}
|
||||||
onDelete={() => handleDeleteSession(session.id)}
|
agentId={agentId}
|
||||||
onPress={() => setActiveSessionId(agentId, session.id)}
|
onDelete={() => handleDeleteSession(session.id)}
|
||||||
/>
|
onPress={() => setActiveSessionId(agentId, session.id)}
|
||||||
|
/>
|
||||||
|
{index === sessions.length - 1 && isLoadingMore && (
|
||||||
|
<div className="flex justify-center py-2">
|
||||||
|
<Spin size="small" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</StyledVirtualList>
|
</StyledVirtualList>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user