feat: add ragflow web project & add pnpm workspace file
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IToken } from '@/interfaces/database/chat';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import type { TableProps } from 'antd';
|
||||
import { Button, Modal, Space, Table } from 'antd';
|
||||
import { useOperateApiKey } from '../hooks';
|
||||
|
||||
const ChatApiKeyModal = ({
|
||||
dialogId,
|
||||
hideModal,
|
||||
idKey,
|
||||
}: IModalProps<any> & { dialogId?: string; idKey: string }) => {
|
||||
const { createToken, removeToken, tokenList, listLoading, creatingLoading } =
|
||||
useOperateApiKey(idKey, dialogId);
|
||||
const { t } = useTranslate('chat');
|
||||
|
||||
const columns: TableProps<IToken>['columns'] = [
|
||||
{
|
||||
title: 'Token',
|
||||
dataIndex: 'token',
|
||||
key: 'token',
|
||||
render: (text) => <a>{text}</a>,
|
||||
},
|
||||
{
|
||||
title: t('created'),
|
||||
dataIndex: 'create_date',
|
||||
key: 'create_date',
|
||||
render: (text) => formatDate(text),
|
||||
},
|
||||
{
|
||||
title: t('action'),
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<CopyToClipboard text={record.token}></CopyToClipboard>
|
||||
<DeleteOutlined onClick={() => removeToken(record.token)} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={t('apiKey')}
|
||||
open
|
||||
onCancel={hideModal}
|
||||
cancelButtonProps={{ style: { display: 'none' } }}
|
||||
style={{ top: 300 }}
|
||||
onOk={hideModal}
|
||||
width={'50vw'}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tokenList}
|
||||
rowKey={'token'}
|
||||
loading={listLoading}
|
||||
pagination={false}
|
||||
/>
|
||||
<Button
|
||||
onClick={createToken}
|
||||
loading={creatingLoading}
|
||||
disabled={tokenList?.length > 0}
|
||||
>
|
||||
{t('createNewKey')}
|
||||
</Button>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatApiKeyModal;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
|
||||
import { LangfuseCard } from '@/pages/user-setting/setting-model/langfuse';
|
||||
import apiDoc from '@parent/docs/references/http_api_reference.md';
|
||||
import MarkdownPreview from '@uiw/react-markdown-preview';
|
||||
import { Button, Card, Flex, Space } from 'antd';
|
||||
import ChatApiKeyModal from '../chat-api-key-modal';
|
||||
import { usePreviewChat } from '../hooks';
|
||||
import BackendServiceApi from './backend-service-api';
|
||||
import MarkdownToc from './markdown-toc';
|
||||
|
||||
const ApiContent = ({
|
||||
id,
|
||||
idKey,
|
||||
hideChatPreviewCard = false,
|
||||
}: {
|
||||
id?: string;
|
||||
idKey: string;
|
||||
hideChatPreviewCard?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslate('chat');
|
||||
const {
|
||||
visible: apiKeyVisible,
|
||||
hideModal: hideApiKeyModal,
|
||||
showModal: showApiKeyModal,
|
||||
} = useSetModalState();
|
||||
// const { embedVisible, hideEmbedModal, showEmbedModal, embedToken } =
|
||||
// useShowEmbedModal(idKey);
|
||||
|
||||
const { handlePreview } = usePreviewChat(idKey);
|
||||
|
||||
return (
|
||||
<div className="pb-2">
|
||||
<Flex vertical gap={'middle'}>
|
||||
<BackendServiceApi show={showApiKeyModal}></BackendServiceApi>
|
||||
{!hideChatPreviewCard && (
|
||||
<Card title={`${name} Web App`}>
|
||||
<Flex gap={8} vertical>
|
||||
<Space size={'middle'}>
|
||||
<Button onClick={handlePreview}>{t('preview')}</Button>
|
||||
{/* <Button onClick={() => showEmbedModal(id)}>
|
||||
{t('embedded')}
|
||||
</Button> */}
|
||||
</Space>
|
||||
</Flex>
|
||||
</Card>
|
||||
)}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<MarkdownToc content={apiDoc} />
|
||||
</div>
|
||||
<MarkdownPreview source={apiDoc}></MarkdownPreview>
|
||||
</Flex>
|
||||
{apiKeyVisible && (
|
||||
<ChatApiKeyModal
|
||||
hideModal={hideApiKeyModal}
|
||||
dialogId={id}
|
||||
idKey={idKey}
|
||||
></ChatApiKeyModal>
|
||||
)}
|
||||
{/* {embedVisible && (
|
||||
<EmbedModal
|
||||
token={embedToken}
|
||||
visible={embedVisible}
|
||||
hideModal={hideEmbedModal}
|
||||
></EmbedModal>
|
||||
)} */}
|
||||
<LangfuseCard></LangfuseCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiContent;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Button, Card, Flex, Space, Typography } from 'antd';
|
||||
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import styles from './index.less';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
const BackendServiceApi = ({ show }: { show(): void }) => {
|
||||
const { t } = useTranslate('chat');
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<Space size={'large'}>
|
||||
<span>RAGFlow API</span>
|
||||
<Button onClick={show} type="primary">
|
||||
{t('apiKey')}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Flex gap={8} align="center">
|
||||
<b>{t('backendServiceApi')}</b>
|
||||
<Paragraph
|
||||
copyable={{ text: `${location.origin}` }}
|
||||
className={styles.apiLinkText}
|
||||
>
|
||||
{location.origin}
|
||||
</Paragraph>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackendServiceApi;
|
||||
@@ -0,0 +1,20 @@
|
||||
.chartWrapper {
|
||||
height: 40vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.chartItem {
|
||||
height: 300px;
|
||||
padding: 10px 0 50px;
|
||||
}
|
||||
|
||||
.chartLabel {
|
||||
display: inline-block;
|
||||
padding-left: 60px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.apiLinkText {
|
||||
.linkText();
|
||||
margin: 0 !important;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { Modal } from 'antd';
|
||||
import ApiContent from './api-content';
|
||||
|
||||
const ChatOverviewModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
id,
|
||||
idKey,
|
||||
}: IModalProps<any> & { id: string; name?: string; idKey: string }) => {
|
||||
const { t } = useTranslate('chat');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={t('overview')}
|
||||
open={visible}
|
||||
onCancel={hideModal}
|
||||
cancelButtonProps={{ style: { display: 'none' } }}
|
||||
onOk={hideModal}
|
||||
width={'100vw'}
|
||||
okText={t('close', { keyPrefix: 'common' })}
|
||||
>
|
||||
<ApiContent id={id} idKey={idKey}></ApiContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatOverviewModal;
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Anchor } from 'antd';
|
||||
import type { AnchorLinkItemProps } from 'antd/es/anchor/Anchor';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
interface MarkdownTocProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const MarkdownToc: React.FC<MarkdownTocProps> = ({ content }) => {
|
||||
const [items, setItems] = useState<AnchorLinkItemProps[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const generateTocItems = () => {
|
||||
const headings = document.querySelectorAll(
|
||||
'.wmde-markdown h2, .wmde-markdown h3',
|
||||
);
|
||||
const tocItems: AnchorLinkItemProps[] = [];
|
||||
let currentH2Item: AnchorLinkItemProps | null = null;
|
||||
|
||||
headings.forEach((heading) => {
|
||||
const title = heading.textContent || '';
|
||||
const id = heading.id;
|
||||
const isH2 = heading.tagName.toLowerCase() === 'h2';
|
||||
|
||||
if (id && title) {
|
||||
const item: AnchorLinkItemProps = {
|
||||
key: id,
|
||||
href: `#${id}`,
|
||||
title,
|
||||
};
|
||||
|
||||
if (isH2) {
|
||||
currentH2Item = item;
|
||||
tocItems.push(item);
|
||||
} else {
|
||||
if (currentH2Item) {
|
||||
if (!currentH2Item.children) {
|
||||
currentH2Item.children = [];
|
||||
}
|
||||
currentH2Item.children.push(item);
|
||||
} else {
|
||||
tocItems.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setItems(tocItems.slice(1));
|
||||
};
|
||||
|
||||
setTimeout(generateTocItems, 100);
|
||||
}, [content]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="markdown-toc bg-bg-base text-text-primary shadow shadow-text-secondary"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
right: 20,
|
||||
top: 100,
|
||||
bottom: 150,
|
||||
width: 200,
|
||||
padding: '10px',
|
||||
maxHeight: 'calc(100vh - 170px)',
|
||||
overflowY: 'auto',
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<Anchor items={items} affix={false} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownToc;
|
||||
@@ -0,0 +1,40 @@
|
||||
import LineChart from '@/components/line-chart';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { IStats } from '@/interfaces/database/chat';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import { useSelectChartStatsList } from '../hooks';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
const StatsLineChart = ({ statsType }: { statsType: keyof IStats }) => {
|
||||
const { t } = useTranslate('chat');
|
||||
const chartList = useSelectChartStatsList();
|
||||
const list =
|
||||
chartList[statsType]?.map((x) => ({
|
||||
...x,
|
||||
xAxis: formatDate(x.xAxis),
|
||||
})) ?? [];
|
||||
|
||||
return (
|
||||
<div className={styles.chartItem}>
|
||||
<b className={styles.chartLabel}>{t(camelCase(statsType))}</b>
|
||||
<LineChart data={list}></LineChart>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StatsChart = () => {
|
||||
return (
|
||||
<div className={styles.chartWrapper}>
|
||||
<StatsLineChart statsType={'pv'}></StatsLineChart>
|
||||
<StatsLineChart statsType={'round'}></StatsLineChart>
|
||||
<StatsLineChart statsType={'speed'}></StatsLineChart>
|
||||
<StatsLineChart statsType={'thumb_up'}></StatsLineChart>
|
||||
<StatsLineChart statsType={'tokens'}></StatsLineChart>
|
||||
<StatsLineChart statsType={'uv'}></StatsLineChart>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatsChart;
|
||||
@@ -0,0 +1,21 @@
|
||||
.codeCard {
|
||||
.clearCardBody();
|
||||
}
|
||||
|
||||
.codeText {
|
||||
padding: 10px;
|
||||
background-color: #ffffff09;
|
||||
}
|
||||
|
||||
.id {
|
||||
.linkText();
|
||||
}
|
||||
|
||||
.darkBg {
|
||||
background-color: rgb(69, 68, 68);
|
||||
}
|
||||
|
||||
.darkId {
|
||||
color: white;
|
||||
.darkBg();
|
||||
}
|
||||
170
ragflow_web/src/components/api-service/embed-modal/index.tsx
Normal file
170
ragflow_web/src/components/api-service/embed-modal/index.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||
import HightLightMarkdown from '@/components/highlight-markdown';
|
||||
import { SharedFrom } from '@/constants/chat';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import {
|
||||
Card,
|
||||
Checkbox,
|
||||
Form,
|
||||
Modal,
|
||||
Select,
|
||||
Tabs,
|
||||
TabsProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { useIsDarkTheme } from '@/components/theme-provider';
|
||||
import {
|
||||
LanguageAbbreviation,
|
||||
LanguageAbbreviationMap,
|
||||
} from '@/constants/common';
|
||||
import { cn } from '@/lib/utils';
|
||||
import styles from './index.less';
|
||||
|
||||
const { Paragraph, Link } = Typography;
|
||||
|
||||
const EmbedModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
token = '',
|
||||
form,
|
||||
beta = '',
|
||||
isAgent,
|
||||
}: IModalProps<any> & {
|
||||
token: string;
|
||||
form: SharedFrom;
|
||||
beta: string;
|
||||
isAgent: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslate('chat');
|
||||
const isDarkTheme = useIsDarkTheme();
|
||||
|
||||
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>
|
||||
~~~
|
||||
`;
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: t('fullScreenTitle'),
|
||||
children: (
|
||||
<Card
|
||||
title={t('fullScreenDescription')}
|
||||
extra={<CopyToClipboard text={text}></CopyToClipboard>}
|
||||
className={styles.codeCard}
|
||||
>
|
||||
<div className="p-2">
|
||||
<h2 className="mb-3">Option:</h2>
|
||||
|
||||
<Form.Item
|
||||
label={t('avatarHidden')}
|
||||
labelCol={{ span: 6 }}
|
||||
wrapperCol={{ span: 18 }}
|
||||
>
|
||||
<Checkbox
|
||||
checked={visibleAvatar}
|
||||
onChange={(e) => setVisibleAvatar(e.target.checked)}
|
||||
></Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('locale')}
|
||||
labelCol={{ span: 6 }}
|
||||
wrapperCol={{ span: 18 }}
|
||||
>
|
||||
<Select
|
||||
placeholder="Select a locale"
|
||||
onChange={(value) => setLocale(value)}
|
||||
options={languageOptions}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<HightLightMarkdown>{text}</HightLightMarkdown>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: t('partialTitle'),
|
||||
children: t('comingSoon'),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: t('extensionTitle'),
|
||||
children: t('comingSoon'),
|
||||
},
|
||||
];
|
||||
|
||||
const onChange = (key: string) => {
|
||||
console.log(key);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('embedIntoSite', { keyPrefix: 'common' })}
|
||||
open={visible}
|
||||
style={{ top: 300 }}
|
||||
width={'50vw'}
|
||||
onOk={hideModal}
|
||||
onCancel={hideModal}
|
||||
>
|
||||
<Tabs defaultActiveKey="1" items={items} onChange={onChange} />
|
||||
<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>
|
||||
<Paragraph
|
||||
copyable={{ text: token }}
|
||||
className={cn(styles.id, {
|
||||
[styles.darkId]: isDarkTheme,
|
||||
})}
|
||||
>
|
||||
{token}
|
||||
</Paragraph>
|
||||
<Link
|
||||
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"
|
||||
>
|
||||
{t('howUseId', { keyPrefix: isAgent ? 'flow' : 'chat' })}
|
||||
</Link>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmbedModal;
|
||||
179
ragflow_web/src/components/api-service/hooks.ts
Normal file
179
ragflow_web/src/components/api-service/hooks.ts
Normal file
@@ -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,
|
||||
};
|
||||
};
|
||||
33
ragflow_web/src/components/auto-keywords-form-field.tsx
Normal file
33
ragflow_web/src/components/auto-keywords-form-field.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { FormLayout } from '@/constants/form';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { SliderInputFormField } from './slider-input-form-field';
|
||||
|
||||
export function AutoKeywordsFormField() {
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
|
||||
return (
|
||||
<SliderInputFormField
|
||||
name={'parser_config.auto_keywords'}
|
||||
label={t('autoKeywords')}
|
||||
max={30}
|
||||
min={0}
|
||||
tooltip={t('autoKeywordsTip')}
|
||||
layout={FormLayout.Horizontal}
|
||||
></SliderInputFormField>
|
||||
);
|
||||
}
|
||||
|
||||
export function AutoQuestionsFormField() {
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
|
||||
return (
|
||||
<SliderInputFormField
|
||||
name={'parser_config.auto_questions'}
|
||||
label={t('autoQuestions')}
|
||||
max={10}
|
||||
min={0}
|
||||
tooltip={t('autoQuestionsTip')}
|
||||
layout={FormLayout.Horizontal}
|
||||
></SliderInputFormField>
|
||||
);
|
||||
}
|
||||
48
ragflow_web/src/components/auto-keywords-item.tsx
Normal file
48
ragflow_web/src/components/auto-keywords-item.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { Flex, Form, InputNumber, Slider } from 'antd';
|
||||
|
||||
export const AutoKeywordsItem = () => {
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
|
||||
return (
|
||||
<Form.Item label={t('autoKeywords')} tooltip={t('autoKeywordsTip')}>
|
||||
<Flex gap={20} align="center">
|
||||
<Flex flex={1}>
|
||||
<Form.Item
|
||||
name={['parser_config', 'auto_keywords']}
|
||||
noStyle
|
||||
initialValue={0}
|
||||
>
|
||||
<Slider max={30} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item name={['parser_config', 'auto_keywords']} noStyle>
|
||||
<InputNumber max={30} min={0} />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export const AutoQuestionsItem = () => {
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
|
||||
return (
|
||||
<Form.Item label={t('autoQuestions')} tooltip={t('autoQuestionsTip')}>
|
||||
<Flex gap={20} align="center">
|
||||
<Flex flex={1}>
|
||||
<Form.Item
|
||||
name={['parser_config', 'auto_questions']}
|
||||
noStyle
|
||||
initialValue={0}
|
||||
>
|
||||
<Slider max={10} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item name={['parser_config', 'auto_questions']} noStyle>
|
||||
<InputNumber max={10} min={0} />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
99
ragflow_web/src/components/avatar-upload.tsx
Normal file
99
ragflow_web/src/components/avatar-upload.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { transformFile2Base64 } from '@/utils/file-util';
|
||||
import { Pencil, Plus, XIcon } from 'lucide-react';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
|
||||
type AvatarUploadProps = {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
tips?: string;
|
||||
};
|
||||
|
||||
export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
|
||||
function AvatarUpload({ value, onChange, tips }, ref) {
|
||||
const { t } = useTranslation();
|
||||
const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
async (ev) => {
|
||||
const file = ev.target?.files?.[0];
|
||||
if (/\.(jpg|jpeg|png|webp|bmp)$/i.test(file?.name ?? '')) {
|
||||
const str = await transformFile2Base64(file!);
|
||||
setAvatarBase64Str(str);
|
||||
onChange?.(str);
|
||||
}
|
||||
ev.target.value = '';
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
setAvatarBase64Str('');
|
||||
onChange?.('');
|
||||
}, [onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setAvatarBase64Str(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className="flex justify-start items-end space-x-2">
|
||||
<div className="relative group">
|
||||
{!avatarBase64Str ? (
|
||||
<div className="w-[64px] h-[64px] grid place-content-center border border-dashed bg-bg-input rounded-md">
|
||||
<div className="flex flex-col items-center">
|
||||
<Plus />
|
||||
<p>{t('common.upload')}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-[64px] h-[64px] relative grid place-content-center">
|
||||
<Avatar className="w-[64px] h-[64px] rounded-md">
|
||||
<AvatarImage className=" block" src={avatarBase64Str} alt="" />
|
||||
<AvatarFallback></AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="absolute inset-0 bg-[#000]/20 group-hover:bg-[#000]/60">
|
||||
<Pencil
|
||||
size={20}
|
||||
className="absolute right-2 bottom-0 opacity-50 hidden group-hover:block"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleRemove}
|
||||
size="icon"
|
||||
className="border-background focus-visible:border-background absolute -top-2 -right-2 size-6 rounded-full border-2 shadow-none z-10"
|
||||
aria-label="Remove image"
|
||||
type="button"
|
||||
>
|
||||
<XIcon className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Input
|
||||
placeholder=""
|
||||
type="file"
|
||||
title=""
|
||||
accept="image/*"
|
||||
className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
onChange={handleChange}
|
||||
ref={ref}
|
||||
/>
|
||||
</div>
|
||||
<div className="margin-1 text-text-secondary">
|
||||
{tips ?? t('knowledgeConfiguration.photoTip')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
62
ragflow_web/src/components/bulk-operate-bar.tsx
Normal file
62
ragflow_web/src/components/bulk-operate-bar.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { BrushCleaning } from 'lucide-react';
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
import { ConfirmDeleteDialog } from './confirm-delete-dialog';
|
||||
import { Separator } from './ui/separator';
|
||||
|
||||
export type BulkOperateItemType = {
|
||||
id: string;
|
||||
label: ReactNode;
|
||||
icon: ReactNode;
|
||||
onClick(): void;
|
||||
};
|
||||
|
||||
type BulkOperateBarProps = {
|
||||
list: BulkOperateItemType[];
|
||||
count: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function BulkOperateBar({
|
||||
list,
|
||||
count,
|
||||
className,
|
||||
}: BulkOperateBarProps) {
|
||||
const isDeleteItem = useCallback((id: string) => {
|
||||
return id === 'delete';
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card className={cn('mb-4', className)}>
|
||||
<CardContent className="p-1 pl-5 flex items-center gap-6">
|
||||
<section className="text-text-sub-title-invert flex items-center gap-2">
|
||||
<span>Selected: {count} Files</span>
|
||||
<BrushCleaning className="size-3" />
|
||||
</section>
|
||||
<Separator orientation={'vertical'} className="h-3"></Separator>
|
||||
<ul className="flex gap-2">
|
||||
{list.map((x) => (
|
||||
<li
|
||||
key={x.id}
|
||||
className={cn({ ['text-state-error']: isDeleteItem(x.id) })}
|
||||
>
|
||||
<ConfirmDeleteDialog
|
||||
hidden={!isDeleteItem(x.id)}
|
||||
onOk={x.onClick}
|
||||
>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
onClick={isDeleteItem(x.id) ? () => {} : x.onClick}
|
||||
>
|
||||
{x.icon} {x.label}
|
||||
</Button>
|
||||
</ConfirmDeleteDialog>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
11
ragflow_web/src/components/canvas/background.tsx
Normal file
11
ragflow_web/src/components/canvas/background.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Background } from '@xyflow/react';
|
||||
|
||||
export function AgentBackground() {
|
||||
return (
|
||||
<Background
|
||||
color="var(--text-primary)"
|
||||
bgColor="rgb(var(--bg-canvas))"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
);
|
||||
}
|
||||
17
ragflow_web/src/components/card-container.tsx
Normal file
17
ragflow_web/src/components/card-container.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
type CardContainerProps = { className?: string } & PropsWithChildren;
|
||||
|
||||
export function CardContainer({ children, className }: CardContainerProps) {
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Plus, X } from 'lucide-react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Separator } from '../ui/separator';
|
||||
|
||||
export function DynamicPageRange() {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
|
||||
const { fields, remove, append } = useFieldArray({
|
||||
name: 'parser_config.pages',
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormLabel tooltip={t('knowledgeDetails.pageRangesTip')}>
|
||||
{t('knowledgeDetails.pageRanges')}
|
||||
</FormLabel>
|
||||
{fields.map((field, index) => {
|
||||
const typeField = `parser_config.pages.${index}.from`;
|
||||
return (
|
||||
<div key={field.id} className="flex items-center gap-2 pt-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={typeField}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-2/5">
|
||||
<FormDescription />
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={t('common.pleaseInput')}
|
||||
className="!m-0"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Separator className="w-3 "></Separator>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`parser_config.pages.${index}.to`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormDescription />
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={t('common.pleaseInput')}
|
||||
className="!m-0"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
onClick={() => append({ from: 1, to: 100 })}
|
||||
className="mt-4 border-dashed w-full"
|
||||
variant={'outline'}
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
{t('knowledgeDetails.addPage')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
112
ragflow_web/src/components/chunk-method-dialog/hooks.ts
Normal file
112
ragflow_web/src/components/chunk-method-dialog/hooks.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { useSelectParserList } from '@/hooks/user-setting-hooks';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
const ParserListMap = new Map([
|
||||
[
|
||||
['pdf'],
|
||||
[
|
||||
'naive',
|
||||
'resume',
|
||||
'manual',
|
||||
'paper',
|
||||
'book',
|
||||
'laws',
|
||||
'presentation',
|
||||
'one',
|
||||
'qa',
|
||||
'knowledge_graph',
|
||||
],
|
||||
],
|
||||
[
|
||||
['doc', 'docx'],
|
||||
[
|
||||
'naive',
|
||||
'resume',
|
||||
'book',
|
||||
'laws',
|
||||
'one',
|
||||
'qa',
|
||||
'manual',
|
||||
'knowledge_graph',
|
||||
],
|
||||
],
|
||||
[
|
||||
['xlsx', 'xls'],
|
||||
['naive', 'qa', 'table', 'one', 'knowledge_graph'],
|
||||
],
|
||||
[['ppt', 'pptx'], ['presentation']],
|
||||
[
|
||||
['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tif', 'tiff', 'webp', 'svg', 'ico'],
|
||||
['picture'],
|
||||
],
|
||||
[
|
||||
['txt'],
|
||||
[
|
||||
'naive',
|
||||
'resume',
|
||||
'book',
|
||||
'laws',
|
||||
'one',
|
||||
'qa',
|
||||
'table',
|
||||
'knowledge_graph',
|
||||
],
|
||||
],
|
||||
[
|
||||
['csv'],
|
||||
[
|
||||
'naive',
|
||||
'resume',
|
||||
'book',
|
||||
'laws',
|
||||
'one',
|
||||
'qa',
|
||||
'table',
|
||||
'knowledge_graph',
|
||||
],
|
||||
],
|
||||
[['md'], ['naive', 'qa', 'knowledge_graph']],
|
||||
[['json'], ['naive', 'knowledge_graph']],
|
||||
[['eml'], ['email']],
|
||||
]);
|
||||
|
||||
const getParserList = (
|
||||
values: string[],
|
||||
parserList: Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
}>,
|
||||
) => {
|
||||
return parserList.filter((x) => values?.some((y) => y === x.value));
|
||||
};
|
||||
|
||||
export const useFetchParserListOnMount = (documentExtension: string) => {
|
||||
const parserList = useSelectParserList();
|
||||
|
||||
const nextParserList = useMemo(() => {
|
||||
const key = [...ParserListMap.keys()].find((x) =>
|
||||
x.some((y) => y === documentExtension),
|
||||
);
|
||||
if (key) {
|
||||
const values = ParserListMap.get(key);
|
||||
return getParserList(values ?? [], parserList);
|
||||
}
|
||||
|
||||
return getParserList(
|
||||
['naive', 'resume', 'book', 'laws', 'one', 'qa', 'table'],
|
||||
parserList,
|
||||
);
|
||||
}, [parserList, documentExtension]);
|
||||
|
||||
return { parserList: nextParserList };
|
||||
};
|
||||
|
||||
const hideAutoKeywords = ['qa', 'table', 'resume', 'knowledge_graph', 'tag'];
|
||||
|
||||
export const useShowAutoKeywords = () => {
|
||||
const showAutoKeywords = useCallback((selectedTag: string) => {
|
||||
return hideAutoKeywords.every((x) => selectedTag !== x);
|
||||
}, []);
|
||||
|
||||
return showAutoKeywords;
|
||||
};
|
||||
384
ragflow_web/src/components/chunk-method-dialog/index.tsx
Normal file
384
ragflow_web/src/components/chunk-method-dialog/index.tsx
Normal file
@@ -0,0 +1,384 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { DocumentParserType } from '@/constants/knowledge';
|
||||
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IParserConfig } from '@/interfaces/database/document';
|
||||
import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
|
||||
import {
|
||||
ChunkMethodItem,
|
||||
EnableTocToggle,
|
||||
ParseTypeItem,
|
||||
} from '@/pages/dataset/dataset-setting/configuration/common-item';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import omit from 'lodash/omit';
|
||||
import {} from 'module';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
AutoKeywordsFormField,
|
||||
AutoQuestionsFormField,
|
||||
} from '../auto-keywords-form-field';
|
||||
import { DataFlowSelect } from '../data-pipeline-select';
|
||||
import { DelimiterFormField } from '../delimiter-form-field';
|
||||
import { EntityTypesFormField } from '../entity-types-form-field';
|
||||
import { ExcelToHtmlFormField } from '../excel-to-html-form-field';
|
||||
import { FormContainer } from '../form-container';
|
||||
import { LayoutRecognizeFormField } from '../layout-recognize-form-field';
|
||||
import { MaxTokenNumberFormField } from '../max-token-number-from-field';
|
||||
import { ButtonLoading } from '../ui/button';
|
||||
import { Input } from '../ui/input';
|
||||
import { DynamicPageRange } from './dynamic-page-range';
|
||||
import { useShowAutoKeywords } from './hooks';
|
||||
import {
|
||||
useDefaultParserValues,
|
||||
useFillDefaultValueOnMount,
|
||||
} from './use-default-parser-values';
|
||||
|
||||
const FormId = 'ChunkMethodDialogForm';
|
||||
|
||||
interface IProps
|
||||
extends IModalProps<{
|
||||
parserId: string;
|
||||
parserConfig: IChangeParserConfigRequestBody;
|
||||
}> {
|
||||
loading: boolean;
|
||||
parserId: string;
|
||||
pipelineId?: string;
|
||||
parserConfig: IParserConfig;
|
||||
documentExtension: string;
|
||||
documentId: string;
|
||||
}
|
||||
|
||||
const hidePagesChunkMethods = [
|
||||
DocumentParserType.Qa,
|
||||
DocumentParserType.Table,
|
||||
DocumentParserType.Picture,
|
||||
DocumentParserType.Resume,
|
||||
DocumentParserType.One,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
];
|
||||
|
||||
export function ChunkMethodDialog({
|
||||
hideModal,
|
||||
onOk,
|
||||
parserId,
|
||||
pipelineId,
|
||||
documentExtension,
|
||||
visible,
|
||||
parserConfig,
|
||||
loading,
|
||||
}: IProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
|
||||
|
||||
const useGraphRag = useMemo(() => {
|
||||
return knowledgeDetails.parser_config?.graphrag?.use_graphrag;
|
||||
}, [knowledgeDetails.parser_config?.graphrag?.use_graphrag]);
|
||||
|
||||
const defaultParserValues = useDefaultParserValues();
|
||||
|
||||
const fillDefaultParserValue = useFillDefaultValueOnMount();
|
||||
|
||||
const FormSchema = z
|
||||
.object({
|
||||
parseType: z.number(),
|
||||
parser_id: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: t('common.pleaseSelect'),
|
||||
})
|
||||
.trim(),
|
||||
pipeline_id: z.string().optional(),
|
||||
parser_config: z.object({
|
||||
task_page_size: z.coerce.number().optional(),
|
||||
layout_recognize: z.string().optional(),
|
||||
chunk_token_num: z.coerce.number().optional(),
|
||||
delimiter: z.string().optional(),
|
||||
auto_keywords: z.coerce.number().optional(),
|
||||
auto_questions: z.coerce.number().optional(),
|
||||
html4excel: z.boolean().optional(),
|
||||
toc_extraction: z.boolean().optional(),
|
||||
// raptor: z
|
||||
// .object({
|
||||
// use_raptor: z.boolean().optional(),
|
||||
// prompt: z.string().optional().optional(),
|
||||
// max_token: z.coerce.number().optional(),
|
||||
// threshold: z.coerce.number().optional(),
|
||||
// max_cluster: z.coerce.number().optional(),
|
||||
// random_seed: z.coerce.number().optional(),
|
||||
// })
|
||||
// .optional(),
|
||||
// graphrag: z.object({
|
||||
// use_graphrag: z.boolean().optional(),
|
||||
// }),
|
||||
entity_types: z.array(z.string()).optional(),
|
||||
pages: z
|
||||
.array(z.object({ from: z.coerce.number(), to: z.coerce.number() }))
|
||||
.optional(),
|
||||
}),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.parseType === 2 && !data.pipeline_id) {
|
||||
ctx.addIssue({
|
||||
path: ['pipeline_id'],
|
||||
message: t('common.pleaseSelect'),
|
||||
code: 'custom',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
parser_id: parserId || '',
|
||||
pipeline_id: pipelineId || '',
|
||||
parseType: pipelineId ? 2 : 1,
|
||||
parser_config: defaultParserValues,
|
||||
},
|
||||
});
|
||||
|
||||
const layoutRecognize = useWatch({
|
||||
name: 'parser_config.layout_recognize',
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const selectedTag = useWatch({
|
||||
name: 'parser_id',
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const isPdf = documentExtension === 'pdf';
|
||||
|
||||
const showPages = useMemo(() => {
|
||||
return isPdf && hidePagesChunkMethods.every((x) => x !== selectedTag);
|
||||
}, [selectedTag, isPdf]);
|
||||
|
||||
const showOne = useMemo(() => {
|
||||
return (
|
||||
isPdf &&
|
||||
hidePagesChunkMethods
|
||||
.filter((x) => x !== DocumentParserType.One)
|
||||
.every((x) => x !== selectedTag)
|
||||
);
|
||||
}, [selectedTag, isPdf]);
|
||||
|
||||
const showMaxTokenNumber =
|
||||
selectedTag === DocumentParserType.Naive ||
|
||||
selectedTag === DocumentParserType.KnowledgeGraph;
|
||||
|
||||
const showEntityTypes = selectedTag === DocumentParserType.KnowledgeGraph;
|
||||
|
||||
const showExcelToHtml =
|
||||
selectedTag === DocumentParserType.Naive && documentExtension === 'xlsx';
|
||||
|
||||
const showAutoKeywords = useShowAutoKeywords();
|
||||
|
||||
async function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
console.log('🚀 ~ onSubmit ~ data:', data);
|
||||
const nextData = {
|
||||
...data,
|
||||
parser_config: {
|
||||
...data.parser_config,
|
||||
pages: data.parser_config?.pages?.map((x: any) => [x.from, x.to]) ?? [],
|
||||
},
|
||||
};
|
||||
console.log('🚀 ~ onSubmit ~ nextData:', nextData);
|
||||
const ret = await onOk?.(nextData);
|
||||
if (ret) {
|
||||
hideModal?.();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const pages =
|
||||
parserConfig?.pages?.map((x) => ({ from: x[0], to: x[1] })) ?? [];
|
||||
form.reset({
|
||||
parser_id: parserId || '',
|
||||
pipeline_id: pipelineId || '',
|
||||
parseType: pipelineId ? 2 : 1,
|
||||
parser_config: fillDefaultParserValue({
|
||||
pages: pages.length > 0 ? pages : [{ from: 1, to: 1024 }],
|
||||
...omit(parserConfig, 'pages'),
|
||||
// graphrag: {
|
||||
// use_graphrag: get(
|
||||
// parserConfig,
|
||||
// 'graphrag.use_graphrag',
|
||||
// useGraphRag,
|
||||
// ),
|
||||
// },
|
||||
}),
|
||||
});
|
||||
}
|
||||
}, [
|
||||
fillDefaultParserValue,
|
||||
form,
|
||||
knowledgeDetails.parser_config,
|
||||
parserConfig,
|
||||
parserId,
|
||||
pipelineId,
|
||||
useGraphRag,
|
||||
visible,
|
||||
]);
|
||||
const parseType = useWatch({
|
||||
control: form.control,
|
||||
name: 'parseType',
|
||||
defaultValue: pipelineId ? 2 : 1,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (parseType === 1) {
|
||||
form.setValue('pipeline_id', '');
|
||||
}
|
||||
}, [parseType, form]);
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent className="max-w-[50vw] text-text-primary">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('knowledgeDetails.chunkMethod')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6 max-h-[70vh] overflow-auto"
|
||||
id={FormId}
|
||||
>
|
||||
<FormContainer>
|
||||
<ParseTypeItem />
|
||||
{parseType === 1 && <ChunkMethodItem></ChunkMethodItem>}
|
||||
{parseType === 2 && (
|
||||
<DataFlowSelect
|
||||
isMult={false}
|
||||
// toDataPipeline={navigateToAgents}
|
||||
formFieldName="pipeline_id"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="parser_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('knowledgeDetails.chunkMethod')}</FormLabel>
|
||||
<FormControl>
|
||||
<RAGFlowSelect
|
||||
{...field}
|
||||
options={parserList}
|
||||
></RAGFlowSelect>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
{showPages && parseType === 1 && (
|
||||
<DynamicPageRange></DynamicPageRange>
|
||||
)}
|
||||
{showPages && parseType === 1 && layoutRecognize && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="parser_config.task_page_size"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel
|
||||
tooltip={t('knowledgeDetails.taskPageSizeTip')}
|
||||
>
|
||||
{t('knowledgeDetails.taskPageSize')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type={'number'}
|
||||
min={1}
|
||||
max={128}
|
||||
></Input>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</FormContainer>
|
||||
{parseType === 1 && (
|
||||
<>
|
||||
<FormContainer
|
||||
show={showOne || showMaxTokenNumber}
|
||||
className="space-y-3"
|
||||
>
|
||||
{showOne && (
|
||||
<LayoutRecognizeFormField></LayoutRecognizeFormField>
|
||||
)}
|
||||
{showMaxTokenNumber && (
|
||||
<>
|
||||
<MaxTokenNumberFormField
|
||||
max={
|
||||
selectedTag === DocumentParserType.KnowledgeGraph
|
||||
? 8192 * 2
|
||||
: 2048
|
||||
}
|
||||
></MaxTokenNumberFormField>
|
||||
<DelimiterFormField></DelimiterFormField>
|
||||
</>
|
||||
)}
|
||||
</FormContainer>
|
||||
<FormContainer
|
||||
show={showAutoKeywords(selectedTag) || showExcelToHtml}
|
||||
className="space-y-3"
|
||||
>
|
||||
{selectedTag === DocumentParserType.Naive && (
|
||||
<EnableTocToggle />
|
||||
)}
|
||||
{showAutoKeywords(selectedTag) && (
|
||||
<>
|
||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||
</>
|
||||
)}
|
||||
{showExcelToHtml && (
|
||||
<ExcelToHtmlFormField></ExcelToHtmlFormField>
|
||||
)}
|
||||
</FormContainer>
|
||||
{/* {showRaptorParseConfiguration(
|
||||
selectedTag as DocumentParserType,
|
||||
) && (
|
||||
<FormContainer>
|
||||
<RaptorFormFields></RaptorFormFields>
|
||||
</FormContainer>
|
||||
)} */}
|
||||
{/* {showGraphRagItems(selectedTag as DocumentParserType) &&
|
||||
useGraphRag && (
|
||||
<FormContainer>
|
||||
<UseGraphRagFormField></UseGraphRagFormField>
|
||||
</FormContainer>
|
||||
)} */}
|
||||
{showEntityTypes && (
|
||||
<EntityTypesFormField></EntityTypesFormField>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
<DialogFooter>
|
||||
<ButtonLoading type="submit" form={FormId} loading={loading}>
|
||||
{t('common.save')}
|
||||
</ButtonLoading>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { IParserConfig } from '@/interfaces/database/document';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ParseDocumentType } from '../layout-recognize-form-field';
|
||||
|
||||
export function useDefaultParserValues() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const defaultParserValues = useMemo(() => {
|
||||
const defaultParserValues = {
|
||||
task_page_size: 12,
|
||||
layout_recognize: ParseDocumentType.DeepDOC,
|
||||
chunk_token_num: 512,
|
||||
delimiter: '\n',
|
||||
auto_keywords: 0,
|
||||
auto_questions: 0,
|
||||
html4excel: false,
|
||||
toc_extraction: false,
|
||||
// raptor: {
|
||||
// use_raptor: false,
|
||||
// prompt: t('knowledgeConfiguration.promptText'),
|
||||
// max_token: 256,
|
||||
// threshold: 0.1,
|
||||
// max_cluster: 64,
|
||||
// random_seed: 0,
|
||||
// },
|
||||
// graphrag: {
|
||||
// use_graphrag: false,
|
||||
// },
|
||||
entity_types: [],
|
||||
pages: [],
|
||||
};
|
||||
|
||||
return defaultParserValues;
|
||||
}, [t]);
|
||||
|
||||
return defaultParserValues;
|
||||
}
|
||||
|
||||
export function useFillDefaultValueOnMount() {
|
||||
const defaultParserValues = useDefaultParserValues();
|
||||
|
||||
const fillDefaultValue = useCallback(
|
||||
(parserConfig: IParserConfig) => {
|
||||
return Object.entries(defaultParserValues).reduce<Record<string, any>>(
|
||||
(pre, [key, value]) => {
|
||||
if (key in parserConfig) {
|
||||
pre[key] = parserConfig[key as keyof IParserConfig];
|
||||
} else {
|
||||
pre[key] = value;
|
||||
}
|
||||
return pre;
|
||||
},
|
||||
{},
|
||||
);
|
||||
},
|
||||
[defaultParserValues],
|
||||
);
|
||||
|
||||
return fillDefaultValue;
|
||||
}
|
||||
161
ragflow_web/src/components/chunk-method-modal/hooks.ts
Normal file
161
ragflow_web/src/components/chunk-method-modal/hooks.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { DocumentParserType } from '@/constants/knowledge';
|
||||
import { useHandleChunkMethodSelectChange } from '@/hooks/logic-hooks';
|
||||
import { useSelectParserList } from '@/hooks/user-setting-hooks';
|
||||
import { FormInstance } from 'antd';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
const ParserListMap = new Map([
|
||||
[
|
||||
['pdf'],
|
||||
[
|
||||
DocumentParserType.Naive,
|
||||
DocumentParserType.Resume,
|
||||
DocumentParserType.Manual,
|
||||
DocumentParserType.Paper,
|
||||
DocumentParserType.Book,
|
||||
DocumentParserType.Laws,
|
||||
DocumentParserType.Presentation,
|
||||
DocumentParserType.One,
|
||||
DocumentParserType.Qa,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
],
|
||||
],
|
||||
[
|
||||
['doc', 'docx'],
|
||||
[
|
||||
DocumentParserType.Naive,
|
||||
DocumentParserType.Resume,
|
||||
DocumentParserType.Book,
|
||||
DocumentParserType.Laws,
|
||||
DocumentParserType.One,
|
||||
DocumentParserType.Qa,
|
||||
DocumentParserType.Manual,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
],
|
||||
],
|
||||
[
|
||||
['xlsx', 'xls'],
|
||||
[
|
||||
DocumentParserType.Naive,
|
||||
DocumentParserType.Qa,
|
||||
DocumentParserType.Table,
|
||||
DocumentParserType.One,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
],
|
||||
],
|
||||
[['ppt', 'pptx'], [DocumentParserType.Presentation]],
|
||||
[
|
||||
['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tif', 'tiff', 'webp', 'svg', 'ico'],
|
||||
[DocumentParserType.Picture],
|
||||
],
|
||||
[
|
||||
['txt'],
|
||||
[
|
||||
DocumentParserType.Naive,
|
||||
DocumentParserType.Resume,
|
||||
DocumentParserType.Book,
|
||||
DocumentParserType.Laws,
|
||||
DocumentParserType.One,
|
||||
DocumentParserType.Qa,
|
||||
DocumentParserType.Table,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
],
|
||||
],
|
||||
[
|
||||
['csv'],
|
||||
[
|
||||
DocumentParserType.Naive,
|
||||
DocumentParserType.Resume,
|
||||
DocumentParserType.Book,
|
||||
DocumentParserType.Laws,
|
||||
DocumentParserType.One,
|
||||
DocumentParserType.Qa,
|
||||
DocumentParserType.Table,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
],
|
||||
],
|
||||
[
|
||||
['md'],
|
||||
[
|
||||
DocumentParserType.Naive,
|
||||
DocumentParserType.Qa,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
],
|
||||
],
|
||||
[['json'], [DocumentParserType.Naive, DocumentParserType.KnowledgeGraph]],
|
||||
[['eml'], [DocumentParserType.Email]],
|
||||
]);
|
||||
|
||||
const getParserList = (
|
||||
values: string[],
|
||||
parserList: Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
}>,
|
||||
) => {
|
||||
return parserList.filter((x) => values?.some((y) => y === x.value));
|
||||
};
|
||||
|
||||
export const useFetchParserListOnMount = (
|
||||
documentId: string,
|
||||
parserId: DocumentParserType,
|
||||
documentExtension: string,
|
||||
form: FormInstance,
|
||||
) => {
|
||||
const [selectedTag, setSelectedTag] = useState<DocumentParserType>();
|
||||
const parserList = useSelectParserList();
|
||||
const handleChunkMethodSelectChange = useHandleChunkMethodSelectChange(form);
|
||||
|
||||
const nextParserList = useMemo(() => {
|
||||
const key = [...ParserListMap.keys()].find((x) =>
|
||||
x.some((y) => y === documentExtension),
|
||||
);
|
||||
if (key) {
|
||||
const values = ParserListMap.get(key);
|
||||
return getParserList(values ?? [], parserList);
|
||||
}
|
||||
|
||||
return getParserList(
|
||||
[
|
||||
DocumentParserType.Naive,
|
||||
DocumentParserType.Resume,
|
||||
DocumentParserType.Book,
|
||||
DocumentParserType.Laws,
|
||||
DocumentParserType.One,
|
||||
DocumentParserType.Qa,
|
||||
DocumentParserType.Table,
|
||||
],
|
||||
parserList,
|
||||
);
|
||||
}, [parserList, documentExtension]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTag(parserId);
|
||||
}, [parserId, documentId]);
|
||||
|
||||
const handleChange = (tag: string) => {
|
||||
handleChunkMethodSelectChange(tag);
|
||||
setSelectedTag(tag as DocumentParserType);
|
||||
};
|
||||
|
||||
return { parserList: nextParserList, handleChange, selectedTag };
|
||||
};
|
||||
|
||||
const hideAutoKeywords = [
|
||||
DocumentParserType.Qa,
|
||||
DocumentParserType.Table,
|
||||
DocumentParserType.Resume,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
DocumentParserType.Tag,
|
||||
];
|
||||
|
||||
export const useShowAutoKeywords = () => {
|
||||
const showAutoKeywords = useCallback(
|
||||
(selectedTag: DocumentParserType | undefined) => {
|
||||
return hideAutoKeywords.every((x) => selectedTag !== x);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return showAutoKeywords;
|
||||
};
|
||||
14
ragflow_web/src/components/chunk-method-modal/index.less
Normal file
14
ragflow_web/src/components/chunk-method-modal/index.less
Normal file
@@ -0,0 +1,14 @@
|
||||
.pageInputNumber {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.questionIcon {
|
||||
margin-inline-start: 4px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
cursor: help;
|
||||
writing-mode: horizontal-tb;
|
||||
}
|
||||
|
||||
.chunkMethod {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
350
ragflow_web/src/components/chunk-method-modal/index.tsx
Normal file
350
ragflow_web/src/components/chunk-method-modal/index.tsx
Normal file
@@ -0,0 +1,350 @@
|
||||
import MaxTokenNumber from '@/components/max-token-number';
|
||||
import { IModalManagerChildrenProps } from '@/components/modal-manager';
|
||||
import {
|
||||
MinusCircleOutlined,
|
||||
PlusOutlined,
|
||||
QuestionCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import omit from 'lodash/omit';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useFetchParserListOnMount, useShowAutoKeywords } from './hooks';
|
||||
|
||||
import { DocumentParserType } from '@/constants/knowledge';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/knowledge-hooks';
|
||||
import { IParserConfig } from '@/interfaces/database/document';
|
||||
import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
|
||||
import { get } from 'lodash';
|
||||
import { AutoKeywordsItem, AutoQuestionsItem } from '../auto-keywords-item';
|
||||
import { DatasetConfigurationContainer } from '../dataset-configuration-container';
|
||||
import Delimiter from '../delimiter';
|
||||
import EntityTypesItem from '../entity-types-item';
|
||||
import ExcelToHtml from '../excel-to-html';
|
||||
import LayoutRecognize from '../layout-recognize';
|
||||
import ParseConfiguration, {
|
||||
showRaptorParseConfiguration,
|
||||
} from '../parse-configuration';
|
||||
import {
|
||||
UseGraphRagItem,
|
||||
showGraphRagItems,
|
||||
} from '../parse-configuration/graph-rag-items';
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
|
||||
loading: boolean;
|
||||
onOk: (
|
||||
parserId: DocumentParserType | undefined,
|
||||
parserConfig: IChangeParserConfigRequestBody,
|
||||
) => void;
|
||||
showModal?(): void;
|
||||
parserId: DocumentParserType;
|
||||
parserConfig: IParserConfig;
|
||||
documentExtension: string;
|
||||
documentId: string;
|
||||
}
|
||||
|
||||
const hidePagesChunkMethods = [
|
||||
DocumentParserType.Qa,
|
||||
DocumentParserType.Table,
|
||||
DocumentParserType.Picture,
|
||||
DocumentParserType.Resume,
|
||||
DocumentParserType.One,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
];
|
||||
|
||||
const ChunkMethodModal: React.FC<IProps> = ({
|
||||
documentId,
|
||||
parserId,
|
||||
onOk,
|
||||
hideModal,
|
||||
visible,
|
||||
documentExtension,
|
||||
parserConfig,
|
||||
loading,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const { parserList, handleChange, selectedTag } = useFetchParserListOnMount(
|
||||
documentId,
|
||||
parserId,
|
||||
documentExtension,
|
||||
form,
|
||||
);
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
|
||||
|
||||
const useGraphRag = useMemo(() => {
|
||||
return knowledgeDetails.parser_config?.graphrag?.use_graphrag;
|
||||
}, [knowledgeDetails.parser_config?.graphrag?.use_graphrag]);
|
||||
|
||||
const handleOk = async () => {
|
||||
const values = await form.validateFields();
|
||||
const parser_config = {
|
||||
...values.parser_config,
|
||||
pages: values.pages?.map((x: any) => [x.from, x.to]) ?? [],
|
||||
};
|
||||
onOk(selectedTag, parser_config);
|
||||
};
|
||||
|
||||
const isPdf = documentExtension === 'pdf';
|
||||
|
||||
const showPages = useMemo(() => {
|
||||
return isPdf && hidePagesChunkMethods.every((x) => x !== selectedTag);
|
||||
}, [selectedTag, isPdf]);
|
||||
|
||||
const showOne = useMemo(() => {
|
||||
return (
|
||||
isPdf &&
|
||||
hidePagesChunkMethods
|
||||
.filter((x) => x !== DocumentParserType.One)
|
||||
.every((x) => x !== selectedTag)
|
||||
);
|
||||
}, [selectedTag, isPdf]);
|
||||
|
||||
const showMaxTokenNumber =
|
||||
selectedTag === DocumentParserType.Naive ||
|
||||
selectedTag === DocumentParserType.KnowledgeGraph;
|
||||
|
||||
const showEntityTypes = selectedTag === DocumentParserType.KnowledgeGraph;
|
||||
|
||||
const showExcelToHtml =
|
||||
selectedTag === DocumentParserType.Naive && documentExtension === 'xlsx';
|
||||
|
||||
const showAutoKeywords = useShowAutoKeywords();
|
||||
|
||||
const afterClose = () => {
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const pages =
|
||||
parserConfig?.pages?.map((x) => ({ from: x[0], to: x[1] })) ?? [];
|
||||
form.setFieldsValue({
|
||||
pages: pages.length > 0 ? pages : [{ from: 1, to: 1024 }],
|
||||
parser_config: {
|
||||
...omit(parserConfig, 'pages'),
|
||||
graphrag: {
|
||||
use_graphrag: get(
|
||||
parserConfig,
|
||||
'graphrag.use_graphrag',
|
||||
useGraphRag,
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
form,
|
||||
knowledgeDetails.parser_config,
|
||||
parserConfig,
|
||||
useGraphRag,
|
||||
visible,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('chunkMethod')}
|
||||
open={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={hideModal}
|
||||
afterClose={afterClose}
|
||||
confirmLoading={loading}
|
||||
width={700}
|
||||
>
|
||||
<Space size={[0, 8]} wrap>
|
||||
<Form.Item label={t('chunkMethod')} className={styles.chunkMethod}>
|
||||
<Select
|
||||
style={{ width: 160 }}
|
||||
onChange={handleChange}
|
||||
value={selectedTag}
|
||||
options={parserList}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Divider></Divider>
|
||||
<Form
|
||||
name="dynamic_form_nest_item"
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
className="space-y-4"
|
||||
>
|
||||
{showPages && (
|
||||
<>
|
||||
<Space>
|
||||
<p>{t('pageRanges')}:</p>
|
||||
<Tooltip title={t('pageRangesTip')}>
|
||||
<QuestionCircleOutlined
|
||||
className={styles.questionIcon}
|
||||
></QuestionCircleOutlined>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
<Form.List name="pages">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Space
|
||||
key={key}
|
||||
style={{
|
||||
display: 'flex',
|
||||
}}
|
||||
align="baseline"
|
||||
>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'from']}
|
||||
dependencies={name > 0 ? [name - 1, 'to'] : []}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('fromMessage'),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (
|
||||
name === 0 ||
|
||||
!value ||
|
||||
getFieldValue(['pages', name - 1, 'to']) < value
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(
|
||||
new Error(t('greaterThanPrevious')),
|
||||
);
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('fromPlaceholder')}
|
||||
min={0}
|
||||
precision={0}
|
||||
className={styles.pageInputNumber}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'to']}
|
||||
dependencies={[name, 'from']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('toMessage'),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (
|
||||
!value ||
|
||||
getFieldValue(['pages', name, 'from']) < value
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(
|
||||
new Error(t('greaterThan')),
|
||||
);
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('toPlaceholder')}
|
||||
min={0}
|
||||
precision={0}
|
||||
className={styles.pageInputNumber}
|
||||
/>
|
||||
</Form.Item>
|
||||
{name > 0 && (
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
)}
|
||||
</Space>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
{t('addPage')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showPages && (
|
||||
<Form.Item
|
||||
noStyle
|
||||
dependencies={[['parser_config', 'layout_recognize']]}
|
||||
>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue(['parser_config', 'layout_recognize']) && (
|
||||
<Form.Item
|
||||
name={['parser_config', 'task_page_size']}
|
||||
label={t('taskPageSize')}
|
||||
tooltip={t('taskPageSizeTip')}
|
||||
initialValue={12}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('taskPageSizeMessage'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} max={128} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
)}
|
||||
<DatasetConfigurationContainer show={showOne || showMaxTokenNumber}>
|
||||
{showOne && <LayoutRecognize></LayoutRecognize>}
|
||||
{showMaxTokenNumber && (
|
||||
<>
|
||||
<MaxTokenNumber
|
||||
max={
|
||||
selectedTag === DocumentParserType.KnowledgeGraph
|
||||
? 8192 * 2
|
||||
: 2048
|
||||
}
|
||||
></MaxTokenNumber>
|
||||
<Delimiter></Delimiter>
|
||||
</>
|
||||
)}
|
||||
</DatasetConfigurationContainer>
|
||||
<DatasetConfigurationContainer
|
||||
show={showAutoKeywords(selectedTag) || showExcelToHtml}
|
||||
>
|
||||
{showAutoKeywords(selectedTag) && (
|
||||
<>
|
||||
<AutoKeywordsItem></AutoKeywordsItem>
|
||||
<AutoQuestionsItem></AutoQuestionsItem>
|
||||
</>
|
||||
)}
|
||||
{showExcelToHtml && <ExcelToHtml></ExcelToHtml>}
|
||||
</DatasetConfigurationContainer>
|
||||
{showRaptorParseConfiguration(selectedTag) && (
|
||||
<DatasetConfigurationContainer>
|
||||
<ParseConfiguration></ParseConfiguration>
|
||||
</DatasetConfigurationContainer>
|
||||
)}
|
||||
{showGraphRagItems(selectedTag) && useGraphRag && (
|
||||
<UseGraphRagItem></UseGraphRagItem>
|
||||
)}
|
||||
{showEntityTypes && <EntityTypesItem></EntityTypesItem>}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default ChunkMethodModal;
|
||||
127
ragflow_web/src/components/collapse.tsx
Normal file
127
ragflow_web/src/components/collapse.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CollapsibleProps } from '@radix-ui/react-collapsible';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { IconFontFill } from './icon-font';
|
||||
|
||||
type CollapseProps = Omit<CollapsibleProps, 'title'> & {
|
||||
title?: ReactNode;
|
||||
rightContent?: ReactNode;
|
||||
} & PropsWithChildren;
|
||||
|
||||
export function Collapse({
|
||||
title,
|
||||
children,
|
||||
rightContent,
|
||||
open = true,
|
||||
defaultOpen = false,
|
||||
onOpenChange,
|
||||
disabled,
|
||||
}: CollapseProps) {
|
||||
const [currentOpen, setCurrentOpen] = useState(open);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentOpen(open);
|
||||
}, [open]);
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
setCurrentOpen(open);
|
||||
onOpenChange?.(open);
|
||||
},
|
||||
[onOpenChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
defaultOpen={defaultOpen}
|
||||
open={currentOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<CollapsibleTrigger className={'w-full'}>
|
||||
<section className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<IconFontFill
|
||||
name={`more`}
|
||||
className={cn('size-4', {
|
||||
'rotate-90': !currentOpen,
|
||||
})}
|
||||
></IconFontFill>
|
||||
<div
|
||||
className={cn('text-text-secondary', {
|
||||
'text-text-primary': open,
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
<div>{rightContent}</div>
|
||||
</section>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-2">{children}</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export type NodeCollapsibleProps<T extends any[]> = {
|
||||
items?: T;
|
||||
children: (item: T[0], idx: number) => ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
export function NodeCollapsible<T extends any[]>({
|
||||
items = [] as unknown as T,
|
||||
children,
|
||||
className,
|
||||
}: NodeCollapsibleProps<T>) {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
const nextClassName = cn('space-y-2', className);
|
||||
|
||||
const nextItems = items.every((x) => Array.isArray(x)) ? items.flat() : items;
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
className={cn('relative', nextClassName)}
|
||||
>
|
||||
{nextItems.slice(0, 3).map(children)}
|
||||
<CollapsibleContent className={nextClassName}>
|
||||
{nextItems.slice(3).map((x, idx) => children(x, idx + 3))}
|
||||
</CollapsibleContent>
|
||||
{nextItems.length > 3 && (
|
||||
<CollapsibleTrigger
|
||||
asChild
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="absolute left-1/2 -translate-x-1/2 bottom-0 translate-y-1/2 cursor-pointer"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'size-3 bg-text-secondary rounded-full flex items-center justify-center',
|
||||
{ 'bg-text-primary': isOpen },
|
||||
)}
|
||||
>
|
||||
{isOpen ? (
|
||||
<ChevronUp className="stroke-bg-component" />
|
||||
) : (
|
||||
<ChevronDown className="stroke-bg-component" />
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
)}
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
71
ragflow_web/src/components/confirm-delete-dialog.tsx
Normal file
71
ragflow_web/src/components/confirm-delete-dialog.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { DialogProps } from '@radix-ui/react-dialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
onOk?: (...args: any[]) => any;
|
||||
onCancel?: (...args: any[]) => any;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export function ConfirmDeleteDialog({
|
||||
children,
|
||||
title,
|
||||
onOk,
|
||||
onCancel,
|
||||
hidden = false,
|
||||
onOpenChange,
|
||||
open,
|
||||
defaultOpen,
|
||||
}: IProps & DialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (hidden) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog
|
||||
onOpenChange={onOpenChange}
|
||||
open={open}
|
||||
defaultOpen={defaultOpen}
|
||||
>
|
||||
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
|
||||
<AlertDialogContent
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{title ?? t('common.deleteModalTitle')}
|
||||
</AlertDialogTitle>
|
||||
{/* <AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete your
|
||||
account and remove your data from our servers.
|
||||
</AlertDialogDescription> */}
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={onCancel}>
|
||||
{t('common.no')}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-state-error text-text-primary"
|
||||
onClick={onOk}
|
||||
>
|
||||
{t('common.yes')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
27
ragflow_web/src/components/copy-to-clipboard.tsx
Normal file
27
ragflow_web/src/components/copy-to-clipboard.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { CheckOutlined, CopyOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { CopyToClipboard as Clipboard, Props } from 'react-copy-to-clipboard';
|
||||
|
||||
const CopyToClipboard = ({ text }: Props) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const { t } = useTranslate('common');
|
||||
|
||||
const handleCopy = () => {
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip title={copied ? t('copied') : t('copy')}>
|
||||
<Clipboard text={text} onCopy={handleCopy}>
|
||||
{copied ? <CheckOutlined /> : <CopyOutlined />}
|
||||
</Clipboard>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyToClipboard;
|
||||
75
ragflow_web/src/components/cross-language-form-field.tsx
Normal file
75
ragflow_web/src/components/cross-language-form-field.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from '@/components/ui/form';
|
||||
import { MultiSelect } from '@/components/ui/multi-select';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { t } from 'i18next';
|
||||
import { toLower } from 'lodash';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Languages = [
|
||||
'English',
|
||||
'Chinese',
|
||||
'Spanish',
|
||||
'French',
|
||||
'German',
|
||||
'Japanese',
|
||||
'Korean',
|
||||
'Vietnamese',
|
||||
];
|
||||
|
||||
export const crossLanguageOptions = Languages.map((x) => ({
|
||||
label: t('language.' + toLower(x)),
|
||||
value: x,
|
||||
}));
|
||||
|
||||
type CrossLanguageItemProps = {
|
||||
name?: string;
|
||||
vertical?: boolean;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export const CrossLanguageFormField = ({
|
||||
name = 'prompt_config.cross_languages',
|
||||
vertical = true,
|
||||
label,
|
||||
}: CrossLanguageItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem
|
||||
className={cn('flex', {
|
||||
'gap-2': vertical,
|
||||
'flex-col': vertical,
|
||||
'justify-between': !vertical,
|
||||
'items-center': !vertical,
|
||||
})}
|
||||
>
|
||||
<FormLabel tooltip={t('chat.crossLanguageTip')}>
|
||||
{label || t('chat.crossLanguage')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<MultiSelect
|
||||
options={crossLanguageOptions}
|
||||
placeholder={t('fileManager.pleaseSelect')}
|
||||
maxCount={100}
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
modalPopover
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
40
ragflow_web/src/components/cross-language-item.tsx
Normal file
40
ragflow_web/src/components/cross-language-item.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Select as AntSelect, Form } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Languages = [
|
||||
'English',
|
||||
'Chinese',
|
||||
'Spanish',
|
||||
'French',
|
||||
'German',
|
||||
'Japanese',
|
||||
'Korean',
|
||||
'Vietnamese',
|
||||
];
|
||||
|
||||
const options = Languages.map((x) => ({ label: x, value: x }));
|
||||
|
||||
type CrossLanguageItemProps = {
|
||||
name?: string | Array<string>;
|
||||
};
|
||||
|
||||
export const CrossLanguageItem = ({
|
||||
name = ['prompt_config', 'cross_languages'],
|
||||
}: CrossLanguageItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
label={t('chat.crossLanguage')}
|
||||
name={name}
|
||||
tooltip={t('chat.crossLanguageTip')}
|
||||
>
|
||||
<AntSelect
|
||||
options={options}
|
||||
allowClear
|
||||
placeholder={t('common.languagePlaceholder')}
|
||||
mode="multiple"
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
177
ragflow_web/src/components/data-pipeline-select/index.tsx
Normal file
177
ragflow_web/src/components/data-pipeline-select/index.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { AgentCategory } from '@/constants/agent';
|
||||
import { FormLayout } from '@/constants/form';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { useFetchAgentList } from '@/hooks/use-agent-request';
|
||||
import { buildSelectOptions } from '@/utils/component-util';
|
||||
import { ArrowUpRight } from 'lucide-react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { SelectWithSearch } from '../originui/select-with-search';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '../ui/form';
|
||||
import { MultiSelect } from '../ui/multi-select';
|
||||
export interface IDataPipelineSelectNode {
|
||||
id?: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
showToDataPipeline?: boolean;
|
||||
formFieldName: string;
|
||||
isMult?: boolean;
|
||||
setDataList?: (data: IDataPipelineSelectNode[]) => void;
|
||||
layout?: FormLayout;
|
||||
}
|
||||
|
||||
export function DataFlowSelect(props: IProps) {
|
||||
const {
|
||||
showToDataPipeline,
|
||||
formFieldName,
|
||||
isMult = false,
|
||||
setDataList,
|
||||
layout = FormLayout.Vertical,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
const form = useFormContext();
|
||||
const { navigateToAgents } = useNavigatePage();
|
||||
const toDataPipLine = () => {
|
||||
navigateToAgents();
|
||||
};
|
||||
const { data: dataPipelineOptions } = useFetchAgentList({
|
||||
canvas_category: AgentCategory.DataflowCanvas,
|
||||
});
|
||||
const options = useMemo(() => {
|
||||
const option = buildSelectOptions(
|
||||
dataPipelineOptions?.canvas,
|
||||
'id',
|
||||
'title',
|
||||
);
|
||||
|
||||
return option || [];
|
||||
}, [dataPipelineOptions]);
|
||||
|
||||
const nodes = useMemo(() => {
|
||||
return (
|
||||
dataPipelineOptions?.canvas?.map((item) => {
|
||||
return {
|
||||
id: item?.id,
|
||||
name: item?.title,
|
||||
avatar: item?.avatar,
|
||||
};
|
||||
}) || []
|
||||
);
|
||||
}, [dataPipelineOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
setDataList?.(nodes);
|
||||
}, [nodes, setDataList]);
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={formFieldName}
|
||||
render={({ field }) => (
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
{layout === FormLayout.Vertical && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-2 justify-between ">
|
||||
<FormLabel
|
||||
// tooltip={t('dataFlowTip')}
|
||||
className="text-sm text-text-primary whitespace-wrap "
|
||||
>
|
||||
{t('manualSetup')}
|
||||
</FormLabel>
|
||||
{showToDataPipeline && (
|
||||
<div
|
||||
className="text-sm flex text-text-primary cursor-pointer"
|
||||
onClick={toDataPipLine}
|
||||
>
|
||||
{t('buildItFromScratch')}
|
||||
<ArrowUpRight size={14} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground">
|
||||
<FormControl>
|
||||
<>
|
||||
{!isMult && (
|
||||
<SelectWithSearch
|
||||
{...field}
|
||||
placeholder={t('dataFlowPlaceholder')}
|
||||
options={options}
|
||||
triggerClassName="!bg-bg-base"
|
||||
/>
|
||||
)}
|
||||
{isMult && (
|
||||
<MultiSelect
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t('dataFlowPlaceholder')}
|
||||
options={options}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{layout === FormLayout.Horizontal && (
|
||||
<div className="flex gap-1 items-center">
|
||||
<div className="flex gap-2 justify-between w-1/4">
|
||||
<FormLabel
|
||||
// tooltip={t('dataFlowTip')}
|
||||
className="text-sm text-text-secondary whitespace-wrap "
|
||||
>
|
||||
{t('manualSetup')}
|
||||
</FormLabel>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground w-3/4 flex flex-col items-end">
|
||||
{showToDataPipeline && (
|
||||
<div
|
||||
className="text-sm flex text-text-primary cursor-pointer"
|
||||
onClick={toDataPipLine}
|
||||
>
|
||||
{t('buildItFromScratch')}
|
||||
<ArrowUpRight size={14} />
|
||||
</div>
|
||||
)}
|
||||
<FormControl>
|
||||
<>
|
||||
{!isMult && (
|
||||
<SelectWithSearch
|
||||
{...field}
|
||||
placeholder={t('dataFlowPlaceholder')}
|
||||
options={options}
|
||||
/>
|
||||
)}
|
||||
{isMult && (
|
||||
<MultiSelect
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t('dataFlowPlaceholder')}
|
||||
options={options}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex pt-1">
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
type DatasetConfigurationContainerProps = {
|
||||
className?: string;
|
||||
show?: boolean;
|
||||
} & PropsWithChildren;
|
||||
|
||||
export function DatasetConfigurationContainer({
|
||||
children,
|
||||
className,
|
||||
show = true,
|
||||
}: DatasetConfigurationContainerProps) {
|
||||
return show ? (
|
||||
<div
|
||||
className={cn(
|
||||
'border p-2 rounded-lg bg-slate-50 dark:bg-gray-600',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
}
|
||||
85
ragflow_web/src/components/delimiter-form-field.tsx
Normal file
85
ragflow_web/src/components/delimiter-form-field.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { forwardRef } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from './ui/form';
|
||||
import { Input, InputProps } from './ui/input';
|
||||
|
||||
interface IProps {
|
||||
value?: string | undefined;
|
||||
onChange?: (val: string | undefined) => void;
|
||||
}
|
||||
|
||||
export const DelimiterInput = forwardRef<HTMLInputElement, InputProps & IProps>(
|
||||
({ value, onChange, maxLength, defaultValue, ...props }, ref) => {
|
||||
const nextValue = value
|
||||
?.replaceAll('\n', '\\n')
|
||||
.replaceAll('\t', '\\t')
|
||||
.replaceAll('\r', '\\r');
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
const nextValue = val
|
||||
.replaceAll('\\n', '\n')
|
||||
.replaceAll('\\t', '\t')
|
||||
.replaceAll('\\r', '\r');
|
||||
onChange?.(nextValue);
|
||||
};
|
||||
return (
|
||||
<Input
|
||||
value={nextValue}
|
||||
onChange={handleInputChange}
|
||||
maxLength={maxLength}
|
||||
defaultValue={defaultValue}
|
||||
ref={ref}
|
||||
className={cn('bg-bg-base', props.className)}
|
||||
{...props}
|
||||
></Input>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export function DelimiterFormField() {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'parser_config.delimiter'}
|
||||
render={({ field }) => {
|
||||
if (typeof field.value === 'undefined') {
|
||||
// default value set
|
||||
form.setValue('parser_config.delimiter', '\n');
|
||||
}
|
||||
return (
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center gap-1">
|
||||
<FormLabel
|
||||
required
|
||||
tooltip={t('knowledgeDetails.delimiterTip')}
|
||||
className="text-sm text-text-secondary whitespace-break-spaces w-1/4"
|
||||
>
|
||||
{t('knowledgeDetails.delimiter')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<DelimiterInput {...field}></DelimiterInput>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
42
ragflow_web/src/components/delimiter.tsx
Normal file
42
ragflow_web/src/components/delimiter.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Form, Input } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IProps {
|
||||
value?: string | undefined;
|
||||
onChange?: (val: string | undefined) => void;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
export const DelimiterInput = ({ value, onChange, maxLength }: IProps) => {
|
||||
const nextValue = value?.replaceAll('\n', '\\n');
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
const nextValue = val.replaceAll('\\n', '\n');
|
||||
onChange?.(nextValue);
|
||||
};
|
||||
return (
|
||||
<Input
|
||||
value={nextValue}
|
||||
onChange={handleInputChange}
|
||||
maxLength={maxLength}
|
||||
></Input>
|
||||
);
|
||||
};
|
||||
|
||||
const Delimiter = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
name={['parser_config', 'delimiter']}
|
||||
label={t('knowledgeDetails.delimiter')}
|
||||
initialValue={`\n`}
|
||||
rules={[{ required: true }]}
|
||||
tooltip={t('knowledgeDetails.delimiterTip')}
|
||||
>
|
||||
<DelimiterInput />
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default Delimiter;
|
||||
119
ragflow_web/src/components/edit-tag/index.tsx
Normal file
119
ragflow_web/src/components/edit-tag/index.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { X } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '../ui/hover-card';
|
||||
import { Input } from '../ui/input';
|
||||
interface EditTagsProps {
|
||||
value?: string[];
|
||||
onChange?: (tags: string[]) => void;
|
||||
}
|
||||
|
||||
const EditTag = React.forwardRef<HTMLDivElement, EditTagsProps>(
|
||||
({ value = [], onChange }: EditTagsProps) => {
|
||||
const [inputVisible, setInputVisible] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputVisible) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [inputVisible]);
|
||||
|
||||
const handleClose = (removedTag: string) => {
|
||||
const newTags = value?.filter((tag) => tag !== removedTag);
|
||||
onChange?.(newTags ?? []);
|
||||
};
|
||||
|
||||
const showInput = () => {
|
||||
setInputVisible(true);
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleInputConfirm = () => {
|
||||
if (inputValue && value) {
|
||||
const newTags = inputValue
|
||||
.split(';')
|
||||
.map((tag) => tag.trim())
|
||||
.filter((tag) => tag && !value.includes(tag));
|
||||
onChange?.([...value, ...newTags]);
|
||||
}
|
||||
setInputVisible(false);
|
||||
setInputValue('');
|
||||
};
|
||||
|
||||
const forMap = (tag: string) => {
|
||||
return (
|
||||
<HoverCard key={tag}>
|
||||
<HoverCardContent side="top">{tag}</HoverCardContent>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="w-fit flex items-center justify-center gap-2 border-dashed border px-2 py-1 rounded-sm bg-bg-card">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="max-w-80 overflow-hidden text-ellipsis">
|
||||
{tag}
|
||||
</div>
|
||||
<X
|
||||
className="w-4 h-4 text-muted-foreground hover:text-primary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleClose(tag);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
</HoverCard>
|
||||
);
|
||||
};
|
||||
|
||||
const tagChild = value?.map(forMap);
|
||||
|
||||
const tagPlusStyle: React.CSSProperties = {
|
||||
borderStyle: 'dashed',
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{inputVisible && (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="h-8 bg-bg-card mb-1"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleInputConfirm}
|
||||
onKeyDown={(e) => {
|
||||
if (e?.key === 'Enter') {
|
||||
handleInputConfirm();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-2 py-1">
|
||||
{Array.isArray(tagChild) && tagChild.length > 0 && <>{tagChild}</>}
|
||||
{!inputVisible && (
|
||||
<Button
|
||||
variant="dashed"
|
||||
className="w-fit flex items-center justify-center gap-2 bg-bg-card"
|
||||
onClick={showInput}
|
||||
style={tagPlusStyle}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default EditTag;
|
||||
98
ragflow_web/src/components/editable-cell.tsx
Normal file
98
ragflow_web/src/components/editable-cell.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Form, FormInstance, Input, InputRef, Typography } from 'antd';
|
||||
import { omit } from 'lodash';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
|
||||
const EditableContext = React.createContext<FormInstance<any> | null>(null);
|
||||
const { Text } = Typography;
|
||||
|
||||
interface EditableRowProps {
|
||||
index: number;
|
||||
}
|
||||
|
||||
interface Item {
|
||||
key: string;
|
||||
name: string;
|
||||
age: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
export const EditableRow: React.FC<EditableRowProps> = ({ ...props }) => {
|
||||
const [form] = Form.useForm();
|
||||
return (
|
||||
<Form form={form} component={false}>
|
||||
<EditableContext.Provider value={form}>
|
||||
<tr {...omit(props, 'index')} />
|
||||
</EditableContext.Provider>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
interface EditableCellProps {
|
||||
title: React.ReactNode;
|
||||
editable: boolean;
|
||||
children: React.ReactNode;
|
||||
dataIndex: keyof Item;
|
||||
record: Item;
|
||||
handleSave: (record: Item) => void;
|
||||
}
|
||||
|
||||
export const EditableCell: React.FC<EditableCellProps> = ({
|
||||
title,
|
||||
editable,
|
||||
children,
|
||||
dataIndex,
|
||||
record,
|
||||
handleSave,
|
||||
...restProps
|
||||
}) => {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const form = useContext(EditableContext)!;
|
||||
|
||||
useEffect(() => {
|
||||
if (editing) {
|
||||
inputRef.current!.focus();
|
||||
}
|
||||
}, [editing]);
|
||||
|
||||
const toggleEdit = () => {
|
||||
setEditing(!editing);
|
||||
form.setFieldsValue({ [dataIndex]: record[dataIndex] });
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
toggleEdit();
|
||||
handleSave({ ...record, ...values });
|
||||
} catch (errInfo) {
|
||||
console.log('Save failed:', errInfo);
|
||||
}
|
||||
};
|
||||
|
||||
let childNode = children;
|
||||
|
||||
if (editable) {
|
||||
childNode = editing ? (
|
||||
<Form.Item
|
||||
style={{ margin: 0, minWidth: 70 }}
|
||||
name={dataIndex}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `${title} is required.`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input ref={inputRef} onPressEnter={save} onBlur={save} />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<div onClick={toggleEdit} className="editable-cell-value-wrap">
|
||||
<Text>{children}</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <td {...restProps}>{childNode}</td>;
|
||||
};
|
||||
48
ragflow_web/src/components/embed-container.tsx
Normal file
48
ragflow_web/src/components/embed-container.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useFetchAppConf } from '@/hooks/logic-hooks';
|
||||
import { RefreshCcw } from 'lucide-react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { RAGFlowAvatar } from './ragflow-avatar';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
type EmbedContainerProps = {
|
||||
title: string;
|
||||
avatar?: string;
|
||||
handleReset?(): void;
|
||||
} & PropsWithChildren;
|
||||
|
||||
export function EmbedContainer({
|
||||
title,
|
||||
avatar,
|
||||
children,
|
||||
handleReset,
|
||||
}: EmbedContainerProps) {
|
||||
const appConf = useFetchAppConf();
|
||||
|
||||
return (
|
||||
<section className="h-[100vh] flex justify-center items-center">
|
||||
<div className="w-40 flex gap-2 absolute left-3 top-12 items-center">
|
||||
<img src="/logo.svg" alt="" />
|
||||
<span className="text-2xl font-bold">{appConf.appName}</span>
|
||||
</div>
|
||||
<div className=" w-[80vw] border rounded-lg">
|
||||
<div className="flex justify-between items-center border-b p-3">
|
||||
<div className="flex gap-2 items-center">
|
||||
<RAGFlowAvatar avatar={avatar} name={title} isPerson />
|
||||
<div className="text-xl text-foreground">{title}</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
className="text-sm text-foreground cursor-pointer"
|
||||
onClick={handleReset}
|
||||
>
|
||||
<div className="flex gap-1 items-center">
|
||||
<RefreshCcw size={14} />
|
||||
<span className="text-lg ">Reset</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
268
ragflow_web/src/components/embed-dialog/index.tsx
Normal file
268
ragflow_web/src/components/embed-dialog/index.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||
import HightLightMarkdown from '@/components/highlight-markdown';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { SharedFrom } from '@/constants/chat';
|
||||
import {
|
||||
LanguageAbbreviation,
|
||||
LanguageAbbreviationMap,
|
||||
} from '@/constants/common';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { Routes } from '@/routes';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
const FormSchema = z.object({
|
||||
visibleAvatar: z.boolean(),
|
||||
locale: z.string(),
|
||||
embedType: z.enum(['fullscreen', 'widget']),
|
||||
enableStreaming: z.boolean(),
|
||||
});
|
||||
|
||||
type IProps = IModalProps<any> & {
|
||||
token: string;
|
||||
from: SharedFrom;
|
||||
beta: string;
|
||||
isAgent: boolean;
|
||||
};
|
||||
|
||||
function EmbedDialog({
|
||||
hideModal,
|
||||
token = '',
|
||||
from,
|
||||
beta = '',
|
||||
isAgent,
|
||||
}: IProps) {
|
||||
const { t } = useTranslate('chat');
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
visibleAvatar: false,
|
||||
locale: '',
|
||||
embedType: 'fullscreen' as const,
|
||||
enableStreaming: false,
|
||||
},
|
||||
});
|
||||
|
||||
const values = useWatch({ control: form.control });
|
||||
|
||||
const languageOptions = useMemo(() => {
|
||||
return Object.values(LanguageAbbreviation).map((x) => ({
|
||||
label: LanguageAbbreviationMap[x],
|
||||
value: x,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const generateIframeSrc = useCallback(() => {
|
||||
const { visibleAvatar, locale, embedType, enableStreaming } = values;
|
||||
const baseRoute =
|
||||
embedType === 'widget'
|
||||
? Routes.ChatWidget
|
||||
: from === SharedFrom.Agent
|
||||
? Routes.AgentShare
|
||||
: Routes.ChatShare;
|
||||
let src = `${location.origin}${baseRoute}?shared_id=${token}&from=${from}&auth=${beta}`;
|
||||
if (visibleAvatar) {
|
||||
src += '&visible_avatar=1';
|
||||
}
|
||||
if (locale) {
|
||||
src += `&locale=${locale}`;
|
||||
}
|
||||
if (enableStreaming) {
|
||||
src += '&streaming=true';
|
||||
}
|
||||
return src;
|
||||
}, [beta, from, token, values]);
|
||||
|
||||
const text = useMemo(() => {
|
||||
const iframeSrc = generateIframeSrc();
|
||||
const { embedType } = values;
|
||||
|
||||
if (embedType === 'widget') {
|
||||
const { enableStreaming } = values;
|
||||
const streamingParam = enableStreaming
|
||||
? '&streaming=true'
|
||||
: '&streaming=false';
|
||||
return `
|
||||
~~~ html
|
||||
<iframe src="${iframeSrc}&mode=master${streamingParam}"
|
||||
style="position:fixed;bottom:0;right:0;width:100px;height:100px;border:none;background:transparent;z-index:9999"
|
||||
frameborder="0" allow="microphone;camera"></iframe>
|
||||
<script>
|
||||
window.addEventListener('message',e=>{
|
||||
if(e.origin!=='${location.origin.replace(/:\d+/, ':9222')}')return;
|
||||
if(e.data.type==='CREATE_CHAT_WINDOW'){
|
||||
if(document.getElementById('chat-win'))return;
|
||||
const i=document.createElement('iframe');
|
||||
i.id='chat-win';i.src=e.data.src;
|
||||
i.style.cssText='position:fixed;bottom:104px;right:24px;width:380px;height:500px;border:none;background:transparent;z-index:9998;display:none';
|
||||
i.frameBorder='0';i.allow='microphone;camera';
|
||||
document.body.appendChild(i);
|
||||
}else if(e.data.type==='TOGGLE_CHAT'){
|
||||
const w=document.getElementById('chat-win');
|
||||
if(w)w.style.display=e.data.isOpen?'block':'none';
|
||||
}else if(e.data.type==='SCROLL_PASSTHROUGH')window.scrollBy(0,e.data.deltaY);
|
||||
});
|
||||
</script>
|
||||
~~~
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
~~~ html
|
||||
<iframe
|
||||
src="${iframeSrc}"
|
||||
style="width: 100%; height: 100%; min-height: 600px"
|
||||
frameborder="0"
|
||||
>
|
||||
</iframe>
|
||||
~~~
|
||||
`;
|
||||
}
|
||||
}, [generateIframeSrc, values]);
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t('embedIntoSite', { keyPrefix: 'common' })}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<section className="w-full overflow-auto space-y-5 text-sm text-text-secondary">
|
||||
<Form {...form}>
|
||||
<form className="space-y-5">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="embedType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Embed Type</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="flex flex-col space-y-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="fullscreen" id="fullscreen" />
|
||||
<Label htmlFor="fullscreen" className="text-sm">
|
||||
Fullscreen Chat (Traditional iframe)
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="widget" id="widget" />
|
||||
<Label htmlFor="widget" className="text-sm">
|
||||
Floating Widget (Intercom-style)
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="visibleAvatar"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('avatarHidden')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{values.embedType === 'widget' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enableStreaming"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Enable Streaming Responses</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="locale"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('locale')}</FormLabel>
|
||||
<FormControl>
|
||||
<SelectWithSearch
|
||||
{...field}
|
||||
options={languageOptions}
|
||||
></SelectWithSearch>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
<div className="max-h-[350px] overflow-auto">
|
||||
<span>{t('embedCode', { keyPrefix: 'search' })}</span>
|
||||
<div className="max-h-full overflow-y-auto">
|
||||
<HightLightMarkdown>{text}</HightLightMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className=" font-medium mt-4 mb-1">
|
||||
{t(isAgent ? 'flow' : 'chat', { keyPrefix: 'header' })}
|
||||
<span className="ml-1 inline-block">ID</span>
|
||||
</div>
|
||||
<div className="bg-bg-card rounded-lg flex justify-between p-2">
|
||||
<span>{token} </span>
|
||||
<CopyToClipboard text={token}></CopyToClipboard>
|
||||
</div>
|
||||
<a
|
||||
className="cursor-pointer text-accent-primary 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,87 @@
|
||||
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
|
||||
import { useFetchManualSystemTokenList } from '@/hooks/user-setting-hooks';
|
||||
import { useCallback } from 'react';
|
||||
import message from '../ui/message';
|
||||
|
||||
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 };
|
||||
};
|
||||
|
||||
export 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,
|
||||
};
|
||||
};
|
||||
77
ragflow_web/src/components/empty/empty.tsx
Normal file
77
ragflow_web/src/components/empty/empty.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { t } from 'i18next';
|
||||
|
||||
type EmptyProps = {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const EmptyIcon = () => (
|
||||
<svg
|
||||
width="184"
|
||||
height="152"
|
||||
viewBox="0 0 184 152"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>{t('common.noData')}</title>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<g transform="translate(24 31.67)">
|
||||
<ellipse
|
||||
fillOpacity=".8"
|
||||
fill="#F5F5F7"
|
||||
cx="67.797"
|
||||
cy="106.89"
|
||||
rx="67.797"
|
||||
ry="12.668"
|
||||
></ellipse>
|
||||
<path
|
||||
d="M122.034 69.674L98.109 40.229c-1.148-1.386-2.826-2.225-4.593-2.225h-51.44c-1.766 0-3.444.839-4.592 2.225L13.56 69.674v15.383h108.475V69.674z"
|
||||
fill="#AEB8C2"
|
||||
></path>
|
||||
<path
|
||||
d="M101.537 86.214L80.63 61.102c-1.001-1.207-2.507-1.867-4.048-1.867H31.724c-1.54 0-3.047.66-4.048 1.867L6.769 86.214v13.792h94.768V86.214z"
|
||||
fill="url(#linearGradient-1)"
|
||||
transform="translate(13.56)"
|
||||
></path>
|
||||
<path
|
||||
d="M33.83 0h67.933a4 4 0 0 1 4 4v93.344a4 4 0 0 1-4 4H33.83a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"
|
||||
fill="#F5F5F7"
|
||||
></path>
|
||||
<path
|
||||
d="M42.678 9.953h50.237a2 2 0 0 1 2 2V36.91a2 2 0 0 1-2 2H42.678a2 2 0 0 1-2-2V11.953a2 2 0 0 1 2-2zM42.94 49.767h49.713a2.262 2.262 0 1 1 0 4.524H42.94a2.262 2.262 0 0 1 0-4.524zM42.94 61.53h49.713a2.262 2.262 0 1 1 0 4.525H42.94a2.262 2.262 0 0 1 0-4.525zM121.813 105.032c-.775 3.071-3.497 5.36-6.735 5.36H20.515c-3.238 0-5.96-2.29-6.734-5.36a7.309 7.309 0 0 1-.222-1.79V69.675h26.318c2.907 0 5.25 2.448 5.25 5.42v.04c0 2.971 2.37 5.37 5.277 5.37h34.785c2.907 0 5.277-2.421 5.277-5.393V75.1c0-2.972 2.343-5.426 5.25-5.426h26.318v33.569c0 .617-.077 1.216-.221 1.789z"
|
||||
fill="#DCE0E6"
|
||||
></path>
|
||||
</g>
|
||||
<path
|
||||
d="M149.121 33.292l-6.83 2.65a1 1 0 0 1-1.317-1.23l1.937-6.207c-2.589-2.944-4.109-6.534-4.109-10.408C138.802 8.102 148.92 0 161.402 0 173.881 0 184 8.102 184 18.097c0 9.995-10.118 18.097-22.599 18.097-4.528 0-8.744-1.066-12.28-2.902z"
|
||||
fill="#DCE0E6"
|
||||
></path>
|
||||
<g transform="translate(149.65 15.383)" fill="#FFF">
|
||||
<ellipse cx="20.654" cy="3.167" rx="2.849" ry="2.815"></ellipse>
|
||||
<path d="M5.698 5.63H0L2.898.704zM9.259.704h4.985V5.63H9.259z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Empty = (props: EmptyProps) => {
|
||||
const { className, children } = props;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col justify-center items-center text-center gap-3',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<EmptyIcon />
|
||||
{!children && (
|
||||
<div className="empty-text mt-4 text-text-secondary">
|
||||
{t('common.noData')}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Empty;
|
||||
47
ragflow_web/src/components/entity-types-form-field.tsx
Normal file
47
ragflow_web/src/components/entity-types-form-field.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import EditTag from './edit-tag';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from './ui/form';
|
||||
|
||||
type EntityTypesFormFieldProps = {
|
||||
name?: string;
|
||||
};
|
||||
export function EntityTypesFormField({
|
||||
name = 'parser_config.entity_types',
|
||||
}: EntityTypesFormFieldProps) {
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel className="text-sm whitespace-nowrap w-1/4">
|
||||
<span className="text-red-600">*</span> {t('entityTypes')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<EditTag {...field}></EditTag>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
33
ragflow_web/src/components/entity-types-item.tsx
Normal file
33
ragflow_web/src/components/entity-types-item.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { Form } from 'antd';
|
||||
import EditTag from './edit-tag';
|
||||
|
||||
const initialEntityTypes = [
|
||||
'organization',
|
||||
'person',
|
||||
'geo',
|
||||
'event',
|
||||
'category',
|
||||
];
|
||||
|
||||
type IProps = {
|
||||
field?: string[];
|
||||
};
|
||||
|
||||
const EntityTypesItem = ({
|
||||
field = ['parser_config', 'entity_types'],
|
||||
}: IProps) => {
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
return (
|
||||
<Form.Item
|
||||
name={field}
|
||||
label={t('entityTypes')}
|
||||
rules={[{ required: true }]}
|
||||
initialValue={initialEntityTypes}
|
||||
>
|
||||
<EditTag value={field}></EditTag>
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntityTypesItem;
|
||||
53
ragflow_web/src/components/excel-to-html-form-field.tsx
Normal file
53
ragflow_web/src/components/excel-to-html-form-field.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from './ui/form';
|
||||
import { Switch } from './ui/switch';
|
||||
|
||||
export function ExcelToHtmlFormField() {
|
||||
const form = useFormContext();
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="parser_config.html4excel"
|
||||
render={({ field }) => {
|
||||
if (typeof field.value === 'undefined') {
|
||||
// default value set
|
||||
form.setValue('parser_config.html4excel', false);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormItem defaultChecked={false} className=" items-center space-y-0 ">
|
||||
<div className="flex items-center gap-1">
|
||||
<FormLabel
|
||||
tooltip={t('html4excelTip')}
|
||||
className="text-sm text-text-secondary whitespace-break-spaces w-1/4"
|
||||
>
|
||||
{t('html4excel')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
19
ragflow_web/src/components/excel-to-html.tsx
Normal file
19
ragflow_web/src/components/excel-to-html.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { Form, Switch } from 'antd';
|
||||
|
||||
const ExcelToHtml = () => {
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
return (
|
||||
<Form.Item
|
||||
name={['parser_config', 'html4excel']}
|
||||
label={t('html4excel')}
|
||||
initialValue={false}
|
||||
valuePropName="checked"
|
||||
tooltip={t('html4excelTip')}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExcelToHtml;
|
||||
4
ragflow_web/src/components/file-icon/index.less
Normal file
4
ragflow_web/src/components/file-icon/index.less
Normal file
@@ -0,0 +1,4 @@
|
||||
.thumbnailImg {
|
||||
display: inline-block;
|
||||
max-width: 20px;
|
||||
}
|
||||
33
ragflow_web/src/components/file-icon/index.tsx
Normal file
33
ragflow_web/src/components/file-icon/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { getExtension } from '@/utils/document-util';
|
||||
import SvgIcon from '../svg-icon';
|
||||
|
||||
import { useFetchDocumentThumbnailsByIds } from '@/hooks/document-hooks';
|
||||
import { useEffect } from 'react';
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const FileIcon = ({ name, id }: IProps) => {
|
||||
const fileExtension = getExtension(name);
|
||||
|
||||
const { data: fileThumbnails, setDocumentIds } =
|
||||
useFetchDocumentThumbnailsByIds();
|
||||
const fileThumbnail = fileThumbnails[id];
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
setDocumentIds([id]);
|
||||
}
|
||||
}, [id, setDocumentIds]);
|
||||
|
||||
return fileThumbnail ? (
|
||||
<img src={fileThumbnail} className={styles.thumbnailImg}></img>
|
||||
) : (
|
||||
<SvgIcon name={`file-icon/${fileExtension}`} width={24}></SvgIcon>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileIcon;
|
||||
62
ragflow_web/src/components/file-status-badge.tsx
Normal file
62
ragflow_web/src/components/file-status-badge.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
// src/pages/dataset/file-logs/file-status-badge.tsx
|
||||
import { RunningStatus } from '@/pages/dataset/dataset/constant';
|
||||
import { FC } from 'react';
|
||||
/**
|
||||
* params: status: 0 not run yet 1 running, 2 cancel, 3 success, 4 fail
|
||||
*/
|
||||
interface StatusBadgeProps {
|
||||
// status: 'Success' | 'Failed' | 'Running' | 'Pending';
|
||||
status: RunningStatus;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const FileStatusBadge: FC<StatusBadgeProps> = ({ status, name }) => {
|
||||
const getStatusColor = () => {
|
||||
// #3ba05c → rgb(59, 160, 92) // state-success
|
||||
// #d8494b → rgb(216, 73, 75) // state-error
|
||||
// #00beb4 → rgb(0, 190, 180) // accent-primary
|
||||
// #faad14 → rgb(250, 173, 20) // state-warning
|
||||
switch (status) {
|
||||
case RunningStatus.DONE:
|
||||
return `bg-[rgba(59,160,92,0.1)] text-state-success`;
|
||||
case RunningStatus.FAIL:
|
||||
return `bg-[rgba(216,73,75,0.1)] text-state-error`;
|
||||
case RunningStatus.RUNNING:
|
||||
return `bg-[rgba(0,190,180,0.1)] text-accent-primary`;
|
||||
case RunningStatus.UNSTART:
|
||||
return `bg-[rgba(250,173,20,0.1)] text-state-warning`;
|
||||
default:
|
||||
return 'bg-gray-500/10 text-text-secondary';
|
||||
}
|
||||
};
|
||||
|
||||
const getBgStatusColor = () => {
|
||||
// #3ba05c → rgb(59, 160, 92) // state-success
|
||||
// #d8494b → rgb(216, 73, 75) // state-error
|
||||
// #00beb4 → rgb(0, 190, 180) // accent-primary
|
||||
// #faad14 → rgb(250, 173, 20) // state-warning
|
||||
switch (status) {
|
||||
case RunningStatus.DONE:
|
||||
return `bg-[rgba(59,160,92,1)] text-state-success`;
|
||||
case RunningStatus.FAIL:
|
||||
return `bg-[rgba(216,73,75,1)] text-state-error`;
|
||||
case RunningStatus.RUNNING:
|
||||
return `bg-[rgba(0,190,180,1)] text-accent-primary`;
|
||||
case RunningStatus.UNSTART:
|
||||
return `bg-[rgba(250,173,20,1)] text-state-warning`;
|
||||
default:
|
||||
return `bg-[rgba(117,120,122,1)] text-text-secondary`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center w-[75px] px-2 py-1 rounded-full text-xs font-medium ${getStatusColor()}`}
|
||||
>
|
||||
<div className={`w-1 h-1 mr-1 rounded-full ${getBgStatusColor()}`}></div>
|
||||
{name || ''}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileStatusBadge;
|
||||
126
ragflow_web/src/components/file-upload-dialog/index.tsx
Normal file
126
ragflow_web/src/components/file-upload-dialog/index.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { ButtonLoading } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { TFunction } from 'i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import { FileUploader } from '../file-uploader';
|
||||
import { RAGFlowFormItem } from '../ragflow-form';
|
||||
import { Form } from '../ui/form';
|
||||
import { Switch } from '../ui/switch';
|
||||
|
||||
function buildUploadFormSchema(t: TFunction) {
|
||||
const FormSchema = z.object({
|
||||
parseOnCreation: z.boolean().optional(),
|
||||
fileList: z
|
||||
.array(z.instanceof(File))
|
||||
.min(1, { message: t('fileManager.pleaseUploadAtLeastOneFile') }),
|
||||
});
|
||||
|
||||
return FormSchema;
|
||||
}
|
||||
|
||||
export type UploadFormSchemaType = z.infer<
|
||||
ReturnType<typeof buildUploadFormSchema>
|
||||
>;
|
||||
|
||||
const UploadFormId = 'UploadFormId';
|
||||
|
||||
type UploadFormProps = {
|
||||
submit: (values?: UploadFormSchemaType) => void;
|
||||
showParseOnCreation?: boolean;
|
||||
};
|
||||
function UploadForm({ submit, showParseOnCreation }: UploadFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const FormSchema = buildUploadFormSchema(t);
|
||||
|
||||
type UploadFormSchemaType = z.infer<typeof FormSchema>;
|
||||
const form = useForm<UploadFormSchemaType>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
parseOnCreation: false,
|
||||
fileList: [],
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(submit)}
|
||||
id={UploadFormId}
|
||||
className="space-y-4"
|
||||
>
|
||||
{showParseOnCreation && (
|
||||
<RAGFlowFormItem
|
||||
name="parseOnCreation"
|
||||
label={t('fileManager.parseOnCreation')}
|
||||
>
|
||||
{(field) => (
|
||||
<Switch
|
||||
onCheckedChange={field.onChange}
|
||||
checked={field.value}
|
||||
></Switch>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
<RAGFlowFormItem name="fileList" label={t('fileManager.file')}>
|
||||
{(field) => (
|
||||
<FileUploader
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
accept={{ '*': [] }}
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
type FileUploadDialogProps = IModalProps<UploadFormSchemaType> &
|
||||
Pick<UploadFormProps, 'showParseOnCreation'>;
|
||||
export function FileUploadDialog({
|
||||
hideModal,
|
||||
onOk,
|
||||
loading,
|
||||
showParseOnCreation = false,
|
||||
}: FileUploadDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('fileManager.uploadFile')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Tabs defaultValue="account">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-4">
|
||||
<TabsTrigger value="account">{t('fileManager.local')}</TabsTrigger>
|
||||
<TabsTrigger value="password">{t('fileManager.s3')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="account">
|
||||
<UploadForm
|
||||
submit={onOk!}
|
||||
showParseOnCreation={showParseOnCreation}
|
||||
></UploadForm>
|
||||
</TabsContent>
|
||||
<TabsContent value="password">{t('common.comingSoon')}</TabsContent>
|
||||
</Tabs>
|
||||
<DialogFooter>
|
||||
<ButtonLoading type="submit" loading={loading} form={UploadFormId}>
|
||||
{t('common.save')}
|
||||
</ButtonLoading>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
13
ragflow_web/src/components/file-upload-modal/index.less
Normal file
13
ragflow_web/src/components/file-upload-modal/index.less
Normal file
@@ -0,0 +1,13 @@
|
||||
.uploader {
|
||||
:global {
|
||||
.ant-upload-list {
|
||||
max-height: 40vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uploadLimit {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
}
|
||||
191
ragflow_web/src/components/file-upload-modal/index.tsx
Normal file
191
ragflow_web/src/components/file-upload-modal/index.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { InboxOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Checkbox,
|
||||
Flex,
|
||||
Modal,
|
||||
Progress,
|
||||
Segmented,
|
||||
Tabs,
|
||||
TabsProps,
|
||||
Upload,
|
||||
UploadFile,
|
||||
UploadProps,
|
||||
} from 'antd';
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
const { Dragger } = Upload;
|
||||
|
||||
const FileUpload = ({
|
||||
directory,
|
||||
fileList,
|
||||
setFileList,
|
||||
uploadProgress,
|
||||
}: {
|
||||
directory: boolean;
|
||||
fileList: UploadFile[];
|
||||
setFileList: Dispatch<SetStateAction<UploadFile[]>>;
|
||||
uploadProgress?: number;
|
||||
}) => {
|
||||
const { t } = useTranslate('fileManager');
|
||||
const props: UploadProps = {
|
||||
multiple: true,
|
||||
onRemove: (file) => {
|
||||
const index = fileList.indexOf(file);
|
||||
const newFileList = fileList.slice();
|
||||
newFileList.splice(index, 1);
|
||||
setFileList(newFileList);
|
||||
},
|
||||
beforeUpload: (file: UploadFile) => {
|
||||
setFileList((pre) => {
|
||||
return [...pre, file];
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
directory,
|
||||
fileList,
|
||||
progress: {
|
||||
strokeWidth: 2,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Progress percent={uploadProgress} showInfo={false} />
|
||||
<Dragger {...props} className={styles.uploader}>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">{t('uploadTitle')}</p>
|
||||
<p className="ant-upload-hint">{t('uploadDescription')}</p>
|
||||
{false && <p className={styles.uploadLimit}>{t('uploadLimit')}</p>}
|
||||
</Dragger>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IFileUploadModalProps
|
||||
extends IModalProps<
|
||||
{ parseOnCreation: boolean; directoryFileList: UploadFile[] } | UploadFile[]
|
||||
> {
|
||||
uploadFileList?: UploadFile[];
|
||||
setUploadFileList?: Dispatch<SetStateAction<UploadFile[]>>;
|
||||
uploadProgress?: number;
|
||||
setUploadProgress?: Dispatch<SetStateAction<number>>;
|
||||
}
|
||||
|
||||
const FileUploadModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
loading,
|
||||
onOk: onFileUploadOk,
|
||||
uploadFileList: fileList,
|
||||
setUploadFileList: setFileList,
|
||||
uploadProgress,
|
||||
setUploadProgress,
|
||||
}: IFileUploadModalProps) => {
|
||||
const { t } = useTranslate('fileManager');
|
||||
const [value, setValue] = useState<string | number>('local');
|
||||
const [parseOnCreation, setParseOnCreation] = useState(false);
|
||||
const [currentFileList, setCurrentFileList] = useState<UploadFile[]>([]);
|
||||
const [directoryFileList, setDirectoryFileList] = useState<UploadFile[]>([]);
|
||||
|
||||
const clearFileList = () => {
|
||||
if (setFileList) {
|
||||
setFileList([]);
|
||||
setUploadProgress?.(0);
|
||||
} else {
|
||||
setCurrentFileList([]);
|
||||
}
|
||||
setDirectoryFileList([]);
|
||||
};
|
||||
|
||||
const onOk = async () => {
|
||||
if (uploadProgress === 100) {
|
||||
hideModal?.();
|
||||
return;
|
||||
}
|
||||
|
||||
const ret = await onFileUploadOk?.(
|
||||
fileList
|
||||
? { parseOnCreation, directoryFileList }
|
||||
: [...currentFileList, ...directoryFileList],
|
||||
);
|
||||
return ret;
|
||||
};
|
||||
|
||||
const afterClose = () => {
|
||||
clearFileList();
|
||||
};
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: t('file'),
|
||||
children: (
|
||||
<FileUpload
|
||||
directory={false}
|
||||
fileList={fileList ? fileList : currentFileList}
|
||||
setFileList={setFileList ? setFileList : setCurrentFileList}
|
||||
uploadProgress={uploadProgress}
|
||||
></FileUpload>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: t('directory'),
|
||||
children: (
|
||||
<FileUpload
|
||||
directory
|
||||
fileList={directoryFileList}
|
||||
setFileList={setDirectoryFileList}
|
||||
uploadProgress={uploadProgress}
|
||||
></FileUpload>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={t('uploadFile')}
|
||||
open={visible}
|
||||
onOk={onOk}
|
||||
onCancel={hideModal}
|
||||
confirmLoading={loading}
|
||||
afterClose={afterClose}
|
||||
>
|
||||
<Flex gap={'large'} vertical>
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: t('local'), value: 'local' },
|
||||
{ label: t('s3'), value: 's3' },
|
||||
]}
|
||||
block
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
/>
|
||||
{value === 'local' ? (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={parseOnCreation}
|
||||
onChange={(e) => setParseOnCreation(e.target.checked)}
|
||||
>
|
||||
{t('parseOnCreation')}
|
||||
</Checkbox>
|
||||
<Tabs defaultActiveKey="1" items={items} />
|
||||
</>
|
||||
) : (
|
||||
t('comingSoon', { keyPrefix: 'common' })
|
||||
)}
|
||||
</Flex>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileUploadModal;
|
||||
1436
ragflow_web/src/components/file-upload.tsx
Normal file
1436
ragflow_web/src/components/file-upload.tsx
Normal file
File diff suppressed because it is too large
Load Diff
337
ragflow_web/src/components/file-uploader.tsx
Normal file
337
ragflow_web/src/components/file-uploader.tsx
Normal file
@@ -0,0 +1,337 @@
|
||||
// https://github.com/sadmann7/file-uploader
|
||||
|
||||
'use client';
|
||||
|
||||
import { FileText, Upload, X } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import Dropzone, {
|
||||
type DropzoneProps,
|
||||
type FileRejection,
|
||||
} from 'react-dropzone';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { useControllableState } from '@/hooks/use-controllable-state';
|
||||
import { cn, formatBytes } from '@/lib/utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function isFileWithPreview(file: File): file is File & { preview: string } {
|
||||
return 'preview' in file && typeof file.preview === 'string';
|
||||
}
|
||||
|
||||
interface FileCardProps {
|
||||
file: File;
|
||||
onRemove: () => void;
|
||||
progress?: number;
|
||||
}
|
||||
|
||||
interface FilePreviewProps {
|
||||
file: File & { preview: string };
|
||||
}
|
||||
|
||||
function FilePreview({ file }: FilePreviewProps) {
|
||||
if (file.type.startsWith('image/')) {
|
||||
return (
|
||||
<img
|
||||
src={file.preview}
|
||||
alt={file.name}
|
||||
width={48}
|
||||
height={48}
|
||||
loading="lazy"
|
||||
className="aspect-square shrink-0 rounded-md object-cover"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FileText className="size-10 text-muted-foreground" aria-hidden="true" />
|
||||
);
|
||||
}
|
||||
|
||||
function FileCard({ file, progress, onRemove }: FileCardProps) {
|
||||
return (
|
||||
<div className="relative flex items-center gap-2.5">
|
||||
<div className="flex flex-1 gap-2.5">
|
||||
{isFileWithPreview(file) ? <FilePreview file={file} /> : null}
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="flex flex-col gap-px">
|
||||
<p className="line-clamp-1 text-sm font-medium text-foreground/80">
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatBytes(file.size)}
|
||||
</p>
|
||||
</div>
|
||||
{progress ? <Progress value={progress} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={onRemove}
|
||||
>
|
||||
<X className="size-4" aria-hidden="true" />
|
||||
<span className="sr-only">Remove file</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface FileUploaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Value of the uploader.
|
||||
* @type File[]
|
||||
* @default undefined
|
||||
* @example value={files}
|
||||
*/
|
||||
value?: File[];
|
||||
|
||||
/**
|
||||
* Function to be called when the value changes.
|
||||
* @type (files: File[]) => void
|
||||
* @default undefined
|
||||
* @example onValueChange={(files) => setFiles(files)}
|
||||
*/
|
||||
onValueChange?: (files: File[]) => void;
|
||||
|
||||
/**
|
||||
* Function to be called when files are uploaded.
|
||||
* @type (files: File[]) => Promise<void>
|
||||
* @default undefined
|
||||
* @example onUpload={(files) => uploadFiles(files)}
|
||||
*/
|
||||
onUpload?: (files: File[]) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Progress of the uploaded files.
|
||||
* @type Record<string, number> | undefined
|
||||
* @default undefined
|
||||
* @example progresses={{ "file1.png": 50 }}
|
||||
*/
|
||||
progresses?: Record<string, number>;
|
||||
|
||||
/**
|
||||
* Accepted file types for the uploader.
|
||||
* @type { [key: string]: string[]}
|
||||
* @default
|
||||
* ```ts
|
||||
* { "image/*": [] }
|
||||
* ```
|
||||
* @example accept={["image/png", "image/jpeg"]}
|
||||
*/
|
||||
accept?: DropzoneProps['accept'];
|
||||
|
||||
/**
|
||||
* Maximum file size for the uploader.
|
||||
* @type number | undefined
|
||||
* @default 1024 * 1024 * 2 // 2MB
|
||||
* @example maxSize={1024 * 1024 * 2} // 2MB
|
||||
*/
|
||||
maxSize?: DropzoneProps['maxSize'];
|
||||
|
||||
/**
|
||||
* Maximum number of files for the uploader.
|
||||
* @type number | undefined
|
||||
* @default 1
|
||||
* @example maxFileCount={4}
|
||||
*/
|
||||
maxFileCount?: DropzoneProps['maxFiles'];
|
||||
|
||||
/**
|
||||
* Whether the uploader should accept multiple files.
|
||||
* @type boolean
|
||||
* @default false
|
||||
* @example multiple
|
||||
*/
|
||||
multiple?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the uploader is disabled.
|
||||
* @type boolean
|
||||
* @default false
|
||||
* @example disabled
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function FileUploader(props: FileUploaderProps) {
|
||||
const {
|
||||
value: valueProp,
|
||||
onValueChange,
|
||||
onUpload,
|
||||
progresses,
|
||||
accept = {
|
||||
'image/*': [],
|
||||
},
|
||||
maxSize = 1024 * 1024 * 10000000,
|
||||
maxFileCount = 100000000000,
|
||||
multiple = false,
|
||||
disabled = false,
|
||||
className,
|
||||
description,
|
||||
...dropzoneProps
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
const [files, setFiles] = useControllableState({
|
||||
prop: valueProp,
|
||||
onChange: onValueChange,
|
||||
});
|
||||
|
||||
const onDrop = React.useCallback(
|
||||
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
|
||||
if (!multiple && maxFileCount === 1 && acceptedFiles.length > 1) {
|
||||
toast.error('Cannot upload more than 1 file at a time');
|
||||
return;
|
||||
}
|
||||
|
||||
if ((files?.length ?? 0) + acceptedFiles.length > maxFileCount) {
|
||||
toast.error(`Cannot upload more than ${maxFileCount} files`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newFiles = acceptedFiles.map((file) =>
|
||||
Object.assign(file, {
|
||||
preview: URL.createObjectURL(file),
|
||||
}),
|
||||
);
|
||||
|
||||
const updatedFiles = files ? [...files, ...newFiles] : newFiles;
|
||||
|
||||
setFiles(updatedFiles);
|
||||
|
||||
if (rejectedFiles.length > 0) {
|
||||
rejectedFiles.forEach(({ file }) => {
|
||||
toast.error(`File ${file.name} was rejected`);
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
onUpload &&
|
||||
updatedFiles.length > 0 &&
|
||||
updatedFiles.length <= maxFileCount
|
||||
) {
|
||||
const target =
|
||||
updatedFiles.length > 0 ? `${updatedFiles.length} files` : `file`;
|
||||
|
||||
toast.promise(onUpload(updatedFiles), {
|
||||
loading: `Uploading ${target}...`,
|
||||
success: () => {
|
||||
setFiles([]);
|
||||
return `${target} uploaded`;
|
||||
},
|
||||
error: `Failed to upload ${target}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
[files, maxFileCount, multiple, onUpload, setFiles],
|
||||
);
|
||||
|
||||
function onRemove(index: number) {
|
||||
if (!files) return;
|
||||
const newFiles = files.filter((_, i) => i !== index);
|
||||
setFiles(newFiles);
|
||||
onValueChange?.(newFiles);
|
||||
}
|
||||
|
||||
// Revoke preview url when component unmounts
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (!files) return;
|
||||
files.forEach((file) => {
|
||||
if (isFileWithPreview(file)) {
|
||||
URL.revokeObjectURL(file.preview);
|
||||
}
|
||||
});
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const isDisabled = disabled || (files?.length ?? 0) >= maxFileCount;
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col gap-6 overflow-hidden">
|
||||
<Dropzone
|
||||
onDrop={onDrop}
|
||||
accept={accept}
|
||||
maxSize={maxSize}
|
||||
maxFiles={maxFileCount}
|
||||
multiple={maxFileCount > 1 || multiple}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{({ getRootProps, getInputProps, isDragActive }) => (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={cn(
|
||||
'group relative grid h-72 w-full cursor-pointer place-items-center rounded-lg border-2 border-dashed border-muted-foreground/25 px-5 py-2.5 text-center transition hover:bg-muted/25',
|
||||
'ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
isDragActive && 'border-muted-foreground/50',
|
||||
isDisabled && 'pointer-events-none opacity-60',
|
||||
className,
|
||||
)}
|
||||
{...dropzoneProps}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive ? (
|
||||
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
|
||||
<div className="rounded-full border border-dashed p-3">
|
||||
<Upload
|
||||
className="size-7 text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<p className="font-medium text-muted-foreground">
|
||||
Drop the files here
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
|
||||
<div className="rounded-full border border-dashed p-3">
|
||||
<Upload
|
||||
className="size-7 text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-px">
|
||||
<p className="font-medium text-muted-foreground">
|
||||
{t('knowledgeDetails.uploadTitle')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground/70">
|
||||
{description || t('knowledgeDetails.uploadDescription')}
|
||||
{/* You can upload
|
||||
{maxFileCount > 1
|
||||
? ` ${maxFileCount === Infinity ? 'multiple' : maxFileCount}
|
||||
files (up to ${formatBytes(maxSize)} each)`
|
||||
: ` a file with ${formatBytes(maxSize)}`} */}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
{files?.length ? (
|
||||
<ScrollArea className="h-fit w-full px-3">
|
||||
<div className="flex max-h-48 flex-col gap-4">
|
||||
{files?.map((file, index) => (
|
||||
<FileCard
|
||||
key={index}
|
||||
file={file}
|
||||
onRemove={() => onRemove(index)}
|
||||
progress={progresses?.[file.name]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/* floating-chat-widget-markdown.less */
|
||||
|
||||
.widget-citation-popover {
|
||||
max-width: 90vw;
|
||||
/* Use viewport width for better responsiveness */
|
||||
width: max-content;
|
||||
|
||||
.ant-popover-inner {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ant-popover-inner-content {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive breakpoints for popover width */
|
||||
@media (min-width: 480px) {
|
||||
.widget-citation-popover {
|
||||
max-width: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-citation-content {
|
||||
|
||||
p,
|
||||
div,
|
||||
span,
|
||||
button {
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.floating-chat-widget {
|
||||
|
||||
/* General styles for markdown content within the widget */
|
||||
p,
|
||||
div,
|
||||
ul,
|
||||
ol,
|
||||
blockquote {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Enhanced image styles */
|
||||
img,
|
||||
.ant-image,
|
||||
.ant-image-img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
border-radius: 8px;
|
||||
margin: 8px 0 !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
317
ragflow_web/src/components/floating-chat-widget-markdown.tsx
Normal file
317
ragflow_web/src/components/floating-chat-widget-markdown.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
import Image from '@/components/image';
|
||||
import SvgIcon from '@/components/svg-icon';
|
||||
import {
|
||||
useFetchDocumentThumbnailsByIds,
|
||||
useGetDocumentUrl,
|
||||
} from '@/hooks/document-hooks';
|
||||
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
|
||||
import {
|
||||
preprocessLaTeX,
|
||||
replaceThinkToSection,
|
||||
showImage,
|
||||
} from '@/utils/chat';
|
||||
import { getExtension } from '@/utils/document-util';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Flex, Popover, Tooltip } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import DOMPurify from 'dompurify';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import { omit } from 'lodash';
|
||||
import { pipe } from 'lodash/fp';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Markdown from 'react-markdown';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import {
|
||||
oneDark,
|
||||
oneLight,
|
||||
} from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import { visitParents } from 'unist-util-visit-parents';
|
||||
import { currentReg, replaceTextByOldReg } from '../pages/next-chats/utils';
|
||||
import styles from './floating-chat-widget-markdown.less';
|
||||
import { useIsDarkTheme } from './theme-provider';
|
||||
|
||||
const getChunkIndex = (match: string) => Number(match.replace(/\[|\]/g, ''));
|
||||
|
||||
const FloatingChatWidgetMarkdown = ({
|
||||
reference,
|
||||
clickDocumentButton,
|
||||
content,
|
||||
}: {
|
||||
content: string;
|
||||
loading: boolean;
|
||||
reference: IReference;
|
||||
clickDocumentButton?: (documentId: string, chunk: IReferenceChunk) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { setDocumentIds, data: fileThumbnails } =
|
||||
useFetchDocumentThumbnailsByIds();
|
||||
const getDocumentUrl = useGetDocumentUrl();
|
||||
const isDarkTheme = useIsDarkTheme();
|
||||
|
||||
const contentWithCursor = useMemo(() => {
|
||||
let text = content === '' ? t('chat.searching') : content;
|
||||
const nextText = replaceTextByOldReg(text);
|
||||
return pipe(replaceThinkToSection, preprocessLaTeX)(nextText);
|
||||
}, [content, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const docAggs = reference?.doc_aggs;
|
||||
const docList = Array.isArray(docAggs)
|
||||
? docAggs
|
||||
: Object.values(docAggs ?? {});
|
||||
setDocumentIds(docList.map((x: any) => x.doc_id).filter(Boolean));
|
||||
}, [reference, setDocumentIds]);
|
||||
|
||||
const handleDocumentButtonClick = useCallback(
|
||||
(
|
||||
documentId: string,
|
||||
chunk: IReferenceChunk,
|
||||
isPdf: boolean,
|
||||
documentUrl?: string,
|
||||
) =>
|
||||
() => {
|
||||
if (!documentId) return;
|
||||
if (!isPdf && documentUrl) {
|
||||
window.open(documentUrl, '_blank');
|
||||
} else if (clickDocumentButton) {
|
||||
clickDocumentButton(documentId, chunk);
|
||||
}
|
||||
},
|
||||
[clickDocumentButton],
|
||||
);
|
||||
|
||||
const rehypeWrapReference = () => (tree: any) => {
|
||||
visitParents(tree, 'text', (node, ancestors) => {
|
||||
const latestAncestor = ancestors[ancestors.length - 1];
|
||||
if (
|
||||
latestAncestor.tagName !== 'custom-typography' &&
|
||||
latestAncestor.tagName !== 'code'
|
||||
) {
|
||||
node.type = 'element';
|
||||
node.tagName = 'custom-typography';
|
||||
node.properties = {};
|
||||
node.children = [{ type: 'text', value: node.value }];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getReferenceInfo = useCallback(
|
||||
(chunkIndex: number) => {
|
||||
const chunkItem = reference?.chunks?.[chunkIndex];
|
||||
if (!chunkItem) return null;
|
||||
const docAggsArray = Array.isArray(reference?.doc_aggs)
|
||||
? reference.doc_aggs
|
||||
: Object.values(reference?.doc_aggs ?? {});
|
||||
const document = docAggsArray.find(
|
||||
(x: any) => x?.doc_id === chunkItem?.document_id,
|
||||
) as any;
|
||||
const documentId = document?.doc_id;
|
||||
const documentUrl =
|
||||
document?.url ?? (documentId ? getDocumentUrl(documentId) : undefined);
|
||||
const fileThumbnail = documentId ? fileThumbnails[documentId] : '';
|
||||
const fileExtension = documentId
|
||||
? getExtension(document?.doc_name ?? '')
|
||||
: '';
|
||||
return {
|
||||
documentUrl,
|
||||
fileThumbnail,
|
||||
fileExtension,
|
||||
imageId: chunkItem.image_id,
|
||||
chunkItem,
|
||||
documentId,
|
||||
document,
|
||||
};
|
||||
},
|
||||
[fileThumbnails, reference, getDocumentUrl],
|
||||
);
|
||||
|
||||
const getPopoverContent = useCallback(
|
||||
(chunkIndex: number) => {
|
||||
const info = getReferenceInfo(chunkIndex);
|
||||
|
||||
if (!info) {
|
||||
return (
|
||||
<div className="p-2 text-xs text-red-500">
|
||||
Error: Missing document information.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
documentUrl,
|
||||
fileThumbnail,
|
||||
fileExtension,
|
||||
imageId,
|
||||
chunkItem,
|
||||
documentId,
|
||||
document,
|
||||
} = info;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`popover-content-${chunkItem.id}`}
|
||||
className="flex gap-2 widget-citation-content"
|
||||
>
|
||||
{imageId && (
|
||||
<Popover
|
||||
placement="left"
|
||||
content={
|
||||
<Image
|
||||
id={imageId}
|
||||
className="max-w-[80vw] max-h-[60vh] rounded"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Image
|
||||
id={imageId}
|
||||
className="w-24 h-24 object-contain rounded m-1 cursor-pointer"
|
||||
/>
|
||||
</Popover>
|
||||
)}
|
||||
<div className="space-y-2 flex-1 min-w-0">
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(chunkItem?.content ?? ''),
|
||||
}}
|
||||
className="max-h-[250px] overflow-y-auto text-xs leading-relaxed p-2 bg-gray-50 dark:bg-gray-800 rounded prose-sm"
|
||||
></div>
|
||||
{documentId && (
|
||||
<Flex gap={'small'} align="center">
|
||||
{fileThumbnail ? (
|
||||
<img
|
||||
src={fileThumbnail}
|
||||
alt={document?.doc_name}
|
||||
className="w-6 h-6 rounded"
|
||||
/>
|
||||
) : (
|
||||
<SvgIcon name={`file-icon/${fileExtension}`} width={20} />
|
||||
)}
|
||||
<Tooltip
|
||||
title={
|
||||
!documentUrl && fileExtension !== 'pdf'
|
||||
? 'Document link unavailable'
|
||||
: document.doc_name
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
className="p-0 text-xs break-words h-auto text-left flex-1"
|
||||
onClick={handleDocumentButtonClick(
|
||||
documentId,
|
||||
chunkItem,
|
||||
fileExtension === 'pdf',
|
||||
documentUrl,
|
||||
)}
|
||||
disabled={!documentUrl && fileExtension !== 'pdf'}
|
||||
style={{ whiteSpace: 'normal' }}
|
||||
>
|
||||
<span className="truncate">
|
||||
{document?.doc_name ?? 'Unnamed Document'}
|
||||
</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[getReferenceInfo, handleDocumentButtonClick],
|
||||
);
|
||||
|
||||
const renderReference = useCallback(
|
||||
(text: string) => {
|
||||
return reactStringReplace(text, currentReg, (match, i) => {
|
||||
const chunkIndex = getChunkIndex(match);
|
||||
const info = getReferenceInfo(chunkIndex);
|
||||
|
||||
if (!info) {
|
||||
return (
|
||||
<Tooltip key={`err-tooltip-${i}`} title="Reference unavailable">
|
||||
<InfoCircleOutlined className={styles.referenceIcon} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const { imageId, chunkItem, documentId, fileExtension, documentUrl } =
|
||||
info;
|
||||
|
||||
if (showImage(chunkItem?.doc_type)) {
|
||||
return (
|
||||
<Image
|
||||
key={`img-${i}`}
|
||||
id={imageId}
|
||||
className="block object-contain max-w-full max-h-48 rounded my-2 cursor-pointer"
|
||||
onClick={handleDocumentButtonClick(
|
||||
documentId,
|
||||
chunkItem,
|
||||
fileExtension === 'pdf',
|
||||
documentUrl,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover content={getPopoverContent(chunkIndex)} key={`popover-${i}`}>
|
||||
<InfoCircleOutlined className={styles.referenceIcon} />
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
},
|
||||
[getPopoverContent, getReferenceInfo, handleDocumentButtonClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="floating-chat-widget">
|
||||
<Markdown
|
||||
rehypePlugins={[rehypeWrapReference, rehypeKatex, rehypeRaw]}
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
className="text-sm leading-relaxed space-y-2 prose-sm max-w-full"
|
||||
components={
|
||||
{
|
||||
'custom-typography': ({ children }: { children: string }) =>
|
||||
renderReference(children),
|
||||
code(props: any) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { children, className, node, ...rest } = props;
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return match ? (
|
||||
<SyntaxHighlighter
|
||||
{...omit(rest, 'inline')}
|
||||
PreTag="div"
|
||||
language={match[1]}
|
||||
style={isDarkTheme ? oneDark : oneLight}
|
||||
wrapLongLines
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code
|
||||
{...rest}
|
||||
className={classNames(
|
||||
className,
|
||||
'text-wrap text-xs bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
} as any
|
||||
}
|
||||
>
|
||||
{contentWithCursor}
|
||||
</Markdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FloatingChatWidgetMarkdown;
|
||||
708
ragflow_web/src/components/floating-chat-widget.tsx
Normal file
708
ragflow_web/src/components/floating-chat-widget.tsx
Normal file
@@ -0,0 +1,708 @@
|
||||
import PdfDrawer from '@/components/pdf-drawer';
|
||||
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { useFetchExternalChatInfo } from '@/hooks/use-chat-request';
|
||||
import i18n from '@/locales/config';
|
||||
import { MessageCircle, Minimize2, Send, X } from 'lucide-react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
useGetSharedChatSearchParams,
|
||||
useSendSharedMessage,
|
||||
} from '../pages/next-chats/hooks/use-send-shared-message';
|
||||
import FloatingChatWidgetMarkdown from './floating-chat-widget-markdown';
|
||||
|
||||
const FloatingChatWidget = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isMinimized, setIsMinimized] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [lastResponseId, setLastResponseId] = useState<string | null>(null);
|
||||
const [displayMessages, setDisplayMessages] = useState<any[]>([]);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { sharedId: conversationId, locale } = useGetSharedChatSearchParams();
|
||||
|
||||
// Check if we're in button-only mode or window-only mode
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const mode = urlParams.get('mode') || 'full'; // 'button', 'window', or 'full'
|
||||
const enableStreaming = urlParams.get('streaming') === 'true'; // Only enable if explicitly set to true
|
||||
|
||||
const {
|
||||
handlePressEnter,
|
||||
handleInputChange,
|
||||
value: hookValue,
|
||||
sendLoading,
|
||||
derivedMessages,
|
||||
hasError,
|
||||
} = useSendSharedMessage();
|
||||
|
||||
// Sync our local input with the hook's value when needed
|
||||
useEffect(() => {
|
||||
if (hookValue && hookValue !== inputValue) {
|
||||
setInputValue(hookValue);
|
||||
}
|
||||
}, [hookValue, inputValue]);
|
||||
|
||||
const { data: chatInfo } = useFetchExternalChatInfo();
|
||||
|
||||
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
|
||||
useClickDrawer();
|
||||
|
||||
// PDF drawer state tracking
|
||||
useEffect(() => {
|
||||
// Drawer state management
|
||||
}, [visible, documentId, selectedChunk]);
|
||||
|
||||
// Play sound when opening
|
||||
const playNotificationSound = useCallback(() => {
|
||||
try {
|
||||
const audioContext = new (window.AudioContext ||
|
||||
(window as any).webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.frequency.value = 800;
|
||||
oscillator.type = 'sine';
|
||||
|
||||
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(
|
||||
0.01,
|
||||
audioContext.currentTime + 0.3,
|
||||
);
|
||||
|
||||
oscillator.start(audioContext.currentTime);
|
||||
oscillator.stop(audioContext.currentTime + 0.3);
|
||||
} catch (error) {
|
||||
// Silent fail if audio not supported
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Play sound for AI responses (Intercom-style)
|
||||
const playResponseSound = useCallback(() => {
|
||||
try {
|
||||
const audioContext = new (window.AudioContext ||
|
||||
(window as any).webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.frequency.value = 600;
|
||||
oscillator.type = 'sine';
|
||||
|
||||
gainNode.gain.setValueAtTime(0.2, audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(
|
||||
0.01,
|
||||
audioContext.currentTime + 0.2,
|
||||
);
|
||||
|
||||
oscillator.start(audioContext.currentTime);
|
||||
oscillator.stop(audioContext.currentTime + 0.2);
|
||||
} catch (error) {
|
||||
// Silent fail if audio not supported
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Set loaded state and locale
|
||||
useEffect(() => {
|
||||
// Set component as loaded after a brief moment to prevent flash
|
||||
const timer = setTimeout(() => {
|
||||
setIsLoaded(true);
|
||||
// Tell parent window that we're ready to be shown
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'WIDGET_READY',
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}, 50);
|
||||
|
||||
if (locale && i18n.language !== locale) {
|
||||
i18n.changeLanguage(locale);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [locale]);
|
||||
|
||||
// Handle message display based on streaming preference
|
||||
useEffect(() => {
|
||||
if (!derivedMessages) {
|
||||
setDisplayMessages([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (enableStreaming) {
|
||||
// Show messages as they stream
|
||||
setDisplayMessages(derivedMessages);
|
||||
} else {
|
||||
// Only show complete messages (non-streaming mode)
|
||||
const completeMessages = derivedMessages.filter((msg, index) => {
|
||||
// Always show user messages immediately
|
||||
if (msg.role === MessageType.User) return true;
|
||||
|
||||
// For AI messages, only show when response is complete (not loading)
|
||||
if (msg.role === MessageType.Assistant) {
|
||||
return !sendLoading || index < derivedMessages.length - 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
setDisplayMessages(completeMessages);
|
||||
}
|
||||
}, [derivedMessages, enableStreaming, sendLoading]);
|
||||
|
||||
// Auto-scroll to bottom when display messages change
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [displayMessages]);
|
||||
|
||||
// Render different content based on mode
|
||||
// Master mode - handles everything and creates second iframe dynamically
|
||||
useEffect(() => {
|
||||
if (mode !== 'master') return;
|
||||
// Create the chat window iframe dynamically when needed
|
||||
const createChatWindow = () => {
|
||||
// Check if iframe already exists in parent document
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'CREATE_CHAT_WINDOW',
|
||||
src: window.location.href.replace('mode=master', 'mode=window'),
|
||||
},
|
||||
'*',
|
||||
);
|
||||
};
|
||||
|
||||
createChatWindow();
|
||||
|
||||
// Listen for our own toggle events to show/hide the dynamic iframe
|
||||
const handleToggle = (e: MessageEvent) => {
|
||||
if (e.source === window) return; // Ignore our own messages
|
||||
|
||||
const chatWindow = document.getElementById(
|
||||
'dynamic-chat-window',
|
||||
) as HTMLIFrameElement;
|
||||
if (chatWindow && e.data.type === 'TOGGLE_CHAT') {
|
||||
chatWindow.style.display = e.data.isOpen ? 'block' : 'none';
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleToggle);
|
||||
return () => window.removeEventListener('message', handleToggle);
|
||||
}, [mode]);
|
||||
|
||||
// Play sound only when AI response is complete (not streaming chunks)
|
||||
useEffect(() => {
|
||||
if (derivedMessages && derivedMessages.length > 0 && !sendLoading) {
|
||||
const lastMessage = derivedMessages[derivedMessages.length - 1];
|
||||
if (
|
||||
lastMessage.role === MessageType.Assistant &&
|
||||
lastMessage.id !== lastResponseId &&
|
||||
derivedMessages.length > 1
|
||||
) {
|
||||
setLastResponseId(lastMessage.id || '');
|
||||
playResponseSound();
|
||||
}
|
||||
}
|
||||
}, [derivedMessages, sendLoading, lastResponseId, playResponseSound]);
|
||||
|
||||
const toggleChat = useCallback(() => {
|
||||
if (mode === 'button') {
|
||||
// In button mode, communicate with parent window to show/hide chat window
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'TOGGLE_CHAT',
|
||||
isOpen: !isOpen,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
setIsOpen(!isOpen);
|
||||
if (!isOpen) {
|
||||
playNotificationSound();
|
||||
}
|
||||
} else {
|
||||
// In full mode, handle locally
|
||||
if (!isOpen) {
|
||||
setIsOpen(true);
|
||||
setIsMinimized(false);
|
||||
playNotificationSound();
|
||||
} else {
|
||||
setIsOpen(false);
|
||||
setIsMinimized(false);
|
||||
}
|
||||
}
|
||||
}, [isOpen, mode, playNotificationSound]);
|
||||
|
||||
const minimizeChat = useCallback(() => {
|
||||
setIsMinimized(true);
|
||||
}, []);
|
||||
|
||||
const handleSendMessage = useCallback(() => {
|
||||
if (!inputValue.trim() || sendLoading) return;
|
||||
|
||||
// Update the hook's internal state first
|
||||
const syntheticEvent = {
|
||||
target: { value: inputValue },
|
||||
currentTarget: { value: inputValue },
|
||||
preventDefault: () => {},
|
||||
} as any;
|
||||
|
||||
handleInputChange(syntheticEvent);
|
||||
|
||||
// Wait for state to update, then send
|
||||
setTimeout(() => {
|
||||
handlePressEnter([]);
|
||||
// Clear our local input after sending
|
||||
setInputValue('');
|
||||
}, 50);
|
||||
}, [inputValue, sendLoading, handleInputChange, handlePressEnter]);
|
||||
|
||||
const handleKeyPress = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
},
|
||||
[handleSendMessage],
|
||||
);
|
||||
|
||||
if (!conversationId) {
|
||||
return (
|
||||
<div className="fixed bottom-5 right-5 z-50">
|
||||
<div className="bg-red-500 text-white p-4 rounded-lg shadow-lg">
|
||||
Error: No conversation ID provided
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Remove the blocking return - we'll handle visibility with CSS instead
|
||||
|
||||
const messageCount = displayMessages?.length || 0;
|
||||
|
||||
// Show just the button in master mode
|
||||
if (mode === 'master') {
|
||||
return (
|
||||
<div
|
||||
className={`fixed bottom-6 right-6 z-50 transition-opacity duration-300 ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newIsOpen = !isOpen;
|
||||
setIsOpen(newIsOpen);
|
||||
if (newIsOpen) playNotificationSound();
|
||||
|
||||
// Tell the parent to show/hide the dynamic iframe
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'TOGGLE_CHAT',
|
||||
isOpen: newIsOpen,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}}
|
||||
className={`w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${
|
||||
isOpen ? 'scale-95' : 'scale-100 hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`transition-transform duration-300 ${isOpen ? 'rotate-45' : 'rotate-0'}`}
|
||||
>
|
||||
{isOpen ? <X size={24} /> : <MessageCircle size={24} />}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Unread Badge */}
|
||||
{!isOpen && messageCount > 0 && (
|
||||
<div className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white text-xs font-bold rounded-full flex items-center justify-center animate-pulse">
|
||||
{messageCount > 9 ? '9+' : messageCount}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === 'button') {
|
||||
// Only render the floating button
|
||||
return (
|
||||
<div
|
||||
className={`fixed bottom-6 right-6 z-50 transition-opacity duration-300 ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleChat}
|
||||
className={`w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${
|
||||
isOpen ? 'scale-95' : 'scale-100 hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`transition-transform duration-300 ${isOpen ? 'rotate-45' : 'rotate-0'}`}
|
||||
>
|
||||
{isOpen ? <X size={24} /> : <MessageCircle size={24} />}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Unread Badge */}
|
||||
{!isOpen && messageCount > 0 && (
|
||||
<div className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white text-xs font-bold rounded-full flex items-center justify-center animate-pulse">
|
||||
{messageCount > 9 ? '9+' : messageCount}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === 'window') {
|
||||
// Only render the chat window (always open)
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`fixed top-0 left-0 z-50 bg-blue-600 rounded-2xl transition-all duration-300 ease-out h-[500px] w-[380px] overflow-hidden ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-t-2xl">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center">
|
||||
<MessageCircle size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm">
|
||||
{chatInfo?.title || 'Chat Support'}
|
||||
</h3>
|
||||
<p className="text-xs text-blue-100">
|
||||
We typically reply instantly
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages and Input */}
|
||||
<div
|
||||
className="flex flex-col h-[436px] bg-white"
|
||||
style={{ borderRadius: '0 0 16px 16px' }}
|
||||
>
|
||||
<div
|
||||
className="flex-1 overflow-y-auto p-4 space-y-4"
|
||||
onWheel={(e) => {
|
||||
const element = e.currentTarget;
|
||||
const isAtTop = element.scrollTop === 0;
|
||||
const isAtBottom =
|
||||
element.scrollTop + element.clientHeight >=
|
||||
element.scrollHeight - 1;
|
||||
|
||||
// Allow scroll to pass through to parent when at boundaries
|
||||
if ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0)) {
|
||||
e.preventDefault();
|
||||
// Let the parent handle the scroll
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'SCROLL_PASSTHROUGH',
|
||||
deltaY: e.deltaY,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{displayMessages?.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex ${message.role === MessageType.User ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[280px] px-4 py-2 rounded-2xl ${
|
||||
message.role === MessageType.User
|
||||
? 'bg-blue-600 text-white rounded-br-md'
|
||||
: 'bg-gray-100 text-gray-800 rounded-bl-md'
|
||||
}`}
|
||||
>
|
||||
{message.role === MessageType.User ? (
|
||||
<p className="text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{message.content}
|
||||
</p>
|
||||
) : (
|
||||
<FloatingChatWidgetMarkdown
|
||||
loading={false}
|
||||
content={message.content}
|
||||
reference={
|
||||
message.reference || {
|
||||
doc_aggs: [],
|
||||
chunks: [],
|
||||
total: 0,
|
||||
}
|
||||
}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Clean Typing Indicator */}
|
||||
{sendLoading && !enableStreaming && (
|
||||
<div className="flex justify-start pl-4">
|
||||
<div className="flex space-x-1">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div>
|
||||
<div
|
||||
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"
|
||||
style={{ animationDelay: '0.1s' }}
|
||||
></div>
|
||||
<div
|
||||
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"
|
||||
style={{ animationDelay: '0.2s' }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="border-t border-gray-200 p-4">
|
||||
<div className="flex items-end space-x-3">
|
||||
<div className="flex-1">
|
||||
<textarea
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
setInputValue(newValue);
|
||||
handleInputChange(e);
|
||||
}}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Type your message..."
|
||||
rows={1}
|
||||
className="w-full resize-none border border-gray-300 rounded-2xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
|
||||
style={{ minHeight: '44px', maxHeight: '120px' }}
|
||||
disabled={hasError || sendLoading}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSendMessage}
|
||||
disabled={!inputValue.trim() || sendLoading}
|
||||
className="p-3 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Send size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PdfDrawer
|
||||
visible={visible}
|
||||
hideModal={hideModal}
|
||||
documentId={documentId}
|
||||
chunk={selectedChunk}
|
||||
width={'100vw'}
|
||||
height={'100vh'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} // Full mode - render everything together (original behavior)
|
||||
return (
|
||||
<div
|
||||
className={`transition-opacity duration-300 ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
>
|
||||
{/* Chat Widget Container */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className={`fixed bottom-24 right-6 z-50 bg-blue-600 rounded-2xl transition-all duration-300 ease-out ${
|
||||
isMinimized ? 'h-16' : 'h-[500px]'
|
||||
} w-[380px] overflow-hidden`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-t-2xl">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center">
|
||||
<MessageCircle size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm">
|
||||
{chatInfo?.title || 'Chat Support'}
|
||||
</h3>
|
||||
<p className="text-xs text-blue-100">
|
||||
We typically reply instantly
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={minimizeChat}
|
||||
className="p-1.5 hover:bg-white hover:bg-opacity-20 rounded-full transition-colors"
|
||||
>
|
||||
<Minimize2 size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleChat}
|
||||
className="p-1.5 hover:bg-white hover:bg-opacity-20 rounded-full transition-colors"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages Container */}
|
||||
{!isMinimized && (
|
||||
<div
|
||||
className="flex flex-col h-[436px] bg-white"
|
||||
style={{ borderRadius: '0 0 16px 16px' }}
|
||||
>
|
||||
<div
|
||||
className="flex-1 overflow-y-auto p-4 space-y-4"
|
||||
onWheel={(e) => {
|
||||
const element = e.currentTarget;
|
||||
const isAtTop = element.scrollTop === 0;
|
||||
const isAtBottom =
|
||||
element.scrollTop + element.clientHeight >=
|
||||
element.scrollHeight - 1;
|
||||
|
||||
// Allow scroll to pass through to parent when at boundaries
|
||||
if (
|
||||
(isAtTop && e.deltaY < 0) ||
|
||||
(isAtBottom && e.deltaY > 0)
|
||||
) {
|
||||
e.preventDefault();
|
||||
// Let the parent handle the scroll
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'SCROLL_PASSTHROUGH',
|
||||
deltaY: e.deltaY,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{displayMessages?.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex ${message.role === MessageType.User ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[280px] px-4 py-2 rounded-2xl ${
|
||||
message.role === MessageType.User
|
||||
? 'bg-blue-600 text-white rounded-br-md'
|
||||
: 'bg-gray-100 text-gray-800 rounded-bl-md'
|
||||
}`}
|
||||
>
|
||||
{message.role === MessageType.User ? (
|
||||
<p className="text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{message.content}
|
||||
</p>
|
||||
) : (
|
||||
<FloatingChatWidgetMarkdown
|
||||
loading={false}
|
||||
content={message.content}
|
||||
reference={
|
||||
message.reference || {
|
||||
doc_aggs: [],
|
||||
chunks: [],
|
||||
total: 0,
|
||||
}
|
||||
}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Typing Indicator */}
|
||||
{sendLoading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-gray-100 rounded-2xl rounded-bl-md px-4 py-3">
|
||||
<div className="flex space-x-1">
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||
<div
|
||||
className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
|
||||
style={{ animationDelay: '0.1s' }}
|
||||
></div>
|
||||
<div
|
||||
className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
|
||||
style={{ animationDelay: '0.2s' }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="border-t border-gray-200 p-4">
|
||||
<div className="flex items-end space-x-3">
|
||||
<div className="flex-1">
|
||||
<textarea
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
setInputValue(newValue);
|
||||
// Also update the hook's state
|
||||
handleInputChange(e);
|
||||
}}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Type your message..."
|
||||
rows={1}
|
||||
className="w-full resize-none border border-gray-300 rounded-2xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
|
||||
style={{ minHeight: '44px', maxHeight: '120px' }}
|
||||
disabled={hasError || sendLoading}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSendMessage}
|
||||
disabled={!inputValue.trim() || sendLoading}
|
||||
className="p-3 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Send size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Floating Button */}
|
||||
<div className="fixed bottom-6 right-6 z-50">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleChat}
|
||||
className={`w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${
|
||||
isOpen ? 'scale-95' : 'scale-100 hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`transition-transform duration-300 ${isOpen ? 'rotate-45' : 'rotate-0'}`}
|
||||
>
|
||||
{isOpen ? <X size={24} /> : <MessageCircle size={24} />}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Unread Badge */}
|
||||
{!isOpen && messageCount > 0 && (
|
||||
<div className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white text-xs font-bold rounded-full flex items-center justify-center animate-pulse">
|
||||
{messageCount > 9 ? '9+' : messageCount}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<PdfDrawer
|
||||
visible={visible}
|
||||
hideModal={hideModal}
|
||||
documentId={documentId}
|
||||
chunk={selectedChunk}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FloatingChatWidget;
|
||||
21
ragflow_web/src/components/form-container.tsx
Normal file
21
ragflow_web/src/components/form-container.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
export type FormContainerProps = {
|
||||
className?: string;
|
||||
show?: boolean;
|
||||
} & PropsWithChildren;
|
||||
|
||||
export function FormContainer({
|
||||
children,
|
||||
show = true,
|
||||
className,
|
||||
}: FormContainerProps) {
|
||||
return show ? (
|
||||
<section className={cn('border rounded-lg p-5 space-y-5', className)}>
|
||||
{children}
|
||||
</section>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
}
|
||||
19
ragflow_web/src/components/highlight-markdown/index.less
Normal file
19
ragflow_web/src/components/highlight-markdown/index.less
Normal file
@@ -0,0 +1,19 @@
|
||||
.text {
|
||||
.chunkText;
|
||||
font-size: 16px;
|
||||
li {
|
||||
padding: 4px 0px;
|
||||
}
|
||||
// p {
|
||||
// white-space: pre-wrap; // https://stackoverflow.com/questions/60332183/new-line-with-react-markdown
|
||||
// }
|
||||
}
|
||||
|
||||
.code {
|
||||
padding: 3px 6px 6px;
|
||||
margin: 0;
|
||||
white-space: break-spaces;
|
||||
background-color: rgba(129, 139, 152, 0.12);
|
||||
border-radius: 4px;
|
||||
color: var(--ant-color-text-base);
|
||||
}
|
||||
59
ragflow_web/src/components/highlight-markdown/index.tsx
Normal file
59
ragflow_web/src/components/highlight-markdown/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import classNames from 'classnames';
|
||||
import Markdown from 'react-markdown';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import {
|
||||
oneDark,
|
||||
oneLight,
|
||||
} from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
|
||||
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
|
||||
|
||||
import { preprocessLaTeX } from '@/utils/chat';
|
||||
import { useIsDarkTheme } from '../theme-provider';
|
||||
import styles from './index.less';
|
||||
|
||||
const HightLightMarkdown = ({
|
||||
children,
|
||||
}: {
|
||||
children: string | null | undefined;
|
||||
}) => {
|
||||
const isDarkTheme = useIsDarkTheme();
|
||||
|
||||
return (
|
||||
<Markdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeRaw, rehypeKatex]}
|
||||
className={classNames(styles.text)}
|
||||
components={
|
||||
{
|
||||
code(props: any) {
|
||||
const { children, className, ...rest } = props;
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return match ? (
|
||||
<SyntaxHighlighter
|
||||
{...rest}
|
||||
PreTag="div"
|
||||
language={match[1]}
|
||||
style={isDarkTheme ? oneDark : oneLight}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code {...rest} className={`${className} ${styles.code}`}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
} as any
|
||||
}
|
||||
>
|
||||
{children ? preprocessLaTeX(children) : children}
|
||||
</Markdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default HightLightMarkdown;
|
||||
66
ragflow_web/src/components/home-card.tsx
Normal file
66
ragflow_web/src/components/home-card.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface IProps {
|
||||
data: {
|
||||
name: string;
|
||||
description?: string;
|
||||
avatar?: string;
|
||||
update_time?: string | number;
|
||||
};
|
||||
onClick?: () => void;
|
||||
moreDropdown: React.ReactNode;
|
||||
sharedBadge?: ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
export function HomeCard({
|
||||
data,
|
||||
onClick,
|
||||
moreDropdown,
|
||||
sharedBadge,
|
||||
icon,
|
||||
}: IProps) {
|
||||
return (
|
||||
<Card
|
||||
onClick={() => {
|
||||
// navigateToSearch(data?.id);
|
||||
onClick?.();
|
||||
}}
|
||||
>
|
||||
<CardContent className="p-4 flex gap-2 items-start group h-full">
|
||||
<div className="flex justify-between mb-4">
|
||||
<RAGFlowAvatar
|
||||
className="w-[32px] h-[32px]"
|
||||
avatar={data.avatar}
|
||||
name={data.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between gap-1 flex-1 h-full w-[calc(100%-50px)]">
|
||||
<section className="flex justify-between">
|
||||
<section className="flex flex-1 min-w-0 gap-1 items-center">
|
||||
<div className="text-base font-bold leading-snug truncate">
|
||||
{data.name}
|
||||
</div>
|
||||
{icon}
|
||||
</section>
|
||||
{moreDropdown}
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-1 mt-1">
|
||||
<div className="whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{data.description}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-sm opacity-80 whitespace-nowrap">
|
||||
{formatDate(data.update_time)}
|
||||
</p>
|
||||
{sharedBadge}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
21
ragflow_web/src/components/hooks/use-mobile.tsx
Normal file
21
ragflow_web/src/components/hooks/use-mobile.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from 'react';
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener('change', onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener('change', onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
||||
194
ragflow_web/src/components/hooks/use-toast.tsx
Normal file
194
ragflow_web/src/components/hooks/use-toast.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
'use client';
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from 'react';
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from '@/registry/default/ui/toast';
|
||||
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: 'ADD_TOAST',
|
||||
UPDATE_TOAST: 'UPDATE_TOAST',
|
||||
DISMISS_TOAST: 'DISMISS_TOAST',
|
||||
REMOVE_TOAST: 'REMOVE_TOAST',
|
||||
} as const;
|
||||
|
||||
let count = 0;
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType['ADD_TOAST'];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType['UPDATE_TOAST'];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType['DISMISS_TOAST'];
|
||||
toastId?: ToasterToast['id'];
|
||||
}
|
||||
| {
|
||||
type: ActionType['REMOVE_TOAST'];
|
||||
toastId?: ToasterToast['id'];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: 'REMOVE_TOAST',
|
||||
toastId: toastId,
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'ADD_TOAST':
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
|
||||
case 'UPDATE_TOAST':
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||
),
|
||||
};
|
||||
|
||||
case 'DISMISS_TOAST': {
|
||||
const { toastId } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t,
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'REMOVE_TOAST':
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState);
|
||||
});
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, 'id'>;
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: 'UPDATE_TOAST',
|
||||
toast: { ...props, id },
|
||||
});
|
||||
const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });
|
||||
|
||||
dispatch({
|
||||
type: 'ADD_TOAST',
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
|
||||
};
|
||||
}
|
||||
|
||||
export { toast, useToast };
|
||||
47
ragflow_web/src/components/icon-font.tsx
Normal file
47
ragflow_web/src/components/icon-font.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { FileIconMap } from '@/constants/file';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getExtension } from '@/utils/document-util';
|
||||
|
||||
type IconFontType = {
|
||||
name: string;
|
||||
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const IconFont = ({ name, className }: IconFontType) => (
|
||||
<svg className={cn('size-4', className)}>
|
||||
<use xlinkHref={`#icon-${name}`} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export function IconFontFill({
|
||||
name,
|
||||
className,
|
||||
isFill = true,
|
||||
}: IconFontType & { isFill?: boolean }) {
|
||||
return (
|
||||
<span className={cn('size-4', className)}>
|
||||
<svg
|
||||
className={cn('size-4', className)}
|
||||
style={{ fill: isFill ? 'currentColor' : '' }}
|
||||
>
|
||||
<use xlinkHref={`#icon-${name}`} />
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function FileIcon({
|
||||
name,
|
||||
className,
|
||||
type,
|
||||
}: IconFontType & { type?: string }) {
|
||||
const isFolder = type === 'folder';
|
||||
return (
|
||||
<span className={cn('size-4', className)}>
|
||||
<IconFont
|
||||
name={isFolder ? 'file-sub' : FileIconMap[getExtension(name)]}
|
||||
></IconFont>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
35
ragflow_web/src/components/image/index.tsx
Normal file
35
ragflow_web/src/components/image/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { api_host } from '@/utils/api';
|
||||
import classNames from 'classnames';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||
|
||||
interface IImage {
|
||||
id: string;
|
||||
className: string;
|
||||
onClick?(): void;
|
||||
}
|
||||
|
||||
const Image = ({ id, className, ...props }: IImage) => {
|
||||
return (
|
||||
<img
|
||||
{...props}
|
||||
src={`${api_host}/document/image/${id}`}
|
||||
alt=""
|
||||
className={classNames('max-w-[45vw] max-h-[40wh] block', className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Image;
|
||||
|
||||
export const ImageWithPopover = ({ id }: { id: string }) => {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Image id={id} className="max-h-[100px] inline-block"></Image>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<Image id={id} className="max-w-[100px] object-contain"></Image>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
430
ragflow_web/src/components/indented-tree/indented-tree.tsx
Normal file
430
ragflow_web/src/components/indented-tree/indented-tree.tsx
Normal file
@@ -0,0 +1,430 @@
|
||||
import { Rect } from '@antv/g';
|
||||
import {
|
||||
Badge,
|
||||
BaseBehavior,
|
||||
BaseNode,
|
||||
CommonEvent,
|
||||
ExtensionCategory,
|
||||
Graph,
|
||||
NodeEvent,
|
||||
Point,
|
||||
Polyline,
|
||||
PolylineStyleProps,
|
||||
register,
|
||||
subStyleProps,
|
||||
treeToGraphData,
|
||||
} from '@antv/g6';
|
||||
import { TreeData } from '@antv/g6/lib/types';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
||||
import { useIsDarkTheme } from '../theme-provider';
|
||||
|
||||
const rootId = 'root';
|
||||
|
||||
const COLORS = [
|
||||
'#5B8FF9',
|
||||
'#F6BD16',
|
||||
'#5AD8A6',
|
||||
'#945FB9',
|
||||
'#E86452',
|
||||
'#6DC8EC',
|
||||
'#FF99C3',
|
||||
'#1E9493',
|
||||
'#FF9845',
|
||||
'#5D7092',
|
||||
];
|
||||
|
||||
const TreeEvent = {
|
||||
COLLAPSE_EXPAND: 'collapse-expand',
|
||||
WHEEL: 'canvas:wheel',
|
||||
};
|
||||
|
||||
class IndentedNode extends BaseNode {
|
||||
static defaultStyleProps = {
|
||||
ports: [
|
||||
{
|
||||
key: 'in',
|
||||
placement: 'right-bottom',
|
||||
},
|
||||
{
|
||||
key: 'out',
|
||||
placement: 'left-bottom',
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
constructor(options: any) {
|
||||
Object.assign(options.style, IndentedNode.defaultStyleProps);
|
||||
super(options);
|
||||
}
|
||||
|
||||
get childrenData() {
|
||||
return this.attributes.context?.model.getChildrenData(this.id);
|
||||
}
|
||||
|
||||
getKeyStyle(attributes: any) {
|
||||
const [width, height] = this.getSize(attributes);
|
||||
const keyStyle = super.getKeyStyle(attributes);
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
...keyStyle,
|
||||
fill: 'transparent',
|
||||
};
|
||||
}
|
||||
|
||||
drawKeyShape(attributes: any, container: any) {
|
||||
const keyStyle = this.getKeyStyle(attributes);
|
||||
return this.upsert('key', Rect, keyStyle, container);
|
||||
}
|
||||
|
||||
getLabelStyle(attributes: any) {
|
||||
if (attributes.label === false || !attributes.labelText) return false;
|
||||
return subStyleProps(this.getGraphicStyle(attributes), 'label') as any;
|
||||
}
|
||||
|
||||
drawIconArea(attributes: any, container: any) {
|
||||
const [, h] = this.getSize(attributes);
|
||||
const iconAreaStyle = {
|
||||
fill: 'transparent',
|
||||
height: 30,
|
||||
width: 12,
|
||||
x: -6,
|
||||
y: h,
|
||||
zIndex: -1,
|
||||
};
|
||||
this.upsert('icon-area', Rect, iconAreaStyle, container);
|
||||
}
|
||||
|
||||
forwardEvent(target: any, type: any, listener: any) {
|
||||
if (target && !Reflect.has(target, '__bind__')) {
|
||||
Reflect.set(target, '__bind__', true);
|
||||
target.addEventListener(type, listener);
|
||||
}
|
||||
}
|
||||
|
||||
getCountStyle(attributes: any) {
|
||||
const { collapsed, color } = attributes;
|
||||
if (collapsed) {
|
||||
const [, height] = this.getSize(attributes);
|
||||
return {
|
||||
backgroundFill: color,
|
||||
cursor: 'pointer',
|
||||
fill: '#fff',
|
||||
fontSize: 8,
|
||||
padding: [0, 10],
|
||||
text: `${this.childrenData?.length}`,
|
||||
textAlign: 'center',
|
||||
y: height + 8,
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
drawCountShape(attributes: any, container: any) {
|
||||
const countStyle = this.getCountStyle(attributes);
|
||||
const btn = this.upsert('count', Badge, countStyle as any, container);
|
||||
|
||||
this.forwardEvent(btn, CommonEvent.CLICK, (event: any) => {
|
||||
event.stopPropagation();
|
||||
attributes.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {
|
||||
id: this.id,
|
||||
collapsed: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
isShowCollapse(attributes: any) {
|
||||
return (
|
||||
!attributes.collapsed &&
|
||||
Array.isArray(this.childrenData) &&
|
||||
this.childrenData?.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
getCollapseStyle(attributes: any) {
|
||||
const { showIcon, color } = attributes;
|
||||
if (!this.isShowCollapse(attributes)) return false;
|
||||
const [, height] = this.getSize(attributes);
|
||||
return {
|
||||
visibility: showIcon ? 'visible' : 'hidden',
|
||||
backgroundFill: color,
|
||||
backgroundHeight: 12,
|
||||
backgroundWidth: 12,
|
||||
cursor: 'pointer',
|
||||
fill: '#fff',
|
||||
fontFamily: 'iconfont',
|
||||
fontSize: 8,
|
||||
text: '\ue6e4',
|
||||
textAlign: 'center',
|
||||
x: -1, // half of edge line width
|
||||
y: height + 8,
|
||||
};
|
||||
}
|
||||
|
||||
drawCollapseShape(attributes: any, container: any) {
|
||||
const iconStyle = this.getCollapseStyle(attributes);
|
||||
const btn = this.upsert(
|
||||
'collapse-expand',
|
||||
Badge,
|
||||
iconStyle as any,
|
||||
container,
|
||||
);
|
||||
|
||||
this.forwardEvent(btn, CommonEvent.CLICK, (event: any) => {
|
||||
event.stopPropagation();
|
||||
attributes.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {
|
||||
id: this.id,
|
||||
collapsed: !attributes.collapsed,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getAddStyle(attributes: any) {
|
||||
const { collapsed, showIcon } = attributes;
|
||||
if (collapsed) return false;
|
||||
const [, height] = this.getSize(attributes);
|
||||
const color = '#ddd';
|
||||
const lineWidth = 1;
|
||||
|
||||
return {
|
||||
visibility: showIcon ? 'visible' : 'hidden',
|
||||
backgroundFill: '#fff',
|
||||
backgroundHeight: 12,
|
||||
backgroundLineWidth: lineWidth,
|
||||
backgroundStroke: color,
|
||||
backgroundWidth: 12,
|
||||
cursor: 'pointer',
|
||||
fill: color,
|
||||
fontFamily: 'iconfont',
|
||||
text: '\ue664',
|
||||
textAlign: 'center',
|
||||
x: -1,
|
||||
y: height + (this.isShowCollapse(attributes) ? 22 : 8),
|
||||
};
|
||||
}
|
||||
|
||||
render(attributes = this.parsedAttributes, container = this) {
|
||||
super.render(attributes, container);
|
||||
|
||||
this.drawCountShape(attributes, container);
|
||||
|
||||
this.drawIconArea(attributes, container);
|
||||
this.drawCollapseShape(attributes, container);
|
||||
}
|
||||
}
|
||||
|
||||
class IndentedEdge extends Polyline {
|
||||
getControlPoints(
|
||||
attributes: Required<PolylineStyleProps>,
|
||||
sourcePoint: Point,
|
||||
targetPoint: Point,
|
||||
) {
|
||||
const [sx] = sourcePoint;
|
||||
const [, ty] = targetPoint;
|
||||
return [[sx, ty]] as any;
|
||||
}
|
||||
}
|
||||
|
||||
class CollapseExpandTree extends BaseBehavior {
|
||||
constructor(context: any, options: any) {
|
||||
super(context, options);
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
update(options: any) {
|
||||
this.unbindEvents();
|
||||
super.update(options);
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
const { graph } = this.context;
|
||||
|
||||
graph.on(NodeEvent.POINTER_ENTER, this.showIcon);
|
||||
graph.on(NodeEvent.POINTER_LEAVE, this.hideIcon);
|
||||
graph.on(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);
|
||||
}
|
||||
|
||||
unbindEvents() {
|
||||
const { graph } = this.context;
|
||||
|
||||
graph.off(NodeEvent.POINTER_ENTER, this.showIcon);
|
||||
graph.off(NodeEvent.POINTER_LEAVE, this.hideIcon);
|
||||
graph.off(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);
|
||||
}
|
||||
|
||||
status = 'idle';
|
||||
|
||||
showIcon = (event: any) => {
|
||||
this.setIcon(event, true);
|
||||
};
|
||||
|
||||
hideIcon = (event: any) => {
|
||||
this.setIcon(event, false);
|
||||
};
|
||||
|
||||
setIcon = (event: any, show: boolean) => {
|
||||
if (this.status !== 'idle') return;
|
||||
const { target } = event;
|
||||
const id = target.id;
|
||||
const { graph, element } = this.context;
|
||||
graph.updateNodeData([{ id, style: { showIcon: show } }]);
|
||||
element?.draw({ animation: false, silence: true });
|
||||
};
|
||||
|
||||
onCollapseExpand = async (event: any) => {
|
||||
this.status = 'busy';
|
||||
const { id, collapsed } = event;
|
||||
const { graph } = this.context;
|
||||
if (collapsed) await graph.collapseElement(id);
|
||||
else await graph.expandElement(id);
|
||||
this.status = 'idle';
|
||||
};
|
||||
}
|
||||
|
||||
register(ExtensionCategory.NODE, 'indented', IndentedNode);
|
||||
register(ExtensionCategory.EDGE, 'indented', IndentedEdge);
|
||||
register(
|
||||
ExtensionCategory.BEHAVIOR,
|
||||
'collapse-expand-tree',
|
||||
CollapseExpandTree,
|
||||
);
|
||||
|
||||
interface IProps {
|
||||
data: TreeData;
|
||||
show: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
function fallbackRender({ error }: FallbackProps) {
|
||||
// Call resetErrorBoundary() to reset the error boundary and retry the render.
|
||||
|
||||
return (
|
||||
<div role="alert">
|
||||
<p>Something went wrong:</p>
|
||||
<pre style={{ color: 'red' }}>{error.message}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const IndentedTree = ({ data, show, style = {} }: IProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const graphRef = useRef<Graph | null>(null);
|
||||
const assignIds = React.useCallback(function assignIds(
|
||||
node: TreeData,
|
||||
parentId: string = '',
|
||||
index = 0,
|
||||
) {
|
||||
if (!node.id) node.id = parentId ? `${parentId}-${index}` : 'root';
|
||||
if (node.children) {
|
||||
node.children.forEach((child, idx) => assignIds(child, node.id, idx));
|
||||
}
|
||||
}, []);
|
||||
const isDark = useIsDarkTheme();
|
||||
const render = useCallback(
|
||||
async (data: TreeData) => {
|
||||
const graph: Graph = new Graph({
|
||||
container: containerRef.current!,
|
||||
x: 60,
|
||||
node: {
|
||||
type: 'indented',
|
||||
style: {
|
||||
size: (d) => [d.id.length * 6 + 10, 20],
|
||||
labelBackground: (datum) => datum.id === rootId,
|
||||
labelBackgroundRadius: 0,
|
||||
labelBackgroundFill: '#576286',
|
||||
labelFill: isDark ? '#fff' : '#333',
|
||||
// labelFill: (datum) => (datum.id === rootId ? '#fff' : '#666'),
|
||||
labelText: (d) => d.style?.labelText || d.id,
|
||||
labelTextAlign: (datum) =>
|
||||
datum.id === rootId ? 'center' : 'left',
|
||||
labelTextBaseline: 'top',
|
||||
color: (datum: any) => {
|
||||
const depth = graph.getAncestorsData(datum.id, 'tree').length - 1;
|
||||
return COLORS[depth % COLORS.length] || '#576286';
|
||||
},
|
||||
},
|
||||
state: {
|
||||
selected: {
|
||||
lineWidth: 0,
|
||||
labelFill: '#40A8FF',
|
||||
labelBackground: true,
|
||||
labelFontWeight: 'normal',
|
||||
labelBackgroundFill: '#e8f7ff',
|
||||
labelBackgroundRadius: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
edge: {
|
||||
type: 'indented',
|
||||
style: {
|
||||
radius: 16,
|
||||
lineWidth: 2,
|
||||
sourcePort: 'out',
|
||||
targetPort: 'in',
|
||||
stroke: (datum: any) => {
|
||||
const depth = graph.getAncestorsData(datum.source, 'tree').length;
|
||||
return COLORS[depth % COLORS.length] || 'black';
|
||||
},
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
type: 'indented',
|
||||
direction: 'LR',
|
||||
isHorizontal: true,
|
||||
indent: 40,
|
||||
getHeight: () => 20,
|
||||
getVGap: () => 10,
|
||||
},
|
||||
behaviors: [
|
||||
'scroll-canvas',
|
||||
'collapse-expand-tree',
|
||||
{
|
||||
type: 'click-select',
|
||||
enable: (event: any) =>
|
||||
event.targetType === 'node' && event.target.id !== rootId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (graphRef.current) {
|
||||
graphRef.current.destroy();
|
||||
}
|
||||
|
||||
graphRef.current = graph;
|
||||
|
||||
assignIds(data);
|
||||
|
||||
graph?.setData(treeToGraphData(data));
|
||||
|
||||
graph?.render();
|
||||
},
|
||||
[assignIds],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(data)) {
|
||||
render(data);
|
||||
}
|
||||
}, [render, data]);
|
||||
|
||||
return (
|
||||
<ErrorBoundary fallbackRender={fallbackRender}>
|
||||
<div
|
||||
id="tree"
|
||||
ref={containerRef}
|
||||
style={{
|
||||
width: '90vw',
|
||||
height: '80vh',
|
||||
display: show ? 'block' : 'none',
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndentedTree;
|
||||
30
ragflow_web/src/components/indented-tree/modal.tsx
Normal file
30
ragflow_web/src/components/indented-tree/modal.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import IndentedTree from './indented-tree';
|
||||
|
||||
import { useFetchKnowledgeGraph } from '@/hooks/knowledge-hooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { Modal } from 'antd';
|
||||
|
||||
const IndentedTreeModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
}: IModalProps<any> & { documentId: string }) => {
|
||||
const { data } = useFetchKnowledgeGraph();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('chunk.mind')}
|
||||
open={visible}
|
||||
onCancel={hideModal}
|
||||
width={'90vw'}
|
||||
footer={null}
|
||||
>
|
||||
<section>
|
||||
<IndentedTree data={data?.mind_map} show></IndentedTree>
|
||||
</section>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndentedTreeModal;
|
||||
155
ragflow_web/src/components/knowledge-base-item.tsx
Normal file
155
ragflow_web/src/components/knowledge-base-item.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { DocumentParserType } from '@/constants/knowledge';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
|
||||
import { useBuildQueryVariableOptions } from '@/pages/agent/hooks/use-get-begin-query';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { Avatar as AntAvatar, Form, Select, Space } from 'antd';
|
||||
import { toLower } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RAGFlowAvatar } from './ragflow-avatar';
|
||||
import { FormControl, FormField, FormItem, FormLabel } from './ui/form';
|
||||
import { MultiSelect } from './ui/multi-select';
|
||||
|
||||
interface KnowledgeBaseItemProps {
|
||||
label?: string;
|
||||
tooltipText?: string;
|
||||
name?: string;
|
||||
required?: boolean;
|
||||
onChange?(): void;
|
||||
}
|
||||
|
||||
const KnowledgeBaseItem = ({
|
||||
label,
|
||||
tooltipText,
|
||||
name,
|
||||
required = true,
|
||||
onChange,
|
||||
}: KnowledgeBaseItemProps) => {
|
||||
const { t } = useTranslate('chat');
|
||||
|
||||
const { list: knowledgeList } = useFetchKnowledgeList(true);
|
||||
|
||||
const filteredKnowledgeList = knowledgeList.filter(
|
||||
(x) => x.parser_id !== DocumentParserType.Tag,
|
||||
);
|
||||
|
||||
const knowledgeOptions = filteredKnowledgeList.map((x) => ({
|
||||
label: (
|
||||
<Space>
|
||||
<AntAvatar size={20} icon={<UserOutlined />} src={x.avatar} />
|
||||
{x.name}
|
||||
</Space>
|
||||
),
|
||||
value: x.id,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
label={label || t('knowledgeBases')}
|
||||
name={name || 'kb_ids'}
|
||||
tooltip={tooltipText || t('knowledgeBasesTip')}
|
||||
rules={[
|
||||
{
|
||||
required,
|
||||
message: t('knowledgeBasesMessage'),
|
||||
type: 'array',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
options={knowledgeOptions}
|
||||
placeholder={t('knowledgeBasesMessage')}
|
||||
onChange={onChange}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseItem;
|
||||
|
||||
function buildQueryVariableOptionsByShowVariable(showVariable?: boolean) {
|
||||
return showVariable ? useBuildQueryVariableOptions : () => [];
|
||||
}
|
||||
|
||||
export function KnowledgeBaseFormField({
|
||||
showVariable = false,
|
||||
}: {
|
||||
showVariable?: boolean;
|
||||
}) {
|
||||
const form = useFormContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { list: knowledgeList } = useFetchKnowledgeList(true);
|
||||
|
||||
const filteredKnowledgeList = knowledgeList.filter(
|
||||
(x) => x.parser_id !== DocumentParserType.Tag,
|
||||
);
|
||||
|
||||
const nextOptions = buildQueryVariableOptionsByShowVariable(showVariable)();
|
||||
|
||||
const knowledgeOptions = filteredKnowledgeList.map((x) => ({
|
||||
label: x.name,
|
||||
value: x.id,
|
||||
icon: () => (
|
||||
<RAGFlowAvatar className="size-4 mr-2" avatar={x.avatar} name={x.name} />
|
||||
),
|
||||
}));
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (showVariable) {
|
||||
return [
|
||||
{
|
||||
label: t('knowledgeDetails.dataset'),
|
||||
options: knowledgeOptions,
|
||||
},
|
||||
...nextOptions.map((x) => {
|
||||
return {
|
||||
...x,
|
||||
options: x.options
|
||||
.filter((y) => toLower(y.type).includes('string'))
|
||||
.map((x) => ({
|
||||
...x,
|
||||
icon: () => (
|
||||
<RAGFlowAvatar
|
||||
className="size-4 mr-2"
|
||||
avatar={x.label}
|
||||
name={x.label}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
};
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
return knowledgeOptions;
|
||||
}, [knowledgeOptions, nextOptions, showVariable, t]);
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="kb_ids"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('chat.knowledgeBasesTip')}>
|
||||
{t('chat.knowledgeBases')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<MultiSelect
|
||||
options={options}
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t('chat.knowledgeBasesMessage')}
|
||||
variant="inverted"
|
||||
maxCount={100}
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
128
ragflow_web/src/components/large-model-form-field.tsx
Normal file
128
ragflow_web/src/components/large-model-form-field.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { LlmModelType } from '@/constants/knowledge';
|
||||
import { t } from 'i18next';
|
||||
import { Funnel } from 'lucide-react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import { NextInnerLLMSelectProps, NextLLMSelect } from './llm-select/next';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
const ModelTypes = [
|
||||
{
|
||||
title: t('flow.allModels'),
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
title: t('flow.textOnlyModels'),
|
||||
value: LlmModelType.Chat,
|
||||
},
|
||||
{
|
||||
title: t('flow.multimodalModels'),
|
||||
value: LlmModelType.Image2text,
|
||||
},
|
||||
];
|
||||
|
||||
export const LargeModelFilterFormSchema = {
|
||||
llm_filter: z.string().optional(),
|
||||
};
|
||||
|
||||
type LargeModelFormFieldProps = Pick<
|
||||
NextInnerLLMSelectProps,
|
||||
'showSpeech2TextModel'
|
||||
>;
|
||||
export function LargeModelFormField({
|
||||
showSpeech2TextModel: showTTSModel,
|
||||
}: LargeModelFormFieldProps) {
|
||||
const form = useFormContext();
|
||||
const { t } = useTranslation();
|
||||
const filter = useWatch({ control: form.control, name: 'llm_filter' });
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="llm_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('chat.modelTip')}>
|
||||
{t('chat.model')}
|
||||
</FormLabel>
|
||||
<section className="flex gap-2.5">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="llm_filter"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant={'ghost'}>
|
||||
<Funnel />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{ModelTypes.map((x) => (
|
||||
<DropdownMenuItem
|
||||
key={x.value}
|
||||
onClick={() => {
|
||||
field.onChange(x.value);
|
||||
}}
|
||||
>
|
||||
{x.title}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormControl>
|
||||
<NextLLMSelect
|
||||
{...field}
|
||||
filter={filter}
|
||||
showSpeech2TextModel={showTTSModel}
|
||||
/>
|
||||
</FormControl>
|
||||
</section>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function LargeModelFormFieldWithoutFilter() {
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="llm_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<NextLLMSelect {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
110
ragflow_web/src/components/layout-recognize-form-field.tsx
Normal file
110
ragflow_web/src/components/layout-recognize-form-field.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { LlmModelType } from '@/constants/knowledge';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useSelectLlmOptionsByModelType } from '@/hooks/llm-hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { camelCase } from 'lodash';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { SelectWithSearch } from './originui/select-with-search';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from './ui/form';
|
||||
|
||||
export const enum ParseDocumentType {
|
||||
DeepDOC = 'DeepDOC',
|
||||
PlainText = 'Plain Text',
|
||||
MinerU = 'MinerU',
|
||||
}
|
||||
|
||||
export function LayoutRecognizeFormField({
|
||||
name = 'parser_config.layout_recognize',
|
||||
horizontal = true,
|
||||
optionsWithoutLLM,
|
||||
label,
|
||||
}: {
|
||||
name?: string;
|
||||
horizontal?: boolean;
|
||||
optionsWithoutLLM?: { value: string; label: string }[];
|
||||
label?: ReactNode;
|
||||
}) {
|
||||
const form = useFormContext();
|
||||
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
const allOptions = useSelectLlmOptionsByModelType();
|
||||
|
||||
const options = useMemo(() => {
|
||||
const list = optionsWithoutLLM
|
||||
? optionsWithoutLLM
|
||||
: [
|
||||
ParseDocumentType.DeepDOC,
|
||||
ParseDocumentType.PlainText,
|
||||
ParseDocumentType.MinerU,
|
||||
].map((x) => ({
|
||||
label: x === ParseDocumentType.PlainText ? t(camelCase(x)) : x,
|
||||
value: x,
|
||||
}));
|
||||
|
||||
const image2TextList = allOptions[LlmModelType.Image2text].map((x) => {
|
||||
return {
|
||||
...x,
|
||||
options: x.options.map((y) => {
|
||||
return {
|
||||
...y,
|
||||
label: (
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
{y.label}
|
||||
<span className="text-red-500 text-sm">Experimental</span>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
return [...list, ...image2TextList];
|
||||
}, [allOptions, optionsWithoutLLM, t]);
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className={'items-center space-y-0 '}>
|
||||
<div
|
||||
className={cn('flex', {
|
||||
'flex-col ': !horizontal,
|
||||
'items-center': horizontal,
|
||||
})}
|
||||
>
|
||||
<FormLabel
|
||||
tooltip={t('layoutRecognizeTip')}
|
||||
className={cn('text-sm text-text-secondary whitespace-wrap', {
|
||||
['w-1/4']: horizontal,
|
||||
})}
|
||||
>
|
||||
{label || t('layoutRecognize')}
|
||||
</FormLabel>
|
||||
<div className={horizontal ? 'w-3/4' : 'w-full'}>
|
||||
<FormControl>
|
||||
<SelectWithSearch
|
||||
{...field}
|
||||
options={options}
|
||||
></SelectWithSearch>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className={horizontal ? 'w-1/4' : 'w-full'}></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
55
ragflow_web/src/components/layout-recognize.tsx
Normal file
55
ragflow_web/src/components/layout-recognize.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { LlmModelType } from '@/constants/knowledge';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useSelectLlmOptionsByModelType } from '@/hooks/llm-hooks';
|
||||
import { Form, Select } from 'antd';
|
||||
import { camelCase } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const enum DocumentType {
|
||||
DeepDOC = 'DeepDOC',
|
||||
PlainText = 'Plain Text',
|
||||
}
|
||||
|
||||
const LayoutRecognize = () => {
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
const allOptions = useSelectLlmOptionsByModelType();
|
||||
|
||||
const options = useMemo(() => {
|
||||
const list = [DocumentType.DeepDOC, DocumentType.PlainText].map((x) => ({
|
||||
label: x === DocumentType.PlainText ? t(camelCase(x)) : 'DeepDoc',
|
||||
value: x,
|
||||
}));
|
||||
|
||||
const image2TextList = allOptions[LlmModelType.Image2text].map((x) => {
|
||||
return {
|
||||
...x,
|
||||
options: x.options.map((y) => {
|
||||
return {
|
||||
...y,
|
||||
label: (
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
{y.label}
|
||||
<span className="text-red-500 text-sm">Experimental</span>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
return [...list, ...image2TextList];
|
||||
}, [allOptions, t]);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
name={['parser_config', 'layout_recognize']}
|
||||
label={t('layoutRecognize')}
|
||||
initialValue={DocumentType.DeepDOC}
|
||||
tooltip={t('layoutRecognizeTip')}
|
||||
>
|
||||
<Select options={options} popupMatchSelectWidth={false} />
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutRecognize;
|
||||
51
ragflow_web/src/components/line-chart/index.tsx
Normal file
51
ragflow_web/src/components/line-chart/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
CartesianGrid,
|
||||
Legend,
|
||||
Line,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
import { CategoricalChartProps } from 'recharts/types/chart/generateCategoricalChart';
|
||||
|
||||
interface IProps extends CategoricalChartProps {
|
||||
data?: Array<{ xAxis: string; yAxis: number }>;
|
||||
showLegend?: boolean;
|
||||
}
|
||||
|
||||
const RagLineChart = ({ data, showLegend = false }: IProps) => {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart
|
||||
// width={500}
|
||||
// height={300}
|
||||
data={data}
|
||||
margin={
|
||||
{
|
||||
// top: 5,
|
||||
// right: 30,
|
||||
// left: 20,
|
||||
// bottom: 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="xAxis" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
{showLegend && <Legend />}
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="yAxis"
|
||||
stroke="#8884d8"
|
||||
activeDot={{ r: 8 }}
|
||||
/>
|
||||
{/* <Line type="monotone" dataKey="uv" stroke="#82ca9d" /> */}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default RagLineChart;
|
||||
177
ragflow_web/src/components/list-filter-bar/filter-popover.tsx
Normal file
177
ragflow_web/src/components/list-filter-bar/filter-popover.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { PropsWithChildren, useCallback, useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { ZodArray, ZodString, z } from 'zod';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { t } from 'i18next';
|
||||
import { FilterChange, FilterCollection, FilterValue } from './interface';
|
||||
|
||||
export type CheckboxFormMultipleProps = {
|
||||
filters?: FilterCollection[];
|
||||
value?: FilterValue;
|
||||
onChange?: FilterChange;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
setOpen(open: boolean): void;
|
||||
};
|
||||
|
||||
function CheckboxFormMultiple({
|
||||
filters = [],
|
||||
value,
|
||||
onChange,
|
||||
setOpen,
|
||||
}: CheckboxFormMultipleProps) {
|
||||
const fieldsDict = filters?.reduce<Record<string, Array<any>>>((pre, cur) => {
|
||||
pre[cur.field] = [];
|
||||
return pre;
|
||||
}, {});
|
||||
|
||||
const FormSchema = z.object(
|
||||
filters.reduce<Record<string, ZodArray<ZodString, 'many'>>>((pre, cur) => {
|
||||
pre[cur.field] = z.array(z.string());
|
||||
|
||||
// .refine((value) => value.some((item) => item), {
|
||||
// message: 'You have to select at least one item.',
|
||||
// });
|
||||
return pre;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: fieldsDict,
|
||||
});
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
onChange?.(data);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
onChange?.(fieldsDict);
|
||||
setOpen(false);
|
||||
}, [fieldsDict, onChange, setOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
form.reset(value);
|
||||
}, [form, value]);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8 px-5 py-2.5"
|
||||
onReset={() => form.reset()}
|
||||
>
|
||||
{filters.map((x) => (
|
||||
<FormField
|
||||
key={x.field}
|
||||
control={form.control}
|
||||
name={x.field}
|
||||
render={() => (
|
||||
<FormItem className="space-y-4">
|
||||
<div>
|
||||
<FormLabel className="text-base text-text-sub-title-invert">
|
||||
{x.label}
|
||||
</FormLabel>
|
||||
</div>
|
||||
{x.list.map((item) => (
|
||||
<FormField
|
||||
key={item.id}
|
||||
control={form.control}
|
||||
name={x.field}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between text-text-primary text-xs">
|
||||
<FormItem
|
||||
key={item.id}
|
||||
className="flex flex-row space-x-3 space-y-0 items-center "
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(item.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, item.id])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) => value !== item.id,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel>{item.label}</FormLabel>
|
||||
</FormItem>
|
||||
<span className=" text-sm">{item.count}</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<div className="flex justify-end gap-5">
|
||||
<Button
|
||||
type="button"
|
||||
variant={'outline'}
|
||||
size={'sm'}
|
||||
onClick={onReset}
|
||||
>
|
||||
{t('common.clear')}
|
||||
</Button>
|
||||
<Button type="submit" size={'sm'}>
|
||||
{t('common.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterPopover({
|
||||
children,
|
||||
value,
|
||||
onChange,
|
||||
onOpenChange,
|
||||
filters,
|
||||
}: PropsWithChildren & Omit<CheckboxFormMultipleProps, 'setOpen'>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const onOpenChangeFun = useCallback(
|
||||
(e: boolean) => {
|
||||
onOpenChange?.(e);
|
||||
setOpen(e);
|
||||
},
|
||||
[onOpenChange],
|
||||
);
|
||||
return (
|
||||
<Popover open={open} onOpenChange={onOpenChangeFun}>
|
||||
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<CheckboxFormMultiple
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
filters={filters}
|
||||
setOpen={setOpen}
|
||||
></CheckboxFormMultiple>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
102
ragflow_web/src/components/list-filter-bar/index.tsx
Normal file
102
ragflow_web/src/components/list-filter-bar/index.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Funnel } from 'lucide-react';
|
||||
import React, {
|
||||
ChangeEventHandler,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { IconFont } from '../icon-font';
|
||||
import { Button, ButtonProps } from '../ui/button';
|
||||
import { SearchInput } from '../ui/input';
|
||||
import { CheckboxFormMultipleProps, FilterPopover } from './filter-popover';
|
||||
|
||||
interface IProps {
|
||||
title?: ReactNode;
|
||||
searchString?: string;
|
||||
onSearchChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
showFilter?: boolean;
|
||||
leftPanel?: ReactNode;
|
||||
}
|
||||
|
||||
export const FilterButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ButtonProps & { count?: number }
|
||||
>(({ count = 0, ...props }, ref) => {
|
||||
return (
|
||||
<Button variant="secondary" {...props} ref={ref}>
|
||||
{/* <span
|
||||
className={cn({
|
||||
'text-text-primary': count > 0,
|
||||
'text-text-sub-title-invert': count === 0,
|
||||
})}
|
||||
>
|
||||
Filter
|
||||
</span> */}
|
||||
{count > 0 && (
|
||||
<span className="rounded-full bg-text-badge px-1 text-xs ">
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
<Funnel />
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
export default function ListFilterBar({
|
||||
title,
|
||||
children,
|
||||
searchString,
|
||||
onSearchChange,
|
||||
showFilter = true,
|
||||
leftPanel,
|
||||
value,
|
||||
onChange,
|
||||
onOpenChange,
|
||||
filters,
|
||||
className,
|
||||
icon,
|
||||
}: PropsWithChildren<IProps & Omit<CheckboxFormMultipleProps, 'setOpen'>> & {
|
||||
className?: string;
|
||||
icon?: ReactNode;
|
||||
}) {
|
||||
const filterCount = useMemo(() => {
|
||||
return typeof value === 'object' && value !== null
|
||||
? Object.values(value).reduce((pre, cur) => {
|
||||
return pre + cur.length;
|
||||
}, 0)
|
||||
: 0;
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className={cn('flex justify-between mb-5 items-center', className)}>
|
||||
<div className="text-2xl font-semibold flex items-center gap-2.5">
|
||||
{typeof icon === 'string' ? (
|
||||
<IconFont name={icon} className="size-6"></IconFont>
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
{leftPanel || title}
|
||||
</div>
|
||||
<div className="flex gap-5 items-center">
|
||||
{showFilter && (
|
||||
<FilterPopover
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
filters={filters}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<FilterButton count={filterCount}></FilterButton>
|
||||
</FilterPopover>
|
||||
)}
|
||||
|
||||
<SearchInput
|
||||
value={searchString}
|
||||
onChange={onSearchChange}
|
||||
className="w-32"
|
||||
></SearchInput>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
ragflow_web/src/components/list-filter-bar/interface.ts
Normal file
15
ragflow_web/src/components/list-filter-bar/interface.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type FilterType = {
|
||||
id: string;
|
||||
label: string | JSX.Element;
|
||||
count?: number;
|
||||
};
|
||||
|
||||
export type FilterCollection = {
|
||||
field: string;
|
||||
label: string;
|
||||
list: FilterType[];
|
||||
};
|
||||
|
||||
export type FilterValue = Record<string, Array<string>>;
|
||||
|
||||
export type FilterChange = (value: FilterValue) => void;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useGetPaginationWithRouter } from '@/hooks/logic-hooks';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { FilterChange, FilterValue } from './interface';
|
||||
|
||||
export function useHandleFilterSubmit() {
|
||||
const [filterValue, setFilterValue] = useState<FilterValue>({});
|
||||
const { setPagination } = useGetPaginationWithRouter();
|
||||
const handleFilterSubmit: FilterChange = useCallback(
|
||||
(value) => {
|
||||
setFilterValue(value);
|
||||
setPagination({ page: 1 });
|
||||
},
|
||||
[setPagination],
|
||||
);
|
||||
|
||||
return { filterValue, setFilterValue, handleFilterSubmit };
|
||||
}
|
||||
3
ragflow_web/src/components/llm-select/index.less
Normal file
3
ragflow_web/src/components/llm-select/index.less
Normal file
@@ -0,0 +1,3 @@
|
||||
.llmLabel {
|
||||
font-size: 14px;
|
||||
}
|
||||
67
ragflow_web/src/components/llm-select/index.tsx
Normal file
67
ragflow_web/src/components/llm-select/index.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { LlmModelType } from '@/constants/knowledge';
|
||||
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
|
||||
import { Popover as AntPopover, Select as AntSelect } from 'antd';
|
||||
import LlmSettingItems from '../llm-setting-items';
|
||||
|
||||
interface IProps {
|
||||
id?: string;
|
||||
value?: string;
|
||||
onInitialValue?: (value: string, option: any) => void;
|
||||
onChange?: (value: string, option: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const LLMSelect = ({
|
||||
id,
|
||||
value,
|
||||
onInitialValue,
|
||||
onChange,
|
||||
disabled,
|
||||
}: IProps) => {
|
||||
const modelOptions = useComposeLlmOptionsByModelTypes([
|
||||
LlmModelType.Chat,
|
||||
LlmModelType.Image2text,
|
||||
]);
|
||||
|
||||
if (onInitialValue && value) {
|
||||
for (const modelOption of modelOptions) {
|
||||
for (const option of modelOption.options) {
|
||||
if (option.value === value) {
|
||||
onInitialValue(value, option);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content = (
|
||||
<div style={{ width: 400 }}>
|
||||
<LlmSettingItems
|
||||
onChange={onChange}
|
||||
formItemLayout={{ labelCol: { span: 10 }, wrapperCol: { span: 14 } }}
|
||||
></LlmSettingItems>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<AntPopover
|
||||
content={content}
|
||||
trigger="click"
|
||||
placement="left"
|
||||
arrow={false}
|
||||
destroyTooltipOnHide
|
||||
>
|
||||
<AntSelect
|
||||
options={modelOptions}
|
||||
style={{ width: '100%' }}
|
||||
dropdownStyle={{ display: 'none' }}
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</AntPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export default LLMSelect;
|
||||
28
ragflow_web/src/components/llm-select/llm-label.tsx
Normal file
28
ragflow_web/src/components/llm-select/llm-label.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getLLMIconName, getLlmNameAndFIdByLlmId } from '@/utils/llm-util';
|
||||
import { memo } from 'react';
|
||||
import { LlmIcon } from '../svg-icon';
|
||||
|
||||
interface IProps {
|
||||
id?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const LLMLabel = ({ value }: IProps) => {
|
||||
const { llmName, fId } = getLlmNameAndFIdByLlmId(value);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<LlmIcon
|
||||
name={getLLMIconName(fId, llmName)}
|
||||
width={20}
|
||||
height={20}
|
||||
size={'small'}
|
||||
/>
|
||||
<span className="flex-1 truncate"> {llmName}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LLMLabel);
|
||||
73
ragflow_web/src/components/llm-select/next.tsx
Normal file
73
ragflow_web/src/components/llm-select/next.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { LlmModelType } from '@/constants/knowledge';
|
||||
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
|
||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
import { forwardRef, memo, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LlmSettingFieldItems } from '../llm-setting-items/next';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||
import { Select, SelectTrigger, SelectValue } from '../ui/select';
|
||||
|
||||
export interface NextInnerLLMSelectProps {
|
||||
id?: string;
|
||||
value?: string;
|
||||
onInitialValue?: (value: string, option: any) => void;
|
||||
onChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
filter?: string;
|
||||
showSpeech2TextModel?: boolean;
|
||||
}
|
||||
|
||||
const NextInnerLLMSelect = forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
NextInnerLLMSelectProps
|
||||
>(({ value, disabled, filter, showSpeech2TextModel = false }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const ttsModel = useMemo(() => {
|
||||
return showSpeech2TextModel ? [LlmModelType.Speech2text] : [];
|
||||
}, [showSpeech2TextModel]);
|
||||
|
||||
const modelTypes = useMemo(() => {
|
||||
if (filter === LlmModelType.Chat) {
|
||||
return [LlmModelType.Chat];
|
||||
} else if (filter === LlmModelType.Image2text) {
|
||||
return [LlmModelType.Image2text, ...ttsModel];
|
||||
} else {
|
||||
return [LlmModelType.Chat, LlmModelType.Image2text, ...ttsModel];
|
||||
}
|
||||
}, [filter, ttsModel]);
|
||||
|
||||
const modelOptions = useComposeLlmOptionsByModelTypes(modelTypes);
|
||||
|
||||
return (
|
||||
<Select disabled={disabled} value={value}>
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<SelectTrigger
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsPopoverOpen(true);
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<SelectValue placeholder={t('common.pleaseSelect')}>
|
||||
{
|
||||
modelOptions
|
||||
.flatMap((x) => x.options)
|
||||
.find((x) => x.value === value)?.label
|
||||
}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side={'left'}>
|
||||
<LlmSettingFieldItems options={modelOptions}></LlmSettingFieldItems>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Select>
|
||||
);
|
||||
});
|
||||
|
||||
NextInnerLLMSelect.displayName = 'LLMSelect';
|
||||
|
||||
export const NextLLMSelect = memo(NextInnerLLMSelect);
|
||||
6
ragflow_web/src/components/llm-setting-items/index.less
Normal file
6
ragflow_web/src/components/llm-setting-items/index.less
Normal file
@@ -0,0 +1,6 @@
|
||||
.sliderInputNumber {
|
||||
width: 80px;
|
||||
}
|
||||
.variableSlider {
|
||||
width: 100%;
|
||||
}
|
||||
350
ragflow_web/src/components/llm-setting-items/index.tsx
Normal file
350
ragflow_web/src/components/llm-setting-items/index.tsx
Normal file
@@ -0,0 +1,350 @@
|
||||
import {
|
||||
LlmModelType,
|
||||
ModelVariableType,
|
||||
settledModelVariableMap,
|
||||
} from '@/constants/knowledge';
|
||||
import { Flex, Form, InputNumber, Select, Slider, Switch, Tooltip } from 'antd';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
|
||||
import { setChatVariableEnabledFieldValuePage } from '@/utils/chat';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps {
|
||||
prefix?: string;
|
||||
formItemLayout?: any;
|
||||
handleParametersChange?(value: ModelVariableType): void;
|
||||
onChange?(value: string, option: any): void;
|
||||
}
|
||||
|
||||
const LlmSettingItems = ({ prefix, formItemLayout = {}, onChange }: IProps) => {
|
||||
const form = Form.useFormInstance();
|
||||
const { t } = useTranslate('chat');
|
||||
const parameterOptions = Object.values(ModelVariableType).map((x) => ({
|
||||
label: t(camelCase(x)),
|
||||
value: x,
|
||||
}));
|
||||
|
||||
const handleParametersChange = useCallback(
|
||||
(value: ModelVariableType) => {
|
||||
const variable = settledModelVariableMap[value];
|
||||
let nextVariable: Record<string, any> = variable;
|
||||
if (prefix) {
|
||||
nextVariable = { [prefix]: variable };
|
||||
}
|
||||
const variableCheckBoxFieldMap = setChatVariableEnabledFieldValuePage();
|
||||
form.setFieldsValue({ ...nextVariable, ...variableCheckBoxFieldMap });
|
||||
},
|
||||
[form, prefix],
|
||||
);
|
||||
|
||||
const memorizedPrefix = useMemo(() => (prefix ? [prefix] : []), [prefix]);
|
||||
|
||||
const modelOptions = useComposeLlmOptionsByModelTypes([
|
||||
LlmModelType.Chat,
|
||||
LlmModelType.Image2text,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label={t('model')}
|
||||
name="llm_id"
|
||||
tooltip={t('modelTip')}
|
||||
{...formItemLayout}
|
||||
rules={[{ required: true, message: t('modelMessage') }]}
|
||||
>
|
||||
<Select
|
||||
options={modelOptions}
|
||||
showSearch
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="border rounded-md">
|
||||
<div className="flex justify-between bg-slate-100 p-2 mb-2">
|
||||
<div className="space-x-1 items-center">
|
||||
<span className="text-lg font-semibold">{t('freedom')}</span>
|
||||
<Tooltip title={t('freedomTip')}>
|
||||
<QuestionCircleOutlined></QuestionCircleOutlined>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="w-1/4 min-w-32">
|
||||
<Form.Item
|
||||
label={t('freedom')}
|
||||
name="parameter"
|
||||
tooltip={t('freedomTip')}
|
||||
initialValue={ModelVariableType.Precise}
|
||||
labelCol={{ span: 0 }}
|
||||
wrapperCol={{ span: 24 }}
|
||||
className="m-0"
|
||||
>
|
||||
<Select<ModelVariableType>
|
||||
options={parameterOptions}
|
||||
onChange={handleParametersChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pr-2">
|
||||
<Form.Item
|
||||
label={t('temperature')}
|
||||
tooltip={t('temperatureTip')}
|
||||
{...formItemLayout}
|
||||
>
|
||||
<Flex gap={20} align="center">
|
||||
<Form.Item
|
||||
name={'temperatureEnabled'}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Switch size="small" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
dependencies={['temperatureEnabled']}
|
||||
shouldUpdate
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const disabled = !getFieldValue('temperatureEnabled');
|
||||
return (
|
||||
<>
|
||||
<Flex flex={1}>
|
||||
<Form.Item
|
||||
name={[...memorizedPrefix, 'temperature']}
|
||||
noStyle
|
||||
>
|
||||
<Slider
|
||||
className={styles.variableSlider}
|
||||
max={1}
|
||||
step={0.01}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item
|
||||
name={[...memorizedPrefix, 'temperature']}
|
||||
noStyle
|
||||
>
|
||||
<InputNumber
|
||||
className={styles.sliderInputNumber}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('topP')}
|
||||
tooltip={t('topPTip')}
|
||||
{...formItemLayout}
|
||||
>
|
||||
<Flex gap={20} align="center">
|
||||
<Form.Item name={'topPEnabled'} valuePropName="checked" noStyle>
|
||||
<Switch size="small" />
|
||||
</Form.Item>
|
||||
<Form.Item noStyle dependencies={['topPEnabled']} shouldUpdate>
|
||||
{({ getFieldValue }) => {
|
||||
const disabled = !getFieldValue('topPEnabled');
|
||||
return (
|
||||
<>
|
||||
<Flex flex={1}>
|
||||
<Form.Item name={[...memorizedPrefix, 'top_p']} noStyle>
|
||||
<Slider
|
||||
className={styles.variableSlider}
|
||||
max={1}
|
||||
step={0.01}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item name={[...memorizedPrefix, 'top_p']} noStyle>
|
||||
<InputNumber
|
||||
className={styles.sliderInputNumber}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('presencePenalty')}
|
||||
tooltip={t('presencePenaltyTip')}
|
||||
{...formItemLayout}
|
||||
>
|
||||
<Flex gap={20} align="center">
|
||||
<Form.Item
|
||||
name={'presencePenaltyEnabled'}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Switch size="small" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
dependencies={['presencePenaltyEnabled']}
|
||||
shouldUpdate
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const disabled = !getFieldValue('presencePenaltyEnabled');
|
||||
return (
|
||||
<>
|
||||
<Flex flex={1}>
|
||||
<Form.Item
|
||||
name={[...memorizedPrefix, 'presence_penalty']}
|
||||
noStyle
|
||||
>
|
||||
<Slider
|
||||
className={styles.variableSlider}
|
||||
max={1}
|
||||
step={0.01}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item
|
||||
name={[...memorizedPrefix, 'presence_penalty']}
|
||||
noStyle
|
||||
>
|
||||
<InputNumber
|
||||
className={styles.sliderInputNumber}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('frequencyPenalty')}
|
||||
tooltip={t('frequencyPenaltyTip')}
|
||||
{...formItemLayout}
|
||||
>
|
||||
<Flex gap={20} align="center">
|
||||
<Form.Item
|
||||
name={'frequencyPenaltyEnabled'}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Switch size="small" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
dependencies={['frequencyPenaltyEnabled']}
|
||||
shouldUpdate
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const disabled = !getFieldValue('frequencyPenaltyEnabled');
|
||||
return (
|
||||
<>
|
||||
<Flex flex={1}>
|
||||
<Form.Item
|
||||
name={[...memorizedPrefix, 'frequency_penalty']}
|
||||
noStyle
|
||||
>
|
||||
<Slider
|
||||
className={styles.variableSlider}
|
||||
max={1}
|
||||
step={0.01}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item
|
||||
name={[...memorizedPrefix, 'frequency_penalty']}
|
||||
noStyle
|
||||
>
|
||||
<InputNumber
|
||||
className={styles.sliderInputNumber}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('maxTokens')}
|
||||
tooltip={t('maxTokensTip')}
|
||||
{...formItemLayout}
|
||||
>
|
||||
<Flex gap={20} align="center">
|
||||
<Form.Item
|
||||
name={'maxTokensEnabled'}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Switch size="small" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
dependencies={['maxTokensEnabled']}
|
||||
shouldUpdate
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const disabled = !getFieldValue('maxTokensEnabled');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex flex={1}>
|
||||
<Form.Item
|
||||
name={[...memorizedPrefix, 'max_tokens']}
|
||||
noStyle
|
||||
>
|
||||
<Slider
|
||||
className={styles.variableSlider}
|
||||
max={128000}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item
|
||||
name={[...memorizedPrefix, 'max_tokens']}
|
||||
noStyle
|
||||
>
|
||||
<InputNumber
|
||||
disabled={disabled}
|
||||
className={styles.sliderInputNumber}
|
||||
max={128000}
|
||||
min={0}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LlmSettingItems;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { LlmModelType } from '@/constants/knowledge';
|
||||
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SelectWithSearch } from '../originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '../ragflow-form';
|
||||
|
||||
export type LLMFormFieldProps = {
|
||||
options?: any[];
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export function LLMFormField({ options, name }: LLMFormFieldProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const modelOptions = useComposeLlmOptionsByModelTypes([
|
||||
LlmModelType.Chat,
|
||||
LlmModelType.Image2text,
|
||||
]);
|
||||
|
||||
return (
|
||||
<RAGFlowFormItem name={name || 'llm_id'} label={t('chat.model')}>
|
||||
<SelectWithSearch options={options || modelOptions}></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
}
|
||||
151
ragflow_web/src/components/llm-setting-items/next.tsx
Normal file
151
ragflow_web/src/components/llm-setting-items/next.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { ModelVariableType } from '@/constants/knowledge';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { camelCase } from 'lodash';
|
||||
import { useCallback } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '../ui/form';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '../ui/select';
|
||||
import { LLMFormField } from './llm-form-field';
|
||||
import { SliderInputSwitchFormField } from './slider';
|
||||
import { useHandleFreedomChange } from './use-watch-change';
|
||||
|
||||
interface LlmSettingFieldItemsProps {
|
||||
prefix?: string;
|
||||
options?: any[];
|
||||
}
|
||||
|
||||
export const LLMIdFormField = {
|
||||
llm_id: z.string(),
|
||||
};
|
||||
|
||||
export const LlmSettingEnabledSchema = {
|
||||
temperatureEnabled: z.boolean().optional(),
|
||||
topPEnabled: z.boolean().optional(),
|
||||
presencePenaltyEnabled: z.boolean().optional(),
|
||||
frequencyPenaltyEnabled: z.boolean().optional(),
|
||||
maxTokensEnabled: z.boolean().optional(),
|
||||
};
|
||||
|
||||
export const LlmSettingFieldSchema = {
|
||||
temperature: z.coerce.number().optional(),
|
||||
top_p: z.number().optional(),
|
||||
presence_penalty: z.coerce.number().optional(),
|
||||
frequency_penalty: z.coerce.number().optional(),
|
||||
max_tokens: z.number().optional(),
|
||||
};
|
||||
|
||||
export const LlmSettingSchema = {
|
||||
...LLMIdFormField,
|
||||
...LlmSettingFieldSchema,
|
||||
...LlmSettingEnabledSchema,
|
||||
};
|
||||
|
||||
export function LlmSettingFieldItems({
|
||||
prefix,
|
||||
options,
|
||||
}: LlmSettingFieldItemsProps) {
|
||||
const form = useFormContext();
|
||||
const { t } = useTranslate('chat');
|
||||
|
||||
const getFieldWithPrefix = useCallback(
|
||||
(name: string) => {
|
||||
return prefix ? `${prefix}.${name}` : name;
|
||||
},
|
||||
[prefix],
|
||||
);
|
||||
|
||||
const handleChange = useHandleFreedomChange(getFieldWithPrefix);
|
||||
|
||||
const parameterOptions = Object.values(ModelVariableType).map((x) => ({
|
||||
label: t(camelCase(x)),
|
||||
value: x,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<LLMFormField options={options}></LLMFormField>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'parameter'}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex justify-between items-center">
|
||||
<FormLabel className="flex-1">{t('freedom')}</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(val) => {
|
||||
handleChange(val);
|
||||
field.onChange(val);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="flex-1 !m-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{parameterOptions.map((x) => (
|
||||
<SelectItem value={x.value} key={x.value}>
|
||||
{x.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SliderInputSwitchFormField
|
||||
name={getFieldWithPrefix('temperature')}
|
||||
checkName="temperatureEnabled"
|
||||
label="temperature"
|
||||
max={1}
|
||||
step={0.01}
|
||||
min={0}
|
||||
></SliderInputSwitchFormField>
|
||||
<SliderInputSwitchFormField
|
||||
name={getFieldWithPrefix('top_p')}
|
||||
checkName="topPEnabled"
|
||||
label="topP"
|
||||
max={1}
|
||||
step={0.01}
|
||||
min={0}
|
||||
></SliderInputSwitchFormField>
|
||||
<SliderInputSwitchFormField
|
||||
name={getFieldWithPrefix('presence_penalty')}
|
||||
checkName="presencePenaltyEnabled"
|
||||
label="presencePenalty"
|
||||
max={1}
|
||||
step={0.01}
|
||||
min={0}
|
||||
></SliderInputSwitchFormField>
|
||||
<SliderInputSwitchFormField
|
||||
name={getFieldWithPrefix('frequency_penalty')}
|
||||
checkName="frequencyPenaltyEnabled"
|
||||
label="frequencyPenalty"
|
||||
max={1}
|
||||
step={0.01}
|
||||
min={0}
|
||||
></SliderInputSwitchFormField>
|
||||
<SliderInputSwitchFormField
|
||||
name={getFieldWithPrefix('max_tokens')}
|
||||
checkName="maxTokensEnabled"
|
||||
label="maxTokens"
|
||||
max={128000}
|
||||
min={0}
|
||||
></SliderInputSwitchFormField>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
101
ragflow_web/src/components/llm-setting-items/slider.tsx
Normal file
101
ragflow_web/src/components/llm-setting-items/slider.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { SingleFormSlider } from '../ui/dual-range-slider';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '../ui/form';
|
||||
import { NumberInput } from '../ui/input';
|
||||
import { Switch } from '../ui/switch';
|
||||
|
||||
type SliderInputSwitchFormFieldProps = {
|
||||
max?: number;
|
||||
min?: number;
|
||||
step?: number;
|
||||
name: string;
|
||||
label: string;
|
||||
defaultValue?: number;
|
||||
onChange?: (value: number) => void;
|
||||
className?: string;
|
||||
checkName: string;
|
||||
};
|
||||
|
||||
export function SliderInputSwitchFormField({
|
||||
max,
|
||||
min,
|
||||
step,
|
||||
label,
|
||||
name,
|
||||
defaultValue,
|
||||
onChange,
|
||||
className,
|
||||
checkName,
|
||||
}: SliderInputSwitchFormFieldProps) {
|
||||
const form = useFormContext();
|
||||
const disabled = !form.watch(checkName);
|
||||
const { t } = useTranslate('chat');
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={name}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t(`${label}Tip`)}>{t(label)}</FormLabel>
|
||||
<div
|
||||
className={cn('flex items-center gap-4 justify-between', className)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={checkName}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormControl>
|
||||
<SingleFormSlider
|
||||
{...field}
|
||||
onChange={(value: number) => {
|
||||
onChange?.(value);
|
||||
field.onChange(value);
|
||||
}}
|
||||
max={max}
|
||||
min={min}
|
||||
step={step}
|
||||
disabled={disabled}
|
||||
></SingleFormSlider>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<NumberInput
|
||||
disabled={disabled}
|
||||
className="h-7 w-20"
|
||||
max={max}
|
||||
min={min}
|
||||
step={step}
|
||||
{...field}
|
||||
onChange={(value: number) => {
|
||||
onChange?.(value);
|
||||
field.onChange(value);
|
||||
}}
|
||||
></NumberInput>
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { settledModelVariableMap } from '@/constants/knowledge';
|
||||
import { AgentFormContext } from '@/pages/agent/context';
|
||||
import useGraphStore from '@/pages/agent/store';
|
||||
import { setChatVariableEnabledFieldValuePage } from '@/utils/chat';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
export function useHandleFreedomChange(
|
||||
getFieldWithPrefix: (name: string) => string,
|
||||
) {
|
||||
const form = useFormContext();
|
||||
const node = useContext(AgentFormContext);
|
||||
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
||||
|
||||
const setLLMParameters = useCallback(
|
||||
(values: Record<string, any>, withPrefix: boolean) => {
|
||||
for (const key in values) {
|
||||
if (Object.prototype.hasOwnProperty.call(values, key)) {
|
||||
const realKey = getFieldWithPrefix(key);
|
||||
const element = values[key as keyof typeof values];
|
||||
|
||||
form.setValue(withPrefix ? realKey : key, element);
|
||||
}
|
||||
}
|
||||
},
|
||||
[form, getFieldWithPrefix],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(parameter: string) => {
|
||||
const currentValues = { ...form.getValues() };
|
||||
const values =
|
||||
settledModelVariableMap[
|
||||
parameter as keyof typeof settledModelVariableMap
|
||||
];
|
||||
|
||||
const nextValues = { ...currentValues, ...values };
|
||||
|
||||
if (node?.id) {
|
||||
updateNodeForm(node?.id, nextValues);
|
||||
}
|
||||
|
||||
const variableCheckBoxFieldMap = setChatVariableEnabledFieldValuePage();
|
||||
|
||||
setLLMParameters(values, true);
|
||||
setLLMParameters(variableCheckBoxFieldMap, false);
|
||||
},
|
||||
[form, node?.id, setLLMParameters, updateNodeForm],
|
||||
);
|
||||
|
||||
return handleChange;
|
||||
}
|
||||
51
ragflow_web/src/components/llm-tools-select.tsx
Normal file
51
ragflow_web/src/components/llm-tools-select.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useLlmToolsList } from '@/hooks/plugin-hooks';
|
||||
import { Select, Space } from 'antd';
|
||||
|
||||
interface IProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const LLMToolsSelect = ({ value, onChange, disabled }: IProps) => {
|
||||
const { t } = useTranslate("llmTools");
|
||||
const tools = useLlmToolsList();
|
||||
|
||||
function wrapTranslation(text: string): string {
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
if (text.startsWith("$t:")) {
|
||||
return t(text.substring(3));
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
const toolOptions = tools.map(t => ({
|
||||
label: wrapTranslation(t.displayName),
|
||||
description: wrapTranslation(t.displayDescription),
|
||||
value: t.name,
|
||||
title: wrapTranslation(t.displayDescription),
|
||||
}));
|
||||
|
||||
return (
|
||||
<Select
|
||||
mode="multiple"
|
||||
options={toolOptions}
|
||||
optionRender={option => (
|
||||
<Space size="large">
|
||||
{option.label}
|
||||
{option.data.description}
|
||||
</Space>
|
||||
)}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
></Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default LLMToolsSelect;
|
||||
23
ragflow_web/src/components/max-token-number-from-field.tsx
Normal file
23
ragflow_web/src/components/max-token-number-from-field.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { FormLayout } from '@/constants/form';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { SliderInputFormField } from './slider-input-form-field';
|
||||
|
||||
interface IProps {
|
||||
initialValue?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export function MaxTokenNumberFormField({ max = 2048, initialValue }: IProps) {
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
|
||||
return (
|
||||
<SliderInputFormField
|
||||
name={'parser_config.chunk_token_num'}
|
||||
label={t('chunkTokenNumber')}
|
||||
tooltip={t('chunkTokenNumberTip')}
|
||||
max={max}
|
||||
defaultValue={initialValue ?? 0}
|
||||
layout={FormLayout.Horizontal}
|
||||
></SliderInputFormField>
|
||||
);
|
||||
}
|
||||
37
ragflow_web/src/components/max-token-number.tsx
Normal file
37
ragflow_web/src/components/max-token-number.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { Flex, Form, InputNumber, Slider } from 'antd';
|
||||
|
||||
interface IProps {
|
||||
initialValue?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
const MaxTokenNumber = ({ initialValue = 512, max = 2048 }: IProps) => {
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
|
||||
return (
|
||||
<Form.Item label={t('chunkTokenNumber')} tooltip={t('chunkTokenNumberTip')}>
|
||||
<Flex gap={20} align="center">
|
||||
<Flex flex={1}>
|
||||
<Form.Item
|
||||
name={['parser_config', 'chunk_token_num']}
|
||||
noStyle
|
||||
initialValue={initialValue}
|
||||
rules={[{ required: true, message: t('chunkTokenNumberMessage') }]}
|
||||
>
|
||||
<Slider max={max} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item
|
||||
name={['parser_config', 'chunk_token_num']}
|
||||
noStyle
|
||||
rules={[{ required: true, message: t('chunkTokenNumberMessage') }]}
|
||||
>
|
||||
<InputNumber max={max} min={0} />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaxTokenNumber;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Form, InputNumber } from 'antd';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from './ui/form';
|
||||
import { NumberInput } from './ui/input';
|
||||
|
||||
const MessageHistoryWindowSizeItem = ({
|
||||
initialValue,
|
||||
}: {
|
||||
initialValue: number;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
name={'message_history_window_size'}
|
||||
label={t('flow.messageHistoryWindowSize')}
|
||||
initialValue={initialValue}
|
||||
tooltip={t('flow.messageHistoryWindowSizeTip')}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageHistoryWindowSizeItem;
|
||||
|
||||
export function MessageHistoryWindowSizeFormField() {
|
||||
const form = useFormContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'message_history_window_size'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('flow.messageHistoryWindowSizeTip')}>
|
||||
{t('flow.messageHistoryWindowSize')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<NumberInput {...field}></NumberInput>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
37
ragflow_web/src/components/message-input/index.less
Normal file
37
ragflow_web/src/components/message-input/index.less
Normal file
@@ -0,0 +1,37 @@
|
||||
.messageInputWrapper {
|
||||
margin-right: 20px;
|
||||
padding: '0px 0px 10px 0px';
|
||||
border: 1px solid #d9d9d9;
|
||||
&:hover {
|
||||
border-color: #40a9ff;
|
||||
box-shadow: #40a9ff;
|
||||
}
|
||||
border-radius: 8px;
|
||||
:global(.ant-input-affix-wrapper) {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.documentCard {
|
||||
:global(.ant-card-body) {
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.listWrapper {
|
||||
padding: 0 10px;
|
||||
overflow: auto;
|
||||
max-height: 170px;
|
||||
width: 100%;
|
||||
}
|
||||
.inputWrapper {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.deleteIcon {
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: -4px;
|
||||
color: #d92d20;
|
||||
}
|
||||
372
ragflow_web/src/components/message-input/index.tsx
Normal file
372
ragflow_web/src/components/message-input/index.tsx
Normal file
@@ -0,0 +1,372 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import {
|
||||
useDeleteDocument,
|
||||
useFetchDocumentInfosByIds,
|
||||
useRemoveNextDocument,
|
||||
useUploadAndParseDocument,
|
||||
} from '@/hooks/document-hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getExtension } from '@/utils/document-util';
|
||||
import { formatBytes } from '@/utils/file-util';
|
||||
import {
|
||||
CloseCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { GetProp, UploadFile } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Flex,
|
||||
Input,
|
||||
List,
|
||||
Space,
|
||||
Spin,
|
||||
Typography,
|
||||
Upload,
|
||||
UploadProps,
|
||||
} from 'antd';
|
||||
import get from 'lodash/get';
|
||||
import { CircleStop, Paperclip, SendHorizontal } from 'lucide-react';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import FileIcon from '../file-icon';
|
||||
import styles from './index.less';
|
||||
|
||||
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
||||
const { Text } = Typography;
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const getFileId = (file: UploadFile) => get(file, 'response.data.0');
|
||||
|
||||
const getFileIds = (fileList: UploadFile[]) => {
|
||||
const ids = fileList.reduce((pre, cur) => {
|
||||
return pre.concat(get(cur, 'response.data', []));
|
||||
}, []);
|
||||
|
||||
return ids;
|
||||
};
|
||||
|
||||
const isUploadSuccess = (file: UploadFile) => {
|
||||
const code = get(file, 'response.code');
|
||||
return typeof code === 'number' && code === 0;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
disabled: boolean;
|
||||
value: string;
|
||||
sendDisabled: boolean;
|
||||
sendLoading: boolean;
|
||||
onPressEnter(documentIds: string[]): void;
|
||||
onInputChange: ChangeEventHandler<HTMLTextAreaElement>;
|
||||
conversationId: string;
|
||||
uploadMethod?: string;
|
||||
isShared?: boolean;
|
||||
showUploadIcon?: boolean;
|
||||
createConversationBeforeUploadDocument?(message: string): Promise<any>;
|
||||
stopOutputMessage?(): void;
|
||||
}
|
||||
|
||||
const getBase64 = (file: FileType): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file as any);
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
|
||||
const MessageInput = ({
|
||||
isShared = false,
|
||||
disabled,
|
||||
value,
|
||||
onPressEnter,
|
||||
sendDisabled,
|
||||
sendLoading,
|
||||
onInputChange,
|
||||
conversationId,
|
||||
showUploadIcon = true,
|
||||
createConversationBeforeUploadDocument,
|
||||
uploadMethod = 'upload_and_parse',
|
||||
stopOutputMessage,
|
||||
}: IProps) => {
|
||||
const { t } = useTranslate('chat');
|
||||
const { removeDocument } = useRemoveNextDocument();
|
||||
const { deleteDocument } = useDeleteDocument();
|
||||
const { data: documentInfos, setDocumentIds } = useFetchDocumentInfosByIds();
|
||||
const { uploadAndParseDocument } = useUploadAndParseDocument(uploadMethod);
|
||||
const conversationIdRef = useRef(conversationId);
|
||||
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
if (!file.url && !file.preview) {
|
||||
file.preview = await getBase64(file.originFileObj as FileType);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange: UploadProps['onChange'] = async ({
|
||||
// fileList: newFileList,
|
||||
file,
|
||||
}) => {
|
||||
let nextConversationId: string = conversationId;
|
||||
if (createConversationBeforeUploadDocument) {
|
||||
const creatingRet = await createConversationBeforeUploadDocument(
|
||||
file.name,
|
||||
);
|
||||
if (creatingRet?.code === 0) {
|
||||
nextConversationId = creatingRet.data.id;
|
||||
}
|
||||
}
|
||||
setFileList((list) => {
|
||||
list.push({
|
||||
...file,
|
||||
status: 'uploading',
|
||||
originFileObj: file as any,
|
||||
});
|
||||
return [...list];
|
||||
});
|
||||
const ret = await uploadAndParseDocument({
|
||||
conversationId: nextConversationId,
|
||||
fileList: [file],
|
||||
});
|
||||
setFileList((list) => {
|
||||
const nextList = list.filter((x) => x.uid !== file.uid);
|
||||
nextList.push({
|
||||
...file,
|
||||
originFileObj: file as any,
|
||||
response: ret,
|
||||
percent: 100,
|
||||
status: ret?.code === 0 ? 'done' : 'error',
|
||||
});
|
||||
return nextList;
|
||||
});
|
||||
};
|
||||
|
||||
const isUploadingFile = fileList.some((x) => x.status === 'uploading');
|
||||
|
||||
const handlePressEnter = useCallback(async () => {
|
||||
if (isUploadingFile) return;
|
||||
const ids = getFileIds(fileList.filter((x) => isUploadSuccess(x)));
|
||||
|
||||
onPressEnter(ids);
|
||||
setFileList([]);
|
||||
}, [fileList, onPressEnter, isUploadingFile]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
async (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// check if it was shift + enter
|
||||
if (event.key === 'Enter' && event.shiftKey) return;
|
||||
if (event.key !== 'Enter') return;
|
||||
if (sendDisabled || isUploadingFile || sendLoading) return;
|
||||
|
||||
event.preventDefault();
|
||||
handlePressEnter();
|
||||
},
|
||||
[sendDisabled, isUploadingFile, sendLoading, handlePressEnter],
|
||||
);
|
||||
|
||||
const handleRemove = useCallback(
|
||||
async (file: UploadFile) => {
|
||||
const ids = get(file, 'response.data', []);
|
||||
// Upload Successfully
|
||||
if (Array.isArray(ids) && ids.length) {
|
||||
if (isShared) {
|
||||
await deleteDocument(ids);
|
||||
} else {
|
||||
await removeDocument(ids[0]);
|
||||
}
|
||||
setFileList((preList) => {
|
||||
return preList.filter((x) => getFileId(x) !== ids[0]);
|
||||
});
|
||||
} else {
|
||||
// Upload failed
|
||||
setFileList((preList) => {
|
||||
return preList.filter((x) => x.uid !== file.uid);
|
||||
});
|
||||
}
|
||||
},
|
||||
[removeDocument, deleteDocument, isShared],
|
||||
);
|
||||
|
||||
const handleStopOutputMessage = useCallback(() => {
|
||||
stopOutputMessage?.();
|
||||
}, [stopOutputMessage]);
|
||||
|
||||
const getDocumentInfoById = useCallback(
|
||||
(id: string) => {
|
||||
return documentInfos.find((x) => x.id === id);
|
||||
},
|
||||
[documentInfos],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const ids = getFileIds(fileList);
|
||||
setDocumentIds(ids);
|
||||
}, [fileList, setDocumentIds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
conversationIdRef.current &&
|
||||
conversationId !== conversationIdRef.current
|
||||
) {
|
||||
setFileList([]);
|
||||
}
|
||||
conversationIdRef.current = conversationId;
|
||||
}, [conversationId, setFileList]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
gap={1}
|
||||
vertical
|
||||
className={cn(styles.messageInputWrapper, 'dark:bg-black')}
|
||||
>
|
||||
<TextArea
|
||||
size="large"
|
||||
placeholder={t('sendPlaceholder')}
|
||||
value={value}
|
||||
allowClear
|
||||
disabled={disabled}
|
||||
style={{
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
padding: '0px 10px',
|
||||
marginTop: 10,
|
||||
}}
|
||||
autoSize={{ minRows: 2, maxRows: 10 }}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<Divider style={{ margin: '5px 30px 10px 0px' }} />
|
||||
<Flex justify="space-between" align="center">
|
||||
{fileList.length > 0 && (
|
||||
<List
|
||||
grid={{
|
||||
gutter: 16,
|
||||
xs: 1,
|
||||
sm: 1,
|
||||
md: 1,
|
||||
lg: 1,
|
||||
xl: 2,
|
||||
xxl: 4,
|
||||
}}
|
||||
dataSource={fileList}
|
||||
className={styles.listWrapper}
|
||||
renderItem={(item) => {
|
||||
const id = getFileId(item);
|
||||
const documentInfo = getDocumentInfoById(id);
|
||||
const fileExtension = getExtension(documentInfo?.name ?? '');
|
||||
const fileName = item.originFileObj?.name ?? '';
|
||||
|
||||
return (
|
||||
<List.Item>
|
||||
<Card className={styles.documentCard}>
|
||||
<Flex gap={10} align="center">
|
||||
{item.status === 'uploading' ? (
|
||||
<Spin
|
||||
indicator={
|
||||
<LoadingOutlined style={{ fontSize: 24 }} spin />
|
||||
}
|
||||
/>
|
||||
) : item.status === 'error' ? (
|
||||
<InfoCircleOutlined size={30}></InfoCircleOutlined>
|
||||
) : (
|
||||
<FileIcon id={id} name={fileName}></FileIcon>
|
||||
)}
|
||||
<Flex vertical style={{ width: '90%' }}>
|
||||
<Text
|
||||
ellipsis={{ tooltip: fileName }}
|
||||
className={styles.nameText}
|
||||
>
|
||||
<b> {fileName}</b>
|
||||
</Text>
|
||||
{item.status === 'error' ? (
|
||||
t('uploadFailed')
|
||||
) : (
|
||||
<>
|
||||
{item.percent !== 100 ? (
|
||||
t('uploading')
|
||||
) : !item.response ? (
|
||||
t('parsing')
|
||||
) : (
|
||||
<Space>
|
||||
<span>{fileExtension?.toUpperCase()},</span>
|
||||
<span>
|
||||
{formatBytes(
|
||||
getDocumentInfoById(id)?.size ?? 0,
|
||||
)}
|
||||
</span>
|
||||
</Space>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{item.status !== 'uploading' && (
|
||||
<span className={styles.deleteIcon}>
|
||||
<CloseCircleOutlined
|
||||
onClick={() => handleRemove(item)}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</Card>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Flex
|
||||
gap={5}
|
||||
align="center"
|
||||
justify="flex-end"
|
||||
style={{
|
||||
paddingRight: 10,
|
||||
paddingBottom: 10,
|
||||
width: fileList.length > 0 ? '50%' : '100%',
|
||||
}}
|
||||
>
|
||||
{showUploadIcon && (
|
||||
<Upload
|
||||
onPreview={handlePreview}
|
||||
onChange={handleChange}
|
||||
multiple={false}
|
||||
onRemove={handleRemove}
|
||||
showUploadList={false}
|
||||
beforeUpload={() => {
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<Button type={'primary'} disabled={disabled}>
|
||||
<Paperclip className="size-4" />
|
||||
</Button>
|
||||
</Upload>
|
||||
)}
|
||||
{sendLoading ? (
|
||||
<Button onClick={handleStopOutputMessage}>
|
||||
<CircleStop className="size-5" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handlePressEnter}
|
||||
loading={sendLoading}
|
||||
disabled={sendDisabled || isUploadingFile || sendLoading}
|
||||
>
|
||||
<SendHorizontal className="size-5" />
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MessageInput);
|
||||
188
ragflow_web/src/components/message-input/next.tsx
Normal file
188
ragflow_web/src/components/message-input/next.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
FileUpload,
|
||||
FileUploadDropzone,
|
||||
FileUploadItem,
|
||||
FileUploadItemDelete,
|
||||
FileUploadItemMetadata,
|
||||
FileUploadItemPreview,
|
||||
FileUploadItemProgress,
|
||||
FileUploadList,
|
||||
FileUploadTrigger,
|
||||
type FileUploadProps,
|
||||
} from '@/components/file-upload';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { t } from 'i18next';
|
||||
import { CircleStop, Paperclip, Send, Upload, X } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface IProps {
|
||||
disabled: boolean;
|
||||
value: string;
|
||||
sendDisabled: boolean;
|
||||
sendLoading: boolean;
|
||||
conversationId: string;
|
||||
uploadMethod?: string;
|
||||
isShared?: boolean;
|
||||
showUploadIcon?: boolean;
|
||||
isUploading?: boolean;
|
||||
onPressEnter(...prams: any[]): void;
|
||||
onInputChange: React.ChangeEventHandler<HTMLTextAreaElement>;
|
||||
createConversationBeforeUploadDocument?(message: string): Promise<any>;
|
||||
stopOutputMessage?(): void;
|
||||
onUpload?: NonNullable<FileUploadProps['onUpload']>;
|
||||
removeFile?(file: File): void;
|
||||
}
|
||||
|
||||
export function NextMessageInput({
|
||||
isUploading = false,
|
||||
value,
|
||||
sendDisabled,
|
||||
sendLoading,
|
||||
disabled,
|
||||
showUploadIcon = true,
|
||||
onUpload,
|
||||
onInputChange,
|
||||
stopOutputMessage,
|
||||
onPressEnter,
|
||||
removeFile,
|
||||
}: IProps) {
|
||||
const [files, setFiles] = React.useState<File[]>([]);
|
||||
|
||||
const onFileReject = React.useCallback((file: File, message: string) => {
|
||||
toast(message, {
|
||||
description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const submit = React.useCallback(() => {
|
||||
if (isUploading) return;
|
||||
onPressEnter();
|
||||
setFiles([]);
|
||||
}, [isUploading, onPressEnter]);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
submit();
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = React.useCallback(
|
||||
(event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
submit();
|
||||
},
|
||||
[submit],
|
||||
);
|
||||
|
||||
const handleRemoveFile = React.useCallback(
|
||||
(file: File) => () => {
|
||||
removeFile?.(file);
|
||||
},
|
||||
[removeFile],
|
||||
);
|
||||
|
||||
return (
|
||||
<FileUpload
|
||||
value={files}
|
||||
onValueChange={setFiles}
|
||||
onUpload={onUpload}
|
||||
onFileReject={onFileReject}
|
||||
className="relative w-full items-center "
|
||||
disabled={isUploading || disabled}
|
||||
>
|
||||
<FileUploadDropzone
|
||||
tabIndex={-1}
|
||||
// Prevents the dropzone from triggering on click
|
||||
onClick={(event) => event.preventDefault()}
|
||||
className="absolute top-0 left-0 z-0 flex size-full items-center justify-center rounded-none border-none bg-background/50 p-0 opacity-0 backdrop-blur transition-opacity duration-200 ease-out data-[dragging]:z-10 data-[dragging]:opacity-100"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-1 text-center">
|
||||
<div className="flex items-center justify-center rounded-full border p-2.5">
|
||||
<Upload className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="font-medium text-sm">Drag & drop files here</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Upload max 5 files each up to 5MB
|
||||
</p>
|
||||
</div>
|
||||
</FileUploadDropzone>
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="relative flex w-full flex-col gap-2.5 rounded-md border border-input px-3 py-2 outline-none focus-within:ring-1 focus-within:ring-ring/50"
|
||||
>
|
||||
<FileUploadList
|
||||
orientation="horizontal"
|
||||
className="overflow-x-auto px-0 py-1"
|
||||
>
|
||||
{files.map((file, index) => (
|
||||
<FileUploadItem key={index} value={file} className="max-w-52 p-1.5">
|
||||
<FileUploadItemPreview className="size-8 [&>svg]:size-5">
|
||||
<FileUploadItemProgress variant="fill" />
|
||||
</FileUploadItemPreview>
|
||||
<FileUploadItemMetadata size="sm" />
|
||||
<FileUploadItemDelete asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="-top-1 -right-1 absolute size-4 shrink-0 cursor-pointer rounded-full"
|
||||
onClick={handleRemoveFile(file)}
|
||||
>
|
||||
<X className="size-2.5" />
|
||||
</Button>
|
||||
</FileUploadItemDelete>
|
||||
</FileUploadItem>
|
||||
))}
|
||||
</FileUploadList>
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={onInputChange}
|
||||
placeholder={t('chat.messagePlaceholder')}
|
||||
className="field-sizing-content min-h-10 w-full resize-none border-0 bg-transparent p-0 shadow-none focus-visible:ring-0 dark:bg-transparent"
|
||||
disabled={isUploading || disabled || sendLoading}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<div
|
||||
className={cn('flex items-center justify-between gap-1.5', {
|
||||
'justify-end': !showUploadIcon,
|
||||
})}
|
||||
>
|
||||
{showUploadIcon && (
|
||||
<FileUploadTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-7 rounded-sm"
|
||||
disabled={isUploading || sendLoading}
|
||||
>
|
||||
<Paperclip className="size-3.5" />
|
||||
<span className="sr-only">Attach file</span>
|
||||
</Button>
|
||||
</FileUploadTrigger>
|
||||
)}
|
||||
{sendLoading ? (
|
||||
<Button onClick={stopOutputMessage} className="size-5 rounded-sm">
|
||||
<CircleStop />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="size-5 rounded-sm"
|
||||
disabled={
|
||||
sendDisabled || isUploading || sendLoading || !value.trim()
|
||||
}
|
||||
>
|
||||
<Send />
|
||||
<span className="sr-only">Send message</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</FileUpload>
|
||||
);
|
||||
}
|
||||
51
ragflow_web/src/components/message-item/feedback-modal.tsx
Normal file
51
ragflow_web/src/components/message-item/feedback-modal.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Form, Input, Modal } from 'antd';
|
||||
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
type FieldType = {
|
||||
feedback?: string;
|
||||
};
|
||||
|
||||
const FeedbackModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
onOk,
|
||||
loading,
|
||||
}: IModalProps<IFeedbackRequestBody>) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleOk = useCallback(async () => {
|
||||
const ret = await form.validateFields();
|
||||
return onOk?.({ thumbup: false, feedback: ret.feedback });
|
||||
}, [onOk, form]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Feedback"
|
||||
open={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={hideModal}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
name="basic"
|
||||
labelCol={{ span: 0 }}
|
||||
wrapperCol={{ span: 24 }}
|
||||
style={{ maxWidth: 600 }}
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
name="feedback"
|
||||
rules={[{ required: true, message: 'Please input your feedback!' }]}
|
||||
>
|
||||
<Input.TextArea rows={8} placeholder="Please input your feedback!" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackModal;
|
||||
145
ragflow_web/src/components/message-item/group-button.tsx
Normal file
145
ragflow_web/src/components/message-item/group-button.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { PromptIcon } from '@/assets/icon/next-icon';
|
||||
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { IRemoveMessageById } from '@/hooks/logic-hooks';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
DislikeOutlined,
|
||||
LikeOutlined,
|
||||
PauseCircleOutlined,
|
||||
SoundOutlined,
|
||||
SyncOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Radio, Tooltip } from 'antd';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FeedbackModal from './feedback-modal';
|
||||
import { useRemoveMessage, useSendFeedback, useSpeech } from './hooks';
|
||||
import PromptModal from './prompt-modal';
|
||||
|
||||
interface IProps {
|
||||
messageId: string;
|
||||
content: string;
|
||||
prompt?: string;
|
||||
showLikeButton: boolean;
|
||||
audioBinary?: string;
|
||||
showLoudspeaker?: boolean;
|
||||
}
|
||||
|
||||
export const AssistantGroupButton = ({
|
||||
messageId,
|
||||
content,
|
||||
prompt,
|
||||
audioBinary,
|
||||
showLikeButton,
|
||||
showLoudspeaker = true,
|
||||
}: IProps) => {
|
||||
const { visible, hideModal, showModal, onFeedbackOk, loading } =
|
||||
useSendFeedback(messageId);
|
||||
const {
|
||||
visible: promptVisible,
|
||||
hideModal: hidePromptModal,
|
||||
showModal: showPromptModal,
|
||||
} = useSetModalState();
|
||||
const { t } = useTranslation();
|
||||
const { handleRead, ref, isPlaying } = useSpeech(content, audioBinary);
|
||||
|
||||
const handleLike = useCallback(() => {
|
||||
onFeedbackOk({ thumbup: true });
|
||||
}, [onFeedbackOk]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Radio.Group size="small">
|
||||
<Radio.Button value="a">
|
||||
<CopyToClipboard text={content}></CopyToClipboard>
|
||||
</Radio.Button>
|
||||
{showLoudspeaker && (
|
||||
<Radio.Button value="b" onClick={handleRead}>
|
||||
<Tooltip title={t('chat.read')}>
|
||||
{isPlaying ? <PauseCircleOutlined /> : <SoundOutlined />}
|
||||
</Tooltip>
|
||||
<audio src="" ref={ref}></audio>
|
||||
</Radio.Button>
|
||||
)}
|
||||
{showLikeButton && (
|
||||
<>
|
||||
<Radio.Button value="c" onClick={handleLike}>
|
||||
<LikeOutlined />
|
||||
</Radio.Button>
|
||||
<Radio.Button value="d" onClick={showModal}>
|
||||
<DislikeOutlined />
|
||||
</Radio.Button>
|
||||
</>
|
||||
)}
|
||||
{prompt && (
|
||||
<Radio.Button value="e" onClick={showPromptModal}>
|
||||
<PromptIcon style={{ fontSize: '16px' }} />
|
||||
</Radio.Button>
|
||||
)}
|
||||
</Radio.Group>
|
||||
{visible && (
|
||||
<FeedbackModal
|
||||
visible={visible}
|
||||
hideModal={hideModal}
|
||||
onOk={onFeedbackOk}
|
||||
loading={loading}
|
||||
></FeedbackModal>
|
||||
)}
|
||||
{promptVisible && (
|
||||
<PromptModal
|
||||
visible={promptVisible}
|
||||
hideModal={hidePromptModal}
|
||||
prompt={prompt}
|
||||
></PromptModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface UserGroupButtonProps extends Partial<IRemoveMessageById> {
|
||||
messageId: string;
|
||||
content: string;
|
||||
regenerateMessage?: () => void;
|
||||
sendLoading: boolean;
|
||||
}
|
||||
|
||||
export const UserGroupButton = ({
|
||||
content,
|
||||
messageId,
|
||||
sendLoading,
|
||||
removeMessageById,
|
||||
regenerateMessage,
|
||||
}: UserGroupButtonProps) => {
|
||||
const { onRemoveMessage, loading } = useRemoveMessage(
|
||||
messageId,
|
||||
removeMessageById,
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Radio.Group size="small">
|
||||
<Radio.Button value="a">
|
||||
<CopyToClipboard text={content}></CopyToClipboard>
|
||||
</Radio.Button>
|
||||
{regenerateMessage && (
|
||||
<Radio.Button
|
||||
value="b"
|
||||
onClick={regenerateMessage}
|
||||
disabled={sendLoading}
|
||||
>
|
||||
<Tooltip title={t('chat.regenerate')}>
|
||||
<SyncOutlined spin={sendLoading} />
|
||||
</Tooltip>
|
||||
</Radio.Button>
|
||||
)}
|
||||
{removeMessageById && (
|
||||
<Radio.Button value="c" onClick={onRemoveMessage} disabled={loading}>
|
||||
<Tooltip title={t('common.delete')}>
|
||||
<DeleteOutlined spin={loading} />
|
||||
</Tooltip>
|
||||
</Radio.Button>
|
||||
)}
|
||||
</Radio.Group>
|
||||
);
|
||||
};
|
||||
116
ragflow_web/src/components/message-item/hooks.ts
Normal file
116
ragflow_web/src/components/message-item/hooks.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { useDeleteMessage, useFeedback } from '@/hooks/chat-hooks';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { IRemoveMessageById, useSpeechWithSse } from '@/hooks/logic-hooks';
|
||||
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
|
||||
import { hexStringToUint8Array } from '@/utils/common-util';
|
||||
import { SpeechPlayer } from 'openai-speech-stream-player';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export const useSendFeedback = (messageId: string) => {
|
||||
const { visible, hideModal, showModal } = useSetModalState();
|
||||
const { feedback, loading } = useFeedback();
|
||||
|
||||
const onFeedbackOk = useCallback(
|
||||
async (params: IFeedbackRequestBody) => {
|
||||
const ret = await feedback({
|
||||
...params,
|
||||
messageId: messageId,
|
||||
});
|
||||
|
||||
if (ret === 0) {
|
||||
hideModal();
|
||||
}
|
||||
},
|
||||
[feedback, hideModal, messageId],
|
||||
);
|
||||
|
||||
return {
|
||||
loading,
|
||||
onFeedbackOk,
|
||||
visible,
|
||||
hideModal,
|
||||
showModal,
|
||||
};
|
||||
};
|
||||
|
||||
export const useRemoveMessage = (
|
||||
messageId: string,
|
||||
removeMessageById?: IRemoveMessageById['removeMessageById'],
|
||||
) => {
|
||||
const { deleteMessage, loading } = useDeleteMessage();
|
||||
|
||||
const onRemoveMessage = useCallback(async () => {
|
||||
if (messageId) {
|
||||
const code = await deleteMessage(messageId);
|
||||
if (code === 0) {
|
||||
removeMessageById?.(messageId);
|
||||
}
|
||||
}
|
||||
}, [deleteMessage, messageId, removeMessageById]);
|
||||
|
||||
return { onRemoveMessage, loading };
|
||||
};
|
||||
|
||||
export const useSpeech = (content: string, audioBinary?: string) => {
|
||||
const ref = useRef<HTMLAudioElement>(null);
|
||||
const { read } = useSpeechWithSse();
|
||||
const player = useRef<SpeechPlayer>();
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
|
||||
const initialize = useCallback(async () => {
|
||||
player.current = new SpeechPlayer({
|
||||
audio: ref.current!,
|
||||
onPlaying: () => {
|
||||
setIsPlaying(true);
|
||||
},
|
||||
onPause: () => {
|
||||
setIsPlaying(false);
|
||||
},
|
||||
onChunkEnd: () => {},
|
||||
mimeType: MediaSource.isTypeSupported('audio/mpeg')
|
||||
? 'audio/mpeg'
|
||||
: 'audio/mp4; codecs="mp4a.40.2"', // https://stackoverflow.com/questions/64079424/cannot-replay-mp3-in-firefox-using-mediasource-even-though-it-works-in-chrome
|
||||
});
|
||||
await player.current.init();
|
||||
}, []);
|
||||
|
||||
const pause = useCallback(() => {
|
||||
player.current?.pause();
|
||||
}, []);
|
||||
|
||||
const speech = useCallback(async () => {
|
||||
const response = await read({ text: content });
|
||||
if (response) {
|
||||
player?.current?.feedWithResponse(response);
|
||||
}
|
||||
}, [read, content]);
|
||||
|
||||
const handleRead = useCallback(async () => {
|
||||
if (isPlaying) {
|
||||
setIsPlaying(false);
|
||||
pause();
|
||||
} else {
|
||||
setIsPlaying(true);
|
||||
speech();
|
||||
}
|
||||
}, [setIsPlaying, speech, isPlaying, pause]);
|
||||
|
||||
useEffect(() => {
|
||||
if (audioBinary) {
|
||||
const units = hexStringToUint8Array(audioBinary);
|
||||
if (units) {
|
||||
try {
|
||||
player.current?.feed(units);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [audioBinary]);
|
||||
|
||||
useEffect(() => {
|
||||
initialize();
|
||||
}, [initialize]);
|
||||
|
||||
return { ref, handleRead, isPlaying };
|
||||
};
|
||||
62
ragflow_web/src/components/message-item/index.less
Normal file
62
ragflow_web/src/components/message-item/index.less
Normal file
@@ -0,0 +1,62 @@
|
||||
.messageItem {
|
||||
padding: 24px 0;
|
||||
.messageItemSection {
|
||||
display: inline-block;
|
||||
}
|
||||
.messageItemSectionLeft {
|
||||
width: 80%;
|
||||
}
|
||||
.messageItemContent {
|
||||
display: inline-flex;
|
||||
gap: 20px;
|
||||
}
|
||||
.messageItemContentReverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.messageTextBase() {
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
& > p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.messageText {
|
||||
.chunkText();
|
||||
.messageTextBase();
|
||||
// background-color: #e6f4ff;
|
||||
word-break: break-word;
|
||||
}
|
||||
.messageTextDark {
|
||||
.chunkText();
|
||||
.messageTextBase();
|
||||
word-break: break-word;
|
||||
:global(section.think) {
|
||||
color: rgb(166, 166, 166);
|
||||
border-left-color: rgb(78, 78, 86);
|
||||
}
|
||||
}
|
||||
|
||||
.messageUserText {
|
||||
.chunkText();
|
||||
.messageTextBase();
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
word-break: break-word;
|
||||
text-align: justify;
|
||||
}
|
||||
.messageEmpty {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.thumbnailImg {
|
||||
max-width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.messageItemLeft {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.messageItemRight {
|
||||
text-align: right;
|
||||
}
|
||||
166
ragflow_web/src/components/message-item/index.tsx
Normal file
166
ragflow_web/src/components/message-item/index.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
|
||||
import classNames from 'classnames';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
useFetchDocumentInfosByIds,
|
||||
useFetchDocumentThumbnailsByIds,
|
||||
} from '@/hooks/document-hooks';
|
||||
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { IMessage } from '@/pages/chat/interface';
|
||||
import MarkdownContent from '@/pages/chat/markdown-content';
|
||||
import { Avatar, Flex, Space } from 'antd';
|
||||
import { ReferenceDocumentList } from '../next-message-item/reference-document-list';
|
||||
import { InnerUploadedMessageFiles } from '../next-message-item/uploaded-message-files';
|
||||
import { useTheme } from '../theme-provider';
|
||||
import { AssistantGroupButton, UserGroupButton } from './group-button';
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps extends Partial<IRemoveMessageById>, IRegenerateMessage {
|
||||
item: IMessage;
|
||||
reference: IReference;
|
||||
loading?: boolean;
|
||||
sendLoading?: boolean;
|
||||
visibleAvatar?: boolean;
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
avatarDialog?: string | null;
|
||||
clickDocumentButton?: (documentId: string, chunk: IReferenceChunk) => void;
|
||||
index: number;
|
||||
showLikeButton?: boolean;
|
||||
showLoudspeaker?: boolean;
|
||||
}
|
||||
|
||||
const MessageItem = ({
|
||||
item,
|
||||
reference,
|
||||
loading = false,
|
||||
avatar,
|
||||
avatarDialog,
|
||||
sendLoading = false,
|
||||
clickDocumentButton,
|
||||
index,
|
||||
removeMessageById,
|
||||
regenerateMessage,
|
||||
showLikeButton = true,
|
||||
showLoudspeaker = true,
|
||||
visibleAvatar = true,
|
||||
}: IProps) => {
|
||||
const { theme } = useTheme();
|
||||
const isAssistant = item.role === MessageType.Assistant;
|
||||
const isUser = item.role === MessageType.User;
|
||||
const { data: documentList, setDocumentIds } = useFetchDocumentInfosByIds();
|
||||
const { data: documentThumbnails, setDocumentIds: setIds } =
|
||||
useFetchDocumentThumbnailsByIds();
|
||||
|
||||
const referenceDocumentList = useMemo(() => {
|
||||
return reference?.doc_aggs ?? [];
|
||||
}, [reference?.doc_aggs]);
|
||||
|
||||
const handleRegenerateMessage = useCallback(() => {
|
||||
regenerateMessage?.(item);
|
||||
}, [regenerateMessage, item]);
|
||||
|
||||
useEffect(() => {
|
||||
const ids = item?.doc_ids ?? [];
|
||||
if (ids.length) {
|
||||
setDocumentIds(ids);
|
||||
const documentIds = ids.filter((x) => !(x in documentThumbnails));
|
||||
if (documentIds.length) {
|
||||
setIds(documentIds);
|
||||
}
|
||||
}
|
||||
}, [item.doc_ids, setDocumentIds, setIds, documentThumbnails]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.messageItem, {
|
||||
[styles.messageItemLeft]: item.role === MessageType.Assistant,
|
||||
[styles.messageItemRight]: item.role === MessageType.User,
|
||||
})}
|
||||
>
|
||||
<section
|
||||
className={classNames(styles.messageItemSection, {
|
||||
[styles.messageItemSectionLeft]: item.role === MessageType.Assistant,
|
||||
[styles.messageItemSectionRight]: item.role === MessageType.User,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames(styles.messageItemContent, {
|
||||
[styles.messageItemContentReverse]: item.role === MessageType.User,
|
||||
})}
|
||||
>
|
||||
{visibleAvatar &&
|
||||
(item.role === MessageType.User ? (
|
||||
<Avatar size={40} src={avatar ?? '/logo.svg'} />
|
||||
) : avatarDialog ? (
|
||||
<Avatar size={40} src={avatarDialog} />
|
||||
) : (
|
||||
<AssistantIcon />
|
||||
))}
|
||||
|
||||
<Flex vertical gap={8} flex={1}>
|
||||
<Space>
|
||||
{isAssistant ? (
|
||||
index !== 0 && (
|
||||
<AssistantGroupButton
|
||||
messageId={item.id}
|
||||
content={item.content}
|
||||
prompt={item.prompt}
|
||||
showLikeButton={showLikeButton}
|
||||
audioBinary={item.audio_binary}
|
||||
showLoudspeaker={showLoudspeaker}
|
||||
></AssistantGroupButton>
|
||||
)
|
||||
) : (
|
||||
<UserGroupButton
|
||||
content={item.content}
|
||||
messageId={item.id}
|
||||
removeMessageById={removeMessageById}
|
||||
regenerateMessage={
|
||||
regenerateMessage && handleRegenerateMessage
|
||||
}
|
||||
sendLoading={sendLoading}
|
||||
></UserGroupButton>
|
||||
)}
|
||||
|
||||
{/* <b>{isAssistant ? '' : nickname}</b> */}
|
||||
</Space>
|
||||
<div
|
||||
className={cn(
|
||||
isAssistant
|
||||
? theme === 'dark'
|
||||
? styles.messageTextDark
|
||||
: styles.messageText
|
||||
: styles.messageUserText,
|
||||
{ '!bg-bg-card': !isAssistant },
|
||||
)}
|
||||
>
|
||||
<MarkdownContent
|
||||
loading={loading}
|
||||
content={item.content}
|
||||
reference={reference}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
></MarkdownContent>
|
||||
</div>
|
||||
{isAssistant && referenceDocumentList.length > 0 && (
|
||||
<ReferenceDocumentList
|
||||
list={referenceDocumentList}
|
||||
></ReferenceDocumentList>
|
||||
)}
|
||||
{isUser && documentList.length > 0 && (
|
||||
<InnerUploadedMessageFiles
|
||||
files={documentList}
|
||||
></InnerUploadedMessageFiles>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MessageItem);
|
||||
30
ragflow_web/src/components/message-item/prompt-modal.tsx
Normal file
30
ragflow_web/src/components/message-item/prompt-modal.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
|
||||
import { Modal, Space } from 'antd';
|
||||
import HightLightMarkdown from '../highlight-markdown';
|
||||
import SvgIcon from '../svg-icon';
|
||||
|
||||
const PromptModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
prompt,
|
||||
}: IModalProps<IFeedbackRequestBody> & { prompt?: string }) => {
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<SvgIcon name={`prompt`} width={18}></SvgIcon>
|
||||
Prompt
|
||||
</Space>
|
||||
}
|
||||
width={'80%'}
|
||||
open={visible}
|
||||
onCancel={hideModal}
|
||||
footer={null}
|
||||
>
|
||||
<HightLightMarkdown>{prompt}</HightLightMarkdown>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptModal;
|
||||
75
ragflow_web/src/components/metadata-filter/index.tsx
Normal file
75
ragflow_web/src/components/metadata-filter/index.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { DatasetMetadata } from '@/constants/chat';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { SelectWithSearch } from '../originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '../ragflow-form';
|
||||
import { MetadataFilterConditions } from './metadata-filter-conditions';
|
||||
|
||||
type MetadataFilterProps = {
|
||||
prefix?: string;
|
||||
};
|
||||
|
||||
export const MetadataFilterSchema = {
|
||||
meta_data_filter: z
|
||||
.object({
|
||||
method: z.string().optional(),
|
||||
manual: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
op: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
};
|
||||
|
||||
export function MetadataFilter({ prefix = '' }: MetadataFilterProps) {
|
||||
const { t } = useTranslate('chat');
|
||||
const form = useFormContext();
|
||||
|
||||
const methodName = prefix + 'meta_data_filter.method';
|
||||
|
||||
const kbIds: string[] = useWatch({
|
||||
control: form.control,
|
||||
name: prefix + 'kb_ids',
|
||||
});
|
||||
const metadata = useWatch({
|
||||
control: form.control,
|
||||
name: methodName,
|
||||
});
|
||||
const hasKnowledge = Array.isArray(kbIds) && kbIds.length > 0;
|
||||
|
||||
const MetadataOptions = Object.values(DatasetMetadata).map((x) => {
|
||||
return {
|
||||
value: x,
|
||||
label: t(`meta.${x}`),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasKnowledge && (
|
||||
<RAGFlowFormItem
|
||||
label={t('metadata')}
|
||||
name={methodName}
|
||||
tooltip={t('metadataTip')}
|
||||
>
|
||||
<SelectWithSearch
|
||||
options={MetadataOptions}
|
||||
triggerClassName="!bg-bg-input"
|
||||
/>
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
{hasKnowledge && metadata === DatasetMetadata.Manual && (
|
||||
<MetadataFilterConditions
|
||||
kbIds={kbIds}
|
||||
prefix={prefix}
|
||||
></MetadataFilterConditions>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { useFetchKnowledgeMetadata } from '@/hooks/use-knowledge-request';
|
||||
import { SwitchOperatorOptions } from '@/pages/agent/constant';
|
||||
import { useBuildSwitchOperatorOptions } from '@/pages/agent/form/switch-form';
|
||||
import { Plus, X } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function MetadataFilterConditions({
|
||||
kbIds,
|
||||
prefix = '',
|
||||
}: {
|
||||
kbIds: string[];
|
||||
prefix?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
const name = prefix + 'meta_data_filter.manual';
|
||||
const metadata = useFetchKnowledgeMetadata(kbIds);
|
||||
|
||||
const switchOperatorOptions = useBuildSwitchOperatorOptions();
|
||||
|
||||
const { fields, remove, append } = useFieldArray({
|
||||
name,
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const add = useCallback(
|
||||
(key: string) => () => {
|
||||
append({
|
||||
key,
|
||||
value: '',
|
||||
op: SwitchOperatorOptions[0].value,
|
||||
});
|
||||
},
|
||||
[append],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>{t('chat.conditions')}</FormLabel>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant={'ghost'} type="button">
|
||||
<Plus />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="max-h-[300px] !overflow-y-auto scrollbar-auto">
|
||||
{Object.keys(metadata.data).map((key, idx) => {
|
||||
return (
|
||||
<DropdownMenuItem key={idx} onClick={add(key)}>
|
||||
{key}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="space-y-5">
|
||||
{fields.map((field, index) => {
|
||||
const typeField = `${name}.${index}.key`;
|
||||
return (
|
||||
<div key={field.id} className="flex w-full items-center gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={typeField}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1 overflow-hidden">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t('common.pleaseInput')}
|
||||
></Input>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Separator className="w-3 text-text-secondary" />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`${name}.${index}.op`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1 overflow-hidden">
|
||||
<FormControl>
|
||||
<SelectWithSearch
|
||||
{...field}
|
||||
options={switchOperatorOptions}
|
||||
></SelectWithSearch>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Separator className="w-3 text-text-secondary" />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`${name}.${index}.value`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1 overflow-hidden">
|
||||
<FormControl>
|
||||
<Input placeholder={t('common.pleaseInput')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||
<X className="text-text-sub-title-invert " />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
25
ragflow_web/src/components/modal-manager.tsx
Normal file
25
ragflow_web/src/components/modal-manager.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export interface IModalManagerChildrenProps {
|
||||
showModal(): void;
|
||||
hideModal(): void;
|
||||
visible: boolean;
|
||||
}
|
||||
interface IProps {
|
||||
children: (props: IModalManagerChildrenProps) => React.ReactNode;
|
||||
}
|
||||
|
||||
const ModalManager = ({ children }: IProps) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const showModal = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
const hideModal = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
return children({ visible, showModal, hideModal });
|
||||
};
|
||||
|
||||
export default ModalManager;
|
||||
20
ragflow_web/src/components/more-button.tsx
Normal file
20
ragflow_web/src/components/more-button.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Ellipsis } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { Button, ButtonProps } from './ui/button';
|
||||
|
||||
export const MoreButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, size, ...props }, ref) => {
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size={size || 'icon'}
|
||||
className={cn('invisible group-hover:visible size-3.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<Ellipsis />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
);
|
||||
51
ragflow_web/src/components/new-document-link.tsx
Normal file
51
ragflow_web/src/components/new-document-link.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
getExtension,
|
||||
isSupportedPreviewDocumentType,
|
||||
} from '@/utils/document-util';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps extends React.PropsWithChildren {
|
||||
link?: string;
|
||||
preventDefault?: boolean;
|
||||
color?: string;
|
||||
documentName: string;
|
||||
documentId?: string;
|
||||
prefix?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const NewDocumentLink = ({
|
||||
children,
|
||||
link,
|
||||
preventDefault = false,
|
||||
color = 'rgb(15, 79, 170)',
|
||||
documentId,
|
||||
documentName,
|
||||
prefix = 'file',
|
||||
className,
|
||||
}: IProps) => {
|
||||
let nextLink = link;
|
||||
const extension = getExtension(documentName);
|
||||
if (!link) {
|
||||
nextLink = `/document/${documentId}?ext=${extension}&prefix=${prefix}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
target="_blank"
|
||||
onClick={
|
||||
!preventDefault || isSupportedPreviewDocumentType(extension)
|
||||
? undefined
|
||||
: (e) => e.preventDefault()
|
||||
}
|
||||
href={nextLink}
|
||||
rel="noreferrer"
|
||||
style={{ color: className ? '' : color, wordBreak: 'break-all' }}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewDocumentLink;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user