### What problem does this PR solve? Feat: Modify the style of the dataset page #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.0
| @@ -19,6 +19,7 @@ import weekYear from 'dayjs/plugin/weekYear'; | |||
| import weekday from 'dayjs/plugin/weekday'; | |||
| import React, { ReactNode, useEffect, useState } from 'react'; | |||
| import { ThemeProvider, useTheme } from './components/theme-provider'; | |||
| import { SidebarProvider } from './components/ui/sidebar'; | |||
| import { TooltipProvider } from './components/ui/tooltip'; | |||
| import storage from './utils/authorization-util'; | |||
| @@ -68,7 +69,9 @@ function Root({ children }: React.PropsWithChildren) { | |||
| }} | |||
| locale={locale} | |||
| > | |||
| <App>{children}</App> | |||
| <SidebarProvider> | |||
| <App>{children}</App> | |||
| </SidebarProvider> | |||
| <Sonner position={'top-right'} expand richColors closeButton></Sonner> | |||
| <Toaster /> | |||
| </ConfigProvider> | |||
| @@ -1,4 +1,6 @@ | |||
| import { FileIconMap } from '@/constants/file'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { getExtension } from '@/utils/document-util'; | |||
| type IconFontType = { | |||
| name: string; | |||
| @@ -10,3 +12,18 @@ export const IconFont = ({ name, className }: IconFontType) => ( | |||
| <use xlinkHref={`#icon-${name}`} /> | |||
| </svg> | |||
| ); | |||
| export function FileIcon({ | |||
| name, | |||
| className, | |||
| type, | |||
| }: IconFontType & { type?: string }) { | |||
| const isFolder = type === 'folder'; | |||
| return ( | |||
| <span className={cn('size-4', className)}> | |||
| <IconFont | |||
| name={isFolder ? 'file' : FileIconMap[getExtension(name)]} | |||
| ></IconFont> | |||
| </span> | |||
| ); | |||
| } | |||
| @@ -1,3 +1,4 @@ | |||
| import { cn } from '@/lib/utils'; | |||
| import * as AvatarPrimitive from '@radix-ui/react-avatar'; | |||
| import { random } from 'lodash'; | |||
| import { forwardRef } from 'react'; | |||
| @@ -15,16 +16,24 @@ export const RAGFlowAvatar = forwardRef< | |||
| React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> & { | |||
| name?: string; | |||
| avatar?: string; | |||
| isPerson?: boolean; | |||
| } | |||
| >(({ name, avatar, ...props }, ref) => { | |||
| >(({ name, avatar, isPerson = false, className, ...props }, ref) => { | |||
| const index = random(0, 3); | |||
| console.log('🚀 ~ index:', index); | |||
| const value = Colors[index]; | |||
| return ( | |||
| <Avatar ref={ref} {...props}> | |||
| <Avatar | |||
| ref={ref} | |||
| {...props} | |||
| className={cn(className, { 'rounded-md': !isPerson })} | |||
| > | |||
| <AvatarImage src={avatar} /> | |||
| <AvatarFallback | |||
| className={`bg-gradient-to-b from-[${value.from}] to-[${value.to}]`} | |||
| className={cn( | |||
| `bg-gradient-to-b from-[${value.from}] to-[${value.to}]`, | |||
| { 'rounded-md': !isPerson }, | |||
| )} | |||
| > | |||
| {name?.slice(0, 1)} | |||
| </AvatarFallback> | |||
| @@ -11,7 +11,7 @@ const Switch = React.forwardRef< | |||
| >(({ className, ...props }, ref) => ( | |||
| <SwitchPrimitives.Root | |||
| className={cn( | |||
| 'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-colors-background-core-standard data-[state=unchecked]:bg-colors-background-inverse-standard', | |||
| 'peer inline-flex h-3.5 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-background-checked data-[state=unchecked]:bg-text-sub-title', | |||
| className, | |||
| )} | |||
| {...props} | |||
| @@ -19,7 +19,7 @@ const Switch = React.forwardRef< | |||
| > | |||
| <SwitchPrimitives.Thumb | |||
| className={cn( | |||
| 'pointer-events-none block h-5 w-5 rounded-full bg-colors-text-neutral-strong shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0', | |||
| 'pointer-events-none block size-3 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-2 data-[state=unchecked]:translate-x-0', | |||
| )} | |||
| /> | |||
| </SwitchPrimitives.Root> | |||
| @@ -6,10 +6,10 @@ const Table = React.forwardRef< | |||
| HTMLTableElement, | |||
| React.HTMLAttributes<HTMLTableElement> | |||
| >(({ className, ...props }, ref) => ( | |||
| <div className="relative w-full overflow-auto"> | |||
| <div className="relative w-full overflow-auto rounded-2xl bg-background-card"> | |||
| <table | |||
| ref={ref} | |||
| className={cn('w-full caption-bottom text-sm', className)} | |||
| className={cn('w-full caption-bottom text-sm ', className)} | |||
| {...props} | |||
| /> | |||
| </div> | |||
| @@ -73,7 +73,7 @@ const TableHead = React.forwardRef< | |||
| <th | |||
| ref={ref} | |||
| className={cn( | |||
| 'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0', | |||
| 'h-12 px-4 text-left align-middle font-normal text-text-sub-title [&:has([role=checkbox])]:pr-0', | |||
| className, | |||
| )} | |||
| {...props} | |||
| @@ -87,7 +87,10 @@ const TableCell = React.forwardRef< | |||
| >(({ className, ...props }, ref) => ( | |||
| <td | |||
| ref={ref} | |||
| className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)} | |||
| className={cn( | |||
| 'p-4 align-middle [&:has([role=checkbox])]:pr-0 text-text-title font-normal', | |||
| className, | |||
| )} | |||
| {...props} | |||
| /> | |||
| )); | |||
| @@ -0,0 +1,15 @@ | |||
| export const FileIconMap = { | |||
| doc: 'doc', | |||
| docx: 'doc', | |||
| pdf: 'pdf', | |||
| xls: 'excel', | |||
| xlsx: 'excel', | |||
| ppt: 'ppt', | |||
| pptx: 'ppt', | |||
| jpg: 'jpg', | |||
| jpeg: 'jpg', | |||
| png: 'png', | |||
| txt: 'text', | |||
| csv: 'pdf', | |||
| md: 'md', | |||
| }; | |||
| @@ -17,6 +17,7 @@ body { | |||
| .ant-app { | |||
| height: 100%; | |||
| width: 100%; | |||
| } | |||
| /* Scroll bar stylings */ | |||
| @@ -25,6 +25,7 @@ export interface IKnowledge { | |||
| embd_id: string; | |||
| nickname: string; | |||
| operator_permission: number; | |||
| size: number; | |||
| } | |||
| export interface IKnowledgeResult { | |||
| @@ -119,59 +119,51 @@ export function DatasetTable({ | |||
| return ( | |||
| <div className="w-full"> | |||
| <div className="rounded-md border"> | |||
| <Table> | |||
| <TableHeader> | |||
| {table.getHeaderGroups().map((headerGroup) => ( | |||
| <TableRow key={headerGroup.id}> | |||
| {headerGroup.headers.map((header) => { | |||
| return ( | |||
| <TableHead key={header.id}> | |||
| {header.isPlaceholder | |||
| ? null | |||
| : flexRender( | |||
| header.column.columnDef.header, | |||
| header.getContext(), | |||
| )} | |||
| </TableHead> | |||
| ); | |||
| })} | |||
| <Table> | |||
| <TableHeader> | |||
| {table.getHeaderGroups().map((headerGroup) => ( | |||
| <TableRow key={headerGroup.id}> | |||
| {headerGroup.headers.map((header) => { | |||
| return ( | |||
| <TableHead key={header.id}> | |||
| {header.isPlaceholder | |||
| ? null | |||
| : flexRender( | |||
| header.column.columnDef.header, | |||
| header.getContext(), | |||
| )} | |||
| </TableHead> | |||
| ); | |||
| })} | |||
| </TableRow> | |||
| ))} | |||
| </TableHeader> | |||
| <TableBody className="relative"> | |||
| {table.getRowModel().rows?.length ? ( | |||
| table.getRowModel().rows.map((row) => ( | |||
| <TableRow | |||
| key={row.id} | |||
| data-state={row.getIsSelected() && 'selected'} | |||
| > | |||
| {row.getVisibleCells().map((cell) => ( | |||
| <TableCell | |||
| key={cell.id} | |||
| className={cell.column.columnDef.meta?.cellClassName} | |||
| > | |||
| {flexRender(cell.column.columnDef.cell, cell.getContext())} | |||
| </TableCell> | |||
| ))} | |||
| </TableRow> | |||
| ))} | |||
| </TableHeader> | |||
| <TableBody className="relative"> | |||
| {table.getRowModel().rows?.length ? ( | |||
| table.getRowModel().rows.map((row) => ( | |||
| <TableRow | |||
| key={row.id} | |||
| data-state={row.getIsSelected() && 'selected'} | |||
| > | |||
| {row.getVisibleCells().map((cell) => ( | |||
| <TableCell | |||
| key={cell.id} | |||
| className={cell.column.columnDef.meta?.cellClassName} | |||
| > | |||
| {flexRender( | |||
| cell.column.columnDef.cell, | |||
| cell.getContext(), | |||
| )} | |||
| </TableCell> | |||
| ))} | |||
| </TableRow> | |||
| )) | |||
| ) : ( | |||
| <TableRow> | |||
| <TableCell | |||
| colSpan={columns.length} | |||
| className="h-24 text-center" | |||
| > | |||
| No results. | |||
| </TableCell> | |||
| </TableRow> | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| </div> | |||
| )) | |||
| ) : ( | |||
| <TableRow> | |||
| <TableCell colSpan={columns.length} className="h-24 text-center"> | |||
| No results. | |||
| </TableCell> | |||
| </TableRow> | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| <div className="flex items-center justify-end space-x-2 py-4"> | |||
| <div className="flex-1 text-sm text-muted-foreground"> | |||
| {table.getFilteredSelectedRowModel().rows.length} of{' '} | |||
| @@ -60,7 +60,7 @@ export default function Dataset() { | |||
| }); | |||
| return ( | |||
| <section className="p-8"> | |||
| <section className="p-5"> | |||
| <ListFilterBar | |||
| title="Dataset" | |||
| onSearchChange={handleInputChange} | |||
| @@ -1,4 +1,4 @@ | |||
| import SvgIcon from '@/components/svg-icon'; | |||
| import { FileIcon } from '@/components/icon-font'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Checkbox } from '@/components/ui/checkbox'; | |||
| import { Switch } from '@/components/ui/switch'; | |||
| @@ -12,7 +12,6 @@ import { useSetDocumentStatus } from '@/hooks/use-document-request'; | |||
| import { IDocumentInfo } from '@/interfaces/database/document'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { getExtension } from '@/utils/document-util'; | |||
| import { ColumnDef } from '@tanstack/table-core'; | |||
| import { ArrowUpDown } from 'lucide-react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| @@ -88,10 +87,7 @@ export function useDatasetTableColumns({ | |||
| row.original.kb_id, | |||
| )} | |||
| > | |||
| <SvgIcon | |||
| name={`file-icon/${getExtension(name)}`} | |||
| width={24} | |||
| ></SvgIcon> | |||
| <FileIcon name={name}></FileIcon> | |||
| <span className={cn('truncate')}>{name}</span> | |||
| </div> | |||
| </TooltipTrigger> | |||
| @@ -1,55 +1,78 @@ | |||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | |||
| import { RAGFlowAvatar } from '@/components/ragflow-avatar'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { useSecondPathName } from '@/hooks/route-hook'; | |||
| import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { cn, formatBytes } from '@/lib/utils'; | |||
| import { Routes } from '@/routes'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { Banknote, LayoutGrid, User } from 'lucide-react'; | |||
| import { formatPureDate } from '@/utils/date'; | |||
| import { Banknote, Database, FileSearch2 } from 'lucide-react'; | |||
| import { useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { useHandleMenuClick } from './hooks'; | |||
| const items = [ | |||
| { icon: User, label: 'Dataset', key: Routes.DatasetBase }, | |||
| { | |||
| icon: LayoutGrid, | |||
| label: 'Retrieval testing', | |||
| key: Routes.DatasetTesting, | |||
| }, | |||
| { icon: Banknote, label: 'Settings', key: Routes.DatasetSetting }, | |||
| ]; | |||
| export function SideBar() { | |||
| const pathName = useSecondPathName(); | |||
| const { handleMenuClick } = useHandleMenuClick(); | |||
| const { data } = useFetchKnowledgeBaseConfiguration(); | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <aside className="w-60 relative border-r "> | |||
| <div className="p-6 space-y-2 border-b"> | |||
| <Avatar className="size-20 rounded-lg"> | |||
| <AvatarImage src={data.avatar} /> | |||
| <AvatarFallback className="rounded-lg">CN</AvatarFallback> | |||
| </Avatar> | |||
| const items = useMemo(() => { | |||
| return [ | |||
| { | |||
| icon: Database, | |||
| label: t(`knowledgeDetails.dataset`), | |||
| key: Routes.DatasetBase, | |||
| }, | |||
| { | |||
| icon: FileSearch2, | |||
| label: t(`knowledgeDetails.testing`), | |||
| key: Routes.DatasetTesting, | |||
| }, | |||
| { | |||
| icon: Banknote, | |||
| label: t(`knowledgeDetails.configuration`), | |||
| key: Routes.DatasetSetting, | |||
| }, | |||
| ]; | |||
| }, [t]); | |||
| <h3 className="text-lg font-semibold mb-2 line-clamp-1">{data.name}</h3> | |||
| <div className="text-sm opacity-80"> | |||
| {data.doc_num} files | {data.chunk_num} chunks | |||
| </div> | |||
| <div className="text-sm opacity-80"> | |||
| Created {formatDate(data.create_time)} | |||
| return ( | |||
| <aside className="relative p-5 space-y-8"> | |||
| <div className="flex gap-2.5 max-w-[200px] items-center"> | |||
| <RAGFlowAvatar | |||
| avatar={data.avatar} | |||
| name={data.name} | |||
| className="size-16" | |||
| ></RAGFlowAvatar> | |||
| <div className=" text-text-sub-title text-xs space-y-1"> | |||
| <h3 className="text-lg font-semibold line-clamp-1 text-text-title"> | |||
| {data.name} | |||
| </h3> | |||
| <div className="flex justify-between"> | |||
| <span>{data.doc_num} files</span> | |||
| <span>{formatBytes(data.size)}</span> | |||
| </div> | |||
| <div>Created {formatPureDate(data.create_time)}</div> | |||
| </div> | |||
| </div> | |||
| <div className="mt-4"> | |||
| <div className="w-[200px] flex flex-col gap-5"> | |||
| {items.map((item, itemIdx) => { | |||
| const active = '/' + pathName === item.key; | |||
| return ( | |||
| <Button | |||
| key={itemIdx} | |||
| variant={active ? 'secondary' : 'ghost'} | |||
| className={cn('w-full justify-start gap-2.5 p-6 relative')} | |||
| className={cn( | |||
| 'w-full justify-start gap-2.5 px-3 relative h-10 text-text-sub-title-invert', | |||
| { | |||
| 'bg-background-card': active, | |||
| 'text-text-title': active, | |||
| }, | |||
| )} | |||
| onClick={handleMenuClick(item.key)} | |||
| > | |||
| <item.icon className="w-6 h-6" /> | |||
| <item.icon className="size-4" /> | |||
| <span>{item.label}</span> | |||
| </Button> | |||
| ); | |||
| @@ -14,8 +14,8 @@ import { | |||
| import { ArrowUpDown } from 'lucide-react'; | |||
| import * as React from 'react'; | |||
| import { FileIcon } from '@/components/icon-font'; | |||
| import { RenameDialog } from '@/components/rename-dialog'; | |||
| import SvgIcon from '@/components/svg-icon'; | |||
| import { TableEmpty, TableSkeleton } from '@/components/table-skeleton'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Checkbox } from '@/components/ui/checkbox'; | |||
| @@ -39,7 +39,6 @@ import { IFile } from '@/interfaces/database/file-manager'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { formatFileSize } from '@/utils/common-util'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { getExtension } from '@/utils/document-util'; | |||
| import { pick } from 'lodash'; | |||
| import { useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| @@ -148,10 +147,9 @@ export function FilesTable({ | |||
| <Tooltip> | |||
| <TooltipTrigger asChild> | |||
| <div className="flex gap-2"> | |||
| <SvgIcon | |||
| name={`file-icon/${isFolder ? 'folder' : getExtension(name)}`} | |||
| width={24} | |||
| ></SvgIcon> | |||
| <span className="size-4"> | |||
| <FileIcon name={name} type={type}></FileIcon> | |||
| </span> | |||
| <span | |||
| className={cn('truncate', { ['cursor-pointer']: isFolder })} | |||
| onClick={handleNameClick} | |||
| @@ -262,54 +260,50 @@ export function FilesTable({ | |||
| return ( | |||
| <div className="w-full"> | |||
| <div className="rounded-md border"> | |||
| <Table> | |||
| <TableHeader> | |||
| {table.getHeaderGroups().map((headerGroup) => ( | |||
| <TableRow key={headerGroup.id}> | |||
| {headerGroup.headers.map((header) => { | |||
| return ( | |||
| <TableHead key={header.id}> | |||
| {header.isPlaceholder | |||
| ? null | |||
| : flexRender( | |||
| header.column.columnDef.header, | |||
| header.getContext(), | |||
| )} | |||
| </TableHead> | |||
| ); | |||
| })} | |||
| <Table> | |||
| <TableHeader> | |||
| {table.getHeaderGroups().map((headerGroup) => ( | |||
| <TableRow key={headerGroup.id}> | |||
| {headerGroup.headers.map((header) => { | |||
| return ( | |||
| <TableHead key={header.id}> | |||
| {header.isPlaceholder | |||
| ? null | |||
| : flexRender( | |||
| header.column.columnDef.header, | |||
| header.getContext(), | |||
| )} | |||
| </TableHead> | |||
| ); | |||
| })} | |||
| </TableRow> | |||
| ))} | |||
| </TableHeader> | |||
| <TableBody> | |||
| {loading ? ( | |||
| <TableSkeleton columnsLength={columns.length}></TableSkeleton> | |||
| ) : table.getRowModel().rows?.length ? ( | |||
| table.getRowModel().rows.map((row) => ( | |||
| <TableRow | |||
| key={row.id} | |||
| data-state={row.getIsSelected() && 'selected'} | |||
| > | |||
| {row.getVisibleCells().map((cell) => ( | |||
| <TableCell | |||
| key={cell.id} | |||
| className={cell.column.columnDef.meta?.cellClassName} | |||
| > | |||
| {flexRender(cell.column.columnDef.cell, cell.getContext())} | |||
| </TableCell> | |||
| ))} | |||
| </TableRow> | |||
| ))} | |||
| </TableHeader> | |||
| <TableBody> | |||
| {loading ? ( | |||
| <TableSkeleton columnsLength={columns.length}></TableSkeleton> | |||
| ) : table.getRowModel().rows?.length ? ( | |||
| table.getRowModel().rows.map((row) => ( | |||
| <TableRow | |||
| key={row.id} | |||
| data-state={row.getIsSelected() && 'selected'} | |||
| > | |||
| {row.getVisibleCells().map((cell) => ( | |||
| <TableCell | |||
| key={cell.id} | |||
| className={cell.column.columnDef.meta?.cellClassName} | |||
| > | |||
| {flexRender( | |||
| cell.column.columnDef.cell, | |||
| cell.getContext(), | |||
| )} | |||
| </TableCell> | |||
| ))} | |||
| </TableRow> | |||
| )) | |||
| ) : ( | |||
| <TableEmpty columnsLength={columns.length}></TableEmpty> | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| </div> | |||
| )) | |||
| ) : ( | |||
| <TableEmpty columnsLength={columns.length}></TableEmpty> | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| <div className="flex items-center justify-end space-x-2 py-4"> | |||
| <div className="flex-1 text-sm text-muted-foreground"> | |||
| {table.getFilteredSelectedRowModel().rows.length} of {total} row(s) | |||
| @@ -88,7 +88,7 @@ export default function Files() { | |||
| > | |||
| <DropdownMenu> | |||
| <DropdownMenuTrigger asChild> | |||
| <Button variant={'tertiary'} size={'sm'}> | |||
| <Button> | |||
| <Upload /> | |||
| {t('knowledgeDetails.addFile')} | |||
| </Button> | |||
| @@ -50,6 +50,7 @@ module.exports = { | |||
| 'text-title-invert': 'var(--text-title-invert)', | |||
| 'background-header-bar': 'var(--background-header-bar)', | |||
| 'background-card': 'var(--background-card)', | |||
| 'background-checked': 'var(--background-checked)', | |||
| primary: { | |||
| DEFAULT: 'hsl(var(--primary))', | |||
| @@ -84,6 +84,8 @@ | |||
| --background-header-bar: rgba(11, 11, 12, 1); | |||
| --text-title-invert: rgba(255, 255, 255, 1); | |||
| --background-card: rgba(22, 22, 24, 0.05); | |||
| --background-checked: rgba(76, 164, 231, 1); | |||
| } | |||
| .dark { | |||
| @@ -189,6 +191,7 @@ | |||
| --text-title-invert: rgba(22, 22, 24, 1); | |||
| --background-card: rgba(255, 255, 255, 0.05); | |||
| --background-checked: rgba(76, 164, 231, 1); | |||
| } | |||
| } | |||