Ver código fonte

Feat: Show agent embed dialog #3221 (#8903)

### 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
balibabu 3 meses atrás
pai
commit
daa1113513
Nenhuma conta vinculada ao e-mail do autor do commit

+ 40
- 0
web/src/components/originui/underline-tabs.tsx Ver arquivo

@@ -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 };

+ 1
- 1
web/src/components/ui/dialog.tsx Ver arquivo

@@ -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}

+ 1
- 1
web/src/components/ui/tabs.tsx Ver arquivo

@@ -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}

+ 132
- 0
web/src/pages/agent/embed-dialog/index.tsx Ver arquivo

@@ -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);

+ 179
- 0
web/src/pages/agent/hooks/use-show-dialog.ts Ver arquivo

@@ -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,
};
};

+ 47
- 15
web/src/pages/agent/index.tsx Ver arquivo

@@ -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>
);
}

Carregando…
Cancelar
Salvar