diff --git a/web/src/components/card-singleline-container/index.less b/web/src/components/card-singleline-container/index.less new file mode 100644 index 000000000..8c3881773 --- /dev/null +++ b/web/src/components/card-singleline-container/index.less @@ -0,0 +1,46 @@ +.single :nth-child(n + 2):not(:last-child) { + display: none; +} +.single :nth-child(-n + 1):not(:last-child) { + display: flex; +} +@media (min-width: 640px) { + .single :nth-child(n + 2):not(:last-child) { + display: none; + } + .single :nth-child(-n + 1):not(:last-child) { + display: flex; + } +} +@media (min-width: 768px) { + .single :nth-child(n + 3):not(:last-child) { + display: none; + } + .single :nth-child(-n + 2):not(:last-child) { + display: flex; + } +} +@media (min-width: 1024px) { + .single :nth-child(n + 4):not(:last-child) { + display: none; + } + .single :nth-child(-n + 3):not(:last-child) { + display: flex; + } +} +@media (min-width: 1280px) { + .single :nth-child(n + 5):not(:last-child) { + display: none; + } + .single :nth-child(-n + 4):not(:last-child) { + display: flex; + } +} +@media (min-width: 1536px) { + .single :nth-child(n + 6):not(:last-child) { + display: none; + } + .single :nth-child(-n + 5):not(:last-child) { + display: flex; + } +} diff --git a/web/src/components/card-singleline-container/index.tsx b/web/src/components/card-singleline-container/index.tsx new file mode 100644 index 000000000..a35a37603 --- /dev/null +++ b/web/src/components/card-singleline-container/index.tsx @@ -0,0 +1,41 @@ +import { cn } from '@/lib/utils'; +import { isValidElement, PropsWithChildren, ReactNode } from 'react'; +import './index.less'; + +type CardContainerProps = { className?: string } & PropsWithChildren; + +export function CardSineLineContainer({ + children, + className, +}: CardContainerProps) { + const flattenChildren = (children: ReactNode): ReactNode[] => { + const result: ReactNode[] = []; + + const traverse = (child: ReactNode) => { + if (Array.isArray(child)) { + child.forEach(traverse); + } else if (isValidElement(child) && child.props.children) { + result.push(child); + } else { + result.push(child); + } + }; + + traverse(children); + return result; + }; + const childArray = flattenChildren(children); + const childCount = childArray.length; + console.log(childArray, childCount); + + return ( +
+ {children} +
+ ); +} diff --git a/web/src/components/home-card.tsx b/web/src/components/home-card.tsx index 67562d60f..8defb4f66 100644 --- a/web/src/components/home-card.tsx +++ b/web/src/components/home-card.tsx @@ -29,7 +29,7 @@ export function HomeCard({ onClick?.(); }} > - +
- - - +
+ + +
); } diff --git a/web/src/components/ui/segmented.tsx b/web/src/components/ui/segmented.tsx index 7aa9565db..8aadc3b21 100644 --- a/web/src/components/ui/segmented.tsx +++ b/web/src/components/ui/segmented.tsx @@ -13,6 +13,33 @@ export interface SegmentedLabeledOption { title?: string; } declare type SegmentedOptions = (SegmentedRawOption | SegmentedLabeledOption)[]; +const segmentedVariants = { + round: { + default: 'rounded-md', + none: 'rounded-none', + sm: 'rounded-sm', + md: 'rounded-md', + lg: 'rounded-lg', + xl: 'rounded-xl', + xxl: 'rounded-2xl', + xxxl: 'rounded-3xl', + full: 'rounded-full', + }, + size: { + default: 'px-1 py-1', + sm: 'px-1 py-1', + md: 'px-2 py-1.5', + lg: 'px-4 px-2', + xl: 'px-5 py-2.5', + xxl: 'px-6 py-3', + }, + buttonSize: { + default: 'px-2 py-1', + md: 'px-2 py-1', + lg: 'px-4 px-1.5', + xl: 'px-6 py-2', + }, +}; export interface SegmentedProps extends Omit, 'onChange'> { options: SegmentedOptions; @@ -24,6 +51,9 @@ export interface SegmentedProps direction?: 'ltr' | 'rtl'; motionName?: string; activeClassName?: string; + rounded?: keyof typeof segmentedVariants.round; + sizeType?: keyof typeof segmentedVariants.size; + buttonSize?: keyof typeof segmentedVariants.buttonSize; } export function Segmented({ @@ -32,6 +62,9 @@ export function Segmented({ onChange, className, activeClassName, + rounded = 'default', + sizeType = 'default', + buttonSize = 'default', }: SegmentedProps) { const [selectedValue, setSelectedValue] = React.useState< SegmentedValue | undefined @@ -45,7 +78,9 @@ export function Segmented({ return (
@@ -57,10 +92,11 @@ export function Segmented({
Only PDF is supported.

We assume that the manual has a hierarchical section structure, using the lowest section titles as basic unit for chunking documents. Therefore, figures and tables in the same section will not be separated, which may result in larger chunk sizes.

`, - naive: `

Supported file formats are MD, MDX, DOCX, XLSX, XLS (Excel 97-2003), PPT, PDF, TXT, JPEG, JPG, PNG, TIF, GIF, CSV, JSON, EML, HTML.

+ naive: `

Supported file formats are MD, MDX, DOCX, XLSX, XLS (Excel 97-2003), PPTX, PDF, TXT, JPEG, JPG, PNG, TIF, GIF, CSV, JSON, EML, HTML.

This method chunks files using a 'naive' method:

  • Use vision detection model to split the texts into smaller segments.
  • @@ -710,7 +710,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s timezone: 'Time zone', timezoneMessage: 'Please input your timezone!', timezonePlaceholder: 'select your timezone', - email: 'Email address', + email: 'Email', emailDescription: 'Once registered, E-mail cannot be changed.', currentPassword: 'Current password', currentPasswordMessage: 'Please input your password!', @@ -865,9 +865,9 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s apiVersion: 'API-Version', apiVersionMessage: 'Please input API version', add: 'Add', - updateDate: 'Update Date', - role: 'Role', - invite: 'Invite', + updateDate: 'Date', + role: 'State', + invite: 'Invite Member', agree: 'Accept', refuse: 'Decline', teamMembers: 'Team Members', @@ -1631,7 +1631,7 @@ This delimiter is used to split the input text into several text pieces echo of email: 'Email', 'text&markdown': 'Text & Markup', word: 'Word', - slides: 'PPT', + slides: 'PPTX', audio: 'Audio', video: 'Video', }, @@ -1803,13 +1803,13 @@ Important structured information may include: names, dates, locations, events, k confirmRerun: 'Confirm Rerun Process', confirmRerunModalContent: `

    - You are about to rerun the process starting from the {{step}} step. + You are about to rerun the process starting from the {{step}} step.

    -

    This will:

    +

    This will:


      -
    • Overwrite existing results from the current step onwards
    • -
    • Create a new log entry for tracking
    • -
    • Previous steps will remain unchanged
    • +
    • • Overwrite existing results from the current step onwards
    • +
    • • Create a new log entry for tracking
    • +
    • • Previous steps will remain unchanged
    `, changeStepModalTitle: 'Step Switch Warning', changeStepModalContent: ` diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 8605246d0..4d2170aa9 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -336,7 +336,7 @@ export default { 我们假设手册具有分层部分结构。 我们使用最低的部分标题作为对文档进行切片的枢轴。 因此,同一部分中的图和表不会被分割,并且块大小可能会很大。

    `, - naive: `

    支持的文件格式为MD、MDX、DOCX、XLSX、XLS (Excel 97-2003)、PPT、PDF、TXT、JPEG、JPG、PNG、TIF、GIF、CSV、JSON、EML、HTML

    + naive: `

    支持的文件格式为MD、MDX、DOCX、XLSX、XLS (Excel 97-2003)、PPTX、PDF、TXT、JPEG、JPG、PNG、TIF、GIF、CSV、JSON、EML、HTML

    此方法将简单的方法应用于块文件:

  • 系统将使用视觉检测模型将连续文本分割成多个片段。
  • @@ -701,7 +701,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 timezone: '时区', timezoneMessage: '请选择时区', timezonePlaceholder: '请选择时区', - email: '邮箱地址', + email: '邮箱', emailDescription: '一旦注册,电子邮件将无法更改。', currentPassword: '当前密码', currentPasswordMessage: '请输入当前密码', @@ -821,9 +821,9 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 apiVersion: 'API版本', apiVersionMessage: '请输入API版本!', add: '添加', - updateDate: '更新日期', - role: '角色', - invite: '邀请', + updateDate: '日期', + role: '状态', + invite: '邀请成员', agree: '同意', refuse: '拒绝', teamMembers: '团队成员', @@ -1690,13 +1690,13 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`, confirmRerun: '确认重新运行流程', confirmRerunModalContent: `

    - 您即将从 {{step}} 步骤开始重新运行该过程 + 您即将从 {{step}} 步骤开始重新运行该过程

    -

    这将:

    +

    这将:

      -
    • 从当前步骤开始覆盖现有结果
    • -
    • 创建新的日志条目进行跟踪
    • -
    • 之前的步骤将保持不变
    • +
    • • 从当前步骤开始覆盖现有结果
    • +
    • • 创建新的日志条目进行跟踪
    • +
    • • 之前的步骤将保持不变
    `, changeStepModalTitle: '切换步骤警告', changeStepModalContent: ` diff --git a/web/src/pages/datasets/dataset-card.tsx b/web/src/pages/datasets/dataset-card.tsx index ff92c85e8..4fd751ac7 100644 --- a/web/src/pages/datasets/dataset-card.tsx +++ b/web/src/pages/datasets/dataset-card.tsx @@ -43,7 +43,7 @@ export function SeeAllCard() { const { navigateToDatasetList } = useNavigatePage(); return ( - + See All diff --git a/web/src/pages/home/agent-list.tsx b/web/src/pages/home/agent-list.tsx index 0f0d6d08f..5b74c3db0 100644 --- a/web/src/pages/home/agent-list.tsx +++ b/web/src/pages/home/agent-list.tsx @@ -1,10 +1,10 @@ +import { HomeCard } from '@/components/home-card'; import { MoreButton } from '@/components/more-button'; import { RenameDialog } from '@/components/rename-dialog'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useFetchAgentListByPage } from '@/hooks/use-agent-request'; import { AgentDropdown } from '../agents/agent-dropdown'; import { useRenameAgent } from '../agents/use-rename-agent'; -import { ApplicationCard } from './application-card'; export function Agents() { const { data } = useFetchAgentListByPage(); @@ -21,9 +21,9 @@ export function Agents() { return ( <> {data.slice(0, 10).map((x) => ( - } - > + > ))} {agentRenameVisible && ( + See All diff --git a/web/src/pages/home/applications.tsx b/web/src/pages/home/applications.tsx index 3c2e3ce59..4eaf52b36 100644 --- a/web/src/pages/home/applications.tsx +++ b/web/src/pages/home/applications.tsx @@ -1,3 +1,4 @@ +import { CardSineLineContainer } from '@/components/card-singleline-container'; import { IconFont } from '@/components/icon-font'; import { Segmented, SegmentedValue } from '@/components/ui/segmented'; import { Routes } from '@/routes'; @@ -34,7 +35,7 @@ export function Applications() { ); const handleChange = (path: SegmentedValue) => { - setVal(path as string); + setVal(path as Routes); }; return ( @@ -51,16 +52,19 @@ export function Applications() { options={options} value={val} onChange={handleChange} - className="bg-bg-card border border-border-button rounded-full" - activeClassName="bg-text-primary border-none" + buttonSize="xl" + // className="bg-bg-card border border-border-button rounded-lg" + // activeClassName="bg-text-primary border-none rounded-lg" >
    -
    + {/*
    */} + {val === Routes.Agents && } {val === Routes.Chats && } {val === Routes.Searches && } {} -
    + + {/*
    */} ); } diff --git a/web/src/pages/home/chat-list.tsx b/web/src/pages/home/chat-list.tsx index 93e1c3de9..6178f85d7 100644 --- a/web/src/pages/home/chat-list.tsx +++ b/web/src/pages/home/chat-list.tsx @@ -1,3 +1,4 @@ +import { HomeCard } from '@/components/home-card'; import { MoreButton } from '@/components/more-button'; import { RenameDialog } from '@/components/rename-dialog'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; @@ -5,7 +6,6 @@ import { useFetchDialogList } from '@/hooks/use-chat-request'; import { useTranslation } from 'react-i18next'; import { ChatDropdown } from '../next-chats/chat-dropdown'; import { useRenameChat } from '../next-chats/hooks/use-rename-chat'; -import { ApplicationCard } from './application-card'; export function ChatList() { const { t } = useTranslation(); @@ -24,12 +24,11 @@ export function ChatList() { return ( <> {data.dialogs.slice(0, 10).map((x) => ( - } - > + > ))} {chatRenameVisible && (
    ) : ( -
    + //
    + {kbs ?.slice(0, 6) .map((dataset) => ( @@ -43,7 +45,8 @@ export function Datasets() {
    -
    + + //
    )}
    {datasetRenameVisible && ( diff --git a/web/src/pages/home/search-list.tsx b/web/src/pages/home/search-list.tsx index b1e7c8955..90cc098d3 100644 --- a/web/src/pages/home/search-list.tsx +++ b/web/src/pages/home/search-list.tsx @@ -1,10 +1,10 @@ +import { HomeCard } from '@/components/home-card'; import { IconFont } from '@/components/icon-font'; import { MoreButton } from '@/components/more-button'; import { RenameDialog } from '@/components/rename-dialog'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useFetchSearchList, useRenameSearch } from '../next-searches/hooks'; import { SearchDropdown } from '../next-searches/search-dropdown'; -import { ApplicationCard } from './application-card'; export function SearchList() { const { data, refetch: refetchList } = useFetchSearchList(); @@ -25,13 +25,9 @@ export function SearchList() { return ( <> {data?.data.search_apps.slice(0, 10).map((x) => ( - } - > + > ))} {openCreateModal && ( +

    -
    +
    {!isSearching && }
    {!isSearching && ( @@ -55,7 +55,7 @@ export default function SearchPage({
    { if (e.key === 'Enter') { diff --git a/web/src/pages/next-search/search-view.tsx b/web/src/pages/next-search/search-view.tsx index fe1bb0d7f..1c4d998cc 100644 --- a/web/src/pages/next-search/search-view.tsx +++ b/web/src/pages/next-search/search-view.tsx @@ -91,7 +91,6 @@ export default function SearchingView({ > RAGFlow

    -
    { setSearchtext(''); handleClickRelatedQuestion(''); }} /> - | + | - ))} + <> +
    + +
    +

    + {t('search.relatedSearch')} +

    +
    + {relatedQuestions?.map((x, idx) => ( + + ))} +
    -
    + )}
    @@ -272,7 +277,6 @@ export default function SearchingView({ )} - {mindMapVisible && (
    + {header} diff --git a/web/src/pages/profile-setting/mcp/index.tsx b/web/src/pages/profile-setting/mcp/index.tsx index 0fddfff65..d2563034b 100644 --- a/web/src/pages/profile-setting/mcp/index.tsx +++ b/web/src/pages/profile-setting/mcp/index.tsx @@ -151,7 +151,7 @@ export default function McpServer() { onOk={onImportOk} > )} - + ); } diff --git a/web/src/pages/user-setting/index.tsx b/web/src/pages/user-setting/index.tsx index d307e3fda..224c2e9e2 100644 --- a/web/src/pages/user-setting/index.tsx +++ b/web/src/pages/user-setting/index.tsx @@ -38,10 +38,18 @@ const UserSetting = () => {
    -
    +
    diff --git a/web/src/pages/user-setting/profile/index.tsx b/web/src/pages/user-setting/profile/index.tsx index 266569fc5..b86e1fd41 100644 --- a/web/src/pages/user-setting/profile/index.tsx +++ b/web/src/pages/user-setting/profile/index.tsx @@ -1,6 +1,7 @@ // src/components/ProfilePage.tsx import { AvatarUpload } from '@/components/avatar-upload'; import PasswordInput from '@/components/originui/password-input'; +import Spotlight from '@/components/spotlight'; import { Button } from '@/components/ui/button'; import { Form, @@ -121,7 +122,8 @@ const ProfilePage: FC = () => { // }; return ( -
    +
    + {/* Header */}

    {t('profile')}

    diff --git a/web/src/pages/user-setting/setting-team/add-user-modal.tsx b/web/src/pages/user-setting/setting-team/add-user-modal.tsx index 5a251dfaf..3a734a6b4 100644 --- a/web/src/pages/user-setting/setting-team/add-user-modal.tsx +++ b/web/src/pages/user-setting/setting-team/add-user-modal.tsx @@ -1,6 +1,18 @@ +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Modal } from '@/components/ui/modal/modal'; import { IModalProps } from '@/interfaces/common'; -import { Form, Input, Modal } from 'antd'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import * as z from 'zod'; const AddingUserModal = ({ visible, @@ -8,42 +20,54 @@ const AddingUserModal = ({ loading, onOk, }: IModalProps) => { - const [form] = Form.useForm(); const { t } = useTranslation(); - type FieldType = { - email?: string; - }; + const formSchema = z.object({ + email: z + .string() + .email() + .min(1, { message: t('common.required') }), + }); - const handleOk = async () => { - const ret = await form.validateFields(); + type FormData = z.infer; - return onOk?.(ret.email); + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + }, + }); + + const handleOk = async (data: FormData) => { + return onOk?.(data.email); }; return ( !open && hideModal?.()} + onOk={form.handleSubmit(handleOk)} confirmLoading={loading} + okText={t('common.ok')} + cancelText={t('common.cancel')} > -
    - - label={t('setting.email')} - name="email" - rules={[{ required: true }]} - > - - + + + ( + + {t('setting.email')} + + + + + + )} + /> +
    ); diff --git a/web/src/pages/user-setting/setting-team/index.less b/web/src/pages/user-setting/setting-team/index.less deleted file mode 100644 index 12c572af9..000000000 --- a/web/src/pages/user-setting/setting-team/index.less +++ /dev/null @@ -1,9 +0,0 @@ -.teamWrapper { - width: 100%; - display: flex; - flex-direction: column; - gap: 20px; - .teamCard { - // width: 100%; - } -} diff --git a/web/src/pages/user-setting/setting-team/index.tsx b/web/src/pages/user-setting/setting-team/index.tsx index a1eaa495e..0481cee4f 100644 --- a/web/src/pages/user-setting/setting-team/index.tsx +++ b/web/src/pages/user-setting/setting-team/index.tsx @@ -1,22 +1,26 @@ +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { useFetchUserInfo, useListTenantUser, } from '@/hooks/user-setting-hooks'; -import { Button, Card, Flex, Space } from 'antd'; import { useTranslation } from 'react-i18next'; -import { TeamOutlined, UserAddOutlined, UserOutlined } from '@ant-design/icons'; +import Spotlight from '@/components/spotlight'; +import { SearchInput } from '@/components/ui/input'; +import { Separator } from '@/components/ui/separator'; +import { UserPlus } from 'lucide-react'; +import { useState } from 'react'; import AddingUserModal from './add-user-modal'; import { useAddUser } from './hooks'; -import styles from './index.less'; import TenantTable from './tenant-table'; import UserTable from './user-table'; -const iconStyle = { fontSize: 20, color: '#1677ff' }; - const UserSettingTeam = () => { const { data: userInfo } = useFetchUserInfo(); const { t } = useTranslation(); + const [searchTerm, setSearchTerm] = useState(''); + const [searchUser, setSearchUser] = useState(''); useListTenantUser(); const { addingTenantModalVisible, @@ -26,38 +30,58 @@ const UserSettingTeam = () => { } = useAddUser(); return ( -
    - - - - {userInfo.nickname} {t('setting.workspace')} - - - +
    + + + + + {userInfo?.nickname} {t('setting.workspace')} + + - - {t('setting.teamMembers')} - - } - bordered={false} - > - + + + + {/* */} + + {t('setting.teamMembers')} + +
    + setSearchUser(e.target.value)} + /> + +
    +
    + + +
    - - {t('setting.joinedTeams')} - - } - bordered={false} - > - + + + + {/* */} + + {t('setting.joinedTeams')} + + setSearchTerm(e.target.value)} + placeholder={t('common.search')} + /> + + + + + {addingTenantModalVisible && ( { +const TenantTable = ({ searchTerm }: { searchTerm: string }) => { const { t } = useTranslation(); const { data, loading } = useListTenant(); const { handleAgree } = useHandleAgreeTenant(); const { data: user } = useFetchUserInfo(); const { handleQuitTenantUser } = useHandleQuitUser(); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>(null); + const sortedData = useMemo(() => { + if (!data || data.length === 0) return data; + let filtered = data; + if (searchTerm) { + filtered = data.filter( + (tenant) => + tenant.nickname.toLowerCase().includes(searchTerm.toLowerCase()) || + tenant.email.toLowerCase().includes(searchTerm.toLowerCase()), + ); + } + if (sortOrder) { + filtered = [...filtered].sort((a, b) => { + const dateA = new Date(a.update_date).getTime(); + const dateB = new Date(b.update_date).getTime(); - const columns: TableProps['columns'] = [ - { - title: t('common.name'), - dataIndex: 'nickname', - key: 'nickname', - }, - { - title: t('setting.email'), - dataIndex: 'email', - key: 'email', - }, - { - title: t('setting.updateDate'), - dataIndex: 'update_date', - key: 'update_date', - render(value) { - return formatDate(value); - }, - }, - { - title: t('common.action'), - key: 'action', - render: (_, { role, tenant_id }) => { - if (role === TenantRole.Invite) { - return ( - - - - - ); - } else if (role === TenantRole.Normal && user.id !== tenant_id) { - return ( - - ); + if (sortOrder === 'asc') { + return dateA - dateB; + } else { + return dateB - dateA; } - }, - }, - ]; + }); + } + + return filtered; + }, [data, sortOrder, searchTerm]); + + const toggleSortOrder = () => { + if (sortOrder === 'asc') { + setSortOrder('desc'); + } else if (sortOrder === 'desc') { + setSortOrder(null); + } else { + setSortOrder('asc'); + } + }; + + const renderSortIcon = () => { + if (sortOrder === 'asc') { + return ; + } else if (sortOrder === 'desc') { + return ; + } else { + return ; + } + }; return ( - - columns={columns} - dataSource={data} - rowKey={'tenant_id'} - loading={loading} - pagination={false} - /> +
    + + + + {t('common.name')} + +
    + {t('setting.updateDate')} + {renderSortIcon()} +
    +
    + {t('setting.email')} + {t('common.action')} +
    +
    + + {loading ? ( + + +
    +
    +
    +
    +
    + ) : sortedData && sortedData.length > 0 ? ( + sortedData.map((tenant) => ( + + + + {tenant.nickname} + + + {formatDate(tenant.update_date)} + + {tenant.email} + + {tenant.role === TenantRole.Invite ? ( +
    + + +
    + ) : tenant.role === TenantRole.Normal && + user.id !== tenant.tenant_id ? ( + + ) : null} +
    +
    + )) + ) : ( + + + {t('common.noData')} + + + )} +
    +
    +
    ); }; diff --git a/web/src/pages/user-setting/setting-team/user-table.tsx b/web/src/pages/user-setting/setting-team/user-table.tsx index d1f2ee638..95d40e95c 100644 --- a/web/src/pages/user-setting/setting-team/user-table.tsx +++ b/web/src/pages/user-setting/setting-team/user-table.tsx @@ -1,75 +1,160 @@ +import { RAGFlowAvatar } from '@/components/ragflow-avatar'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; import { useListTenantUser } from '@/hooks/user-setting-hooks'; -import { ITenantUser } from '@/interfaces/database/user-setting'; import { formatDate } from '@/utils/date'; -import { DeleteOutlined } from '@ant-design/icons'; -import type { TableProps } from 'antd'; -import { Button, Table, Tag } from 'antd'; import { upperFirst } from 'lodash'; +import { ArrowDown, ArrowUp, ArrowUpDown, Trash2 } from 'lucide-react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { TenantRole } from '../constants'; import { useHandleDeleteUser } from './hooks'; -const ColorMap = { - [TenantRole.Normal]: 'green', - [TenantRole.Invite]: 'orange', - [TenantRole.Owner]: 'red', +const ColorMap: Record = { + [TenantRole.Normal]: 'bg-transparent text-text-primary', + [TenantRole.Invite]: 'bg-accent-primary-5 bg-accent-primary rounded-sm', + [TenantRole.Owner]: 'bg-red-100 text-red-800', }; -const UserTable = () => { +const UserTable = ({ searchUser }: { searchUser: string }) => { const { data, loading } = useListTenantUser(); const { handleDeleteTenantUser } = useHandleDeleteUser(); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>(null); const { t } = useTranslation(); + const sortedData = useMemo(() => { + console.log('sortedData', data, searchUser); + if (!data || data.length === 0) return data; + let filtered = data; + if (searchUser) { + filtered = filtered.filter( + (tenant) => + tenant.nickname.toLowerCase().includes(searchUser.toLowerCase()) || + tenant.email.toLowerCase().includes(searchUser.toLowerCase()), + ); + } + if (sortOrder) { + filtered = [...filtered].sort((a, b) => { + const dateA = new Date(a.update_date).getTime(); + const dateB = new Date(b.update_date).getTime(); - const columns: TableProps['columns'] = [ - { - title: t('common.name'), - dataIndex: 'nickname', - key: 'nickname', - }, - { - title: t('setting.email'), - dataIndex: 'email', - key: 'email', - }, - { - title: t('setting.role'), - dataIndex: 'role', - key: 'role', - render(value, { role }) { - return ( - - {upperFirst(role)} - - ); - }, - }, - { - title: t('setting.updateDate'), - dataIndex: 'update_date', - key: 'update_date', - render(value) { - return formatDate(value); - }, - }, - { - title: t('common.action'), - key: 'action', - render: (_, record) => ( - - ), - }, - ]; + if (sortOrder === 'asc') { + return dateA - dateB; + } else { + return dateB - dateA; + } + }); + } + return filtered; + }, [data, sortOrder, searchUser]); + const toggleSortOrder = () => { + if (sortOrder === 'asc') { + setSortOrder('desc'); + } else if (sortOrder === 'desc') { + setSortOrder(null); + } else { + setSortOrder('asc'); + } + }; + + const renderSortIcon = () => { + if (sortOrder === 'asc') { + return ; + } else if (sortOrder === 'desc') { + return ; + } else { + return ; + } + }; return ( - - rowKey={'user_id'} - columns={columns} - dataSource={data} - loading={loading} - pagination={false} - /> +
    + + + + {t('common.name')} + +
    + {t('setting.updateDate')} + {renderSortIcon()} +
    +
    + {t('setting.email')} + {t('setting.role')} + {t('common.action')} +
    +
    + + {loading ? ( + + +
    +
    +
    +
    +
    + ) : sortedData && sortedData.length > 0 ? ( + sortedData.map((record) => ( + + +
    + + {record.nickname} +
    +
    + + {formatDate(record.update_date)} + + {record.email} + + {record.role === TenantRole.Normal && ( + + {upperFirst('Member')} + + )} + {record.role !== TenantRole.Normal && ( + + {upperFirst(record.role)} + + )} + + + + +
    + )) + ) : ( + + + {t('common.noData')} + + + )} +
    +
    +
    ); };