### What problem does this PR solve? Feat: Show agent embed dialog #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| @@ -0,0 +1,40 @@ | |||
| // registry/default/components/comp-430.tsx | |||
| import { cn } from '@/lib/utils'; | |||
| import * as TabsPrimitive from '@radix-ui/react-tabs'; | |||
| import React from 'react'; | |||
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; | |||
| export const UnderlineTabsList = React.forwardRef< | |||
| React.ElementRef<typeof TabsPrimitive.List>, | |||
| React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> | |||
| >(function UnderlineTabsList({ className, ...props }, ref) { | |||
| return ( | |||
| <TabsList | |||
| ref={ref} | |||
| className={cn( | |||
| 'text-foreground h-auto gap-2 rounded-none border-b bg-transparent px-0 py-1', | |||
| className, | |||
| )} | |||
| {...props} | |||
| /> | |||
| ); | |||
| }); | |||
| export const UnderlineTabsTrigger = React.forwardRef< | |||
| React.ElementRef<typeof TabsPrimitive.Trigger>, | |||
| React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> | |||
| >(function UnderlineTabsTrigger({ className, ...props }, ref) { | |||
| return ( | |||
| <TabsTrigger | |||
| ref={ref} | |||
| className={cn( | |||
| 'hover:bg-accent hover:text-foreground data-[state=active]:after:bg-primary data-[state=active]:hover:bg-accent relative after:absolute after:inset-x-0 after:bottom-0 after:-mb-1 after:h-0.5 data-[state=active]:bg-transparent data-[state=active]:shadow-none', | |||
| className, | |||
| )} | |||
| {...props} | |||
| /> | |||
| ); | |||
| }); | |||
| export { Tabs as UnderlineTabs, TabsContent as UnderlineTabsContent }; | |||
| @@ -38,7 +38,7 @@ const DialogContent = React.forwardRef< | |||
| <DialogPrimitive.Content | |||
| ref={ref} | |||
| className={cn( | |||
| 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-colors-background-neutral-standard p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', | |||
| 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-2xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-colors-background-neutral-standard p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', | |||
| className, | |||
| )} | |||
| {...props} | |||
| @@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef< | |||
| <TabsPrimitive.Trigger | |||
| ref={ref} | |||
| className={cn( | |||
| 'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-colors-background-inverse-strong data-[state=active]:text-colors-text-inverse-strong data-[state=active]:shadow-sm', | |||
| 'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-colors-background-inverse-strong data-[state=active]:text-text-title data-[state=active]:shadow-sm', | |||
| className, | |||
| )} | |||
| {...props} | |||
| @@ -0,0 +1,132 @@ | |||
| import CopyToClipboard from '@/components/copy-to-clipboard'; | |||
| import HightLightMarkdown from '@/components/highlight-markdown'; | |||
| import { | |||
| UnderlineTabs, | |||
| UnderlineTabsContent, | |||
| UnderlineTabsList, | |||
| UnderlineTabsTrigger, | |||
| } from '@/components/originui/underline-tabs'; | |||
| import { | |||
| Dialog, | |||
| DialogContent, | |||
| DialogHeader, | |||
| DialogTitle, | |||
| } from '@/components/ui/dialog'; | |||
| import { SharedFrom } from '@/constants/chat'; | |||
| import { | |||
| LanguageAbbreviation, | |||
| LanguageAbbreviationMap, | |||
| } from '@/constants/common'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { memo, useMemo, useState } from 'react'; | |||
| type IProps = IModalProps<any> & { | |||
| token: string; | |||
| form: SharedFrom; | |||
| beta: string; | |||
| isAgent: boolean; | |||
| }; | |||
| function EmbedDialog({ | |||
| hideModal, | |||
| token = '', | |||
| form, | |||
| beta = '', | |||
| isAgent, | |||
| }: IProps) { | |||
| const { t } = useTranslate('chat'); | |||
| const [visibleAvatar, setVisibleAvatar] = useState(false); | |||
| const [locale, setLocale] = useState(''); | |||
| const languageOptions = useMemo(() => { | |||
| return Object.values(LanguageAbbreviation).map((x) => ({ | |||
| label: LanguageAbbreviationMap[x], | |||
| value: x, | |||
| })); | |||
| }, []); | |||
| const generateIframeSrc = () => { | |||
| let src = `${location.origin}/chat/share?shared_id=${token}&from=${form}&auth=${beta}`; | |||
| if (visibleAvatar) { | |||
| src += '&visible_avatar=1'; | |||
| } | |||
| if (locale) { | |||
| src += `&locale=${locale}`; | |||
| } | |||
| return src; | |||
| }; | |||
| const iframeSrc = generateIframeSrc(); | |||
| const text = ` | |||
| ~~~ html | |||
| <iframe | |||
| src="${iframeSrc}" | |||
| style="width: 100%; height: 100%; min-height: 600px" | |||
| frameborder="0" | |||
| > | |||
| </iframe> | |||
| ~~~ | |||
| `; | |||
| return ( | |||
| <Dialog open onOpenChange={hideModal}> | |||
| <DialogContent> | |||
| <DialogHeader> | |||
| <DialogTitle> | |||
| {t('embedIntoSite', { keyPrefix: 'common' })} | |||
| </DialogTitle> | |||
| </DialogHeader> | |||
| <section className="w-full overflow-auto"> | |||
| <UnderlineTabs defaultValue="1" className="w-full"> | |||
| <UnderlineTabsList> | |||
| <UnderlineTabsTrigger value="1"> | |||
| {t('fullScreenTitle')} | |||
| </UnderlineTabsTrigger> | |||
| <UnderlineTabsTrigger value="2"> | |||
| {t('partialTitle')} | |||
| </UnderlineTabsTrigger> | |||
| <UnderlineTabsTrigger value="3"> | |||
| {t('extensionTitle')} | |||
| </UnderlineTabsTrigger> | |||
| </UnderlineTabsList> | |||
| <UnderlineTabsContent value="1"> | |||
| <section> | |||
| <HightLightMarkdown>{text}</HightLightMarkdown> | |||
| </section> | |||
| </UnderlineTabsContent> | |||
| <UnderlineTabsContent value="2"> | |||
| {t('comingSoon')} | |||
| </UnderlineTabsContent> | |||
| <UnderlineTabsContent value="3"> | |||
| {t('comingSoon')} | |||
| </UnderlineTabsContent> | |||
| </UnderlineTabs> | |||
| <div className="text-base font-medium mt-4 mb-1"> | |||
| {t(isAgent ? 'flow' : 'chat', { keyPrefix: 'header' })} | |||
| <span className="ml-1 inline-block">ID</span> | |||
| </div> | |||
| <div className="bg-background-card rounded-md p-2 "> | |||
| {token} <CopyToClipboard text={token}></CopyToClipboard> | |||
| </div> | |||
| <a | |||
| className="pt-3 cursor-pointer text-background-checked inline-block" | |||
| href={ | |||
| isAgent | |||
| ? 'https://ragflow.io/docs/dev/http_api_reference#create-session-with-agent' | |||
| : 'https://ragflow.io/docs/dev/http_api_reference#create-session-with-chat-assistant' | |||
| } | |||
| target="_blank" | |||
| rel="noreferrer" | |||
| > | |||
| {t('howUseId', { keyPrefix: isAgent ? 'flow' : 'chat' })} | |||
| </a> | |||
| </section> | |||
| </DialogContent> | |||
| </Dialog> | |||
| ); | |||
| } | |||
| export default memo(EmbedDialog); | |||
| @@ -0,0 +1,179 @@ | |||
| import { SharedFrom } from '@/constants/chat'; | |||
| import { | |||
| useSetModalState, | |||
| useShowDeleteConfirm, | |||
| useTranslate, | |||
| } from '@/hooks/common-hooks'; | |||
| import { | |||
| useCreateSystemToken, | |||
| useFetchManualSystemTokenList, | |||
| useFetchSystemTokenList, | |||
| useRemoveSystemToken, | |||
| } from '@/hooks/user-setting-hooks'; | |||
| import { IStats } from '@/interfaces/database/chat'; | |||
| import { useQueryClient } from '@tanstack/react-query'; | |||
| import { message } from 'antd'; | |||
| import { useCallback } from 'react'; | |||
| export const useOperateApiKey = (idKey: string, dialogId?: string) => { | |||
| const { removeToken } = useRemoveSystemToken(); | |||
| const { createToken, loading: creatingLoading } = useCreateSystemToken(); | |||
| const { data: tokenList, loading: listLoading } = useFetchSystemTokenList(); | |||
| const showDeleteConfirm = useShowDeleteConfirm(); | |||
| const onRemoveToken = (token: string) => { | |||
| showDeleteConfirm({ | |||
| onOk: () => removeToken(token), | |||
| }); | |||
| }; | |||
| const onCreateToken = useCallback(() => { | |||
| createToken({ [idKey]: dialogId }); | |||
| }, [createToken, idKey, dialogId]); | |||
| return { | |||
| removeToken: onRemoveToken, | |||
| createToken: onCreateToken, | |||
| tokenList, | |||
| creatingLoading, | |||
| listLoading, | |||
| }; | |||
| }; | |||
| type ChartStatsType = { | |||
| [k in keyof IStats]: Array<{ xAxis: string; yAxis: number }>; | |||
| }; | |||
| export const useSelectChartStatsList = (): ChartStatsType => { | |||
| const queryClient = useQueryClient(); | |||
| const data = queryClient.getQueriesData({ queryKey: ['fetchStats'] }); | |||
| const stats: IStats = (data.length > 0 ? data[0][1] : {}) as IStats; | |||
| return Object.keys(stats).reduce((pre, cur) => { | |||
| const item = stats[cur as keyof IStats]; | |||
| if (item.length > 0) { | |||
| pre[cur as keyof IStats] = item.map((x) => ({ | |||
| xAxis: x[0] as string, | |||
| yAxis: x[1] as number, | |||
| })); | |||
| } | |||
| return pre; | |||
| }, {} as ChartStatsType); | |||
| }; | |||
| export const useShowTokenEmptyError = () => { | |||
| const { t } = useTranslate('chat'); | |||
| const showTokenEmptyError = useCallback(() => { | |||
| message.error(t('tokenError')); | |||
| }, [t]); | |||
| return { showTokenEmptyError }; | |||
| }; | |||
| export const useShowBetaEmptyError = () => { | |||
| const { t } = useTranslate('chat'); | |||
| const showBetaEmptyError = useCallback(() => { | |||
| message.error(t('betaError')); | |||
| }, [t]); | |||
| return { showBetaEmptyError }; | |||
| }; | |||
| const getUrlWithToken = (token: string, from: string = 'chat') => { | |||
| const { protocol, host } = window.location; | |||
| return `${protocol}//${host}/chat/share?shared_id=${token}&from=${from}`; | |||
| }; | |||
| const useFetchTokenListBeforeOtherStep = () => { | |||
| const { showTokenEmptyError } = useShowTokenEmptyError(); | |||
| const { showBetaEmptyError } = useShowBetaEmptyError(); | |||
| const { data: tokenList, fetchSystemTokenList } = | |||
| useFetchManualSystemTokenList(); | |||
| let token = '', | |||
| beta = ''; | |||
| if (Array.isArray(tokenList) && tokenList.length > 0) { | |||
| token = tokenList[0].token; | |||
| beta = tokenList[0].beta; | |||
| } | |||
| token = | |||
| Array.isArray(tokenList) && tokenList.length > 0 ? tokenList[0].token : ''; | |||
| const handleOperate = useCallback(async () => { | |||
| const ret = await fetchSystemTokenList(); | |||
| const list = ret; | |||
| if (Array.isArray(list) && list.length > 0) { | |||
| if (!list[0].beta) { | |||
| showBetaEmptyError(); | |||
| return false; | |||
| } | |||
| return list[0]?.token; | |||
| } else { | |||
| showTokenEmptyError(); | |||
| return false; | |||
| } | |||
| }, [fetchSystemTokenList, showBetaEmptyError, showTokenEmptyError]); | |||
| return { | |||
| token, | |||
| beta, | |||
| handleOperate, | |||
| }; | |||
| }; | |||
| export const useShowEmbedModal = () => { | |||
| const { | |||
| visible: embedVisible, | |||
| hideModal: hideEmbedModal, | |||
| showModal: showEmbedModal, | |||
| } = useSetModalState(); | |||
| const { handleOperate, token, beta } = useFetchTokenListBeforeOtherStep(); | |||
| const handleShowEmbedModal = useCallback(async () => { | |||
| const succeed = await handleOperate(); | |||
| if (succeed) { | |||
| showEmbedModal(); | |||
| } | |||
| }, [handleOperate, showEmbedModal]); | |||
| return { | |||
| showEmbedModal: handleShowEmbedModal, | |||
| hideEmbedModal, | |||
| embedVisible, | |||
| embedToken: token, | |||
| beta, | |||
| }; | |||
| }; | |||
| export const usePreviewChat = (idKey: string) => { | |||
| const { handleOperate } = useFetchTokenListBeforeOtherStep(); | |||
| const open = useCallback( | |||
| (t: string) => { | |||
| window.open( | |||
| getUrlWithToken( | |||
| t, | |||
| idKey === 'canvasId' ? SharedFrom.Agent : SharedFrom.Chat, | |||
| ), | |||
| '_blank', | |||
| ); | |||
| }, | |||
| [idKey], | |||
| ); | |||
| const handlePreview = useCallback(async () => { | |||
| const token = await handleOperate(); | |||
| if (token) { | |||
| open(token); | |||
| } | |||
| }, [handleOperate, open]); | |||
| return { | |||
| handlePreview, | |||
| }; | |||
| }; | |||
| @@ -7,13 +7,25 @@ import { | |||
| DropdownMenuSeparator, | |||
| DropdownMenuTrigger, | |||
| } from '@/components/ui/dropdown-menu'; | |||
| import { SharedFrom } from '@/constants/chat'; | |||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | |||
| import { ReactFlowProvider } from '@xyflow/react'; | |||
| import { CodeXml, EllipsisVertical, Forward, Import, Key } from 'lucide-react'; | |||
| import { | |||
| ChevronDown, | |||
| CirclePlay, | |||
| Download, | |||
| History, | |||
| Key, | |||
| Logs, | |||
| ScreenShare, | |||
| Upload, | |||
| } from 'lucide-react'; | |||
| import { ComponentPropsWithoutRef, useCallback } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { useParams } from 'umi'; | |||
| import AgentCanvas from './canvas'; | |||
| import EmbedDialog from './embed-dialog'; | |||
| import { useHandleExportOrImportJsonFile } from './hooks/use-export-json'; | |||
| import { useFetchDataOnMount } from './hooks/use-fetch-data'; | |||
| import { useGetBeginNodeDataQuery } from './hooks/use-get-begin-query'; | |||
| @@ -22,6 +34,7 @@ import { | |||
| useSaveGraph, | |||
| useSaveGraphBeforeOpeningDebugDrawer, | |||
| } from './hooks/use-save-graph'; | |||
| import { useShowEmbedModal } from './hooks/use-show-dialog'; | |||
| import { BeginQuery } from './interface'; | |||
| import { UploadAgentDialog } from './upload-agent-dialog'; | |||
| @@ -30,13 +43,14 @@ function AgentDropdownMenuItem({ | |||
| ...props | |||
| }: ComponentPropsWithoutRef<typeof DropdownMenuItem>) { | |||
| return ( | |||
| <DropdownMenuItem className="flex justify-between items-center" {...props}> | |||
| <DropdownMenuItem className="justify-start" {...props}> | |||
| {children} | |||
| </DropdownMenuItem> | |||
| ); | |||
| } | |||
| export default function Agent() { | |||
| const { id } = useParams(); | |||
| const { navigateToAgentList } = useNavigatePage(); | |||
| const { | |||
| visible: chatDrawerVisible, | |||
| @@ -53,12 +67,9 @@ export default function Agent() { | |||
| hideFileUploadModal, | |||
| } = useHandleExportOrImportJsonFile(); | |||
| const { saveGraph, loading } = useSaveGraph(); | |||
| const { flowDetail } = useFetchDataOnMount(); | |||
| const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); | |||
| const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer); | |||
| const handleRunAgent = useCallback(() => { | |||
| const query: BeginQuery[] = getBeginNodeDataQuery(); | |||
| if (query.length > 0) { | |||
| @@ -68,47 +79,58 @@ export default function Agent() { | |||
| } | |||
| }, [getBeginNodeDataQuery, handleRun, showChatDrawer]); | |||
| const { showEmbedModal, hideEmbedModal, embedVisible, beta } = | |||
| useShowEmbedModal(); | |||
| return ( | |||
| <section className="h-full"> | |||
| <PageHeader back={navigateToAgentList} title={flowDetail.title}> | |||
| <div className="flex items-center gap-2"> | |||
| <ButtonLoading | |||
| variant={'outline'} | |||
| variant={'secondary'} | |||
| onClick={() => saveGraph()} | |||
| loading={loading} | |||
| > | |||
| Save | |||
| </ButtonLoading> | |||
| <Button variant={'outline'} onClick={handleRunAgent}> | |||
| <Button variant={'secondary'} onClick={handleRunAgent}> | |||
| <CirclePlay /> | |||
| Run app | |||
| </Button> | |||
| <Button variant={'outline'}>Publish</Button> | |||
| <Button variant={'secondary'}> | |||
| <History /> | |||
| History version | |||
| </Button> | |||
| <Button variant={'secondary'}> | |||
| <Logs /> | |||
| Log | |||
| </Button> | |||
| <DropdownMenu> | |||
| <DropdownMenuTrigger asChild> | |||
| <Button variant={'icon'} size={'icon'}> | |||
| <EllipsisVertical /> | |||
| <Button variant={'secondary'}> | |||
| <ChevronDown /> Management | |||
| </Button> | |||
| </DropdownMenuTrigger> | |||
| <DropdownMenuContent> | |||
| <AgentDropdownMenuItem onClick={openDocument}> | |||
| API | |||
| <Key /> | |||
| API | |||
| </AgentDropdownMenuItem> | |||
| <DropdownMenuSeparator /> | |||
| <AgentDropdownMenuItem onClick={handleImportJson}> | |||
| <Download /> | |||
| Import | |||
| <Import /> | |||
| </AgentDropdownMenuItem> | |||
| <DropdownMenuSeparator /> | |||
| <AgentDropdownMenuItem onClick={handleExportJson}> | |||
| <Upload /> | |||
| Export | |||
| <Forward /> | |||
| </AgentDropdownMenuItem> | |||
| <DropdownMenuSeparator /> | |||
| <AgentDropdownMenuItem> | |||
| <AgentDropdownMenuItem onClick={showEmbedModal}> | |||
| <ScreenShare /> | |||
| {t('common.embedIntoSite')} | |||
| <CodeXml /> | |||
| </AgentDropdownMenuItem> | |||
| </DropdownMenuContent> | |||
| </DropdownMenu> | |||
| @@ -126,6 +148,16 @@ export default function Agent() { | |||
| onOk={onFileUploadOk} | |||
| ></UploadAgentDialog> | |||
| )} | |||
| {embedVisible && ( | |||
| <EmbedDialog | |||
| visible={embedVisible} | |||
| hideModal={hideEmbedModal} | |||
| token={id!} | |||
| form={SharedFrom.Agent} | |||
| beta={beta} | |||
| isAgent | |||
| ></EmbedDialog> | |||
| )} | |||
| </section> | |||
| ); | |||
| } | |||