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
|
||||
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)
|
||||
if (!result.success) {
|
||||
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 { useCallback } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const useSessions = (agentId: string | null) => {
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
const key = agentId ? client.getSessionPaths(agentId).base : null
|
||||
|
||||
const fetcher = async () => {
|
||||
if (!agentId) throw new Error('No active agent.')
|
||||
const data = await client.listSessions(agentId)
|
||||
return data.data
|
||||
const getKey = (pageIndex: number, previousPageData: ListAgentSessionsResponse | null) => {
|
||||
if (!agentId) return null
|
||||
if (previousPageData && previousPageData.data.length === 0) return null
|
||||
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(
|
||||
async (form: CreateSessionForm): Promise<CreateAgentSessionResponse | null> => {
|
||||
if (!agentId) return null
|
||||
try {
|
||||
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
|
||||
} catch (error) {
|
||||
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
|
||||
try {
|
||||
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
|
||||
} catch (error) {
|
||||
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
|
||||
try {
|
||||
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
|
||||
} catch (error) {
|
||||
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.delete.error.failed')))
|
||||
@ -64,9 +125,14 @@ export const useSessions = (agentId: string | null) => {
|
||||
)
|
||||
|
||||
return {
|
||||
sessions: data ?? [],
|
||||
sessions,
|
||||
total,
|
||||
hasMore,
|
||||
error,
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
isValidating,
|
||||
loadMore,
|
||||
createSession,
|
||||
getSession,
|
||||
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 { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
@ -12,7 +12,8 @@ import {
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { Alert, Spin } from 'antd'
|
||||
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 styled from 'styled-components'
|
||||
|
||||
@ -25,11 +26,39 @@ interface SessionsProps {
|
||||
|
||||
const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
const { t } = useTranslation()
|
||||
const { sessions, isLoading, error, deleteSession } = useSessions(agentId)
|
||||
const { sessions, isLoading, error, deleteSession, hasMore, loadMore, isLoadingMore } = useSessions(agentId)
|
||||
const { chat } = useRuntime()
|
||||
const { activeSessionIdMap } = chat
|
||||
const dispatch = useAppDispatch()
|
||||
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(
|
||||
(agentId: string, sessionId: string | null) => {
|
||||
@ -97,6 +126,7 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
|
||||
return (
|
||||
<StyledVirtualList
|
||||
ref={listRef}
|
||||
className="sessions-tab"
|
||||
list={sessions}
|
||||
estimateSize={() => 9 * 4}
|
||||
@ -108,14 +138,21 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
{t('agent.session.add.title')}
|
||||
</AddButton>
|
||||
}>
|
||||
{(session) => (
|
||||
<SessionItem
|
||||
key={session.id}
|
||||
session={session}
|
||||
agentId={agentId}
|
||||
onDelete={() => handleDeleteSession(session.id)}
|
||||
onPress={() => setActiveSessionId(agentId, session.id)}
|
||||
/>
|
||||
{(session, index) => (
|
||||
<>
|
||||
<SessionItem
|
||||
key={session.id}
|
||||
session={session}
|
||||
agentId={agentId}
|
||||
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>
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user