diff --git a/web/src/hooks/logic-hooks/navigate-hooks.ts b/web/src/hooks/logic-hooks/navigate-hooks.ts index b3d3ca2063..8559a514d8 100644 --- a/web/src/hooks/logic-hooks/navigate-hooks.ts +++ b/web/src/hooks/logic-hooks/navigate-hooks.ts @@ -95,6 +95,13 @@ export const useNavigatePage = () => { [navigate], ); + const navigateToAgentExplore = useCallback( + (id: string) => () => { + navigate(`${Routes.Agent}/${id}/explore`); + }, + [navigate], + ); + const navigateToAgentLogs = useCallback( (id: string) => () => { navigate(`${Routes.AgentLogPage}/${id}`); @@ -176,7 +183,7 @@ export const useNavigatePage = () => { const navigateToDataflowResult = useCallback( (props: NavigateToDataflowResultProps) => () => { - let params: string[] = []; + const params: string[] = []; Object.keys(props).forEach((key) => { if (props[key as keyof typeof props]) { params.push(`${key}=${props[key as keyof typeof props]}`); @@ -203,6 +210,7 @@ export const useNavigatePage = () => { navigateToChunk, navigateToAgents, navigateToAgent, + navigateToAgentExplore, navigateToAgentLogs, navigateToAgentTemplates, navigateToSearchList, diff --git a/web/src/hooks/use-agent-request.ts b/web/src/hooks/use-agent-request.ts index cea6fb25af..b1d011bf09 100644 --- a/web/src/hooks/use-agent-request.ts +++ b/web/src/hooks/use-agent-request.ts @@ -1,8 +1,14 @@ import { FileUploadProps } from '@/components/file-upload'; import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-filter-submit'; import message from '@/components/ui/message'; -import { AgentGlobals, initialBeginValues } from '@/constants/agent'; import { + AgentCategory, + AgentGlobals, + initialBeginValues, +} from '@/constants/agent'; +import { useFetchTenantInfo } from '@/hooks/use-user-setting-request'; +import { + IAgentLogResponse, IAgentLogsRequest, IAgentLogsResponse, IFlow, @@ -20,7 +26,10 @@ import { BeginId } from '@/pages/agent/constant'; import { IInputs } from '@/pages/agent/interface'; import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-shared-message'; import agentService, { + createAgentSession, + deleteAgentSession, fetchAgentLogsByCanvasId, + fetchAgentLogsById, fetchPipeLineList, fetchTrace, fetchWebhookTrace, @@ -39,6 +48,7 @@ import { export const enum AgentApiAction { FetchAgentListByPage = 'fetchAgentListByPage', + FetchAllAgentList = 'fetchAllAgentList', FetchAgentList = 'fetchAgentList', UpdateAgentSetting = 'updateAgentSetting', DeleteAgent = 'deleteAgent', @@ -61,6 +71,11 @@ export const enum AgentApiAction { CancelDataflow = 'cancelDataflow', CancelCanvas = 'cancelCanvas', FetchWebhookTrace = 'fetchWebhookTrace', + FetchSessionsByCanvasId = 'fetchSessionsByCanvasId', + FetchSessionById = 'fetchSessionById', + CreateAgentSession = 'createAgentSession', + DeleteAgentSession = 'deleteAgentSession', + FetchSessionByIdManually = 'fetchSessionByIdManually', } export const EmptyDsl = { @@ -194,6 +209,28 @@ export const useFetchAgentListByPage = () => { }; }; +export function useFetchAllAgentList() { + const { data, isFetching: loading } = useQuery({ + queryKey: [AgentApiAction.FetchAllAgentList], + queryFn: async () => { + const { data } = await agentService.listCanvas( + { + params: { + page: 1, + page_size: 100000, + canvas_category: AgentCategory.AgentCanvas, + }, + }, + true, + ); + + return data?.data?.canvas; + }, + }); + + return { data, loading }; +} + export const useUpdateAgentSetting = () => { const queryClient = useQueryClient(); @@ -627,6 +664,37 @@ export const useFetchAgentLog = (searchParams: IAgentLogsRequest) => { return { data, loading }; }; +export const useFetchSessionsByCanvasId = () => { + const { id: canvasId } = useParams(); + const { data: tenantInfo } = useFetchTenantInfo(); + + const { data, isFetching: loading } = useQuery({ + queryKey: [AgentApiAction.FetchSessionsByCanvasId, canvasId], + initialData: { total: 0, sessions: [] } as IAgentLogsResponse, + gcTime: 0, + enabled: !!canvasId && !isEmpty(tenantInfo), + queryFn: async () => { + if (!canvasId) { + return { total: 0, sessions: [] }; + } + + const { data } = await fetchAgentLogsByCanvasId(canvasId, { + page: 1, + page_size: 100000, + exp_user_id: tenantInfo.tenant_id, + }); + + return data?.data ?? { total: 0, sessions: [] }; + }, + }); + + return { + data: data?.sessions ?? [], + loading, + total: data?.total ?? 0, + }; +}; + export const useFetchExternalAgentInputs = () => { const { sharedId } = useGetSharedChatSearchParams(); @@ -873,3 +941,82 @@ export const useFetchWebhookTrace = (autoStart: boolean = true) => { currentNextSinceTs, }; }; + +export function useCreateAgentSession() { + const queryClient = useQueryClient(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [AgentApiAction.CreateAgentSession], + mutationFn: async (payload: { id: string; name: string }) => { + const { data } = await createAgentSession(payload); + + if (data.code === 0) { + queryClient.invalidateQueries({ + queryKey: [AgentApiAction.FetchSessionsByCanvasId], + }); + } + + return data?.data ?? {}; + }, + }); + + return { data, loading, createAgentSession: mutateAsync }; +} + +export function useDeleteAgentSession() { + const queryClient = useQueryClient(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [AgentApiAction.DeleteAgentSession], + mutationFn: async ({ + canvasId, + sessionId, + }: { + canvasId: string; + sessionId: string; + }) => { + const { data } = await deleteAgentSession(canvasId, sessionId); + + if (data.code === 0) { + queryClient.invalidateQueries({ + queryKey: [AgentApiAction.FetchSessionsByCanvasId], + }); + } + + return data?.code ?? -1; + }, + }); + + return { data, loading, deleteAgentSession: mutateAsync }; +} + +export function useFetchSessionManually() { + const { id: canvasId } = useParams(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [AgentApiAction.FetchSessionByIdManually, canvasId], + mutationFn: async (sessionId) => { + if (!canvasId || !sessionId) { + return null; + } + + const { data } = await fetchAgentLogsById(canvasId, sessionId); + + return data?.data; + }, + }); + + return { data, loading, fetchSessionManually: mutateAsync }; +} diff --git a/web/src/hooks/use-client-search.ts b/web/src/hooks/use-client-search.ts new file mode 100644 index 0000000000..4266549029 --- /dev/null +++ b/web/src/hooks/use-client-search.ts @@ -0,0 +1,61 @@ +import { useDebounce } from 'ahooks'; +import { useCallback, useMemo, useState } from 'react'; + +export interface SearchFilterOptions { + data: T[]; + searchFields: Array string)>; + debounceMs?: number; +} + +export function useClientSearch({ + data, + searchFields, + debounceMs = 300, +}: SearchFilterOptions) { + const [searchKeyword, setSearchKeyword] = useState(''); + + const debouncedSearchKeyword = useDebounce(searchKeyword, { + wait: debounceMs, + }); + + const handleSearchChange = useCallback( + (e: React.ChangeEvent) => { + setSearchKeyword(e.target.value); + }, + [], + ); + + const clearSearch = useCallback(() => { + setSearchKeyword(''); + }, []); + + const filteredData = useMemo(() => { + if (!debouncedSearchKeyword.trim()) { + return data; + } + + const keyword = debouncedSearchKeyword.toLowerCase().trim(); + + return data.filter((item) => { + return searchFields.some((field) => { + let value: string; + + if (typeof field === 'function') { + value = field(item); + } else { + value = String(item[field] ?? ''); + } + + return value?.toLowerCase().includes(keyword); + }); + }); + }, [data, debouncedSearchKeyword, searchFields]); + + return { + filteredData, + searchKeyword, + handleSearchChange, + clearSearch, + isSearching: debouncedSearchKeyword !== searchKeyword, + }; +} diff --git a/web/src/interfaces/database/agent.ts b/web/src/interfaces/database/agent.ts index 7caa7eee83..d543a458bf 100644 --- a/web/src/interfaces/database/agent.ts +++ b/web/src/interfaces/database/agent.ts @@ -256,6 +256,7 @@ export interface IAgentLogResponse { user_id: string; dsl: string; reference: IReference; + name: string; } export interface IAgentLogsResponse { total: number; @@ -269,6 +270,7 @@ export interface IAgentLogsRequest { desc?: boolean; page?: number; page_size?: number; + exp_user_id?: string; // tenant id } export interface IAgentLogMessage { diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 6f1b30725b..ea2c35cd7d 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -468,7 +468,7 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim dataSource: 'Data source', linkSourceSetTip: 'Manage data source linkage with this dataset', linkDataSource: 'Link data source', - tocExtraction: 'TOC enhance', + tocExtraction: 'PageIndex', tocExtractionTip: " For existing chunks, generate a hierarchical table of contents (one directory per file). During queries, when Directory Enhancement is activated, the system will use a large model to determine which directory items are relevant to the user's question, thereby identifying the relevant chunks.", deleteGenerateModalContent: ` @@ -884,7 +884,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s }, cancel: 'Cancel', chatSetting: 'Chat setting', - tocEnhance: 'TOC enhance', + tocEnhance: 'PageIndex', tocEnhanceTip: ` During the parsing of the document, table of contents information was generated (see the 'Enable Table of Contents Extraction' option in the General method). This allows the large model to return table of contents items relevant to the user's query, thereby using these items to retrieve related chunks and apply weighting to these chunks during the sorting process. This approach is derived from mimicking the behavioral logic of how humans search for knowledge in books.`, batchDeleteSessions: 'Batch delete', deleteSelectedConfirm: 'Delete the selected {count} session(s)?', @@ -2581,5 +2581,23 @@ Important structured information may include: names, dates, locations, events, k timeout: 'Timeout', fail: 'Fail', }, + explore: { + title: 'Launch', + canvasList: 'Canvas List', + sessions: 'Sessions', + newSession: 'New Session', + newSessionLabel: 'Start a new conversation', + deleteSession: 'Delete Session', + searchCanvas: 'Search canvas...', + searchSessions: 'Search sessions...', + noCanvasSelected: 'Please select a canvas', + noSessionSelected: 'Please select a session or create a new one', + noSessionsFound: 'No sessions found', + createFirstSession: 'Create your first session', + noCanvasFound: 'No canvas found', + deleteSelectedConfirm: + 'Are you sure you want to delete {{count}} session(s)?', + batchDeleteSessions: 'Delete Sessions', + }, }, }; diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index e7e3650c4a..bb86e89432 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -423,7 +423,7 @@ export default { linkSourceSetTip: '管理与此数据集的数据源链接', linkDataSource: '链接数据源', tocExtractionTip: - '对于已有的chunk生成层级结构的目录信息(每个文件一个目录)。在查询时,激活`目录增强`后,系统会用大模型去判断用户问题和哪些目录项相关,从而找到相关的chunk。', + '对于已有的chunk生成层级结构的目录信息(每个文件一个目录)。在查询时,激活`Page Index`后,系统会用大模型去判断用户问题和哪些目录项相关,从而找到相关的chunk。', deleteGenerateModalContent: `

删除生成的 {{type}} 结果 将从此数据集中移除所有派生实体和关系。 @@ -830,7 +830,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 chatSetting: '聊天设置', avatarHidden: '隐藏头像', locale: '地区', - tocEnhance: '目录增强', + tocEnhance: 'Page Index', tocEnhanceTip: `解析文档时生成了目录信息(见General方法的'启用目录抽取'),让大模型返回和用户问题相关的目录项,从而利用目录项拿到相关chunk,对这些chunk在排序中进行加权。这种方法来源于模仿人类查询书本中知识的行为逻辑`, batchDeleteSessions: '批量删除', deleteSelectedConfirm: '删除选中的 {count} 个会话?', @@ -2188,5 +2188,23 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`, notFoundMemory: '未查询到记忆', addNow: '立即添加', }, + + explore: { + title: '探索', + canvasList: '画布列表', + sessions: '会话列表', + newSession: '新建会话', + newSessionLabel: '开始新对话', + deleteSession: '删除会话', + searchCanvas: '搜索画布...', + searchSessions: '搜索会话...', + noCanvasSelected: '请选择一个画布', + noSessionSelected: '请选择一个会话或创建新会话', + noSessionsFound: '未找到会话', + createFirstSession: '创建您的第一个会话', + noCanvasFound: '未找到画布', + deleteSelectedConfirm: '确定要删除 {{count}} 个会话吗?', + batchDeleteSessions: '删除会话', + }, }, }; diff --git a/web/src/pages/agent/chat/use-send-agent-message.ts b/web/src/pages/agent/chat/use-send-agent-message.ts index cf1bd52f90..f3c191be31 100644 --- a/web/src/pages/agent/chat/use-send-agent-message.ts +++ b/web/src/pages/agent/chat/use-send-agent-message.ts @@ -252,6 +252,7 @@ export const useSendAgentMessage = ({ removeAllMessagesExceptFirst, scrollToBottom, addPrologue, + setDerivedMessages, } = useSelectDerivedMessages(); const { addEventList: addEventListFun } = useContext(AgentChatLogContext); const { @@ -274,10 +275,12 @@ export const useSendAgentMessage = ({ async ({ message, beginInputs, + exploreSessionId, }: { message: Message; messages?: Message[]; beginInputs?: BeginQuery[]; + exploreSessionId?: string; }) => { const params: Record = { id: agentId, @@ -297,7 +300,7 @@ export const useSendAgentMessage = ({ params.files = uploadResponseList; - params.session_id = sessionId; + params.session_id = sessionId || exploreSessionId; } try { @@ -364,28 +367,32 @@ export const useSendAgentMessage = ({ removeAllMessagesExceptFirst, ]); - const handlePressEnter = useCallback(() => { - if (trim(value) === '') return; - const msgBody = buildRequestBody(value); - if (done) { - setValue(''); - sendMessage({ - message: msgBody, - }); - } - addNewestOneQuestion({ ...msgBody, files: fileList }); - setTimeout(() => { - scrollToBottom(); - }, 100); - }, [ - value, - done, - addNewestOneQuestion, - fileList, - setValue, - sendMessage, - scrollToBottom, - ]); + const handlePressEnter = useCallback( + ({ exploreSessionId }: { exploreSessionId?: string } = {}) => { + if (trim(value) === '') return; + const msgBody = buildRequestBody(value); + if (done) { + setValue(''); + sendMessage({ + message: msgBody, + exploreSessionId, + }); + } + addNewestOneQuestion({ ...msgBody, files: fileList }); + setTimeout(() => { + scrollToBottom(); + }, 100); + }, + [ + value, + done, + addNewestOneQuestion, + fileList, + setValue, + sendMessage, + scrollToBottom, + ], + ); const sendedTaskMessage = useRef(false); @@ -471,5 +478,7 @@ export const useSendAgentMessage = ({ addNewestOneAnswer, sendMessage, removeFile, + setDerivedMessages, + addPrologue, }; }; diff --git a/web/src/pages/agent/explore/components/canvas-card.tsx b/web/src/pages/agent/explore/components/canvas-card.tsx new file mode 100644 index 0000000000..12e3f354a7 --- /dev/null +++ b/web/src/pages/agent/explore/components/canvas-card.tsx @@ -0,0 +1,38 @@ +import { RAGFlowAvatar } from '@/components/ragflow-avatar'; +import { Card, CardContent } from '@/components/ui/card'; +import { IFlow } from '@/interfaces/database/agent'; +import { cn } from '@/lib/utils'; + +interface CanvasCardProps { + canvas: IFlow; + selected?: boolean; + onClick: () => void; +} + +export function CanvasCard({ canvas, selected, onClick }: CanvasCardProps) { + return ( + + + +

+
{canvas.title}
+ {canvas.description && ( +
+ {canvas.description} +
+ )} +
+ + + ); +} diff --git a/web/src/pages/agent/explore/components/session-card.tsx b/web/src/pages/agent/explore/components/session-card.tsx new file mode 100644 index 0000000000..bf031c3e66 --- /dev/null +++ b/web/src/pages/agent/explore/components/session-card.tsx @@ -0,0 +1,47 @@ +import { MoreButton } from '@/components/more-button'; +import { Card, CardContent } from '@/components/ui/card'; +import { IAgentLogResponse } from '@/interfaces/database/agent'; +import { cn } from '@/lib/utils'; +import { useTranslation } from 'react-i18next'; +import { SessionDropdown } from './session-dropdown'; + +interface SessionCardProps { + session: IAgentLogResponse & { is_new?: boolean }; + selected?: boolean; + onClick: () => void; + removeTemporarySession?: (sessionId: string) => void; +} + +export function SessionCard({ + session, + selected, + onClick, + removeTemporarySession, +}: SessionCardProps) { + const { t } = useTranslation(); + const isNewSession = session.is_new; + + const displayName = isNewSession ? t('explore.newSession') : session.name; + + return ( + + +
+
{displayName}
+
+ + + +
+
+ ); +} diff --git a/web/src/pages/agent/explore/components/session-chat.tsx b/web/src/pages/agent/explore/components/session-chat.tsx new file mode 100644 index 0000000000..2ada233e7b --- /dev/null +++ b/web/src/pages/agent/explore/components/session-chat.tsx @@ -0,0 +1,190 @@ +import { FileUploadProps } from '@/components/file-upload'; +import { NextMessageInput } from '@/components/message-input/next'; +import MessageItem from '@/components/next-message-item'; +import PdfSheet from '@/components/pdf-drawer'; +import { useClickDrawer } from '@/components/pdf-drawer/hooks'; +import { MessageType } from '@/constants/chat'; +import { useUploadCanvasFileWithProgress } from '@/hooks/use-agent-request'; +import { useFetchUserInfo } from '@/hooks/use-user-setting-request'; +import { IAgentLogResponse } from '@/interfaces/database/agent'; +import { IMessage } from '@/interfaces/database/chat'; +import { BeginQuery } from '@/pages/agent/interface'; +import { ParameterDialog } from '@/pages/agent/share/parameter-dialog'; +import { buildMessageUuidWithRole } from '@/utils/chat'; +import { useCallback, useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useExploreUrlParams } from '../hooks/use-explore-url-params'; +import { useSendSessionMessage } from '../hooks/use-send-session-message'; + +interface SessionChatProps { + session?: IAgentLogResponse; +} + +export function SessionChat({ session }: SessionChatProps) { + const { t } = useTranslation(); + const { data: userInfo } = useFetchUserInfo(); + const { sessionId, isNew } = useExploreUrlParams(); + const hasLocalMessageRef = useRef(false); + + const sessionLoading = false; + + const { + value, + derivedMessages, + scrollRef, + messageContainerRef, + sendLoading, + handleInputChange, + handlePressEnter, + stopOutputMessage, + canvasInfo, + findReferenceByMessageId, + appendUploadResponseList, + removeFile, + parameterDialogVisible, + handleParametersOk, + beginInputs, + shouldShowParameterDialog, + setDerivedMessages, + } = useSendSessionMessage(); + const hasActiveSession = Boolean( + sessionId || isNew || hasLocalMessageRef.current, + ); + + const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = + useClickDrawer(); + + // File upload + const { uploadCanvasFile, loading: isUploading } = + useUploadCanvasFileWithProgress(); + + const handleUploadFile: NonNullable = + useCallback( + async (files, options) => { + const ret = await uploadCanvasFile({ files, options }); + appendUploadResponseList(ret.data, files); + }, + [appendUploadResponseList, uploadCanvasFile], + ); + + useEffect(() => { + shouldShowParameterDialog(); + }, [shouldShowParameterDialog]); + + useEffect(() => { + hasLocalMessageRef.current = false; + }, [sessionId, isNew]); + + useEffect(() => { + if (hasLocalMessageRef.current) { + return; + } + if (sessionId && session?.id === sessionId && session?.message) { + const messages = session.message; + setDerivedMessages(messages as IMessage[]); + } + }, [session?.id, session?.message, sessionId, setDerivedMessages]); + + useEffect(() => { + if (!sessionId && !isNew && !hasLocalMessageRef.current && !sendLoading) { + setDerivedMessages([]); + } + }, [sessionId, isNew, sendLoading, setDerivedMessages]); + + const handleSessionPressEnter = useCallback(async () => { + if (value.trim()) { + hasLocalMessageRef.current = true; + } + return handlePressEnter(); + }, [handlePressEnter, value]); + + return ( + <> +
+ {!hasActiveSession && ( +
+ {t('explore.noSessionSelected')} +
+ )} + + {hasActiveSession && ( +
+ {sessionLoading ? ( +
+ Loading... +
+ ) : derivedMessages.length === 0 ? ( +
+ No messages in this session +
+ ) : ( +
+ {derivedMessages.map((message, i) => ( + + ))} +
+ )} +
+
+ )} +
+ +
+
+ + {parameterDialogVisible && beginInputs.length > 0 && ( + { + const { key, ...rest } = item; + acc[key] = rest; + return acc; + }, + {} as Record>, + )} + /> + )} + + {visible && ( + + )} + + ); +} diff --git a/web/src/pages/agent/explore/components/session-dropdown.tsx b/web/src/pages/agent/explore/components/session-dropdown.tsx new file mode 100644 index 0000000000..e7c9b9a12f --- /dev/null +++ b/web/src/pages/agent/explore/components/session-dropdown.tsx @@ -0,0 +1,72 @@ +import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { useDeleteAgentSession } from '@/hooks/use-agent-request'; +import { IAgentLogResponse } from '@/interfaces/database/agent'; +import { Trash2 } from 'lucide-react'; +import { MouseEventHandler, PropsWithChildren, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useExploreUrlParams } from '../hooks/use-explore-url-params'; + +interface SessionDropdownProps { + session: IAgentLogResponse & { is_new?: boolean }; + removeTemporarySession?: (sessionId: string) => void; +} + +export function SessionDropdown({ + children, + session, + removeTemporarySession, +}: PropsWithChildren) { + const { t } = useTranslation(); + const { canvasId, setSessionId, sessionId } = useExploreUrlParams(); + const { deleteAgentSession } = useDeleteAgentSession(); + + const handleDelete: MouseEventHandler = + useCallback(async () => { + if (session.is_new && removeTemporarySession) { + removeTemporarySession(session.id); + } else if (canvasId) { + const code = await deleteAgentSession({ + canvasId, + sessionId: session.id, + }); + if (code === 0 && sessionId === session.id) { + setSessionId('', true); + } + } + }, [ + session.is_new, + session.id, + removeTemporarySession, + canvasId, + deleteAgentSession, + sessionId, + setSessionId, + ]); + + return ( + + {children} + + + { + e.preventDefault(); + }} + onClick={(e) => { + e.stopPropagation(); + }} + > + {t('common.delete')} + + + + + ); +} diff --git a/web/src/pages/agent/explore/components/session-list.tsx b/web/src/pages/agent/explore/components/session-list.tsx new file mode 100644 index 0000000000..b2929b2695 --- /dev/null +++ b/web/src/pages/agent/explore/components/session-list.tsx @@ -0,0 +1,68 @@ +import { Button } from '@/components/ui/button'; +import { SearchInput } from '@/components/ui/input'; +import { useClientSearch } from '@/hooks/use-client-search'; +import { IAgentLogResponse } from '@/interfaces/database/agent'; +import { Plus } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { useSelectDerivedSessionList } from '../hooks/use-select-derived-session-list'; +import { SessionCard } from './session-card'; + +interface SessionListProps { + selectedSessionId?: string; + onSelectSession: (sessionId: string, isNew?: boolean) => void; +} + +export function SessionList({ + selectedSessionId, + onSelectSession, +}: SessionListProps) { + const { t } = useTranslation(); + + const { sessions, loading, addTemporarySession, removeTemporarySession } = + useSelectDerivedSessionList(); + + const { filteredData, handleSearchChange, searchKeyword } = + useClientSearch({ + data: sessions, + searchFields: ['name'], + }); + + return ( +
+
+
+

{t('explore.sessions')}

+ {sessions.length} +
+ +
+
+ +
+
+ {filteredData.map((session) => ( + onSelectSession(session.id, session.is_new)} + removeTemporarySession={removeTemporarySession} + /> + ))} + {!loading && filteredData.length === 0 && ( +
+ {searchKeyword + ? t('explore.noSessionsFound') + : t('explore.noSessionsFound')} +
+ )} +
+
+ ); +} diff --git a/web/src/pages/agent/explore/hooks/use-explore-url-params.ts b/web/src/pages/agent/explore/hooks/use-explore-url-params.ts new file mode 100644 index 0000000000..e566bbef12 --- /dev/null +++ b/web/src/pages/agent/explore/hooks/use-explore-url-params.ts @@ -0,0 +1,45 @@ +import { useCallback, useMemo } from 'react'; +import { useNavigate, useParams, useSearchParams } from 'react-router'; + +export const useExploreUrlParams = () => { + const { id: canvasId } = useParams(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const sessionId = useMemo( + () => searchParams.get('sessionId') || undefined, + [searchParams], + ); + + const isNew = useMemo( + () => searchParams.get('isNew') || undefined, + [searchParams], + ); + + const setCanvasId = useCallback( + (id: string) => { + navigate(`/agent/${id}/explore`); + }, + [navigate], + ); + + const setSessionId = useCallback( + (id: string, isNewParam?: boolean) => { + const params = new URLSearchParams(); + if (id) params.set('sessionId', id); + if (isNewParam) params.set('isNew', 'true'); + navigate( + `/agent/${canvasId}/explore${params.toString() ? `?${params}` : ''}`, + ); + }, + [canvasId, navigate], + ); + + return { + canvasId, + sessionId, + isNew, + setCanvasId, + setSessionId, + }; +}; diff --git a/web/src/pages/agent/explore/hooks/use-select-derived-session-list.ts b/web/src/pages/agent/explore/hooks/use-select-derived-session-list.ts new file mode 100644 index 0000000000..c31025de73 --- /dev/null +++ b/web/src/pages/agent/explore/hooks/use-select-derived-session-list.ts @@ -0,0 +1,59 @@ +import { useFetchSessionsByCanvasId } from '@/hooks/use-agent-request'; +import { IAgentLogResponse } from '@/interfaces/database/agent'; +import { generateConversationId } from '@/utils/chat'; +import { useCallback, useEffect, useState } from 'react'; +import { useExploreUrlParams } from './use-explore-url-params'; + +export const useSelectDerivedSessionList = () => { + const [list, setList] = useState< + Array + >([]); + + const { data: sessions = [], loading } = useFetchSessionsByCanvasId(); + + const { setSessionId } = useExploreUrlParams(); + + const addTemporarySession = useCallback(() => { + const sessionId = generateConversationId(); + const now = Date.now() / 1000; + + const tempSession: IAgentLogResponse & { is_new?: boolean } = { + id: sessionId, + message: [], + create_date: '', + create_time: now, + update_date: '', + update_time: now, + round: 0, + thumb_up: 0, + errors: '', + source: '', + user_id: '', + dsl: '', + reference: {}, + is_new: true, + }; + + setList([tempSession, ...sessions]); + + setSessionId('', true); + }, [sessions, setSessionId]); + + const removeTemporarySession = useCallback((sessionId: string) => { + setList((prevList) => { + return prevList.filter((session) => session.id !== sessionId); + }); + }, []); + + // Sync server data to local state + useEffect(() => { + setList(sessions); + }, [sessions]); + + return { + sessions: list, + loading, + addTemporarySession, + removeTemporarySession, + }; +}; diff --git a/web/src/pages/agent/explore/hooks/use-send-session-message.ts b/web/src/pages/agent/explore/hooks/use-send-session-message.ts new file mode 100644 index 0000000000..34baaf98a6 --- /dev/null +++ b/web/src/pages/agent/explore/hooks/use-send-session-message.ts @@ -0,0 +1,162 @@ +import sonnerMessage from '@/components/ui/message'; +import { useSetModalState } from '@/hooks/common-hooks'; +import { + useCreateAgentSession, + useFetchAgent, +} from '@/hooks/use-agent-request'; +import { useSendAgentMessage } from '@/pages/agent/chat/use-send-agent-message'; +import { buildBeginInputListFromObject } from '@/pages/agent/form/begin-form/utils'; +import api from '@/utils/api'; +import { get, isEmpty } from 'lodash'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useParams } from 'react-router'; +import { BeginId } from '../../constant'; +import { useExploreUrlParams } from './use-explore-url-params'; + +export const useGetBeginNodePrologue = () => { + const { data } = useFetchAgent(); + const nodes = get(data, 'dsl.graph.nodes', []); + + return useMemo(() => { + const beginNode = nodes.find((node: any) => node.id === BeginId); + const formData: Record = get(beginNode, 'data.form', {}); + if (formData?.enablePrologue) { + return formData?.prologue; + } + }, [nodes]); +}; + +export const useSendSessionMessage = () => { + const { setSessionId, sessionId, isNew } = useExploreUrlParams(); + + const { data: canvasInfo } = useFetchAgent(); + + const { id: canvasId } = useParams(); + + const { createAgentSession } = useCreateAgentSession(); + + const isCreatingSession = useRef(false); + + const [beginParams, setBeginParams] = useState([]); + + const prologue = useGetBeginNodePrologue(); + + const { + visible: parameterDialogVisible, + hideModal: hideParameterDialog, + showModal: showParameterDialog, + } = useSetModalState(); + + const beginInputs = useMemo(() => { + const beginNode = canvasInfo?.dsl?.graph?.nodes?.find( + (node: any) => node.id === BeginId, + ); + const inputs = beginNode?.data?.form?.inputs; + return buildBeginInputListFromObject(inputs || {}); + }, [canvasInfo]); + + const { + setDerivedMessages, + addPrologue, + derivedMessages, + handlePressEnter: handleSendPressEnter, + value, + ...chatLogic + } = useSendAgentMessage({ + url: api.runCanvasExplore(canvasId!), + beginParams, + }); + + const handleParametersOk = useCallback( + (params: any[]) => { + setBeginParams(params); + hideParameterDialog(); + }, + [hideParameterDialog], + ); + + const shouldShowParameterDialog = useCallback(() => { + if (beginInputs.length > 0 && beginParams.length === 0) { + showParameterDialog(); + } + }, [beginInputs, beginParams, showParameterDialog]); + + const handlePressEnter = useCallback(async () => { + if (isCreatingSession.current) { + return; + } + + if ( + prologue && + isEmpty(sessionId) && + !isNew && + derivedMessages.length === 0 + ) { + addPrologue(prologue); + } + + let exploreSessionId = sessionId; + + if (isEmpty(sessionId) && canvasId) { + isCreatingSession.current = true; + try { + const sessionName = value?.trim() || 'New Session'; + const result = await createAgentSession({ + id: canvasId, + name: sessionName, + }); + + exploreSessionId = result.id; + + setSessionId(result.id, false); + + setTimeout(() => { + isCreatingSession.current = false; + }, 100); + } catch (error) { + isCreatingSession.current = false; + sonnerMessage.error('Failed to create session'); + console.error('Failed to create session:', error); + return; + } + } + + return handleSendPressEnter?.({ exploreSessionId }); + }, [ + addPrologue, + canvasId, + createAgentSession, + derivedMessages.length, + handleSendPressEnter, + isNew, + prologue, + sessionId, + setSessionId, + value, + ]); + + useEffect(() => { + if (isNew && isEmpty(sessionId)) { + setDerivedMessages([]); + } + }, [isNew, sessionId, setDerivedMessages]); + + useEffect(() => { + if (prologue && isNew && isEmpty(sessionId)) { + addPrologue(prologue); + } + }, [addPrologue, isNew, prologue, sessionId]); + + return { + ...chatLogic, + value, + derivedMessages, + handlePressEnter, + canvasInfo, + parameterDialogVisible, + handleParametersOk, + beginInputs, + shouldShowParameterDialog, + setDerivedMessages, + }; +}; diff --git a/web/src/pages/agent/explore/index.tsx b/web/src/pages/agent/explore/index.tsx new file mode 100644 index 0000000000..0fda19c353 --- /dev/null +++ b/web/src/pages/agent/explore/index.tsx @@ -0,0 +1,75 @@ +import { PageHeader } from '@/components/page-header'; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@/components/ui/breadcrumb'; +import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; +import { useFetchSessionManually } from '@/hooks/use-agent-request'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router'; +import { useFetchDataOnMount } from '../hooks/use-fetch-data'; +import { SessionChat } from './components/session-chat'; +import { SessionList } from './components/session-list'; +import { useExploreUrlParams } from './hooks/use-explore-url-params'; + +export default function AgentExplore() { + const { sessionId, setSessionId } = useExploreUrlParams(); + const { navigateToAgent } = useNavigatePage(); + const { t } = useTranslation(); + const { id } = useParams(); + const { flowDetail: agentDetail } = useFetchDataOnMount(); + const { fetchSessionManually, data: session } = useFetchSessionManually(); + + const handleBackToAgent = useCallback(() => { + const navigateFn = navigateToAgent(id as string); + navigateFn(); + }, [id, navigateToAgent]); + + const handleSessionSelect = useCallback( + (id: string, isNew?: boolean) => { + setSessionId(id, isNew); + fetchSessionManually(id); + }, + [fetchSessionManually, setSessionId], + ); + + return ( +
+ + + + + + {t('header.flow')} + + + + + + {agentDetail?.title || t('explore.title')} + + + + + + +
+
+ +
+ +
+ +
+
+
+ ); +} diff --git a/web/src/pages/agent/hooks/use-is-webhook.ts b/web/src/pages/agent/hooks/use-is-webhook.ts index 297a511f08..dae93c19ec 100644 --- a/web/src/pages/agent/hooks/use-is-webhook.ts +++ b/web/src/pages/agent/hooks/use-is-webhook.ts @@ -8,3 +8,11 @@ export function useIsWebhookMode() { return beginNode?.data.form?.mode === AgentDialogueMode.Webhook; } + +export function useIsConversationMode() { + const getNode = useGraphStore((state) => state.getNode); + + const beginNode = getNode(BeginId); + + return beginNode?.data.form?.mode === AgentDialogueMode.Conversational; +} diff --git a/web/src/pages/agent/index.tsx b/web/src/pages/agent/index.tsx index 0390453a84..e16a30b07d 100644 --- a/web/src/pages/agent/index.tsx +++ b/web/src/pages/agent/index.tsx @@ -25,6 +25,7 @@ import { ReactFlowProvider } from '@xyflow/react'; import { ChevronDown, CirclePlay, + Compass, History, LaptopMinimalCheck, Logs, @@ -46,7 +47,10 @@ import { useFetchDataOnMount } from './hooks/use-fetch-data'; import { useFetchPipelineLog } from './hooks/use-fetch-pipeline-log'; import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query'; import { useIsPipeline } from './hooks/use-is-pipeline'; -import { useIsWebhookMode } from './hooks/use-is-webhook'; +import { + useIsConversationMode, + useIsWebhookMode, +} from './hooks/use-is-webhook'; import { useRunDataflow } from './hooks/use-run-dataflow'; import { useSaveGraph, @@ -110,10 +114,12 @@ export default function Agent() { const { showEmbedModal, hideEmbedModal, embedVisible, beta } = useShowEmbedModal(); - const { navigateToAgentLogs } = useNavigatePage(); + const { navigateToAgentLogs, navigateToAgentExplore } = useNavigatePage(); const time = useWatchAgentChange(chatDrawerVisible); const isWebhookMode = useIsWebhookMode(); + const isConversationMode = useIsConversationMode(); + // pipeline const { @@ -257,6 +263,15 @@ export default function Agent() { {t('flow.log')} )} + {isConversationMode && ( + + )}