mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-08 04:22:20 +08:00
Refactor: Remove ant design component (#13143)
### What problem does this PR solve? _Briefly describe what this PR aims to solve. Include background context that will help reviewers understand the purpose of the PR._ ### Type of change - [x] Refactoring
This commit is contained in:
@@ -1,16 +1,8 @@
|
||||
import { Toaster as Sonner } from '@/components/ui/sonner';
|
||||
import { Toaster } from '@/components/ui/toaster';
|
||||
import i18n from '@/locales/config';
|
||||
import i18n, { changeLanguageAsync } from '@/locales/config';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { configResponsive } from 'ahooks';
|
||||
import { App, ConfigProvider, ConfigProviderProps, theme } from 'antd';
|
||||
import pt_BR from 'antd/lib/locale/pt_BR';
|
||||
import deDE from 'antd/locale/de_DE';
|
||||
import enUS from 'antd/locale/en_US';
|
||||
import ru_RU from 'antd/locale/ru_RU';
|
||||
import vi_VN from 'antd/locale/vi_VN';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import zh_HK from 'antd/locale/zh_HK';
|
||||
import dayjs from 'dayjs';
|
||||
import advancedFormat from 'dayjs/plugin/advancedFormat';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
@@ -18,13 +10,12 @@ import localeData from 'dayjs/plugin/localeData';
|
||||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||||
import weekYear from 'dayjs/plugin/weekYear';
|
||||
import weekday from 'dayjs/plugin/weekday';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { RouterProvider } from 'react-router';
|
||||
import { ThemeProvider, useTheme } from './components/theme-provider';
|
||||
import { ThemeProvider } from './components/theme-provider';
|
||||
import { SidebarProvider } from './components/ui/sidebar';
|
||||
import { TooltipProvider } from './components/ui/tooltip';
|
||||
import { ThemeEnum } from './constants/common';
|
||||
// import { getRouter } from './routes';
|
||||
import { routers } from './routes';
|
||||
import storage from './utils/authorization-util';
|
||||
|
||||
@@ -47,24 +38,6 @@ dayjs.extend(localeData);
|
||||
dayjs.extend(weekOfYear);
|
||||
dayjs.extend(weekYear);
|
||||
|
||||
const AntLanguageMap = {
|
||||
en: enUS,
|
||||
zh: zhCN,
|
||||
'zh-TRADITIONAL': zh_HK,
|
||||
ru: ru_RU,
|
||||
vi: vi_VN,
|
||||
'pt-BR': pt_BR,
|
||||
de: deDE,
|
||||
};
|
||||
|
||||
// if (process.env.NODE_ENV === 'development') {
|
||||
// const whyDidYouRender = require('@welldone-software/why-did-you-render');
|
||||
// whyDidYouRender(React, {
|
||||
// trackAllPureComponents: true,
|
||||
// trackExtraHooks: [],
|
||||
// logOnDifferentValues: true,
|
||||
// });
|
||||
// }
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
import('@welldone-software/why-did-you-render').then(
|
||||
(whyDidYouRenderModule) => {
|
||||
@@ -78,6 +51,7 @@ if (process.env.NODE_ENV === 'development') {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
@@ -87,53 +61,31 @@ const queryClient = new QueryClient({
|
||||
},
|
||||
});
|
||||
|
||||
type Locale = ConfigProviderProps['locale'];
|
||||
|
||||
function Root({ children }: React.PropsWithChildren) {
|
||||
const { theme: themeragflow } = useTheme();
|
||||
const getLocale = (lng: string) =>
|
||||
AntLanguageMap[lng as keyof typeof AntLanguageMap] ?? enUS;
|
||||
|
||||
const [locale, setLocal] = useState<Locale>(getLocale(storage.getLanguage()));
|
||||
useEffect(() => {
|
||||
const lng = storage.getLanguage();
|
||||
if (lng) {
|
||||
document.documentElement.lang = lng;
|
||||
}
|
||||
}, []);
|
||||
|
||||
i18n.on('languageChanged', function (lng: string) {
|
||||
storage.setLanguage(lng);
|
||||
setLocal(getLocale(lng));
|
||||
// Should reflect to <html lang="...">
|
||||
document.documentElement.lang = lng;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
fontFamily: 'Inter',
|
||||
},
|
||||
algorithm:
|
||||
themeragflow === 'dark'
|
||||
? theme.darkAlgorithm
|
||||
: theme.defaultAlgorithm,
|
||||
}}
|
||||
locale={locale}
|
||||
>
|
||||
<SidebarProvider className="h-full">
|
||||
<App className="w-full h-dvh relative">{children}</App>
|
||||
</SidebarProvider>
|
||||
<Sonner position={'top-right'} expand richColors closeButton></Sonner>
|
||||
<Toaster />
|
||||
</ConfigProvider>
|
||||
{/* <ReactQueryDevtools buttonPosition={'top-left'} initialIsOpen={false} /> */}
|
||||
</>
|
||||
<SidebarProvider className="h-full">
|
||||
<div className="w-full h-dvh relative">{children}</div>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const RootProvider = ({ children }: React.PropsWithChildren) => {
|
||||
useEffect(() => {
|
||||
// Because the language is saved in the backend, a token is required to obtain the api. However, the login page cannot obtain the language through the getUserInfo api, so the language needs to be saved in localstorage.
|
||||
const lng = storage.getLanguage();
|
||||
if (lng) {
|
||||
i18n.changeLanguage(lng);
|
||||
changeLanguageAsync(lng);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -145,6 +97,8 @@ const RootProvider = ({ children }: React.PropsWithChildren) => {
|
||||
storageKey="ragflow-ui-theme"
|
||||
>
|
||||
<Root>{children}</Root>
|
||||
<Sonner position={'top-right'} expand richColors closeButton></Sonner>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</TooltipProvider>
|
||||
@@ -159,16 +113,6 @@ const RouterProviderWrapper: React.FC<{ router: typeof routers }> = ({
|
||||
RouterProviderWrapper.whyDidYouRender = false;
|
||||
|
||||
export default function AppContainer() {
|
||||
// const [router, setRouter] = useState<any>(null);
|
||||
|
||||
// useEffect(() => {
|
||||
// getRouter().then(setRouter);
|
||||
// }, []);
|
||||
|
||||
// if (!router) {
|
||||
// return <div>Loading...</div>;
|
||||
// }
|
||||
|
||||
return (
|
||||
<RootProvider>
|
||||
<RouterProviderWrapper router={routers} />
|
||||
|
||||
@@ -165,3 +165,24 @@ export const useFetchDocx = (filePath: string) => {
|
||||
|
||||
return { succeed, containerRef, error };
|
||||
};
|
||||
|
||||
export const useCatchDocumentError = (url: string) => {
|
||||
const httpHeaders = useMemo(() => {
|
||||
return {
|
||||
[Authorization]: getAuthorization(),
|
||||
};
|
||||
}, []);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const fetchDocument = useCallback(async () => {
|
||||
const { data } = await axios.get(url, { headers: httpHeaders });
|
||||
if (data.code !== 0) {
|
||||
setError(data?.message);
|
||||
}
|
||||
}, [url, httpHeaders]);
|
||||
useEffect(() => {
|
||||
fetchDocument();
|
||||
}, [fetchDocument]);
|
||||
|
||||
return error;
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Spin } from '@/components/ui/spin';
|
||||
import { Authorization } from '@/constants/authorization';
|
||||
import FileError from '@/pages/document-viewer/file-error';
|
||||
import { getAuthorization } from '@/utils/authorization-util';
|
||||
import { useCatchDocumentError } from '../pdf-previewer/hooks';
|
||||
import { useCatchDocumentError } from './hooks';
|
||||
import styles from './index.module.less';
|
||||
type PdfLoaderProps = React.ComponentProps<typeof PdfLoader> & {
|
||||
httpHeaders?: Record<string, string>;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||
import { MessageType, SharedFrom } from '@/constants/chat';
|
||||
import { useFetchExternalAgentInputs } from '@/hooks/use-agent-request';
|
||||
import { useFetchExternalChatInfo } from '@/hooks/use-chat-request';
|
||||
import i18n from '@/locales/config';
|
||||
import i18n, { changeLanguageAsync } from '@/locales/config';
|
||||
import { useSendNextSharedMessage } from '@/pages/agent/hooks/use-send-shared-message';
|
||||
import { MessageCircle, Minimize2, Send, X } from 'lucide-react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
@@ -136,7 +136,7 @@ const FloatingChatWidget = () => {
|
||||
}, 50);
|
||||
|
||||
if (locale && i18n.language !== locale) {
|
||||
i18n.changeLanguage(locale);
|
||||
changeLanguageAsync(locale);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { PromptIcon } from '@/assets/icon/next-icon';
|
||||
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { IRemoveMessageById } from '@/hooks/logic-hooks';
|
||||
import {
|
||||
@@ -10,7 +16,6 @@ import {
|
||||
SoundOutlined,
|
||||
SyncOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Radio, Tooltip } from 'antd';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FeedbackDialog from '../feedback-dialog';
|
||||
@@ -50,34 +55,44 @@ export const AssistantGroupButton = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Radio.Group size="small">
|
||||
<Radio.Button value="a">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="space-x-1"
|
||||
>
|
||||
<ToggleGroupItem value="a">
|
||||
<CopyToClipboard text={content}></CopyToClipboard>
|
||||
</Radio.Button>
|
||||
</ToggleGroupItem>
|
||||
{showLoudspeaker && (
|
||||
<Radio.Button value="b" onClick={handleRead}>
|
||||
<Tooltip title={t('chat.read')}>
|
||||
{isPlaying ? <PauseCircleOutlined /> : <SoundOutlined />}
|
||||
<ToggleGroupItem value="b" onClick={handleRead}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
{isPlaying ? <PauseCircleOutlined /> : <SoundOutlined />}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('chat.read')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<audio src="" ref={ref}></audio>
|
||||
</Radio.Button>
|
||||
</ToggleGroupItem>
|
||||
)}
|
||||
{showLikeButton && (
|
||||
<>
|
||||
<Radio.Button value="c" onClick={handleLike}>
|
||||
<ToggleGroupItem value="c" onClick={handleLike}>
|
||||
<LikeOutlined />
|
||||
</Radio.Button>
|
||||
<Radio.Button value="d" onClick={showModal}>
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="d" onClick={showModal}>
|
||||
<DislikeOutlined />
|
||||
</Radio.Button>
|
||||
</ToggleGroupItem>
|
||||
</>
|
||||
)}
|
||||
{prompt && (
|
||||
<Radio.Button value="e" onClick={showPromptModal}>
|
||||
<ToggleGroupItem value="e" onClick={showPromptModal}>
|
||||
<PromptIcon style={{ fontSize: '16px' }} />
|
||||
</Radio.Button>
|
||||
</ToggleGroupItem>
|
||||
)}
|
||||
</Radio.Group>
|
||||
</ToggleGroup>
|
||||
{visible && (
|
||||
<FeedbackDialog
|
||||
visible={visible}
|
||||
@@ -118,28 +133,39 @@ export const UserGroupButton = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Radio.Group size="small">
|
||||
<Radio.Button value="a">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="space-x-1"
|
||||
>
|
||||
<ToggleGroupItem value="a">
|
||||
<CopyToClipboard text={content}></CopyToClipboard>
|
||||
</Radio.Button>
|
||||
</ToggleGroupItem>
|
||||
{regenerateMessage && (
|
||||
<Radio.Button
|
||||
<ToggleGroupItem
|
||||
value="b"
|
||||
onClick={regenerateMessage}
|
||||
disabled={sendLoading}
|
||||
>
|
||||
<Tooltip title={t('chat.regenerate')}>
|
||||
<SyncOutlined spin={sendLoading} />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SyncOutlined spin={sendLoading} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('chat.regenerate')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</Radio.Button>
|
||||
</ToggleGroupItem>
|
||||
)}
|
||||
{removeMessageById && (
|
||||
<Radio.Button value="c" onClick={onRemoveMessage} disabled={loading}>
|
||||
<Tooltip title={t('common.delete')}>
|
||||
<DeleteOutlined spin={loading} />
|
||||
<ToggleGroupItem value="c" onClick={onRemoveMessage} disabled={loading}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DeleteOutlined spin={loading} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('common.delete')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</Radio.Button>
|
||||
</ToggleGroupItem>
|
||||
)}
|
||||
</Radio.Group>
|
||||
</ToggleGroup>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { PromptIcon } from '@/assets/icon/next-icon';
|
||||
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { IRemoveMessageById } from '@/hooks/logic-hooks';
|
||||
import { AgentChatContext } from '@/pages/agent/context';
|
||||
@@ -13,7 +18,6 @@ import {
|
||||
SoundOutlined,
|
||||
SyncOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Radio, Tooltip } from 'antd';
|
||||
import { Download, NotebookText } from 'lucide-react';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -80,8 +84,13 @@ export const AssistantGroupButton = ({
|
||||
</ToggleGroupItem>
|
||||
{showLoudspeaker && (
|
||||
<ToggleGroupItem value="b" onClick={handleRead}>
|
||||
<Tooltip title={t('chat.read')}>
|
||||
{isPlaying ? <PauseCircleOutlined /> : <SoundOutlined />}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
{isPlaying ? <PauseCircleOutlined /> : <SoundOutlined />}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('chat.read')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<audio src="" ref={ref}></audio>
|
||||
</ToggleGroupItem>
|
||||
@@ -97,9 +106,9 @@ export const AssistantGroupButton = ({
|
||||
</>
|
||||
)}
|
||||
{prompt && (
|
||||
<Radio.Button value="e" onClick={showPromptModal}>
|
||||
<ToggleGroupItem value="e" onClick={showPromptModal}>
|
||||
<PromptIcon style={{ fontSize: '16px' }} />
|
||||
</Radio.Button>
|
||||
</ToggleGroupItem>
|
||||
)}
|
||||
{showLog && (
|
||||
<ToggleGroupItem value="f" onClick={handleShowLogSheet}>
|
||||
@@ -168,28 +177,39 @@ export const UserGroupButton = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Radio.Group size="small">
|
||||
<Radio.Button value="a">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="space-x-1"
|
||||
>
|
||||
<ToggleGroupItem value="a">
|
||||
<CopyToClipboard text={content}></CopyToClipboard>
|
||||
</Radio.Button>
|
||||
</ToggleGroupItem>
|
||||
{regenerateMessage && (
|
||||
<Radio.Button
|
||||
<ToggleGroupItem
|
||||
value="b"
|
||||
onClick={regenerateMessage}
|
||||
disabled={sendLoading}
|
||||
>
|
||||
<Tooltip title={t('chat.regenerate')}>
|
||||
<SyncOutlined spin={sendLoading} />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SyncOutlined spin={sendLoading} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('chat.regenerate')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</Radio.Button>
|
||||
</ToggleGroupItem>
|
||||
)}
|
||||
{removeMessageById && (
|
||||
<Radio.Button value="c" onClick={onRemoveMessage} disabled={loading}>
|
||||
<Tooltip title={t('common.delete')}>
|
||||
<DeleteOutlined spin={loading} />
|
||||
<ToggleGroupItem value="c" onClick={onRemoveMessage} disabled={loading}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DeleteOutlined spin={loading} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('common.delete')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</Radio.Button>
|
||||
</ToggleGroupItem>
|
||||
)}
|
||||
</Radio.Group>
|
||||
</ToggleGroup>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
import { DocumentParserType } from '@/constants/knowledge';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Flex, Form, Input, InputNumber, Slider, Switch } from 'antd';
|
||||
import random from 'lodash/random';
|
||||
|
||||
export const excludedParseMethods = [
|
||||
DocumentParserType.Table,
|
||||
DocumentParserType.Resume,
|
||||
DocumentParserType.One,
|
||||
DocumentParserType.Picture,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
DocumentParserType.Qa,
|
||||
DocumentParserType.Tag,
|
||||
];
|
||||
|
||||
export const showRaptorParseConfiguration = (
|
||||
parserId: DocumentParserType | undefined,
|
||||
) => {
|
||||
return !excludedParseMethods.some((x) => x === parserId);
|
||||
};
|
||||
|
||||
export const excludedTagParseMethods = [
|
||||
DocumentParserType.Table,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
DocumentParserType.Tag,
|
||||
];
|
||||
|
||||
export const showTagItems = (parserId: DocumentParserType) => {
|
||||
return !excludedTagParseMethods.includes(parserId);
|
||||
};
|
||||
|
||||
// The three types "table", "resume" and "one" do not display this configuration.
|
||||
const ParseConfiguration = () => {
|
||||
const form = Form.useFormInstance();
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
|
||||
const handleGenerate = () => {
|
||||
form.setFieldValue(
|
||||
['parser_config', 'raptor', 'random_seed'],
|
||||
random(10000),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name={['parser_config', 'raptor', 'use_raptor']}
|
||||
label={t('useRaptor')}
|
||||
initialValue={false}
|
||||
valuePropName="checked"
|
||||
tooltip={t('useRaptorTip')}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
shouldUpdate={(prevValues, curValues) =>
|
||||
prevValues.parser_config.raptor.use_raptor !==
|
||||
curValues.parser_config.raptor.use_raptor
|
||||
}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const useRaptor = getFieldValue([
|
||||
'parser_config',
|
||||
'raptor',
|
||||
'use_raptor',
|
||||
]);
|
||||
|
||||
return (
|
||||
useRaptor && (
|
||||
<>
|
||||
<Form.Item
|
||||
name={['parser_config', 'raptor', 'prompt']}
|
||||
label={t('prompt')}
|
||||
initialValue={t('promptText')}
|
||||
tooltip={t('promptTip')}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('promptMessage'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea rows={8} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t('maxToken')} tooltip={t('maxTokenTip')}>
|
||||
<Flex gap={20} align="center">
|
||||
<Flex flex={1}>
|
||||
<Form.Item
|
||||
name={['parser_config', 'raptor', 'max_token']}
|
||||
noStyle
|
||||
initialValue={256}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('maxTokenMessage'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Slider max={2048} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item
|
||||
name={['parser_config', 'raptor', 'max_token']}
|
||||
noStyle
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('maxTokenMessage'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber max={2048} min={0} />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('threshold')} tooltip={t('thresholdTip')}>
|
||||
<Flex gap={20} align="center">
|
||||
<Flex flex={1}>
|
||||
<Form.Item
|
||||
name={['parser_config', 'raptor', 'threshold']}
|
||||
noStyle
|
||||
initialValue={0.1}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('thresholdMessage'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
style={{ width: '100%' }}
|
||||
step={0.01}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item
|
||||
name={['parser_config', 'raptor', 'threshold']}
|
||||
noStyle
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('thresholdMessage'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber max={1} min={0} step={0.01} />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('maxCluster')} tooltip={t('maxClusterTip')}>
|
||||
<Flex gap={20} align="center">
|
||||
<Flex flex={1}>
|
||||
<Form.Item
|
||||
name={['parser_config', 'raptor', 'max_cluster']}
|
||||
noStyle
|
||||
initialValue={64}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('maxClusterMessage'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Slider min={1} max={1024} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item
|
||||
name={['parser_config', 'raptor', 'max_cluster']}
|
||||
noStyle
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('maxClusterMessage'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber max={1024} min={1} />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('randomSeed')}>
|
||||
<Flex gap={20} align="center">
|
||||
<Flex flex={1}>
|
||||
<Form.Item
|
||||
name={['parser_config', 'raptor', 'random_seed']}
|
||||
noStyle
|
||||
initialValue={0}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('randomSeedMessage'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item noStyle>
|
||||
<Button type="primary" onClick={handleGenerate}>
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParseConfiguration;
|
||||
@@ -1,146 +0,0 @@
|
||||
import { DocumentParserType } from '@/constants/knowledge';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import random from 'lodash/random';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { SliderInputFormField } from '../slider-input-form-field';
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '../ui/form';
|
||||
import { Input } from '../ui/input';
|
||||
import { Switch } from '../ui/switch';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
|
||||
export const excludedParseMethods = [
|
||||
DocumentParserType.Table,
|
||||
DocumentParserType.Resume,
|
||||
DocumentParserType.One,
|
||||
DocumentParserType.Picture,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
DocumentParserType.Qa,
|
||||
DocumentParserType.Tag,
|
||||
];
|
||||
|
||||
export const showRaptorParseConfiguration = (
|
||||
parserId: DocumentParserType | undefined,
|
||||
) => {
|
||||
return !excludedParseMethods.some((x) => x === parserId);
|
||||
};
|
||||
|
||||
export const excludedTagParseMethods = [
|
||||
DocumentParserType.Table,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
DocumentParserType.Tag,
|
||||
];
|
||||
|
||||
export const showTagItems = (parserId: DocumentParserType) => {
|
||||
return !excludedTagParseMethods.includes(parserId);
|
||||
};
|
||||
|
||||
const UseRaptorField = 'parser_config.raptor.use_raptor';
|
||||
const RandomSeedField = 'parser_config.raptor.random_seed';
|
||||
|
||||
// The three types "table", "resume" and "one" do not display this configuration.
|
||||
|
||||
const RaptorFormFields = () => {
|
||||
const form = useFormContext();
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
const useRaptor = useWatch({ name: UseRaptorField });
|
||||
|
||||
const handleGenerate = useCallback(() => {
|
||||
form.setValue(RandomSeedField, random(10000));
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={UseRaptorField}
|
||||
render={({ field }) => (
|
||||
<FormItem defaultChecked={false}>
|
||||
<FormLabel tooltip={t('useRaptorTip')}>{t('useRaptor')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{useRaptor && (
|
||||
<div className="space-y-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'parser_config.raptor.prompt'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('promptTip')}>{t('prompt')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea {...field} rows={8} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SliderInputFormField
|
||||
name={'parser_config.raptor.max_token'}
|
||||
label={t('maxToken')}
|
||||
tooltip={t('maxTokenTip')}
|
||||
defaultValue={256}
|
||||
max={2048}
|
||||
min={0}
|
||||
></SliderInputFormField>
|
||||
<SliderInputFormField
|
||||
name={'parser_config.raptor.threshold'}
|
||||
label={t('threshold')}
|
||||
tooltip={t('thresholdTip')}
|
||||
defaultValue={0.1}
|
||||
step={0.01}
|
||||
max={1}
|
||||
min={0}
|
||||
></SliderInputFormField>
|
||||
<SliderInputFormField
|
||||
name={'parser_config.raptor.max_cluster'}
|
||||
label={t('maxCluster')}
|
||||
tooltip={t('maxClusterTip')}
|
||||
defaultValue={64}
|
||||
max={1024}
|
||||
min={1}
|
||||
></SliderInputFormField>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'parser_config.raptor.random_seed'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('randomSeed')}</FormLabel>
|
||||
<FormControl defaultValue={0}>
|
||||
<div className="flex gap-4">
|
||||
<Input {...field} />
|
||||
<Button
|
||||
size={'sm'}
|
||||
onClick={handleGenerate}
|
||||
type={'button'}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RaptorFormFields;
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Authorization } from '@/constants/authorization';
|
||||
import { getAuthorization } from '@/utils/authorization-util';
|
||||
import axios from 'axios';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export const useCatchDocumentError = (url: string) => {
|
||||
const httpHeaders = useMemo(() => {
|
||||
return {
|
||||
[Authorization]: getAuthorization(),
|
||||
};
|
||||
}, []);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const fetchDocument = useCallback(async () => {
|
||||
const { data } = await axios.get(url, { headers: httpHeaders });
|
||||
if (data.code !== 0) {
|
||||
setError(data?.message);
|
||||
}
|
||||
}, [url, httpHeaders]);
|
||||
useEffect(() => {
|
||||
fetchDocument();
|
||||
}, [fetchDocument]);
|
||||
|
||||
return error;
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
.documentContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
:global(.PdfHighlighter) {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
:global(.Highlight--scrolledTo .Highlight__part) {
|
||||
overflow-x: hidden;
|
||||
background-color: rgba(255, 226, 143, 1);
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import { IReferenceChunk } from '@/interfaces/database/chat';
|
||||
import { IChunk } from '@/interfaces/database/knowledge';
|
||||
import FileError from '@/pages/document-viewer/file-error';
|
||||
import { Skeleton } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
AreaHighlight,
|
||||
Highlight,
|
||||
IHighlight,
|
||||
PdfHighlighter,
|
||||
PdfLoader,
|
||||
Popup,
|
||||
} from 'react-pdf-highlighter';
|
||||
import { useCatchDocumentError } from './hooks';
|
||||
|
||||
import {
|
||||
useGetChunkHighlights,
|
||||
useGetDocumentUrl,
|
||||
} from '@/hooks/use-document-request';
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface IProps {
|
||||
chunk: IChunk | IReferenceChunk;
|
||||
documentId: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const HighlightPopup = ({
|
||||
comment,
|
||||
}: {
|
||||
comment: { text: string; emoji: string };
|
||||
}) =>
|
||||
comment.text ? (
|
||||
<div className="Highlight__popup">
|
||||
{comment.emoji} {comment.text}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const DocumentPreviewer = ({ chunk, documentId, visible }: IProps) => {
|
||||
const getDocumentUrl = useGetDocumentUrl(documentId);
|
||||
const { highlights: state, setWidthAndHeight } = useGetChunkHighlights(chunk);
|
||||
const ref = useRef<(highlight: IHighlight) => void>(() => {});
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const url = getDocumentUrl();
|
||||
const error = useCatchDocumentError(url);
|
||||
|
||||
const resetHash = () => {};
|
||||
|
||||
useEffect(() => {
|
||||
setLoaded(visible);
|
||||
}, [visible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.length > 0 && loaded) {
|
||||
setLoaded(false);
|
||||
ref.current(state[0]);
|
||||
}
|
||||
}, [state, loaded]);
|
||||
|
||||
return (
|
||||
<div className={styles.documentContainer}>
|
||||
<PdfLoader
|
||||
url={url}
|
||||
beforeLoad={<Skeleton active />}
|
||||
workerSrc="/pdfjs-dist/pdf.worker.min.js"
|
||||
errorMessage={<FileError>{error}</FileError>}
|
||||
>
|
||||
{(pdfDocument) => {
|
||||
pdfDocument.getPage(1).then((page) => {
|
||||
const viewport = page.getViewport({ scale: 1 });
|
||||
const width = viewport.width;
|
||||
const height = viewport.height;
|
||||
setWidthAndHeight(width, height);
|
||||
});
|
||||
|
||||
return (
|
||||
<PdfHighlighter
|
||||
pdfDocument={pdfDocument}
|
||||
enableAreaSelection={(event) => event.altKey}
|
||||
onScrollChange={resetHash}
|
||||
scrollRef={(scrollTo) => {
|
||||
ref.current = scrollTo;
|
||||
setLoaded(true);
|
||||
}}
|
||||
onSelectionFinished={() => null}
|
||||
highlightTransform={(
|
||||
highlight,
|
||||
index,
|
||||
setTip,
|
||||
hideTip,
|
||||
viewportToScaled,
|
||||
screenshot,
|
||||
isScrolledTo,
|
||||
) => {
|
||||
const isTextHighlight = !Boolean(
|
||||
highlight.content && highlight.content.image,
|
||||
);
|
||||
|
||||
const component = isTextHighlight ? (
|
||||
<Highlight
|
||||
isScrolledTo={isScrolledTo}
|
||||
position={highlight.position}
|
||||
comment={highlight.comment}
|
||||
/>
|
||||
) : (
|
||||
<AreaHighlight
|
||||
isScrolledTo={isScrolledTo}
|
||||
highlight={highlight}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popup
|
||||
popupContent={<HighlightPopup {...highlight} />}
|
||||
onMouseOver={(popupContent) =>
|
||||
setTip(highlight, () => popupContent)
|
||||
}
|
||||
onMouseOut={hideTip}
|
||||
key={index}
|
||||
>
|
||||
{component}
|
||||
</Popup>
|
||||
);
|
||||
}}
|
||||
highlights={state}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</PdfLoader>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentPreviewer;
|
||||
470
web/src/components/ui/date-picker.tsx
Normal file
470
web/src/components/ui/date-picker.tsx
Normal file
@@ -0,0 +1,470 @@
|
||||
import { Calendar } from '@/components/originui/calendar';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Locale } from 'date-fns';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
Calendar as CalendarIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
ChangeEvent,
|
||||
forwardRef,
|
||||
InputHTMLAttributes,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from './button';
|
||||
import { TimePicker } from './time-picker';
|
||||
|
||||
type PickerType = 'date' | 'month' | 'year';
|
||||
|
||||
interface DatePickerProps extends Omit<
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
'value' | 'onChange'
|
||||
> {
|
||||
value?: Date | number;
|
||||
onChange?: (date: Date | undefined) => void;
|
||||
showTimeSelect?: boolean;
|
||||
dateFormat?: string;
|
||||
timeFormat?: string;
|
||||
showTimeSelectOnly?: boolean;
|
||||
locale?: Locale;
|
||||
openChange?: (open: boolean) => void;
|
||||
picker?: PickerType;
|
||||
allowInput?: boolean;
|
||||
minYear?: number;
|
||||
maxYear?: number;
|
||||
}
|
||||
|
||||
const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
dateFormat = 'DD/MM/YYYY',
|
||||
timeFormat = 'HH:mm:ss',
|
||||
showTimeSelect = false,
|
||||
showTimeSelectOnly = false,
|
||||
openChange,
|
||||
picker = 'date',
|
||||
allowInput = false,
|
||||
minYear = 1900,
|
||||
maxYear = 2100,
|
||||
placeholder,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [currentYear, setCurrentYear] = useState(() => {
|
||||
const year = value
|
||||
? value instanceof Date
|
||||
? value.getFullYear()
|
||||
: value
|
||||
: new Date().getFullYear();
|
||||
return year;
|
||||
});
|
||||
const [currentMonth, setCurrentMonth] = useState(() => {
|
||||
if (value instanceof Date) {
|
||||
return value.getMonth();
|
||||
}
|
||||
return new Date().getMonth();
|
||||
});
|
||||
|
||||
const selectedDate = useMemo(() => {
|
||||
if (!value) return undefined;
|
||||
if (value instanceof Date) return value;
|
||||
return new Date(value, 0, 1);
|
||||
}, [value]);
|
||||
|
||||
const displayFormat = useMemo(() => {
|
||||
if (picker === 'year') return 'YYYY';
|
||||
if (picker === 'month') return 'MM/YYYY';
|
||||
if (showTimeSelect) return `${dateFormat} ${timeFormat}`;
|
||||
if (showTimeSelectOnly) return timeFormat;
|
||||
return dateFormat;
|
||||
}, [picker, dateFormat, timeFormat, showTimeSelect, showTimeSelectOnly]);
|
||||
|
||||
const formattedValue = useMemo(() => {
|
||||
if (selectedDate && !isNaN(selectedDate.getTime())) {
|
||||
return dayjs(selectedDate).format(displayFormat);
|
||||
}
|
||||
return inputValue || '';
|
||||
}, [selectedDate, displayFormat, inputValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDate && !isNaN(selectedDate.getTime())) {
|
||||
setInputValue(dayjs(selectedDate).format(displayFormat));
|
||||
}
|
||||
}, [selectedDate, displayFormat]);
|
||||
|
||||
const handleDateSelect = (date: Date | undefined) => {
|
||||
if (date && selectedDate) {
|
||||
const valueDate = dayjs(selectedDate);
|
||||
date.setHours(valueDate.hour());
|
||||
date.setMinutes(valueDate.minute());
|
||||
date.setSeconds(valueDate.second());
|
||||
}
|
||||
onChange?.(date);
|
||||
};
|
||||
|
||||
const handleTimeSelect = (date: Date | undefined) => {
|
||||
const valueDate = dayjs(selectedDate);
|
||||
if (selectedDate) {
|
||||
date?.setFullYear(valueDate.year());
|
||||
date?.setMonth(valueDate.month());
|
||||
date?.setDate(valueDate.date());
|
||||
}
|
||||
if (date) {
|
||||
onChange?.(date);
|
||||
} else {
|
||||
valueDate?.hour(0);
|
||||
valueDate?.minute(0);
|
||||
valueDate?.second(0);
|
||||
onChange?.(valueDate.toDate());
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setInputValue(newValue);
|
||||
|
||||
if (allowInput) {
|
||||
const parsed = dayjs(newValue, displayFormat, true);
|
||||
if (parsed.isValid()) {
|
||||
onChange?.(parsed.toDate());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open);
|
||||
openChange?.(open);
|
||||
};
|
||||
|
||||
const handleYearSelect = (year: number) => {
|
||||
if (picker === 'year') {
|
||||
onChange?.(new Date(year, 0, 1));
|
||||
setOpen(false);
|
||||
} else {
|
||||
setCurrentYear(year);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMonthSelect = (month: number) => {
|
||||
if (picker === 'month') {
|
||||
onChange?.(new Date(currentYear, month, 1));
|
||||
setOpen(false);
|
||||
} else {
|
||||
setCurrentMonth(month);
|
||||
}
|
||||
};
|
||||
|
||||
const years = useMemo(() => {
|
||||
const result: number[] = [];
|
||||
const startYear = Math.floor(currentYear / 10) * 10 - 1;
|
||||
for (let i = startYear; i <= startYear + 12; i++) {
|
||||
result.push(i);
|
||||
}
|
||||
return result;
|
||||
}, [currentYear]);
|
||||
|
||||
const months = useMemo(() => {
|
||||
return [
|
||||
{ value: 0, label: t('common.january', 'Jan') },
|
||||
{ value: 1, label: t('common.february', 'Feb') },
|
||||
{ value: 2, label: t('common.march', 'Mar') },
|
||||
{ value: 3, label: t('common.april', 'Apr') },
|
||||
{ value: 4, label: t('common.may', 'May') },
|
||||
{ value: 5, label: t('common.june', 'Jun') },
|
||||
{ value: 6, label: t('common.july', 'Jul') },
|
||||
{ value: 7, label: t('common.august', 'Aug') },
|
||||
{ value: 8, label: t('common.september', 'Sep') },
|
||||
{ value: 9, label: t('common.october', 'Oct') },
|
||||
{ value: 10, label: t('common.november', 'Nov') },
|
||||
{ value: 11, label: t('common.december', 'Dec') },
|
||||
];
|
||||
}, [t]);
|
||||
|
||||
const [view, setView] = useState<'date' | 'month' | 'year'>(
|
||||
picker === 'year' ? 'year' : picker === 'month' ? 'month' : 'date',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setView(
|
||||
picker === 'year' ? 'year' : picker === 'month' ? 'month' : 'date',
|
||||
);
|
||||
}, [picker]);
|
||||
|
||||
const handlePrev = () => {
|
||||
if (view === 'year') {
|
||||
setCurrentYear((prev) => prev - 10);
|
||||
} else if (view === 'month') {
|
||||
setCurrentYear((prev) => prev - 1);
|
||||
} else {
|
||||
const newMonth = currentMonth === 0 ? 11 : currentMonth - 1;
|
||||
const newYear = currentMonth === 0 ? currentYear - 1 : currentYear;
|
||||
setCurrentMonth(newMonth);
|
||||
setCurrentYear(newYear);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (view === 'year') {
|
||||
setCurrentYear((prev) => prev + 10);
|
||||
} else if (view === 'month') {
|
||||
setCurrentYear((prev) => prev + 1);
|
||||
} else {
|
||||
const newMonth = currentMonth === 11 ? 0 : currentMonth + 1;
|
||||
const newYear = currentMonth === 11 ? currentYear + 1 : currentYear;
|
||||
setCurrentMonth(newMonth);
|
||||
setCurrentYear(newYear);
|
||||
}
|
||||
};
|
||||
|
||||
const renderYearPicker = () => (
|
||||
<div className="p-0">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={handlePrev}
|
||||
>
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<span className="text-sm font-medium">
|
||||
{years[1]} - {years[10]}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={handleNext}
|
||||
>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{years.map((year) => {
|
||||
const isSelected = year === (selectedDate?.getFullYear() || 0);
|
||||
const isCurrentDecade = year >= years[1] && year <= years[10];
|
||||
const isDisabled = year < minYear || year > maxYear;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={year}
|
||||
variant={isSelected ? 'default' : 'ghost'}
|
||||
size="lg"
|
||||
disabled={isDisabled}
|
||||
className={cn(
|
||||
'text-sm ',
|
||||
isSelected && 'bg-primary text-primary-foreground',
|
||||
!isCurrentDecade && 'text-muted-foreground opacity-50',
|
||||
!isSelected && isCurrentDecade && 'hover:bg-accent',
|
||||
)}
|
||||
onClick={() => handleYearSelect(year)}
|
||||
>
|
||||
{year}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderMonthPicker = () => (
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={handlePrev}
|
||||
>
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<span
|
||||
className="text-sm font-medium cursor-pointer hover:text-primary"
|
||||
onClick={() => picker !== 'month' && setView('year')}
|
||||
>
|
||||
{currentYear}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={handleNext}
|
||||
>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{months.map((month) => {
|
||||
const isSelected =
|
||||
month.value === (selectedDate?.getMonth() ?? -1) &&
|
||||
currentYear === (selectedDate?.getFullYear() ?? 0);
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={month.value}
|
||||
variant={isSelected ? 'default' : 'ghost'}
|
||||
size="lg"
|
||||
className={cn(
|
||||
'text-sm',
|
||||
isSelected && 'bg-primary text-primary-foreground',
|
||||
)}
|
||||
onClick={() => handleMonthSelect(month.value)}
|
||||
>
|
||||
{month.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderDatePicker = () => (
|
||||
<>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={selectedDate}
|
||||
onSelect={handleDateSelect}
|
||||
month={new Date(currentYear, currentMonth)}
|
||||
onMonthChange={(date) => {
|
||||
setCurrentYear(date.getFullYear());
|
||||
setCurrentMonth(date.getMonth());
|
||||
}}
|
||||
onDayClick={() => {
|
||||
if (!showTimeSelect) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
classNames={{
|
||||
month_caption:
|
||||
'relative mx-10 mb-1 flex h-9 items-center justify-center z-20 [&>span]:cursor-pointer',
|
||||
}}
|
||||
components={{
|
||||
Chevron: (props) => {
|
||||
if (props.orientation === 'left') {
|
||||
return (
|
||||
<ChevronLeftIcon
|
||||
size={16}
|
||||
{...props}
|
||||
aria-hidden="true"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePrev();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
size={16}
|
||||
{...props}
|
||||
aria-hidden="true"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleNext();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{/* <div
|
||||
className="text-center py-1 text-sm cursor-pointer hover:text-primary"
|
||||
onClick={() => setView('month')}
|
||||
>
|
||||
{dayjs(new Date(currentYear, currentMonth)).format('MMMM YYYY')}
|
||||
</div> */}
|
||||
</>
|
||||
);
|
||||
|
||||
const renderPicker = () => {
|
||||
if (view === 'year') return renderYearPicker();
|
||||
if (view === 'month') return renderMonthPicker();
|
||||
return renderDatePicker();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="relative">
|
||||
<Input
|
||||
ref={ref}
|
||||
value={formattedValue}
|
||||
onChange={handleInputChange}
|
||||
readOnly={!allowInput}
|
||||
placeholder={placeholder}
|
||||
className={cn(
|
||||
'bg-bg-card hover:text-text-primary border-border-button w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px]',
|
||||
allowInput ? 'cursor-text' : 'cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<CalendarIcon
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground/80 group-hover:text-foreground shrink-0 transition-colors pointer-events-none"
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-2" align="start">
|
||||
{renderPicker()}
|
||||
{showTimeSelect && picker === 'date' && (
|
||||
<TimePicker
|
||||
value={selectedDate}
|
||||
onChange={(value: Date | undefined) => {
|
||||
handleTimeSelect(value);
|
||||
}}
|
||||
showNow
|
||||
/>
|
||||
)}
|
||||
<div className="w-full flex justify-end mt-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
className="text-sm mr-2"
|
||||
onClick={() => {
|
||||
onChange?.(undefined);
|
||||
setInputValue('');
|
||||
handleOpenChange(false);
|
||||
}}
|
||||
>
|
||||
{t('common.clear', 'Clear')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="text-sm text-text-primary-inverse"
|
||||
onClick={() => {
|
||||
handleOpenChange(false);
|
||||
}}
|
||||
>
|
||||
{t('common.confirm', 'OK')}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
DatePicker.displayName = 'DatePicker';
|
||||
|
||||
export { DatePicker };
|
||||
@@ -1,175 +0,0 @@
|
||||
import { Calendar } from '@/components/originui/calendar';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Locale } from 'date-fns';
|
||||
import dayjs from 'dayjs';
|
||||
import { Calendar as CalendarIcon } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from './button';
|
||||
import { TimePicker } from './time-picker';
|
||||
// import TimePicker from 'react-time-picker';
|
||||
interface DateInputProps extends Omit<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
'value' | 'onChange'
|
||||
> {
|
||||
value?: Date;
|
||||
onChange?: (date: Date | undefined) => void;
|
||||
showTimeSelect?: boolean;
|
||||
dateFormat?: string;
|
||||
timeFormat?: string;
|
||||
showTimeSelectOnly?: boolean;
|
||||
locale?: Locale; // Support for internationalization
|
||||
openChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
dateFormat = 'DD/MM/YYYY',
|
||||
timeFormat = 'HH:mm:ss',
|
||||
showTimeSelect = false,
|
||||
showTimeSelectOnly = false,
|
||||
openChange,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedDate, setSelectedDate] = React.useState<Date | undefined>(
|
||||
value,
|
||||
);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleDateSelect = (date: Date | undefined) => {
|
||||
if (selectedDate) {
|
||||
const valueDate = dayjs(selectedDate);
|
||||
date?.setHours(valueDate.hour());
|
||||
date?.setMinutes(valueDate.minute());
|
||||
date?.setSeconds(valueDate.second());
|
||||
}
|
||||
setSelectedDate(date);
|
||||
// onChange?.(date);
|
||||
};
|
||||
|
||||
const handleTimeSelect = (date: Date | undefined) => {
|
||||
const valueDate = dayjs(selectedDate);
|
||||
if (selectedDate) {
|
||||
date?.setFullYear(valueDate.year());
|
||||
date?.setMonth(valueDate.month());
|
||||
date?.setDate(valueDate.date());
|
||||
}
|
||||
if (date) {
|
||||
// onChange?.(date);
|
||||
setSelectedDate(date);
|
||||
} else {
|
||||
valueDate?.hour(0);
|
||||
valueDate?.minute(0);
|
||||
valueDate?.second(0);
|
||||
// onChange?.(valueDate.toDate());
|
||||
setSelectedDate(valueDate.toDate());
|
||||
}
|
||||
};
|
||||
|
||||
// Determine display format based on the type of date picker
|
||||
let displayFormat = dateFormat;
|
||||
if (showTimeSelect) {
|
||||
displayFormat = `${dateFormat} ${timeFormat}`;
|
||||
} else if (showTimeSelectOnly) {
|
||||
displayFormat = timeFormat;
|
||||
}
|
||||
|
||||
// Format the date according to the specified format
|
||||
const formattedValue = React.useMemo(() => {
|
||||
return selectedDate && !isNaN(selectedDate.getTime())
|
||||
? dayjs(selectedDate).format(displayFormat)
|
||||
: '';
|
||||
}, [selectedDate, displayFormat]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open);
|
||||
openChange?.(open);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
disableOutsideClick
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="relative">
|
||||
<Input
|
||||
ref={ref}
|
||||
value={formattedValue}
|
||||
readOnly
|
||||
className={cn(
|
||||
'bg-bg-card hover:text-text-primary border-border-button w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px] cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<CalendarIcon
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground/80 group-hover:text-foreground shrink-0 transition-colors"
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-2" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={selectedDate}
|
||||
onSelect={handleDateSelect}
|
||||
/>
|
||||
{showTimeSelect && (
|
||||
<TimePicker
|
||||
value={selectedDate}
|
||||
onChange={(value: Date | undefined) => {
|
||||
handleTimeSelect(value);
|
||||
}}
|
||||
showNow
|
||||
/>
|
||||
// <TimePicker onChange={onChange} value={value} />
|
||||
)}
|
||||
<div className="w-full flex justify-end mt-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
className="text-sm mr-2"
|
||||
onClick={() => {
|
||||
onChange?.(value);
|
||||
handleOpenChange(false);
|
||||
}}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="text-sm text-text-primary-inverse "
|
||||
onClick={() => {
|
||||
onChange?.(selectedDate);
|
||||
handleOpenChange(false);
|
||||
}}
|
||||
>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
DateInput.displayName = 'DateInput';
|
||||
|
||||
export { DateInput };
|
||||
@@ -1,8 +1,9 @@
|
||||
// src/components/ui/modal.tsx
|
||||
import { cn } from '@/lib/utils';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { Loader, X } from 'lucide-react';
|
||||
import { AlertCircle, CheckCircle, Info, Loader, X } from 'lucide-react';
|
||||
import { FC, ReactNode, useCallback, useEffect, useMemo } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DialogDescription } from '../dialog';
|
||||
import { createPortalModal } from './modal-manage';
|
||||
@@ -12,13 +13,15 @@ export interface ModalProps {
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
title?: ReactNode;
|
||||
titleClassName?: string;
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
content?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
footerClassName?: string;
|
||||
showfooter?: boolean;
|
||||
className?: string;
|
||||
size?: 'small' | 'default' | 'large';
|
||||
closable?: boolean;
|
||||
showCancel?: boolean;
|
||||
closeIcon?: ReactNode;
|
||||
maskClosable?: boolean;
|
||||
destroyOnClose?: boolean;
|
||||
@@ -33,25 +36,42 @@ export interface ModalProps {
|
||||
disabled?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
zIndex?: number;
|
||||
type?: 'warning' | 'info' | 'success' | 'error' | 'confirm';
|
||||
}
|
||||
|
||||
export interface ModalType extends FC<ModalProps> {
|
||||
show: typeof modalIns.show;
|
||||
hide: typeof modalIns.hide;
|
||||
destroy: typeof modalIns.destroy;
|
||||
warning: (props: Omit<ModalProps, 'open' | 'type'>) => void;
|
||||
info: (props: Omit<ModalProps, 'open' | 'type'>) => void;
|
||||
success: (props: Omit<ModalProps, 'open' | 'type'>) => void;
|
||||
error: (props: Omit<ModalProps, 'open' | 'type'>) => void;
|
||||
confirm: (props: Omit<ModalProps, 'open' | 'type'>) => void;
|
||||
}
|
||||
|
||||
const typeIcons = {
|
||||
warning: <AlertCircle className="w-6 h-6 text-yellow-500" />,
|
||||
info: <Info className="w-6 h-6 text-blue-500" />,
|
||||
success: <CheckCircle className="w-6 h-6 text-green-500" />,
|
||||
error: <AlertCircle className="w-6 h-6 text-red-500" />,
|
||||
confirm: <AlertCircle className="w-6 h-6 text-yellow-500" />,
|
||||
};
|
||||
|
||||
const Modal: ModalType = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
titleClassName,
|
||||
children,
|
||||
content,
|
||||
footer,
|
||||
footerClassName,
|
||||
showfooter = true,
|
||||
className = '',
|
||||
size = 'default',
|
||||
closable = true,
|
||||
showCancel = true,
|
||||
closeIcon = <X className="w-4 h-4" />,
|
||||
maskClosable = true,
|
||||
destroyOnClose = false,
|
||||
@@ -66,6 +86,7 @@ const Modal: ModalType = ({
|
||||
disabled = false,
|
||||
style,
|
||||
zIndex = 50,
|
||||
type,
|
||||
}) => {
|
||||
const sizeClasses = {
|
||||
small: 'max-w-md',
|
||||
@@ -74,7 +95,6 @@ const Modal: ModalType = ({
|
||||
};
|
||||
|
||||
const { t } = useTranslation();
|
||||
// Handle ESC key close
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && maskClosable) {
|
||||
@@ -99,7 +119,6 @@ const Modal: ModalType = ({
|
||||
return;
|
||||
}
|
||||
onOpenChange?.(open);
|
||||
console.log('open', open, onOpenChange);
|
||||
if (open && !disabled) {
|
||||
onOk?.();
|
||||
}
|
||||
@@ -117,16 +136,18 @@ const Modal: ModalType = ({
|
||||
} else {
|
||||
footerTemp = (
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCancel()}
|
||||
className={cn(
|
||||
'px-2 py-1 border border-border-button rounded-md hover:bg-bg-card hover:text-text-primary ',
|
||||
cancelButtonClassName,
|
||||
)}
|
||||
>
|
||||
{cancelText ?? t('modal.cancelText')}
|
||||
</button>
|
||||
{showCancel && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCancel()}
|
||||
className={cn(
|
||||
'px-2 py-1 border border-border-button rounded-md hover:bg-bg-card hover:text-text-primary ',
|
||||
cancelButtonClassName,
|
||||
)}
|
||||
>
|
||||
{cancelText ?? t('modal.cancelText')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
disabled={confirmLoading || disabled}
|
||||
@@ -168,7 +189,28 @@ const Modal: ModalType = ({
|
||||
footerClassName,
|
||||
okButtonClassName,
|
||||
cancelButtonClassName,
|
||||
showCancel,
|
||||
]);
|
||||
|
||||
const contentEl = useMemo(() => {
|
||||
if (type && typeIcons[type]) {
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 mt-0.5">{typeIcons[type]}</div>
|
||||
<div className="flex-1">
|
||||
{title && (
|
||||
<DialogPrimitive.Title className="text-lg font-medium text-foreground mb-2">
|
||||
{title}
|
||||
</DialogPrimitive.Title>
|
||||
)}
|
||||
<div className="text-text-secondary">{content || children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return children;
|
||||
}, [type, title, content, children]);
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Root open={open} onOpenChange={handleChange}>
|
||||
<DialogPrimitive.Portal>
|
||||
@@ -180,22 +222,16 @@ const Modal: ModalType = ({
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
`relative w-[700px] ${full ? 'max-w-full' : sizeClasses[size]} ${className} bg-bg-base rounded-lg shadow-lg border border-border-default transition-all focus-visible:!outline-none`,
|
||||
{ 'pt-10': closable && !title },
|
||||
{ 'pt-10': closable && !title && !type },
|
||||
)}
|
||||
style={style}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DialogDescription></DialogDescription>
|
||||
{/* title */}
|
||||
{title && (
|
||||
{title && !type && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-start px-6 py-4 justify-start',
|
||||
// {
|
||||
// 'justify-end': closable && !title,
|
||||
// 'justify-between': closable && title,
|
||||
// 'justify-start': !closable,
|
||||
// },
|
||||
titleClassName,
|
||||
)}
|
||||
>
|
||||
@@ -218,12 +254,10 @@ const Modal: ModalType = ({
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
|
||||
{/* content */}
|
||||
<div className="py-2 px-6 overflow-y-auto scrollbar-auto max-h-[calc(100vh-280px)] focus-visible:!outline-none">
|
||||
{destroyOnClose && !open ? null : children}
|
||||
{destroyOnClose && !open ? null : contentEl}
|
||||
</div>
|
||||
|
||||
{/* footer */}
|
||||
{footEl}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Overlay>
|
||||
@@ -242,4 +276,49 @@ Modal.show = modalIns
|
||||
Modal.hide = modalIns.hide;
|
||||
Modal.destroy = modalIns.destroy;
|
||||
|
||||
const createStaticModal = (type: ModalProps['type']) => {
|
||||
return (props: Omit<ModalProps, 'open' | 'type'>) => {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
const destroy = () => {
|
||||
root.unmount();
|
||||
container.remove();
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
props.onOk?.();
|
||||
destroy();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
props.onCancel?.();
|
||||
destroy();
|
||||
};
|
||||
|
||||
root.render(
|
||||
<Modal
|
||||
{...props}
|
||||
open={true}
|
||||
type={type}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
handleCancel();
|
||||
}
|
||||
}}
|
||||
maskClosable={false}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Modal.warning = createStaticModal('warning');
|
||||
Modal.info = createStaticModal('info');
|
||||
Modal.success = createStaticModal('success');
|
||||
Modal.error = createStaticModal('error');
|
||||
Modal.confirm = createStaticModal('confirm');
|
||||
|
||||
export { Modal };
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { Authorization } from '@/constants/authorization';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { LanguageTranslationMap } from '@/constants/common';
|
||||
import { FormInstance } from '@/interfaces/antd-compat';
|
||||
import { Pagination } from '@/interfaces/common';
|
||||
import { ResponseType } from '@/interfaces/database/base';
|
||||
import {
|
||||
@@ -10,11 +12,10 @@ import {
|
||||
Message,
|
||||
} from '@/interfaces/database/chat';
|
||||
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
|
||||
import { changeLanguageAsync } from '@/locales/config';
|
||||
import api from '@/utils/api';
|
||||
import { getAuthorization } from '@/utils/authorization-util';
|
||||
import { buildMessageUuid } from '@/utils/chat';
|
||||
import { message } from 'antd';
|
||||
import { FormInstance } from 'antd/lib';
|
||||
import axios from 'axios';
|
||||
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
||||
import { has, isEmpty, omit } from 'lodash';
|
||||
@@ -55,9 +56,9 @@ export const useChangeLanguage = () => {
|
||||
const { saveSetting } = useSaveSetting();
|
||||
|
||||
const changeLanguage = (lng: string) => {
|
||||
i18n.changeLanguage(
|
||||
LanguageTranslationMap[lng as keyof typeof LanguageTranslationMap],
|
||||
);
|
||||
const targetLng =
|
||||
LanguageTranslationMap[lng as keyof typeof LanguageTranslationMap];
|
||||
changeLanguageAsync(targetLng);
|
||||
saveSetting({ language: lng });
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { PaginationProps } from '@/interfaces/antd-compat';
|
||||
import { ResponseGetType, ResponseType } from '@/interfaces/database/base';
|
||||
import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge';
|
||||
import kbService from '@/services/knowledge-service';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useDebounce } from 'ahooks';
|
||||
import { PaginationProps, message } from 'antd';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { PaginationProps } from '@/interfaces/antd-compat';
|
||||
import {
|
||||
IFetchFileListResult,
|
||||
IFolder,
|
||||
@@ -8,7 +9,6 @@ import fileManagerService from '@/services/file-manager-service';
|
||||
import { downloadFileFromBlob } from '@/utils/file-util';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useDebounce } from 'ahooks';
|
||||
import { PaginationProps } from 'antd';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LlmIcon } from '@/components/svg-icon';
|
||||
import message from '@/components/ui/message';
|
||||
import { LlmModelType } from '@/constants/knowledge';
|
||||
import { DefaultOptionType } from '@/interfaces/antd-compat';
|
||||
import { ResponseGetType } from '@/interfaces/database/base';
|
||||
import {
|
||||
IFactory,
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
import userService from '@/services/user-service';
|
||||
import { getLLMIconName, getRealModelName } from '@/utils/llm-util';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { LanguageTranslationMap } from '@/constants/common';
|
||||
import { ResponseGetType } from '@/interfaces/database/base';
|
||||
import { IToken } from '@/interfaces/database/chat';
|
||||
@@ -11,6 +12,8 @@ import {
|
||||
IUserInfo,
|
||||
} from '@/interfaces/database/user-setting';
|
||||
import { ISetLangfuseConfigRequestBody } from '@/interfaces/request/system';
|
||||
import { changeLanguageAsync } from '@/locales/config';
|
||||
import { Routes } from '@/routes';
|
||||
import userService, {
|
||||
addTenantUser,
|
||||
agreeTenant,
|
||||
@@ -18,13 +21,12 @@ import userService, {
|
||||
listTenant,
|
||||
listTenantUser,
|
||||
} from '@/services/user-service';
|
||||
import { history } from '@/utils/simple-history-util';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Modal } from 'antd';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
export const enum UserSettingApiAction {
|
||||
UserInfo = 'userInfo',
|
||||
@@ -54,11 +56,11 @@ export const useFetchUserInfo = (): ResponseGetType<IUserInfo> => {
|
||||
queryFn: async () => {
|
||||
const { data } = await userService.user_info();
|
||||
if (data.code === 0) {
|
||||
i18n.changeLanguage(
|
||||
const targetLng =
|
||||
LanguageTranslationMap[
|
||||
data.data.language as keyof typeof LanguageTranslationMap
|
||||
],
|
||||
);
|
||||
];
|
||||
await changeLanguageAsync(targetLng);
|
||||
}
|
||||
return data?.data ?? {};
|
||||
},
|
||||
@@ -71,6 +73,7 @@ export const useFetchTenantInfo = (
|
||||
showEmptyModelWarn = false,
|
||||
): ResponseGetType<ITenantInfo> => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { data, isFetching: loading } = useQuery({
|
||||
queryKey: [UserSettingApiAction.TenantInfo, showEmptyModelWarn],
|
||||
initialData: {},
|
||||
@@ -94,8 +97,11 @@ export const useFetchTenantInfo = (
|
||||
}}
|
||||
></div>
|
||||
),
|
||||
closable: false,
|
||||
showCancel: false,
|
||||
onOk() {
|
||||
history.push('/user-setting/model');
|
||||
// window.open('/user-setting/model', '_self');
|
||||
navigate(`${Routes.UserSetting}${Routes.Model}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
138
web/src/interfaces/antd-compat.ts
Normal file
138
web/src/interfaces/antd-compat.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { FieldValues, Path, PathValue, UseFormReturn } from 'react-hook-form';
|
||||
|
||||
export type PaginationProps = {
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
total?: number;
|
||||
showSizeChanger?: boolean;
|
||||
showQuickJumper?: boolean;
|
||||
pageSizeOptions?: number[];
|
||||
onChange?: (page: number, pageSize?: number) => void;
|
||||
};
|
||||
|
||||
export type DefaultOptionType = {
|
||||
label: string | React.ReactNode;
|
||||
value: string | number;
|
||||
disabled?: boolean;
|
||||
children?: DefaultOptionType[];
|
||||
};
|
||||
|
||||
export type UploadFile = {
|
||||
uid: string;
|
||||
name: string;
|
||||
status?: 'uploading' | 'done' | 'error' | 'removed';
|
||||
url?: string;
|
||||
thumbUrl?: string;
|
||||
response?: any;
|
||||
error?: any;
|
||||
size?: number;
|
||||
type?: string;
|
||||
lastModified?: number;
|
||||
percent?: number;
|
||||
originFileObj?: File;
|
||||
};
|
||||
|
||||
export type TableRowSelection<T = any> = {
|
||||
selectedRowKeys?: React.Key[];
|
||||
onChange?: (selectedRowKeys: React.Key[], selectedRows: T[]) => void;
|
||||
getCheckboxProps?: (record: T) => {
|
||||
disabled?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type FormInstance<TFieldValues extends FieldValues = FieldValues> = {
|
||||
getFieldValue: (name: string | string[]) => any;
|
||||
getFieldsValue: (names?: string[]) => Record<string, any>;
|
||||
setFieldValue: (name: string | string[], value: any) => void;
|
||||
setFieldsValue: (values: Record<string, any>) => void;
|
||||
resetFields: (fields?: string[]) => void;
|
||||
validateFields: (fields?: string[]) => Promise<any>;
|
||||
getFieldsError: (fields?: string[]) => Array<{
|
||||
name: string | string[];
|
||||
errors: string[];
|
||||
}>;
|
||||
getFieldError: (name: string | string[]) => string[];
|
||||
isFieldTouched: (name: string | string[]) => boolean;
|
||||
isFieldsTouched: (fields?: string[]) => boolean;
|
||||
};
|
||||
|
||||
export type FormListFieldData = {
|
||||
name: number;
|
||||
key: number;
|
||||
isListField?: boolean;
|
||||
fieldKey?: number;
|
||||
};
|
||||
|
||||
export function createFormInstance<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
>(form: UseFormReturn<TFieldValues>): FormInstance<TFieldValues> {
|
||||
return {
|
||||
getFieldValue: (name) => {
|
||||
const path = Array.isArray(name) ? name.join('.') : name;
|
||||
return form.getValues(path as Path<TFieldValues>);
|
||||
},
|
||||
getFieldsValue: (names) => {
|
||||
if (names) {
|
||||
return names.reduce(
|
||||
(acc, name) => {
|
||||
acc[name] = form.getValues(name as Path<TFieldValues>);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
}
|
||||
return form.getValues();
|
||||
},
|
||||
setFieldValue: (name, value) => {
|
||||
const path = Array.isArray(name) ? name.join('.') : name;
|
||||
form.setValue(
|
||||
path as Path<TFieldValues>,
|
||||
value as PathValue<TFieldValues, Path<TFieldValues>>,
|
||||
);
|
||||
},
|
||||
setFieldsValue: (values) => {
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
form.setValue(
|
||||
key as Path<TFieldValues>,
|
||||
value as PathValue<TFieldValues, Path<TFieldValues>>,
|
||||
);
|
||||
});
|
||||
},
|
||||
resetFields: (fields) => {
|
||||
if (fields) {
|
||||
fields.forEach((field) => form.resetField(field as Path<TFieldValues>));
|
||||
} else {
|
||||
form.reset();
|
||||
}
|
||||
},
|
||||
validateFields: async (fields) => {
|
||||
return form
|
||||
.trigger(fields as Path<TFieldValues>[])
|
||||
.then(() => form.getValues());
|
||||
},
|
||||
getFieldsError: (fields) => {
|
||||
const errors = form.formState.errors;
|
||||
return Object.entries(errors).map(([name, error]) => ({
|
||||
name,
|
||||
errors: error ? [String(error.message)] : [],
|
||||
}));
|
||||
},
|
||||
getFieldError: (name) => {
|
||||
const path = Array.isArray(name) ? name.join('.') : name;
|
||||
const error = form.formState.errors[path as Path<TFieldValues>];
|
||||
return error ? [String(error.message)] : [];
|
||||
},
|
||||
isFieldTouched: (name) => {
|
||||
const path = Array.isArray(name) ? name.join('.') : name;
|
||||
return form.formState.touchedFields[path as Path<TFieldValues>] ?? false;
|
||||
},
|
||||
isFieldsTouched: (fields) => {
|
||||
if (fields) {
|
||||
return fields.some(
|
||||
(field) => form.formState.touchedFields[field as Path<TFieldValues>],
|
||||
);
|
||||
}
|
||||
return Object.keys(form.formState.touchedFields).length > 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
.tag {
|
||||
height: 40px;
|
||||
padding: 0 30px;
|
||||
margin: 0 5px;
|
||||
border: 1px solid #000;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checked {
|
||||
color: #1677ff;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.logoWrapper {
|
||||
.pointerCursor;
|
||||
}
|
||||
|
||||
.appIcon {
|
||||
vertical-align: middle;
|
||||
max-width: 36px;
|
||||
}
|
||||
|
||||
.appName {
|
||||
vertical-align: middle;
|
||||
font-family: Inter;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.radioGroup {
|
||||
& label {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
border: 0 !important;
|
||||
background-color: rgba(249, 249, 249, 1);
|
||||
font-weight: @fontWeight700;
|
||||
color: rgba(29, 25, 41, 1);
|
||||
&::before {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
:global(.ant-radio-button-wrapper-checked) {
|
||||
border-radius: 6px !important;
|
||||
& a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
:global(.ant-radio-button-wrapper-checked.dark) {
|
||||
border-radius: 0px !important;
|
||||
& a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
:global(.ant-radio-button-wrapper-checked.dark.first) {
|
||||
border-top-left-radius: 6px !important;
|
||||
border-bottom-left-radius: 6px !important;
|
||||
& a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
:global(.ant-radio-button-wrapper-checked.dark.last) {
|
||||
border-top-right-radius: 6px !important;
|
||||
border-bottom-right-radius: 6px !important;
|
||||
& a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.radioGroupDark {
|
||||
& label {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
border: 0 !important;
|
||||
background-color: rgba(249, 249, 249, 0.25);
|
||||
font-weight: @fontWeight700;
|
||||
color: rgba(29, 25, 41, 1);
|
||||
&::before {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
:global(.ant-radio-button-wrapper-checked) {
|
||||
border-radius: 6px !important;
|
||||
& a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
:global(.ant-radio-button-wrapper-checked.dark) {
|
||||
border-radius: 0px !important;
|
||||
& a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
:global(.ant-radio-button-wrapper-checked.dark.first) {
|
||||
border-top-left-radius: 6px !important;
|
||||
border-bottom-left-radius: 6px !important;
|
||||
& a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
:global(.ant-radio-button-wrapper-checked.dark.last) {
|
||||
border-top-right-radius: 6px !important;
|
||||
border-bottom-right-radius: 6px !important;
|
||||
& a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-radio-button-wrapper-checked {
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
.radioButtonIcon {
|
||||
vertical-align: middle;
|
||||
max-width: 15px;
|
||||
max-height: 15px;
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import FileIcon from '@/assets/svg/file-management.svg';
|
||||
import GraphIcon from '@/assets/svg/graph.svg';
|
||||
import KnowledgeBaseIcon from '@/assets/svg/knowledge-base.svg';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useFetchAppConf } from '@/hooks/logic-hooks';
|
||||
import { useNavigateWithFromState } from '@/hooks/route-hook';
|
||||
import { MessageOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { Flex, Layout, Radio, Space, theme } from 'antd';
|
||||
import { MouseEventHandler, useCallback, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import Toolbar from '../right-toolbar';
|
||||
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import styles from './index.module.less';
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
const RagHeader = () => {
|
||||
const {
|
||||
token: { colorBgContainer },
|
||||
} = theme.useToken();
|
||||
const navigate = useNavigateWithFromState();
|
||||
const { pathname } = useLocation();
|
||||
const { t } = useTranslate('header');
|
||||
const appConf = useFetchAppConf();
|
||||
const { theme: themeRag } = useTheme();
|
||||
const tagsData = useMemo(
|
||||
() => [
|
||||
{ path: '/knowledge', name: t('knowledgeBase'), icon: KnowledgeBaseIcon },
|
||||
{ path: '/chat', name: t('chat'), icon: MessageOutlined },
|
||||
{ path: '/search', name: t('search'), icon: SearchOutlined },
|
||||
{ path: '/agent-list', name: t('flow'), icon: GraphIcon },
|
||||
{ path: '/file', name: t('fileManager'), icon: FileIcon },
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const currentPath = useMemo(() => {
|
||||
return (
|
||||
tagsData.find((x) => pathname.startsWith(x.path))?.name || 'knowledge'
|
||||
);
|
||||
}, [pathname, tagsData]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(path: string): MouseEventHandler =>
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
navigate(path);
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const handleLogoClick = useCallback(() => {
|
||||
navigate('/');
|
||||
}, [navigate]);
|
||||
|
||||
return (
|
||||
<Header
|
||||
style={{
|
||||
padding: '0 16px',
|
||||
background: colorBgContainer,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
height: '72px',
|
||||
}}
|
||||
>
|
||||
<a href={window.location.origin}>
|
||||
<Space
|
||||
size={12}
|
||||
onClick={handleLogoClick}
|
||||
className={styles.logoWrapper}
|
||||
>
|
||||
<img src="/logo.svg" alt="" className={styles.appIcon} />
|
||||
<span className={styles.appName}>{appConf.appName}</span>
|
||||
</Space>
|
||||
</a>
|
||||
<Space size={[0, 8]} wrap>
|
||||
<Radio.Group
|
||||
defaultValue="a"
|
||||
buttonStyle="solid"
|
||||
className={
|
||||
themeRag === 'dark' ? styles.radioGroupDark : styles.radioGroup
|
||||
}
|
||||
value={currentPath}
|
||||
>
|
||||
{tagsData.map((item, index) => (
|
||||
<Radio.Button
|
||||
className={`${themeRag === 'dark' ? 'dark' : 'light'} ${index === 0 ? 'first' : ''} ${index === tagsData.length - 1 ? 'last' : ''}`}
|
||||
value={item.name}
|
||||
key={item.name}
|
||||
>
|
||||
<a href={item.path}>
|
||||
<Flex
|
||||
align="center"
|
||||
gap={8}
|
||||
onClick={handleChange(item.path)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<item.icon
|
||||
className={styles.radioButtonIcon}
|
||||
stroke={item.name === currentPath ? 'black' : 'white'}
|
||||
></item.icon>
|
||||
{item.name}
|
||||
</Flex>
|
||||
</a>
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Space>
|
||||
<Toolbar></Toolbar>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default RagHeader;
|
||||
@@ -1,25 +0,0 @@
|
||||
.toolbarWrapper {
|
||||
:global(.ant-avatar) {
|
||||
background-color: rgba(242, 243, 245, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.circle {
|
||||
display: inline-block;
|
||||
line-height: 32px;
|
||||
width: 32px;
|
||||
text-align: center;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(242, 243, 245, 0.4);
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.language {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { DownOutlined, GithubOutlined } from '@ant-design/icons';
|
||||
import { Dropdown, MenuProps, Space } from 'antd';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import User from '../user';
|
||||
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { LanguageList, LanguageMap, ThemeEnum } from '@/constants/common';
|
||||
import { useChangeLanguage } from '@/hooks/logic-hooks';
|
||||
import {
|
||||
useFetchUserInfo,
|
||||
useListTenant,
|
||||
} from '@/hooks/use-user-setting-request';
|
||||
import { TenantRole } from '@/pages/user-setting/constants';
|
||||
import { BellRing, CircleHelp, MoonIcon, SunIcon } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import styled from './index.less';
|
||||
|
||||
const Circle = ({ children, ...restProps }: React.PropsWithChildren) => {
|
||||
return (
|
||||
<div {...restProps} className={styled.circle}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleGithubCLick = () => {
|
||||
window.open('https://github.com/infiniflow/ragflow', 'target');
|
||||
};
|
||||
|
||||
const handleDocHelpCLick = () => {
|
||||
window.open('https://ragflow.io/docs/dev/category/guides', 'target');
|
||||
};
|
||||
|
||||
const RightToolBar = () => {
|
||||
const { t } = useTranslate('common');
|
||||
const changeLanguage = useChangeLanguage();
|
||||
const { setTheme, theme } = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
data: { language = 'English' },
|
||||
} = useFetchUserInfo();
|
||||
|
||||
const handleItemClick: MenuProps['onClick'] = ({ key }) => {
|
||||
changeLanguage(key);
|
||||
};
|
||||
|
||||
const { data } = useListTenant();
|
||||
|
||||
const showBell = useMemo(() => {
|
||||
return data.some((x) => x.role === TenantRole.Invite);
|
||||
}, [data]);
|
||||
|
||||
const items: MenuProps['items'] = LanguageList.map((x) => ({
|
||||
key: x,
|
||||
label: <span>{LanguageMap[x as keyof typeof LanguageMap]}</span>,
|
||||
})).reduce<MenuProps['items']>((pre, cur) => {
|
||||
return [...pre!, { type: 'divider' }, cur];
|
||||
}, []);
|
||||
|
||||
const onMoonClick = React.useCallback(() => {
|
||||
setTheme(ThemeEnum.Light);
|
||||
}, [setTheme]);
|
||||
const onSunClick = React.useCallback(() => {
|
||||
setTheme(ThemeEnum.Dark);
|
||||
}, [setTheme]);
|
||||
|
||||
const handleBellClick = useCallback(() => {
|
||||
navigate('/user-setting/team');
|
||||
}, [navigate]);
|
||||
|
||||
return (
|
||||
<div className={styled.toolbarWrapper}>
|
||||
<Space wrap size={16}>
|
||||
<Dropdown menu={{ items, onClick: handleItemClick }} placement="bottom">
|
||||
<Space className={styled.language}>
|
||||
<b>{t(camelCase(language))}</b>
|
||||
<DownOutlined />
|
||||
</Space>
|
||||
</Dropdown>
|
||||
<Circle>
|
||||
<GithubOutlined onClick={handleGithubCLick} />
|
||||
</Circle>
|
||||
<Circle>
|
||||
<CircleHelp className="size-4" onClick={handleDocHelpCLick} />
|
||||
</Circle>
|
||||
<Circle>
|
||||
{theme === 'dark' ? (
|
||||
<MoonIcon onClick={onMoonClick} size={20} />
|
||||
) : (
|
||||
<SunIcon onClick={onSunClick} size={20} />
|
||||
)}
|
||||
</Circle>
|
||||
{showBell && (
|
||||
<Circle>
|
||||
<div className="relative" onClick={handleBellClick}>
|
||||
<BellRing className="size-4 " />
|
||||
<span className="absolute size-1 rounded -right-1 -top-1 bg-red-600"></span>
|
||||
</div>
|
||||
</Circle>
|
||||
)}
|
||||
<User></User>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RightToolBar;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
|
||||
import { history } from '@/utils/simple-history-util';
|
||||
import { Avatar } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
import styles from '../../index.module.less';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const { data: userInfo } = useFetchUserInfo();
|
||||
|
||||
const toSetting = () => {
|
||||
history.push('/user-setting');
|
||||
};
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
size={32}
|
||||
onClick={toSetting}
|
||||
className={styles.clickAvailable}
|
||||
src={
|
||||
userInfo.avatar ??
|
||||
'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -3,73 +3,35 @@ import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import { LanguageAbbreviation } from '@/constants/common';
|
||||
import translation_de from './de';
|
||||
import translation_en from './en';
|
||||
import translation_es from './es';
|
||||
import translation_fr from './fr';
|
||||
import translation_id from './id';
|
||||
import translation_it from './it';
|
||||
import translation_ja from './ja';
|
||||
import translation_pt_br from './pt-br';
|
||||
import translation_ru from './ru';
|
||||
import { createTranslationTable, flattenObject } from './until';
|
||||
import translation_vi from './vi';
|
||||
import translation_zh from './zh';
|
||||
import translation_zh_traditional from './zh-traditional';
|
||||
|
||||
import translation_en from './en';
|
||||
|
||||
const languageImports: Record<string, () => Promise<{ default: any }>> = {
|
||||
[LanguageAbbreviation.Zh]: () => import('./zh'),
|
||||
[LanguageAbbreviation.ZhTraditional]: () => import('./zh-traditional'),
|
||||
[LanguageAbbreviation.Id]: () => import('./id'),
|
||||
[LanguageAbbreviation.Ja]: () => import('./ja'),
|
||||
[LanguageAbbreviation.Es]: () => import('./es'),
|
||||
[LanguageAbbreviation.Vi]: () => import('./vi'),
|
||||
[LanguageAbbreviation.Ru]: () => import('./ru'),
|
||||
[LanguageAbbreviation.PtBr]: () => import('./pt-br'),
|
||||
[LanguageAbbreviation.De]: () => import('./de'),
|
||||
[LanguageAbbreviation.Fr]: () => import('./fr'),
|
||||
[LanguageAbbreviation.It]: () => import('./it'),
|
||||
};
|
||||
|
||||
const enFlattened = flattenObject(translation_en);
|
||||
|
||||
export const translationTable = createTranslationTable(
|
||||
[enFlattened],
|
||||
['English'],
|
||||
);
|
||||
|
||||
const resources = {
|
||||
[LanguageAbbreviation.En]: translation_en,
|
||||
[LanguageAbbreviation.Zh]: translation_zh,
|
||||
[LanguageAbbreviation.ZhTraditional]: translation_zh_traditional,
|
||||
[LanguageAbbreviation.Id]: translation_id,
|
||||
[LanguageAbbreviation.Ja]: translation_ja,
|
||||
[LanguageAbbreviation.Es]: translation_es,
|
||||
[LanguageAbbreviation.Vi]: translation_vi,
|
||||
[LanguageAbbreviation.Ru]: translation_ru,
|
||||
[LanguageAbbreviation.PtBr]: translation_pt_br,
|
||||
[LanguageAbbreviation.De]: translation_de,
|
||||
[LanguageAbbreviation.Fr]: translation_fr,
|
||||
[LanguageAbbreviation.It]: translation_it,
|
||||
};
|
||||
const enFlattened = flattenObject(translation_en);
|
||||
const viFlattened = flattenObject(translation_vi);
|
||||
const ruFlattened = flattenObject(translation_ru);
|
||||
const esFlattened = flattenObject(translation_es);
|
||||
const zhFlattened = flattenObject(translation_zh);
|
||||
const jaFlattened = flattenObject(translation_ja);
|
||||
const pt_brFlattened = flattenObject(translation_pt_br);
|
||||
const zh_traditionalFlattened = flattenObject(translation_zh_traditional);
|
||||
const deFlattened = flattenObject(translation_de);
|
||||
const frFlattened = flattenObject(translation_fr);
|
||||
const itFlattened = flattenObject(translation_it);
|
||||
export const translationTable = createTranslationTable(
|
||||
[
|
||||
enFlattened,
|
||||
viFlattened,
|
||||
ruFlattened,
|
||||
esFlattened,
|
||||
zhFlattened,
|
||||
zh_traditionalFlattened,
|
||||
jaFlattened,
|
||||
pt_brFlattened,
|
||||
deFlattened,
|
||||
frFlattened,
|
||||
itFlattened,
|
||||
],
|
||||
[
|
||||
'English',
|
||||
'Vietnamese',
|
||||
'ru',
|
||||
'Spanish',
|
||||
'zh',
|
||||
'zh-TRADITIONAL',
|
||||
'ja',
|
||||
'pt-BR',
|
||||
'Deutsch',
|
||||
'French',
|
||||
'Italian',
|
||||
],
|
||||
);
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.use(LanguageDetector)
|
||||
@@ -85,4 +47,43 @@ i18n
|
||||
},
|
||||
});
|
||||
|
||||
export const loadLanguageAsync = async (lng: string): Promise<void> => {
|
||||
if (i18n.hasResourceBundle(lng, 'translation')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importFn = languageImports[lng];
|
||||
if (!importFn) {
|
||||
console.warn(`Language ${lng} is not supported for lazy loading`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const module = await importFn();
|
||||
const translationData = module.default?.translation || module.default;
|
||||
i18n.addResourceBundle(lng, 'translation', translationData);
|
||||
|
||||
const flattened = flattenObject({ translation: translationData });
|
||||
translationTable.push(flattened);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load language ${lng}:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
export const changeLanguageAsync = async (lng: string): Promise<void> => {
|
||||
if (lng !== 'en' && !i18n.hasResourceBundle(lng, 'translation')) {
|
||||
await loadLanguageAsync(lng);
|
||||
}
|
||||
await i18n.changeLanguage(lng);
|
||||
};
|
||||
|
||||
export const initLanguage = async (): Promise<void> => {
|
||||
const currentLng = i18n.language || localStorage.getItem('lng') || 'en';
|
||||
|
||||
if (currentLng !== 'en' && languageImports[currentLng]) {
|
||||
await loadLanguageAsync(currentLng);
|
||||
await i18n.changeLanguage(currentLng);
|
||||
}
|
||||
};
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -4,10 +4,13 @@ import ReactDOM from 'react-dom/client';
|
||||
import '../tailwind.css';
|
||||
import App from './app';
|
||||
import './global.less';
|
||||
import { initLanguage } from './locales/config';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<Inspector keys={['alt', 'c']} onInspectElement={gotoVSCode} />
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
initLanguage().then(() => {
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<Inspector keys={['alt', 'c']} onInspectElement={gotoVSCode} />
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Routes } from '@/routes';
|
||||
import { history } from '@/utils/simple-history-util';
|
||||
import { Button, Result } from 'antd';
|
||||
import { useLocation } from 'react-router';
|
||||
import { useLocation, useNavigate } from 'react-router';
|
||||
|
||||
const NoFoundPage = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<Result
|
||||
status="404"
|
||||
title="404"
|
||||
subTitle="Page not found, please enter a correct address."
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
history.push(
|
||||
location.pathname.startsWith(Routes.Admin) ? Routes.Admin : '/',
|
||||
);
|
||||
}}
|
||||
>
|
||||
Business
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh]">
|
||||
<div className="text-6xl font-bold text-text-secondary mb-4">404</div>
|
||||
<div className="text-lg text-text-secondary mb-8">
|
||||
Page not found, please enter a correct address.
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate(
|
||||
location.pathname.startsWith(Routes.Admin) ? Routes.Admin : '/',
|
||||
);
|
||||
}}
|
||||
>
|
||||
Business
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FormContainer } from '@/components/form-container';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { TopNFormField } from '@/components/top-n-item';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -12,9 +13,7 @@ import {
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { DatePicker, DatePickerProps } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useForm, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { initialGoogleScholarValues } from '../../constant';
|
||||
@@ -27,7 +26,6 @@ import { FormWrapper } from '../components/form-wrapper';
|
||||
import { Output } from '../components/output';
|
||||
import { QueryVariable } from '../components/query-variable';
|
||||
|
||||
// TODO: To be replaced
|
||||
const YearPicker = ({
|
||||
onChange,
|
||||
value,
|
||||
@@ -35,22 +33,16 @@ const YearPicker = ({
|
||||
onChange?: (val: number | undefined) => void;
|
||||
value?: number | undefined;
|
||||
}) => {
|
||||
const handleChange: DatePickerProps['onChange'] = useCallback(
|
||||
(val: any) => {
|
||||
const nextVal = val?.format('YYYY');
|
||||
onChange?.(nextVal ? Number(nextVal) : undefined);
|
||||
const handleChange = useCallback(
|
||||
(date: Date | undefined) => {
|
||||
onChange?.(date?.getFullYear());
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
// The year needs to be converted into a number and saved to the backend
|
||||
const nextValue = useMemo(() => {
|
||||
if (value) {
|
||||
return dayjs(value.toString());
|
||||
}
|
||||
return undefined;
|
||||
}, [value]);
|
||||
|
||||
return <DatePicker picker="year" onChange={handleChange} value={nextValue} />;
|
||||
const dateValue = value ? new Date(value, 0, 1) : undefined;
|
||||
|
||||
return <DatePicker picker="year" value={dateValue} onChange={handleChange} />;
|
||||
};
|
||||
|
||||
export function GoogleScholarFormWidgets() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AgentGlobals, AgentStructuredOutputField } from '@/constants/agent';
|
||||
import { useFetchAgent } from '@/hooks/use-agent-request';
|
||||
import { DefaultOptionType } from '@/interfaces/antd-compat';
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import {
|
||||
buildNodeOutputOptions,
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
buildUpstreamNodeOutputOptions,
|
||||
isAgentStructured,
|
||||
} from '@/utils/canvas-util';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { t } from 'i18next';
|
||||
import { flatten, isEmpty, toLower } from 'lodash';
|
||||
import get from 'lodash/get';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormInstance } from '@/interfaces/antd-compat';
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { FormInstance } from 'antd';
|
||||
|
||||
export interface IOperatorForm {
|
||||
onValuesChange?(changedValues: any, values: any): void;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useSyncThemeFromParams } from '@/components/theme-provider';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { useUploadCanvasFileWithProgress } from '@/hooks/use-agent-request';
|
||||
import { cn } from '@/lib/utils';
|
||||
import i18n from '@/locales/config';
|
||||
import i18n, { changeLanguageAsync } from '@/locales/config';
|
||||
import DebugContent from '@/pages/agent/debug-content';
|
||||
import { useCacheChatLog } from '@/pages/agent/hooks/use-cache-chat-log';
|
||||
import { useAwaitCompentData } from '@/pages/agent/hooks/use-chat-logic';
|
||||
@@ -88,7 +88,7 @@ const ChatContainer = () => {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (locale && i18n.language !== locale) {
|
||||
i18n.changeLanguage(locale);
|
||||
changeLanguageAsync(locale);
|
||||
}
|
||||
}, [locale, visibleAvatar]);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FormInstance, FormListFieldData } from '@/interfaces/antd-compat';
|
||||
import {
|
||||
DSL,
|
||||
GlobalVariableType,
|
||||
@@ -10,7 +11,6 @@ import { DSLComponents, RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { buildSelectOptions } from '@/utils/component-util';
|
||||
import { buildOptions, removeUselessFieldsFromValues } from '@/utils/form';
|
||||
import { Edge, Node, XYPosition } from '@xyflow/react';
|
||||
import { FormInstance, FormListFieldData } from 'antd';
|
||||
import { humanId } from 'human-id';
|
||||
import {
|
||||
curry,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { DateInput } from '@/components/ui/input-date';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { ColumnDef, Row, Table } from '@tanstack/react-table';
|
||||
import { ListChevronsDownUp, Settings, Trash2 } from 'lucide-react';
|
||||
@@ -209,7 +209,7 @@ export const useMetadataColumns = ({
|
||||
<div key={value}>
|
||||
{row.original.valueType ===
|
||||
metadataValueTypeEnum.time && (
|
||||
<DateInput
|
||||
<DatePicker
|
||||
value={new Date(editingValue.newValue)}
|
||||
onChange={(value) => {
|
||||
const newValue = {
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
import { DynamicForm, FormFieldType } from '@/components/dynamic-form';
|
||||
import EditTag from '@/components/edit-tag';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { DateInput } from '@/components/ui/input-date';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -74,7 +74,7 @@ const ValueInputItem = memo(
|
||||
>
|
||||
<div className="flex-1 w-full">
|
||||
{type === 'time' && (
|
||||
<DateInput
|
||||
<DatePicker
|
||||
value={value as Date}
|
||||
onChange={(value) => {
|
||||
onValueChange(
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import SvgIcon from '@/components/svg-icon';
|
||||
import Divider from '@/components/ui/divider';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useSelectParserList } from '@/hooks/use-user-setting-request';
|
||||
import { Col, Divider, Empty, Row, Typography } from 'antd';
|
||||
import DOMPurify from 'dompurify';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import { useMemo } from 'react';
|
||||
import { TagTabs } from './tag-tabs';
|
||||
import { ImageMap } from './utils';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const CategoryPanel = ({ chunkMethod }: { chunkMethod: string }) => {
|
||||
const parserList = useSelectParserList();
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
@@ -45,24 +43,29 @@ const CategoryPanel = ({ chunkMethod }: { chunkMethod: string }) => {
|
||||
}}
|
||||
></p>
|
||||
<h5 className="font-semibold text-base mt-4 mb-1">{`"${item.title}" ${t('methodExamples')}`}</h5>
|
||||
<Text>{t('methodExamplesDescription')}</Text>
|
||||
<Row gutter={[10, 10]} className="mt-4">
|
||||
<span className="text-text-secondary">
|
||||
{t('methodExamplesDescription')}
|
||||
</span>
|
||||
<div className="grid grid-cols-2 gap-2.5 mt-4">
|
||||
{imageList.map((x) => (
|
||||
<Col span={12} key={x}>
|
||||
<SvgIcon name={x} width={'100%'} className="w-full"></SvgIcon>
|
||||
</Col>
|
||||
<SvgIcon
|
||||
name={x}
|
||||
width={'100%'}
|
||||
className="w-full"
|
||||
key={x}
|
||||
></SvgIcon>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
<h5 className="font-semibold text-base mt-4 mb-1">
|
||||
{item.title} {t('dialogueExamplesTitle')}
|
||||
</h5>
|
||||
<Divider></Divider>
|
||||
</>
|
||||
) : (
|
||||
<Empty description={''} image={null}>
|
||||
<p>{t('methodEmpty')}</p>
|
||||
<div className="flex flex-col items-center justify-center py-8">
|
||||
<p className="text-text-secondary mb-4">{t('methodEmpty')}</p>
|
||||
<SvgIcon name={'chunk-method/chunk-empty'} width={'100%'}></SvgIcon>
|
||||
</Empty>
|
||||
</div>
|
||||
)}
|
||||
{chunkMethod === 'tag' && <TagTabs></TagTabs>}
|
||||
</section>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import CategoryPanel from './category-panel';
|
||||
|
||||
export default ({ parserId }: { parserId: string }) => {
|
||||
const ChunkMethodLearnMore = ({ parserId }: { parserId: string }) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -37,3 +37,5 @@ export default ({ parserId }: { parserId: string }) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChunkMethodLearnMore;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import { MultiSelect } from '@/components/ui/multi-select';
|
||||
import { FormLayout } from '@/constants/form';
|
||||
import { useFetchKnowledgeList } from '@/hooks/use-knowledge-request';
|
||||
import { Form, Select, Space } from 'antd';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -27,13 +26,11 @@ export const TagSetItem = () => {
|
||||
label: x.name,
|
||||
value: x.id,
|
||||
icon: () => (
|
||||
<Space>
|
||||
<RAGFlowAvatar
|
||||
name={x.name}
|
||||
avatar={x.avatar}
|
||||
className="size-4"
|
||||
></RAGFlowAvatar>
|
||||
</Space>
|
||||
<RAGFlowAvatar
|
||||
name={x.name}
|
||||
avatar={x.avatar}
|
||||
className="size-4"
|
||||
></RAGFlowAvatar>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -79,32 +76,6 @@ export const TagSetItem = () => {
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
label={t('knowledgeConfiguration.tagSet')}
|
||||
name={['parser_config', 'tag_kb_ids']}
|
||||
tooltip={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(t('knowledgeConfiguration.tagSetTip')),
|
||||
}}
|
||||
></div>
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
message: t('chat.knowledgeBasesMessage'),
|
||||
type: 'array',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
options={knowledgeOptions}
|
||||
placeholder={t('chat.knowledgeBasesMessage')}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export const TopNTagsItem = () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Segmented } from 'antd';
|
||||
import { SegmentedLabeledOption } from 'antd/es/segmented';
|
||||
import { Segmented, SegmentedLabeledOption } from '@/components/ui/segmented';
|
||||
import { upperFirst } from 'lodash';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -30,6 +29,7 @@ export function TagTabs() {
|
||||
return (
|
||||
<section className="mt-4">
|
||||
<Segmented
|
||||
className="w-fit"
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={(val) => setValue(val as TagType)}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Image as AntImage } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Authorization } from '@/constants/authorization';
|
||||
import { getAuthorization } from '@/utils/authorization-util';
|
||||
|
||||
interface ImageProps {
|
||||
src: string;
|
||||
preview?: boolean;
|
||||
}
|
||||
|
||||
const Image = ({ src, preview = false }: ImageProps) => {
|
||||
const [imageSrc, setImageSrc] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
const response = await fetch(src, {
|
||||
headers: {
|
||||
[Authorization]: getAuthorization(),
|
||||
},
|
||||
});
|
||||
const blob = await response.blob();
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
setImageSrc(objectUrl);
|
||||
} catch (error) {
|
||||
console.error('Failed to load image:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadImage();
|
||||
|
||||
return () => {
|
||||
if (imageSrc) {
|
||||
URL.revokeObjectURL(imageSrc);
|
||||
}
|
||||
};
|
||||
}, [src]);
|
||||
|
||||
return imageSrc ? <AntImage src={imageSrc} preview={preview} /> : null;
|
||||
};
|
||||
|
||||
export default Image;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { useConnectToKnowledge, useRenameFile } from '@/hooks/use-file-request';
|
||||
import { TableRowSelection } from '@/interfaces/antd-compat';
|
||||
import { IFile } from '@/interfaces/database/file-manager';
|
||||
import { TableRowSelection } from 'antd/es/table/interface';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FormInstance } from 'antd';
|
||||
import { FormInstance } from '@/interfaces/antd-compat';
|
||||
|
||||
export interface ISegmentedContentProps {
|
||||
show: boolean;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextMessageInputOnPressEnterParameter } from '@/components/message-input/next';
|
||||
import message from '@/components/ui/message';
|
||||
import { MessageType, SharedFrom } from '@/constants/chat';
|
||||
import {
|
||||
useHandleMessageInputChange,
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
} from '@/hooks/logic-hooks';
|
||||
import { useCreateNextSharedConversation } from '@/hooks/use-chat-request';
|
||||
import { Message } from '@/interfaces/database/chat';
|
||||
import { message } from 'antd';
|
||||
import { get } from 'lodash';
|
||||
import trim from 'lodash/trim';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
useFetchExternalChatInfo,
|
||||
useFetchNextConversationSSE,
|
||||
} from '@/hooks/use-chat-request';
|
||||
import i18n from '@/locales/config';
|
||||
import i18n, { changeLanguageAsync } from '@/locales/config';
|
||||
import { buildMessageUuidWithRole } from '@/utils/chat';
|
||||
import React, { forwardRef, useMemo } from 'react';
|
||||
import { useSendButtonDisabled } from '../hooks/use-button-disabled';
|
||||
@@ -54,7 +54,7 @@ const ChatContainer = () => {
|
||||
}, [from]);
|
||||
React.useEffect(() => {
|
||||
if (locale && i18n.language !== locale) {
|
||||
i18n.changeLanguage(locale);
|
||||
changeLanguageAsync(locale);
|
||||
}
|
||||
}, [locale, visibleAvatar]);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import i18n from '@/locales/config';
|
||||
import i18n, { changeLanguageAsync } from '@/locales/config';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
ISearchAppDetailProps,
|
||||
@@ -24,7 +24,7 @@ export default function ShareSeachPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (locale && i18n.language !== locale) {
|
||||
i18n.changeLanguage(locale);
|
||||
changeLanguageAsync(locale);
|
||||
}
|
||||
}, [locale]);
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Images, SupportedPreviewDocumentTypes } from '@/constants/common';
|
||||
import { UploadFile } from '@/interfaces/antd-compat';
|
||||
import { IReferenceChunk } from '@/interfaces/database/chat';
|
||||
import { IChunk } from '@/interfaces/database/knowledge';
|
||||
import { UploadFile } from 'antd';
|
||||
import { get } from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FileMimeType } from '@/constants/common';
|
||||
import { UploadFile } from '@/interfaces/antd-compat';
|
||||
import fileManagerService from '@/services/file-manager-service';
|
||||
import { UploadFile } from 'antd';
|
||||
|
||||
export const transformFile2Base64 = (
|
||||
val: any,
|
||||
|
||||
@@ -5,7 +5,7 @@ import authorizationUtil, {
|
||||
getAuthorization,
|
||||
redirectToLogin,
|
||||
} from '@/utils/authorization-util';
|
||||
import { notification } from 'antd';
|
||||
import notification from '@/utils/notification';
|
||||
import axios from 'axios';
|
||||
import { convertTheKeysOfTheObjectToSnake } from './common-util';
|
||||
|
||||
|
||||
58
web/src/utils/notification.ts
Normal file
58
web/src/utils/notification.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { ExternalToast, toast } from 'sonner';
|
||||
|
||||
const defaultConfig: ExternalToast = { duration: 4000, position: 'top-right' };
|
||||
|
||||
type NotificationOptions = {
|
||||
message: string;
|
||||
description?: string;
|
||||
duration?: number;
|
||||
};
|
||||
|
||||
const notification = {
|
||||
success: (options: NotificationOptions) => {
|
||||
const messageText = options.description
|
||||
? `${options.message}\n${options.description}`
|
||||
: options.message;
|
||||
toast.success(messageText, {
|
||||
...defaultConfig,
|
||||
duration: options.duration
|
||||
? options.duration * 1000
|
||||
: defaultConfig.duration,
|
||||
});
|
||||
},
|
||||
error: (options: NotificationOptions) => {
|
||||
const messageText = options.description
|
||||
? `${options.message}\n${options.description}`
|
||||
: options.message;
|
||||
toast.error(messageText, {
|
||||
...defaultConfig,
|
||||
duration: options.duration
|
||||
? options.duration * 1000
|
||||
: defaultConfig.duration,
|
||||
});
|
||||
},
|
||||
warning: (options: NotificationOptions) => {
|
||||
const messageText = options.description
|
||||
? `${options.message}\n${options.description}`
|
||||
: options.message;
|
||||
toast.warning(messageText, {
|
||||
...defaultConfig,
|
||||
duration: options.duration
|
||||
? options.duration * 1000
|
||||
: defaultConfig.duration,
|
||||
});
|
||||
},
|
||||
info: (options: NotificationOptions) => {
|
||||
const messageText = options.description
|
||||
? `${options.message}\n${options.description}`
|
||||
: options.message;
|
||||
toast.info(messageText, {
|
||||
...defaultConfig,
|
||||
duration: options.duration
|
||||
? options.duration * 1000
|
||||
: defaultConfig.duration,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default notification;
|
||||
@@ -1,3 +1,4 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { Authorization } from '@/constants/authorization';
|
||||
import { ResponseType } from '@/interfaces/database/base';
|
||||
import i18n from '@/locales/config';
|
||||
@@ -5,7 +6,7 @@ import authorizationUtil, {
|
||||
getAuthorization,
|
||||
redirectToLogin,
|
||||
} from '@/utils/authorization-util';
|
||||
import { message, notification } from 'antd';
|
||||
import notification from '@/utils/notification';
|
||||
import { RequestMethod, extend } from 'umi-request';
|
||||
import { convertTheKeysOfTheObjectToSnake } from './common-util';
|
||||
|
||||
|
||||
@@ -113,6 +113,13 @@ export default defineConfig(({ mode, command }) => {
|
||||
// return 'components';
|
||||
// }
|
||||
|
||||
if (id.includes('src/locales/') && id.endsWith('.ts')) {
|
||||
const match = id.match(/src\/locales\/([^/]+)\.ts$/);
|
||||
if (match) {
|
||||
return `locale-${match[1]}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (id.includes('node_modules')) {
|
||||
if (id.includes('node_modules/d3')) {
|
||||
return 'd3';
|
||||
|
||||
Reference in New Issue
Block a user