refactor(ui): unify top level pages structure, use standard language codes and time zones (#13573)

### What problem does this PR solve?

- Unify top level pages structure
- Standardize locale language codes (BCP 47) and time zones (IANA tz)


> **Note:** 
> Newly created user info brings non-standard default values `timezone:
"UTC+8\tAsia/Shanghai"` and `language: "English"`.


### Type of change

- [x] Refactoring
This commit is contained in:
Jimmy Ben Klieve
2026-03-12 21:01:09 +08:00
committed by GitHub
parent 35cd56f990
commit 1a4dee4313
42 changed files with 673 additions and 1364 deletions

View File

@ -26,8 +26,9 @@ const ApiContent = ({ id, idKey }: { id?: string; idKey: string }) => {
const isDarkTheme = useIsDarkTheme();
return (
<div className="pb-2 flex flex-col w-full">
<BackendServiceApi show={showApiKeyModal}></BackendServiceApi>
<div className="flex flex-col w-full">
<BackendServiceApi show={showApiKeyModal} />
<div className="text-left py-4">
<Button onClick={tocVisible ? hideToc : showToc}>
{tocVisible ? t('hideToc') : t('showToc')}

View File

@ -1,3 +1,4 @@
import { combineRefs } from '@/lib/utils';
import { transformFile2Base64 } from '@/utils/file-util';
import { LucidePencil, LucidePlus, LucideX } from 'lucide-react';
import {
@ -11,7 +12,6 @@ import {
import { useTranslation } from 'react-i18next';
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Modal } from './ui/modal/modal';
type AvatarUploadProps = {
@ -42,6 +42,7 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
const [isCropModalOpen, setIsCropModalOpen] = useState(false);
const [imageToCrop, setImageToCrop] = useState<string | null>(null);
const [cropArea, setCropArea] = useState({ x: 0, y: 0, size: 200 });
const innerInputRef = useRef<HTMLInputElement | null>(null);
const imageRef = useRef<HTMLImageElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
@ -210,7 +211,6 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
const handleWheel = useCallback((e: React.WheelEvent) => {
if (!imageRef.current) return;
e.preventDefault();
const image = imageRef.current;
const delta = e.deltaY > 0 ? 0.9 : 1.1; // Zoom factor
@ -245,10 +245,10 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
}
}, [value]);
/*
useEffect(() => {
const container = containerRef.current;
setTimeout(() => {
console.log('container', container);
// initCropArea();
if (imageToCrop && container && isCropModalOpen) {
container.addEventListener(
@ -265,34 +265,59 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
}
}, 100);
}, [handleWheel, imageToCrop, isCropModalOpen]);
*/
return (
<div className="flex justify-start items-end space-x-2">
<div className="relative group">
<input
placeholder=""
type="file"
title=""
accept="image/*"
className="peer/input size-0 absolute top-0 left-0 opacity-0 pointer-events-none"
onChange={handleChange}
ref={combineRefs(ref, innerInputRef)}
data-testid={uploadInputTestId}
tabIndex={-1}
/>
{!avatarBase64Str ? (
<div
className="
border border-dashed border-borer-button rounded-md size-16
flex flex-col gap-1 justify-center items-center text-sm text-text-secondary transition-colors
group-has-[input:focus-visible]:border-accent-primary group-has-[input:focus-visible]:text-text-primary"
<Button
variant="dashed"
size="icon"
className="size-16 flex flex-col items-center gap-1 !bg-transparent"
onClick={() => {
innerInputRef.current?.click();
}}
>
<LucidePlus className="size-4" />
<span>{t('common.upload')}</span>
</div>
</Button>
) : (
<div className="size-16 relative grid place-content-center">
<Avatar className="size-16 rounded-md">
<AvatarImage className="block" src={avatarBase64Str} alt="" />
<AvatarFallback></AvatarFallback>
</Avatar>
<div
className="
absolute inset-0 bg-black/50 flex items-center justify-center
transition-opacity opacity-0 group-hover:opacity-100 group-has-[input:focus-visible]:opacity-100"
<Button
variant="transparent"
size="icon"
className="group/button size-full p-0 transition-all relative gap-0 overflow-hidden"
onClick={() => {
innerInputRef.current?.click();
}}
>
<LucidePencil className="size-5 opacity-75" />
</div>
<Avatar className="size-full rounded-none">
<AvatarImage className="block" src={avatarBase64Str} alt="" />
<AvatarFallback />
</Avatar>
<div
className="
absolute inset-0 flex items-center justify-center
bg-black/40 opacity-0 transition-opacity
group-hover/button:opacity-100 group-focus-visible/button:opacity-100"
>
<LucidePencil className="size-5 opacity-75" />
</div>
</Button>
<Button
onClick={handleRemove}
@ -306,18 +331,8 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
</Button>
</div>
)}
<Input
placeholder=""
type="file"
title=""
accept="image/*"
className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
onChange={handleChange}
ref={ref}
data-testid={uploadInputTestId}
/>
</div>
<div className="ms-1 text-xs text-text-secondary">
{tips ?? t('knowledgeConfiguration.photoTip')}
</div>
@ -357,7 +372,7 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
height: '300px',
touchAction: 'none',
}}
// onWheel={handleWheel}
onWheel={handleWheel}
>
<img
ref={imageRef}

View File

@ -5,13 +5,13 @@ type CardContainerProps = { className?: string } & PropsWithChildren;
export function CardContainer({ children, className }: CardContainerProps) {
return (
<section
<div
className={cn(
'grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5',
'grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 auto-rows-auto content-start',
className,
)}
>
{children}
</section>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { cn } from '@/lib/utils';
import { isValidElement, PropsWithChildren, ReactNode } from 'react';
import { PropsWithChildren } from 'react';
import './index.less';
type CardContainerProps = { className?: string } & PropsWithChildren;
@ -8,26 +8,6 @@ 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 (
<section
className={cn(

View File

@ -83,8 +83,10 @@ export const EmptyAppCard = (props: {
size?: 'small' | 'large';
children?: React.ReactNode;
testId?: string;
tabIndex?: number;
}) => {
const { type, showIcon, className, isSearch, children, testId } = props;
const { type, showIcon, className, isSearch, children, testId, tabIndex } =
props;
const { t } = useTranslation();
let defaultClass = '';
let style = {};
@ -110,10 +112,10 @@ export const EmptyAppCard = (props: {
<EmptyCard
onClick={isSearch ? undefined : props.onClick}
data-testid={testId}
tabIndex={isSearch ? undefined : 0}
tabIndex={tabIndex ?? (isSearch ? undefined : 0)}
icon={showIcon ? cardData.icon : undefined}
title={isSearch ? notFound : title}
className={cn('cursor-pointer', className)}
className={cn(!isSearch && 'cursor-pointer', className)}
style={style}
// description={EmptyCardData[type].description}
>

View File

@ -94,7 +94,7 @@ export default function ListFilterBar({
return (
<div className={cn('flex justify-between items-center', className)}>
<div className="text-2xl font-semibold flex items-center gap-2.5">
<h1 className="text-2xl font-semibold flex items-center gap-2.5">
{typeof icon === 'string' ? (
// <IconFont name={icon} className="size-6"></IconFont>
<HomeIcon
@ -105,11 +105,11 @@ export default function ListFilterBar({
icon
)}
{leftPanel || title}
</div>
</h1>
<div className="flex gap-4 items-center" role="toolbar">
{preChildren}
{showFilter && (
{filters?.length && showFilter && (
<FilterPopover
value={value}
onChange={onChange}

View File

@ -165,10 +165,8 @@ export const SelectWithSearch = forwardRef<
)}
>
{selectLabel || value ? (
<span className="flex min-w-0 options-center gap-2">
<span className="leading-none truncate">
{selectLabel || value}
</span>
<span className="flex min-w-0 options-center gap-2 truncate">
{selectLabel || value}
</span>
) : (
<span className="text-text-disabled">{placeholder}</span>
@ -209,10 +207,10 @@ export const SelectWithSearch = forwardRef<
<CommandEmpty>
<div dangerouslySetInnerHTML={{ __html: emptyData }}></div>
</CommandEmpty>
{options.map((group, idx) => {
{options.map((group) => {
if (group.options) {
return (
<Fragment key={idx}>
<Fragment key={group.value}>
<CommandGroup heading={group.label} className="mb-1">
{group.options.map((option) => (
<CommandItem

View File

@ -93,7 +93,8 @@ const buttonVariants = cva(
// Static
// Button has no interaction transitions
static: 'text-text-secondary',
static:
'text-text-secondary hover:text-text-primary focus-visible:text-text-primary',
},
size: {
auto: '',

View File

@ -9,8 +9,8 @@ import {
} from '@/components/ui/pagination';
import { RAGFlowSelect, RAGFlowSelectOptionType } from '@/components/ui/select';
import { cn } from '@/lib/utils';
import { t } from 'i18next';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
export type RAGFlowPaginationType = {
showQuickJumper?: boolean;
@ -28,6 +28,7 @@ export function RAGFlowPagination({
onChange,
showSizeChanger = true,
}: RAGFlowPaginationType) {
const { t } = useTranslation();
const [currentPage, setCurrentPage] = useState(1);
const [currentPageSize, setCurrentPageSize] = useState('10');
@ -36,7 +37,7 @@ export function RAGFlowPagination({
label: <span>{t('pagination.page', { page: x })}</span>,
value: x.toString(),
}));
}, []);
}, [t]);
const pages = useMemo(() => {
const num = Math.ceil(total / pageSize);
@ -134,7 +135,7 @@ export function RAGFlowPagination({
}, [pages, currentPage]);
return (
<section className="flex items-center justify-end text-text-sub-title-invert">
<div className="flex items-center justify-end text-text-sub-title-invert">
<span className="mr-4 text-text-primary">
{t('pagination.total', { total: total })}
</span>
@ -181,6 +182,6 @@ export function RAGFlowPagination({
triggerClassName="bg-bg-card border-transparent"
/>
)}
</section>
</div>
);
}

View File

@ -105,8 +105,6 @@ const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
const isObject = typeof option === 'object';
const actualValue = isObject ? option.value : option;
console.log(actualValue);
return (
<Button
key={actualValue}

View File

@ -41,7 +41,7 @@ export const fileIconMap = {
xml: 'xml.svg',
};
// TODO: Need to migrate to standard BCP 47 language tag
// TODO: Use standard BCP 47 language tag and display names
export const LanguageList = [
'English',
'Chinese',
@ -78,8 +78,8 @@ export const LanguageMap = {
export enum LanguageAbbreviation {
En = 'en',
Zh = 'zh',
ZhTraditional = 'zh-TRADITIONAL',
Zh = 'zh-Hans',
ZhTraditional = 'zh-Hant',
Ru = 'ru',
Id = 'id',
Ja = 'ja',
@ -112,8 +112,8 @@ export const LanguageAbbreviationMap = {
export const LanguageTranslationMap = {
English: 'en',
Chinese: 'zh',
'Traditional Chinese': 'zh-TRADITIONAL',
Chinese: 'zh-Hans',
'Traditional Chinese': 'zh-Hant',
Russian: 'ru',
Indonesian: 'id',
Indonesia: 'id',

View File

@ -25,423 +25,36 @@ export enum ProfileSettingRouteKey {
Logout = 'logout',
}
export const TimezoneList = [
'UTC-11\tPacific/Midway',
'UTC-11\tPacific/Niue',
'UTC-11\tPacific/Pago_Pago',
'UTC-10\tAmerica/Adak',
'UTC-10\tPacific/Honolulu',
'UTC-10\tPacific/Rarotonga',
'UTC-10\tPacific/Tahiti',
'UTC-9:30\tPacific/Marquesas',
'UTC-9\tAmerica/Anchorage',
'UTC-9\tAmerica/Juneau',
'UTC-9\tAmerica/Metlakatla',
'UTC-9\tAmerica/Nome',
'UTC-9\tAmerica/Sitka',
'UTC-9\tAmerica/Yakutat',
'UTC-9\tPacific/Gambier',
'UTC-8\tAmerica/Los_Angeles',
'UTC-8\tAmerica/Tijuana',
'UTC-8\tAmerica/Vancouver',
'UTC-8\tPacific/Pitcairn',
'UTC-7\tAmerica/Boise',
'UTC-7\tAmerica/Cambridge_Bay',
'UTC-7\tAmerica/Ciudad_Juarez',
'UTC-7\tAmerica/Creston',
'UTC-7\tAmerica/Dawson',
'UTC-7\tAmerica/Dawson_Creek',
'UTC-7\tAmerica/Denver',
'UTC-7\tAmerica/Edmonton',
'UTC-7\tAmerica/Fort_Nelson',
'UTC-7\tAmerica/Hermosillo',
'UTC-7\tAmerica/Inuvik',
'UTC-7\tAmerica/Mazatlan',
'UTC-7\tAmerica/Phoenix',
'UTC-7\tAmerica/Whitehorse',
'UTC-7\tAmerica/Yellowknife',
'UTC-6\tAmerica/Bahia_Banderas',
'UTC-6\tAmerica/Belize',
'UTC-6\tAmerica/Chicago',
'UTC-6\tAmerica/Chihuahua',
'UTC-6\tAmerica/Costa_Rica',
'UTC-6\tAmerica/El_Salvador',
'UTC-6\tAmerica/Guatemala',
'UTC-6\tAmerica/Indiana/Knox',
'UTC-6\tAmerica/Indiana/Tell_City',
'UTC-6\tAmerica/Managua',
'UTC-6\tAmerica/Matamoros',
'UTC-6\tAmerica/Menominee',
'UTC-6\tAmerica/Merida',
'UTC-6\tAmerica/Mexico_City',
'UTC-6\tAmerica/Monterrey',
'UTC-6\tAmerica/North_Dakota/Beulah',
'UTC-6\tAmerica/North_Dakota/Center',
'UTC-6\tAmerica/North_Dakota/New_Salem',
'UTC-6\tAmerica/Ojinaga',
'UTC-6\tAmerica/Rankin_Inlet',
'UTC-6\tAmerica/Regina',
'UTC-6\tAmerica/Resolute',
'UTC-6\tAmerica/Swift_Current',
'UTC-6\tAmerica/Tegucigalpa',
'UTC-6\tAmerica/Winnipeg',
'UTC-6\tPacific/Easter',
'UTC-6\tPacific/Galapagos',
'UTC-5\tAmerica/Atikokan',
'UTC-5\tAmerica/Bogota',
'UTC-5\tAmerica/Cancun',
'UTC-5\tAmerica/Cayman',
'UTC-5\tAmerica/Detroit',
'UTC-5\tAmerica/Eirunepe',
'UTC-5\tAmerica/Grand_Turk',
'UTC-5\tAmerica/Guayaquil',
'UTC-5\tAmerica/Havana',
'UTC-5\tAmerica/Indiana/Indianapolis',
'UTC-5\tAmerica/Indiana/Marengo',
'UTC-5\tAmerica/Indiana/Petersburg',
'UTC-5\tAmerica/Indiana/Vevay',
'UTC-5\tAmerica/Indiana/Vincennes',
'UTC-5\tAmerica/Indiana/Winamac',
'UTC-5\tAmerica/Iqaluit',
'UTC-5\tAmerica/Jamaica',
'UTC-5\tAmerica/Kentucky/Louisville',
'UTC-5\tAmerica/Kentucky/Monticello',
'UTC-5\tAmerica/Lima',
'UTC-5\tAmerica/Nassau',
'UTC-5\tAmerica/New_York',
'UTC-5\tAmerica/Panama',
'UTC-5\tAmerica/Port-au-Prince',
'UTC-5\tAmerica/Rio_Branco',
'UTC-5\tAmerica/Toronto',
'UTC-4\tAmerica/Anguilla',
'UTC-4\tAmerica/Antigua',
'UTC-4\tAmerica/Aruba',
'UTC-4\tAmerica/Asuncion',
'UTC-4\tAmerica/Barbados',
'UTC-4\tAmerica/Blanc-Sablon',
'UTC-4\tAmerica/Boa_Vista',
'UTC-4\tAmerica/Campo_Grande',
'UTC-4\tAmerica/Caracas',
'UTC-4\tAmerica/Cuiaba',
'UTC-4\tAmerica/Curacao',
'UTC-4\tAmerica/Dominica',
'UTC-4\tAmerica/Glace_Bay',
'UTC-4\tAmerica/Goose_Bay',
'UTC-4\tAmerica/Grenada',
'UTC-4\tAmerica/Guadeloupe',
'UTC-4\tAmerica/Guyana',
'UTC-4\tAmerica/Halifax',
'UTC-4\tAmerica/Kralendijk',
'UTC-4\tAmerica/La_Paz',
'UTC-4\tAmerica/Lower_Princes',
'UTC-4\tAmerica/Manaus',
'UTC-4\tAmerica/Marigot',
'UTC-4\tAmerica/Martinique',
'UTC-4\tAmerica/Moncton',
'UTC-4\tAmerica/Montserrat',
'UTC-4\tAmerica/Porto_Velho',
'UTC-4\tAmerica/Port_of_Spain',
'UTC-4\tAmerica/Puerto_Rico',
'UTC-4\tAmerica/Santiago',
'UTC-4\tAmerica/Santo_Domingo',
'UTC-4\tAmerica/St_Barthelemy',
'UTC-4\tAmerica/St_Kitts',
'UTC-4\tAmerica/St_Lucia',
'UTC-4\tAmerica/St_Thomas',
'UTC-4\tAmerica/St_Vincent',
'UTC-4\tAmerica/Thule',
'UTC-4\tAmerica/Tortola',
'UTC-4\tAtlantic/Bermuda',
'UTC-3:30\tAmerica/St_Johns',
'UTC-3\tAmerica/Araguaina',
'UTC-3\tAmerica/Argentina/Buenos_Aires',
'UTC-3\tAmerica/Argentina/Catamarca',
'UTC-3\tAmerica/Argentina/Cordoba',
'UTC-3\tAmerica/Argentina/Jujuy',
'UTC-3\tAmerica/Argentina/La_Rioja',
'UTC-3\tAmerica/Argentina/Mendoza',
'UTC-3\tAmerica/Argentina/Rio_Gallegos',
'UTC-3\tAmerica/Argentina/Salta',
'UTC-3\tAmerica/Argentina/San_Juan',
'UTC-3\tAmerica/Argentina/San_Luis',
'UTC-3\tAmerica/Argentina/Tucuman',
'UTC-3\tAmerica/Argentina/Ushuaia',
'UTC-3\tAmerica/Bahia',
'UTC-3\tAmerica/Belem',
'UTC-3\tAmerica/Cayenne',
'UTC-3\tAmerica/Fortaleza',
'UTC-3\tAmerica/Maceio',
'UTC-3\tAmerica/Miquelon',
'UTC-3\tAmerica/Montevideo',
'UTC-3\tAmerica/Paramaribo',
'UTC-3\tAmerica/Punta_Arenas',
'UTC-3\tAmerica/Recife',
'UTC-3\tAmerica/Santarem',
'UTC-3\tAmerica/Sao_Paulo',
'UTC-3\tAntarctica/Palmer',
'UTC-3\tAntarctica/Rothera',
'UTC-3\tAtlantic/Stanley',
'UTC-2\tAmerica/Noronha',
'UTC-2\tAmerica/Nuuk',
'UTC-2\tAtlantic/South_Georgia',
'UTC-1\tAmerica/Scoresbysund',
'UTC-1\tAtlantic/Azores',
'UTC-1\tAtlantic/Cape_Verde',
'UTC+0\tAfrica/Abidjan',
'UTC+0\tAfrica/Accra',
'UTC+0\tAfrica/Bamako',
'UTC+0\tAfrica/Banjul',
'UTC+0\tAfrica/Bissau',
'UTC+0\tAfrica/Casablanca',
'UTC+0\tAfrica/Conakry',
'UTC+0\tAfrica/Dakar',
'UTC+0\tAfrica/El_Aaiun',
'UTC+0\tAfrica/Freetown',
'UTC+0\tAfrica/Lome',
'UTC+0\tAfrica/Monrovia',
'UTC+0\tAfrica/Nouakchott',
'UTC+0\tAfrica/Ouagadougou',
'UTC+0\tAfrica/Sao_Tome',
'UTC+0\tAmerica/Danmarkshavn',
'UTC+0\tAntarctica/Troll',
'UTC+0\tAtlantic/Canary',
'UTC+0\tAtlantic/Faroe',
'UTC+0\tAtlantic/Madeira',
'UTC+0\tAtlantic/Reykjavik',
'UTC+0\tAtlantic/St_Helena',
'UTC+0\tEurope/Dublin',
'UTC+0\tEurope/Guernsey',
'UTC+0\tEurope/Isle_of_Man',
'UTC+0\tEurope/Jersey',
'UTC+0\tEurope/Lisbon',
'UTC+0\tEurope/London',
'UTC+1\tAfrica/Algiers',
'UTC+1\tAfrica/Bangui',
'UTC+1\tAfrica/Brazzaville',
'UTC+1\tAfrica/Ceuta',
'UTC+1\tAfrica/Douala',
'UTC+1\tAfrica/Kinshasa',
'UTC+1\tAfrica/Lagos',
'UTC+1\tAfrica/Libreville',
'UTC+1\tAfrica/Luanda',
'UTC+1\tAfrica/Malabo',
'UTC+1\tAfrica/Ndjamena',
'UTC+1\tAfrica/Niamey',
'UTC+1\tAfrica/Porto-Novo',
'UTC+1\tAfrica/Tunis',
'UTC+1\tAfrica/Windhoek',
'UTC+1\tArctic/Longyearbyen',
'UTC+1\tEurope/Amsterdam',
'UTC+1\tEurope/Andorra',
'UTC+1\tEurope/Belgrade',
'UTC+1\tEurope/Berlin',
'UTC+1\tEurope/Bratislava',
'UTC+1\tEurope/Brussels',
'UTC+1\tEurope/Budapest',
'UTC+1\tEurope/Copenhagen',
'UTC+1\tEurope/Gibraltar',
'UTC+1\tEurope/Ljubljana',
'UTC+1\tEurope/Luxembourg',
'UTC+1\tEurope/Madrid',
'UTC+1\tEurope/Malta',
'UTC+1\tEurope/Monaco',
'UTC+1\tEurope/Oslo',
'UTC+1\tEurope/Paris',
'UTC+1\tEurope/Podgorica',
'UTC+1\tEurope/Prague',
'UTC+1\tEurope/Rome',
'UTC+1\tEurope/San_Marino',
'UTC+1\tEurope/Sarajevo',
'UTC+1\tEurope/Skopje',
'UTC+1\tEurope/Stockholm',
'UTC+1\tEurope/Tirane',
'UTC+1\tEurope/Vaduz',
'UTC+1\tEurope/Vatican',
'UTC+1\tEurope/Vienna',
'UTC+1\tEurope/Warsaw',
'UTC+1\tEurope/Zagreb',
'UTC+1\tEurope/Zurich',
'UTC+2\tAfrica/Blantyre',
'UTC+2\tAfrica/Bujumbura',
'UTC+2\tAfrica/Cairo',
'UTC+2\tAfrica/Gaborone',
'UTC+2\tAfrica/Harare',
'UTC+2\tAfrica/Johannesburg',
'UTC+2\tAfrica/Juba',
'UTC+2\tAfrica/Khartoum',
'UTC+2\tAfrica/Kigali',
'UTC+2\tAfrica/Lubumbashi',
'UTC+2\tAfrica/Lusaka',
'UTC+2\tAfrica/Maputo',
'UTC+2\tAfrica/Maseru',
'UTC+2\tAfrica/Mbabane',
'UTC+2\tAfrica/Tripoli',
'UTC+2\tAsia/Beirut',
'UTC+2\tAsia/Famagusta',
'UTC+2\tAsia/Gaza',
'UTC+2\tAsia/Hebron',
'UTC+2\tAsia/Jerusalem',
'UTC+2\tAsia/Nicosia',
'UTC+2\tEurope/Athens',
'UTC+2\tEurope/Bucharest',
'UTC+2\tEurope/Chisinau',
'UTC+2\tEurope/Helsinki',
'UTC+2\tEurope/Kaliningrad',
'UTC+2\tEurope/Kyiv',
'UTC+2\tEurope/Mariehamn',
'UTC+2\tEurope/Riga',
'UTC+2\tEurope/Sofia',
'UTC+2\tEurope/Tallinn',
'UTC+2\tEurope/Vilnius',
'UTC+3\tAfrica/Addis_Ababa',
'UTC+3\tAfrica/Asmara',
'UTC+3\tAfrica/Dar_es_Salaam',
'UTC+3\tAfrica/Djibouti',
'UTC+3\tAfrica/Kampala',
'UTC+3\tAfrica/Mogadishu',
'UTC+3\tAfrica/Nairobi',
'UTC+3\tAntarctica/Syowa',
'UTC+3\tAsia/Aden',
'UTC+3\tAsia/Amman',
'UTC+3\tAsia/Baghdad',
'UTC+3\tAsia/Bahrain',
'UTC+3\tAsia/Damascus',
'UTC+3\tAsia/Kuwait',
'UTC+3\tAsia/Qatar',
'UTC+3\tAsia/Riyadh',
'UTC+3\tEurope/Istanbul',
'UTC+3\tEurope/Kirov',
'UTC+3\tEurope/Minsk',
'UTC+3\tEurope/Moscow',
'UTC+3\tEurope/Simferopol',
'UTC+3\tEurope/Volgograd',
'UTC+3\tIndian/Antananarivo',
'UTC+3\tIndian/Comoro',
'UTC+3\tIndian/Mayotte',
'UTC+3:30\tAsia/Tehran',
'UTC+4\tAsia/Baku',
'UTC+4\tAsia/Dubai',
'UTC+4\tAsia/Muscat',
'UTC+4\tAsia/Tbilisi',
'UTC+4\tAsia/Yerevan',
'UTC+4\tEurope/Astrakhan',
'UTC+4\tEurope/Samara',
'UTC+4\tEurope/Saratov',
'UTC+4\tEurope/Ulyanovsk',
'UTC+4\tIndian/Mahe',
'UTC+4\tIndian/Mauritius',
'UTC+4\tIndian/Reunion',
'UTC+4:30\tAsia/Kabul',
'UTC+5\tAntarctica/Mawson',
'UTC+5\tAsia/Aqtau',
'UTC+5\tAsia/Aqtobe',
'UTC+5\tAsia/Ashgabat',
'UTC+5\tAsia/Atyrau',
'UTC+5\tAsia/Dushanbe',
'UTC+5\tAsia/Karachi',
'UTC+5\tAsia/Oral',
'UTC+5\tAsia/Qyzylorda',
'UTC+5\tAsia/Samarkand',
'UTC+5\tAsia/Tashkent',
'UTC+5\tAsia/Yekaterinburg',
'UTC+5\tIndian/Kerguelen',
'UTC+5\tIndian/Maldives',
'UTC+5:30\tAsia/Colombo',
'UTC+5:30\tAsia/Kolkata',
'UTC+5:45\tAsia/Kathmandu',
'UTC+6\tAntarctica/Vostok',
'UTC+6\tAsia/Almaty',
'UTC+6\tAsia/Bishkek',
'UTC+6\tAsia/Dhaka',
'UTC+6\tAsia/Omsk',
'UTC+6\tAsia/Qostanay',
'UTC+6\tAsia/Thimphu',
'UTC+6\tAsia/Urumqi',
'UTC+6\tIndian/Chagos',
'UTC+6:30\tAsia/Yangon',
'UTC+6:30\tIndian/Cocos',
'UTC+7\tAntarctica/Davis',
'UTC+7\tAsia/Bangkok',
'UTC+7\tAsia/Barnaul',
'UTC+7\tAsia/Hovd',
'UTC+7\tAsia/Ho_Chi_Minh',
'UTC+7\tAsia/Jakarta',
'UTC+7\tAsia/Krasnoyarsk',
'UTC+7\tAsia/Novokuznetsk',
'UTC+7\tAsia/Novosibirsk',
'UTC+7\tAsia/Phnom_Penh',
'UTC+7\tAsia/Pontianak',
'UTC+7\tAsia/Tomsk',
'UTC+7\tAsia/Vientiane',
'UTC+7\tIndian/Christmas',
'UTC+8\tAsia/Brunei',
'UTC+8\tAsia/Choibalsan',
'UTC+8\tAsia/Hong_Kong',
'UTC+8\tAsia/Irkutsk',
'UTC+8\tAsia/Kuala_Lumpur',
'UTC+8\tAsia/Kuching',
'UTC+8\tAsia/Macau',
'UTC+8\tAsia/Makassar',
'UTC+8\tAsia/Manila',
'UTC+8\tAsia/Shanghai',
'UTC+8\tAsia/Singapore',
'UTC+8\tAsia/Taipei',
'UTC+8\tAsia/Ulaanbaatar',
'UTC+8\tAustralia/Perth',
'UTC+8:45\tAustralia/Eucla',
'UTC+9\tAsia/Chita',
'UTC+9\tAsia/Dili',
'UTC+9\tAsia/Jayapura',
'UTC+9\tAsia/Khandyga',
'UTC+9\tAsia/Pyongyang',
'UTC+9\tAsia/Seoul',
'UTC+9\tAsia/Tokyo',
'UTC+9\tAsia/Yakutsk',
'UTC+9\tPacific/Palau',
'UTC+9:30\tAustralia/Adelaide',
'UTC+9:30\tAustralia/Broken_Hill',
'UTC+9:30\tAustralia/Darwin',
'UTC+10\tAntarctica/DumontDUrville',
'UTC+10\tAntarctica/Macquarie',
'UTC+10\tAsia/Ust-Nera',
'UTC+10\tAsia/Vladivostok',
'UTC+10\tAustralia/Brisbane',
'UTC+10\tAustralia/Hobart',
'UTC+10\tAustralia/Lindeman',
'UTC+10\tAustralia/Melbourne',
'UTC+10\tAustralia/Sydney',
'UTC+10\tPacific/Chuuk',
'UTC+10\tPacific/Guam',
'UTC+10\tPacific/Port_Moresby',
'UTC+10\tPacific/Saipan',
'UTC+10:30\tAustralia/Lord_Howe',
'UTC+11\tAntarctica/Casey',
'UTC+11\tAsia/Magadan',
'UTC+11\tAsia/Sakhalin',
'UTC+11\tAsia/Srednekolymsk',
'UTC+11\tPacific/Bougainville',
'UTC+11\tPacific/Efate',
'UTC+11\tPacific/Guadalcanal',
'UTC+11\tPacific/Kosrae',
'UTC+11\tPacific/Norfolk',
'UTC+11\tPacific/Noumea',
'UTC+11\tPacific/Pohnpei',
'UTC+12\tAntarctica/McMurdo',
'UTC+12\tAsia/Anadyr',
'UTC+12\tAsia/Kamchatka',
'UTC+12\tPacific/Auckland',
'UTC+12\tPacific/Fiji',
'UTC+12\tPacific/Funafuti',
'UTC+12\tPacific/Kwajalein',
'UTC+12\tPacific/Majuro',
'UTC+12\tPacific/Nauru',
'UTC+12\tPacific/Tarawa',
'UTC+12\tPacific/Wake',
'UTC+12\tPacific/Wallis',
'UTC+12:45\tPacific/Chatham',
'UTC+13\tPacific/Apia',
'UTC+13\tPacific/Fakaofo',
'UTC+13\tPacific/Kanton',
'UTC+13\tPacific/Tongatapu',
'UTC+14\tPacific/Kiritimati',
];
export const TimezoneList = Object.freeze(
Intl.supportedValuesOf('timeZone')
.map((tz) => {
const dtf = new Intl.DateTimeFormat('en-US', {
hourCycle: 'h24',
timeZone: tz,
timeZoneName: 'longOffset',
});
const offsetString = dtf.formatToParts(new Date()).at(-1)!.value;
const match = /^GMT(?<sign>\+|-)(?<hours>\d{2}):(?<minutes>\d{2})$/i.exec(
offsetString,
);
const hours = match?.groups?.hours ?? '00';
const minutes = match?.groups?.minutes ?? '00';
const sign = match?.groups?.sign;
return Object.freeze({
name: `${offsetString} ${tz}`,
id: tz,
offset:
(sign === '-' ? -1 : 1) * (Number(hours) * 60 + Number(minutes)),
offsetString,
});
})
.sort((a, b) => a.offset - b.offset),
);
const navigatorTz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
export const DEFAULT_TIMEZONE = TimezoneList.find(
(tz) => tz.name === navigatorTz,
)!;

View File

@ -1,7 +1,6 @@
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';
@ -56,9 +55,9 @@ export const useChangeLanguage = () => {
const { saveSetting } = useSaveSetting();
const changeLanguage = (lng: string) => {
const targetLng =
LanguageTranslationMap[lng as keyof typeof LanguageTranslationMap];
changeLanguageAsync(targetLng);
// const targetLng = LanguageTranslationMap[lng as keyof typeof LanguageTranslationMap];
changeLanguageAsync(lng);
saveSetting({ language: lng });
};

View File

@ -1,6 +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';
import { ITenantInfo } from '@/interfaces/database/knowledge';
@ -12,7 +11,11 @@ import {
IUserInfo,
} from '@/interfaces/database/user-setting';
import { ISetLangfuseConfigRequestBody } from '@/interfaces/request/system';
import { changeLanguageAsync } from '@/locales/config';
import {
changeLanguageAsync,
DEFAULT_LANGUAGE_CODE,
supportedLanguages,
} from '@/locales/config';
import { Routes } from '@/routes';
import userService, {
addTenantUser,
@ -47,24 +50,28 @@ export const enum UserSettingApiAction {
}
export const useFetchUserInfo = (): ResponseGetType<IUserInfo> => {
const { i18n } = useTranslation();
const { data, isFetching: loading } = useQuery({
queryKey: [UserSettingApiAction.UserInfo],
initialData: {},
gcTime: 0,
queryFn: async () => {
const { data } = await userService.user_info();
if (data.code === 0) {
const targetLng =
LanguageTranslationMap[
data.data.language as keyof typeof LanguageTranslationMap
];
supportedLanguages.find((lang) => lang.code === data.data.language)
?.code ?? DEFAULT_LANGUAGE_CODE;
if (targetLng) {
await changeLanguageAsync(targetLng);
}
return Object.assign({}, data.data, {
language: targetLng,
});
}
return data?.data ?? {};
return data.data ?? {};
},
});

View File

@ -7,7 +7,6 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { LanguageList, LanguageMap } from '@/constants/common';
import { useChangeLanguage } from '@/hooks/logic-hooks';
import {
useFetchUserInfo,
@ -16,26 +15,25 @@ import {
import { cn } from '@/lib/utils';
import { TenantRole } from '@/pages/user-setting/constants';
import { Routes } from '@/routes';
import { camelCase } from 'lodash';
import { LucideChevronDown, LucideCircleHelp } from 'lucide-react';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useLocation } from 'react-router';
import { BellButton } from './bell-button';
import GlobalNavbar from './global-navbar';
import ThemeButton from './theme-button';
import { supportedLanguages } from '@/locales/config';
export function Header({
className,
...props
}: React.HTMLAttributes<HTMLElement>) {
const { t } = useTranslation();
const { pathname } = useLocation();
const changeLanguage = useChangeLanguage();
const {
data: { language = 'English', avatar, nickname },
data: { language = 'en', avatar, nickname },
} = useFetchUserInfo();
const { data: tenantData } = useListTenant();
@ -44,10 +42,12 @@ export function Header({
[tenantData],
);
const langItems = LanguageList.map((x) => ({
key: x,
label: <span>{LanguageMap[x as keyof typeof LanguageMap]}</span>,
}));
const currentLanguage = supportedLanguages.find((x) => x.code === language);
// const langItems = LanguageList.map((x) => ({
// key: x,
// label: <span>{LanguageMap[x as keyof typeof LanguageMap]}</span>,
// }));
return (
<header
@ -94,19 +94,18 @@ export function Header({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="flex items-center gap-1" variant="ghost">
{t(`common.${camelCase(language)}`)}
<LucideChevronDown className="size-4" />
{currentLanguage?.displayName}
<LucideChevronDown className="size-[1em]" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{langItems.map((x) => (
{supportedLanguages.map((x) => (
<DropdownMenuItem
key={x.key}
onClick={() => changeLanguage(x.key)}
key={x.code}
onClick={() => changeLanguage(x.code)}
>
{x.label}
{x.displayName}
</DropdownMenuItem>
))}
</DropdownMenuContent>

View File

@ -1,4 +1,5 @@
import { clsx, type ClassValue } from 'clsx';
import React from 'react';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
@ -19,6 +20,20 @@ export function formatBytes(
if (bytes === 0) return '0 Byte';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(decimals)} ${
sizeType === 'accurate' ? accurateSizes[i] ?? 'Bytes' : sizes[i] ?? 'Bytes'
sizeType === 'accurate'
? (accurateSizes[i] ?? 'Bytes')
: (sizes[i] ?? 'Bytes')
}`;
}
export function combineRefs<T>(...refs: React.ForwardedRef<T>[]) {
return (node: T) => {
refs.forEach((ref) => {
if (typeof ref === 'function') {
ref(node);
} else if (ref) {
ref.current = node;
}
});
};
}

View File

@ -2300,7 +2300,7 @@ export default {
},
pagination: {
total: 'الإجمالي {{total}}',
page: '{{page}} /الصفحة',
page: '{{page}} / الصفحة',
},
dataflowParser: {
result: 'نتيجة',

View File

@ -2385,7 +2385,7 @@ Important structured information may include: names, dates, locations, events, k
},
pagination: {
total: 'Общо {{total}}',
page: '{{page}} /Страница',
page: '{{page}} / Страница',
},
dataflowParser: {
result: 'Резултат',

View File

@ -1,13 +1,14 @@
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { upperFirst } from 'lodash';
import { initReactI18next } from 'react-i18next';
import { LanguageAbbreviation } from '@/constants/common';
import { createTranslationTable, flattenObject } from './until';
import translation_en from './en';
const languageImports: Record<string, () => Promise<{ default: any }>> = {
[LanguageAbbreviation.En]: () => import('./en'),
[LanguageAbbreviation.Zh]: () => import('./zh'),
[LanguageAbbreviation.ZhTraditional]: () => import('./zh-traditional'),
[LanguageAbbreviation.Id]: () => import('./id'),
@ -23,20 +24,22 @@ const languageImports: Record<string, () => Promise<{ default: any }>> = {
[LanguageAbbreviation.Ar]: () => import('./ar'),
};
const languageAliases: Record<string, string> = {
'pt-br': LanguageAbbreviation.PtBr,
};
const supportedLanguageCodes: Intl.UnicodeBCP47LocaleIdentifier[] =
Object.keys(languageImports);
const normalizeLanguageCode = (lng: string): string => {
return languageAliases[lng] ?? lng;
};
export const supportedLanguages = supportedLanguageCodes.map((code) => {
const locale = new Intl.Locale(code);
const enFlattened = flattenObject(translation_en);
return {
code,
locale,
displayName: upperFirst(
new Intl.DisplayNames(locale, { type: 'language' }).of(code)!,
),
};
});
export const translationTable = createTranslationTable(
[enFlattened],
['English'],
);
export const DEFAULT_LANGUAGE_CODE = LanguageAbbreviation.En;
const resources = {
[LanguageAbbreviation.En]: translation_en,
@ -49,16 +52,17 @@ i18n
detection: {
lookupLocalStorage: 'lng',
},
supportedLngs: Object.values(LanguageAbbreviation),
supportedLngs: supportedLanguageCodes,
resources,
fallbackLng: 'en',
fallbackLng: DEFAULT_LANGUAGE_CODE,
interpolation: {
escapeValue: false,
},
});
export const loadLanguageAsync = async (lng: string): Promise<void> => {
const normalizedLng = normalizeLanguageCode(lng);
// const normalizedLng = normalizeLanguageCode(lng);
const normalizedLng = lng;
if (i18n.hasResourceBundle(normalizedLng, 'translation')) {
return;
@ -74,16 +78,15 @@ export const loadLanguageAsync = async (lng: string): Promise<void> => {
const module = await importFn();
const translationData = module.default?.translation || module.default;
i18n.addResourceBundle(normalizedLng, '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> => {
const normalizedLng = normalizeLanguageCode(lng);
// const normalizedLng = normalizeLanguageCode(lng);
const normalizedLng = lng;
if (
normalizedLng !== LanguageAbbreviation.En &&
!i18n.hasResourceBundle(normalizedLng, 'translation')
@ -94,14 +97,14 @@ export const changeLanguageAsync = async (lng: string): Promise<void> => {
};
export const initLanguage = async (): Promise<void> => {
const currentLng = normalizeLanguageCode(
i18n.language || localStorage.getItem('lng') || LanguageAbbreviation.En,
);
// const currentLng = normalizeLanguageCode(
// i18n.language || localStorage.getItem('lng') || LanguageAbbreviation.En,
// );
if (currentLng !== LanguageAbbreviation.En && languageImports[currentLng]) {
await loadLanguageAsync(currentLng);
await i18n.changeLanguage(currentLng);
}
const currentLng =
i18n.language || localStorage.getItem('lng') || DEFAULT_LANGUAGE_CODE;
await changeLanguageAsync(currentLng);
};
export default i18n;

View File

@ -2444,7 +2444,7 @@ Wichtige strukturierte Informationen können sein: Namen, Daten, Orte, Ereigniss
},
pagination: {
total: 'Gesamt {{total}}',
page: '{{page}} /Seite',
page: '{{page}} / Seite',
},
dataflowParser: {
result: 'Ergebnis',

View File

@ -2482,7 +2482,7 @@ Important structured information may include: names, dates, locations, events, k
},
pagination: {
total: 'Total {{total}}',
page: '{{page}} /Page',
page: '{{page}} / Page',
},
dataflowParser: {
result: 'Result',

View File

@ -1203,7 +1203,7 @@ Quanto sopra è il contenuto che devi riassumere.`,
},
pagination: {
total: 'Totale {{total}}',
page: '{{page}} /Pagina',
page: '{{page}} / Pagina',
},
deleteModal: {
delAgent: 'Elimina agente',

View File

@ -1,60 +0,0 @@
type NestedObject = {
[key: string]: string | NestedObject;
};
type FlattenedObject = {
[key: string]: string;
};
export function flattenObject(
obj: NestedObject,
parentKey: string = '',
): FlattenedObject {
const result: FlattenedObject = {};
for (const [key, value] of Object.entries(obj)) {
const newKey = parentKey ? `${parentKey}.${key}` : key;
if (typeof value === 'object' && value !== null) {
Object.assign(result, flattenObject(value as NestedObject, newKey));
} else {
result[newKey] = value as string;
}
}
return result;
}
type TranslationTableRow = {
key: string;
[language: string]: string;
};
/**
* Creates a translation table from multiple flattened language objects.
* @param langs - A list of flattened language objects.
* @param langKeys - A list of language identifiers (e.g., 'English', 'Vietnamese').
* @returns An array representing the translation table.
*/
export function createTranslationTable(
langs: FlattenedObject[],
langKeys: string[],
): TranslationTableRow[] {
const keys = new Set<string>();
// Collect all unique keys from the language objects
langs.forEach((lang) => {
Object.keys(lang).forEach((key) => keys.add(key));
});
// Build the table
return Array.from(keys).map((key) => {
const row: TranslationTableRow = { key };
langs.forEach((lang, index) => {
const langKey = langKeys[index];
row[langKey] = lang[key] || ''; // Use empty string if key is missing
});
return row;
});
}

View File

@ -229,19 +229,8 @@ function AdminServiceStatus() {
<div className="flex items-center gap-4">
<Popover>
<PopoverTrigger asChild>
<Button
size="icon"
variant="outline"
className="border-0.5"
// className="
// text-text-secondary
// dark:bg-bg-input dark:border-border-button
// hover:bg-border-button dark:hover:bg-border-button
// focus-visible:ring-0 focus-visible:text-text-primary
// focus-visible:bg-border-button focus-visible:border-border-button
// "
>
<LucideFilter className="h-4 w-4" />
<Button size="icon-lg" variant="outline">
<LucideFilter className="size-4" />
</Button>
</PopoverTrigger>

View File

@ -481,12 +481,8 @@ function AdminUserManagement() {
<div className="ml-auto flex justify-end gap-4">
<Popover>
<PopoverTrigger asChild>
<Button
size="icon"
variant="outline"
className="dark:bg-bg-input dark:border-border-button text-text-secondary"
>
<LucideFilter className="h-4 w-4" />
<Button size="icon-lg" variant="outline">
<LucideFilter className="size-4" />
</Button>
</PopoverTrigger>

View File

@ -13,6 +13,7 @@ import {
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchAgentListByPage } from '@/hooks/use-agent-request';
import { Routes } from '@/routes';
import { t } from 'i18next';
import { pick } from 'lodash';
import { Clipboard, ClipboardPlus, FileInput, Plus } from 'lucide-react';
@ -36,6 +37,7 @@ export default function Agents() {
filterValue,
handleFilterSubmit,
} = useFetchAgentListByPage();
const { navigateToAgentTemplates } = useNavigatePage();
const {
@ -72,6 +74,7 @@ export default function Agents() {
);
const [searchUrl, setSearchUrl] = useSearchParams();
const isCreate = searchUrl.get('isCreate') === 'true';
useEffect(() => {
if (isCreate) {
showCreatingModal();
@ -79,153 +82,162 @@ export default function Agents() {
setSearchUrl(searchUrl);
}
}, [isCreate, showCreatingModal, searchUrl, setSearchUrl]);
return (
<>
{(!data?.length || data?.length <= 0) && !searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14 !cursor-default"
isSearch={!!searchString}
type={EmptyCardType.Agent}
// onClick={() => showCreatingModal()}
>
<div className="flex flex-col gap-y-5 text-text-secondary text-sm pt-5">
<div
data-testid="agents-empty-create"
className="flex items-center gap-x-2 hover:text-text-primary cursor-pointer"
onClick={showCreatingModal}
>
<Clipboard size={14} />
{t('flow.createFromBlank')}
</div>
<div
className="flex items-center gap-x-2 hover:text-text-primary cursor-pointer"
onClick={navigateToAgentTemplates}
>
<ClipboardPlus size={14} />
{t('flow.createFromTemplate')}
</div>
<div
className="flex items-center gap-x-2 hover:text-text-primary cursor-pointer"
onClick={handleImportJson}
>
<FileInput size={14} />
{t('flow.importJsonFile')}
</div>
</div>
</EmptyAppCard>
</div>
)}
<section
className="flex flex-col w-full flex-1"
data-testid="agents-list"
>
{(!!data?.length || searchString) && (
<>
<div className="px-8 pt-8 ">
<ListFilterBar
title={t('flow.agents')}
searchString={searchString}
onSearchChange={handleInputChange}
icon="agents"
filters={filters}
onChange={handleFilterSubmit}
value={filterValue}
>
<DropdownMenu>
<DropdownMenuTrigger data-testid="create-agent">
<Button>
<Plus className="h-4 w-4" />
{t('flow.createGraph')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent data-testid="agent-create-menu">
<DropdownMenuItem
justifyBetween={false}
onClick={showCreatingModal}
>
<Clipboard />
{t('flow.createFromBlank')}
</DropdownMenuItem>
<DropdownMenuItem
justifyBetween={false}
onClick={navigateToAgentTemplates}
>
<ClipboardPlus />
{t('flow.createFromTemplate')}
</DropdownMenuItem>
<DropdownMenuItem
data-testid="agent-import-json"
justifyBetween={false}
onClick={handleImportJson}
>
<FileInput />
{t('flow.importJsonFile')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ListFilterBar>
</div>
{(!data?.length || data?.length <= 0) && searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Agent}
onClick={() => showCreatingModal()}
/>
</div>
)}
<div className="flex-1 overflow-auto">
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
{data?.length || searchString ? (
<article className="size-full flex flex-col" data-testid="agents-list">
<header className="px-5 pt-8 mb-4">
<ListFilterBar
title={t('flow.agents')}
searchString={searchString}
onSearchChange={handleInputChange}
icon="agents"
filters={filters}
onChange={handleFilterSubmit}
value={filterValue}
>
<DropdownMenu>
<DropdownMenuTrigger data-testid="create-agent">
<Button>
<Plus className="size-[1em]" />
{t('flow.createGraph')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent data-testid="agent-create-menu">
<DropdownMenuItem
justifyBetween={false}
onClick={showCreatingModal}
>
<Clipboard />
{t('flow.createFromBlank')}
</DropdownMenuItem>
<DropdownMenuItem
justifyBetween={false}
onClick={() => navigateToAgentTemplates()}
>
<ClipboardPlus />
{t('flow.createFromTemplate')}
</DropdownMenuItem>
<DropdownMenuItem
data-testid="agent-import-json"
justifyBetween={false}
onClick={handleImportJson}
>
<FileInput />
{t('flow.importJsonFile')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ListFilterBar>
</header>
{data.length ? (
<>
<CardContainer className="flex-1 overflow-auto px-5">
{data.map((x) => {
return (
<AgentCard
key={x.id}
data={x}
showAgentRenameModal={showAgentRenameModal}
></AgentCard>
/>
);
})}
</CardContainer>
<footer className="mt-4 px-5 pb-5">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={pagination.total}
onChange={handlePageChange}
/>
</footer>
</>
) : (
<div className="flex-1 flex items-center justify-center">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch
type={EmptyCardType.Agent}
onClick={() => showCreatingModal()}
/>
</div>
<div className="mt-8 px-8 pb-8">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={pagination.total}
onChange={handlePageChange}
></RAGFlowPagination>
</div>
</>
)}
{agentRenameVisible && (
<RenameDialog
hideModal={hideAgentRenameModal}
onOk={onAgentRenameOk}
initialName={initialAgentName}
loading={agentRenameLoading}
></RenameDialog>
)}
{creatingVisible && (
<CreateAgentDialog
loading={loading}
visible={creatingVisible}
hideModal={hideCreatingModal}
shouldChooseAgent
onOk={handleCreateAgentOrPipeline}
></CreateAgentDialog>
)}
{fileUploadVisible && (
<UploadAgentDialog
hideModal={hideFileUploadModal}
onOk={onFileUploadOk}
></UploadAgentDialog>
)}
</section>
)}
</article>
) : (
<article
className="size-full flex items-center justify-center"
data-testid="agents-list"
>
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14 !cursor-default"
type={EmptyCardType.Agent}
tabIndex={-1}
// onClick={() => showCreatingModal()}
>
<ul className="flex flex-col gap-y-5 text-text-secondary text-sm pt-5">
<li data-testid="agents-empty-create">
<Button
variant="static"
size="auto"
onClick={showCreatingModal}
>
<Clipboard className="size-[1em]" />
{t('flow.createFromBlank')}
</Button>
</li>
<li>
<Button
asLink
variant="static"
size="auto"
to={Routes.AgentTemplates}
>
<ClipboardPlus className="size-[1em]" />
{t('flow.createFromTemplate')}
</Button>
</li>
<li>
<Button variant="static" size="auto" onClick={handleImportJson}>
<FileInput className="size-[1em]" />
{t('flow.importJsonFile')}
</Button>
</li>
</ul>
</EmptyAppCard>
</article>
)}
{agentRenameVisible && (
<RenameDialog
hideModal={hideAgentRenameModal}
onOk={onAgentRenameOk}
initialName={initialAgentName}
loading={agentRenameLoading}
></RenameDialog>
)}
{creatingVisible && (
<CreateAgentDialog
loading={loading}
visible={creatingVisible}
hideModal={hideCreatingModal}
shouldChooseAgent
onOk={handleCreateAgentOrPipeline}
></CreateAgentDialog>
)}
{fileUploadVisible && (
<UploadAgentDialog
hideModal={hideFileUploadModal}
onOk={onFileUploadOk}
></UploadAgentDialog>
)}
</>
);
}

View File

@ -3,9 +3,8 @@ import {
CheckboxFormMultipleProps,
FilterPopover,
} from '@/components/list-filter-bar/filter-popover';
import { Button } from '@/components/ui/button';
import { SearchInput } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { Segmented } from '@/components/ui/segmented';
import { ChangeEventHandler, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { LogTabs } from './dataset-common';
@ -40,35 +39,23 @@ const DatasetFilter = (
}, [value]);
return (
<div className="flex items-center justify-between mb-4">
<div className="flex space-x-2 bg-bg-card p-1 rounded-md">
<Button
className={cn(
'px-4 py-2 rounded-md hover:text-text-primary hover:bg-bg-base',
<div>
<Segmented
value={active}
options={[
{ value: LogTabs.FILE_LOGS, label: t('knowledgeDetails.fileLogs') },
{
'bg-bg-base text-text-primary': active === LogTabs.FILE_LOGS,
'bg-transparent text-text-secondary ':
active !== LogTabs.FILE_LOGS,
value: LogTabs.DATASET_LOGS,
label: t('knowledgeDetails.datasetLogs'),
},
)}
onClick={() => setActive?.(LogTabs.FILE_LOGS)}
>
{t('knowledgeDetails.fileLogs')}
</Button>
<Button
className={cn(
'px-4 py-2 rounded-md hover:text-text-primary hover:bg-bg-base',
{
'bg-bg-base text-text-primary': active === LogTabs.DATASET_LOGS,
'bg-transparent text-text-secondary ':
active !== LogTabs.DATASET_LOGS,
},
)}
onClick={() => setActive?.(LogTabs.DATASET_LOGS)}
>
{t('knowledgeDetails.datasetLogs')}
</Button>
]}
onChange={(value) =>
setActive?.(value as (typeof LogTabs)[keyof typeof LogTabs])
}
/>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center gap-4">
<FilterPopover
value={value}
onChange={onChange}

View File

@ -187,7 +187,6 @@ const FileLogsPage: FC = () => {
label: t('knowledgeDetails.status'),
list: Object.values(RunningStatus).map((value) => {
// const value = key as RunningStatus;
console.log(value);
return {
id: value,
// label: RunningStatusMap[value].label,
@ -245,7 +244,6 @@ const FileLogsPage: FC = () => {
page: number;
pageSize: number;
}) => {
console.log('Pagination changed:', { page, pageSize });
setPagination({
...pagination,
page,

View File

@ -150,14 +150,19 @@ export const getFileLogsTableColumns = (
accessorKey: 'process_begin_at',
header: ({ column }) => {
return (
<Button
variant="transparent"
className="border-none"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
<div className="flex items-center gap-1">
{t('startDate')}
<ArrowUpDown />
</Button>
<Button
variant="ghost"
size="icon-xs"
onClick={() =>
column.toggleSorting(column.getIsSorted() === 'asc')
}
>
<ArrowUpDown className="size-[1em]" />
</Button>
</div>
);
},
cell: ({ row }) => (
@ -192,8 +197,7 @@ export const getFileLogsTableColumns = (
<div className="flex justify-start space-x-2 opacity-0 group-hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="sm"
className="p-1"
size="icon-sm"
onClick={() => {
showLog(row, LogTabs.FILE_LOGS);
}}
@ -203,8 +207,7 @@ export const getFileLogsTableColumns = (
{row.original.pipeline_id && (
<Button
variant="ghost"
size="sm"
className="p-1"
size="icon-sm"
onClick={navigateToDataflowResult({
id: row.original.id,
[PipelineResultSearchParams.KnowledgeId]: kowledgeId,
@ -261,14 +264,18 @@ export const getDatasetLogsTableColumns = (
accessorKey: 'process_begin_at',
header: ({ column }) => {
return (
<Button
variant="transparent"
className="border-none"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
<div className="flex items-center gap-1">
{t('startDate')}
<ArrowUpDown />
</Button>
<Button
variant="ghost"
size="icon-xs"
onClick={() =>
column.toggleSorting(column.getIsSorted() === 'asc')
}
>
<ArrowUpDown className="size-[1em]" />
</Button>
</div>
);
},
cell: ({ row }) => (
@ -319,11 +326,10 @@ export const getDatasetLogsTableColumns = (
id: 'operations',
header: t('operations'),
cell: ({ row }) => (
<div className="flex justify-start space-x-2 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="flex justify-start space-x-2 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity">
<Button
variant="ghost"
size="sm"
className="p-1"
size="icon-sm"
onClick={() => {
showLog(row, LogTabs.DATASET_LOGS);
}}

View File

@ -67,23 +67,15 @@ export default function Datasets() {
setSearchUrl(searchUrl);
}
}, [isCreate, showModal, searchUrl, setSearchUrl, queryClient]);
return (
<>
<section className="py-4 pt-8 flex-1 flex flex-col">
{(!kbs?.length || kbs?.length <= 0) && !searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Dataset}
onClick={() => showModal()}
/>
</div>
)}
{(!!kbs?.length || searchString) && (
<>
{kbs?.length || searchString ? (
<article
className="size-full flex flex-col"
data-testid="datasets-list"
>
<header className="px-5 pt-8 mb-4">
<ListFilterBar
title={t('header.dataset')}
searchString={searchString}
@ -91,64 +83,77 @@ export default function Datasets() {
value={filterValue}
filters={owners}
onChange={handleFilterSubmit}
className="px-8 mb-4"
icon={'datasets'}
>
<Button onClick={showModal}>
<Plus className="h-4 w-4" />
<Plus className="size-[1em]" />
{t('knowledgeList.createKnowledgeBase')}
</Button>
</ListFilterBar>
{(!kbs?.length || kbs?.length <= 0) && searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Dataset}
onClick={() => showModal()}
/>
</div>
)}
<div className="flex-1">
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
{kbs.map((dataset) => {
return (
<DatasetCard
dataset={dataset}
key={dataset.id}
showDatasetRenameModal={showDatasetRenameModal}
></DatasetCard>
);
})}
</header>
{kbs?.length ? (
<>
<CardContainer className="flex-1 overflow-auto px-5">
{kbs.map((dataset) => (
<DatasetCard
dataset={dataset}
key={dataset.id}
showDatasetRenameModal={showDatasetRenameModal}
/>
))}
</CardContainer>
<footer className="mt-4 px-5 pb-5">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={total}
onChange={handlePageChange}
/>
</footer>
</>
) : (
<div className="flex-1 flex items-center justify-center">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch
type={EmptyCardType.Dataset}
onClick={() => showModal()}
/>
</div>
<div className="mt-8 px-8">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={total}
onChange={handlePageChange}
></RAGFlowPagination>
</div>
</>
)}
{visible && (
<DatasetCreatingDialog
hideModal={hideModal}
onOk={onCreateOk}
loading={creatingLoading}
></DatasetCreatingDialog>
)}
{datasetRenameVisible && (
<RenameDialog
hideModal={hideDatasetRenameModal}
onOk={onDatasetRenameOk}
initialName={initialDatasetName}
loading={datasetRenameLoading}
></RenameDialog>
)}
</section>
)}
</article>
) : (
<article
className="size-full flex items-center justify-center"
data-testid="datasets-list"
>
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
type={EmptyCardType.Dataset}
onClick={() => showModal()}
/>
</article>
)}
{visible && (
<DatasetCreatingDialog
hideModal={hideModal}
onOk={onCreateOk}
loading={creatingLoading}
></DatasetCreatingDialog>
)}
{datasetRenameVisible && (
<RenameDialog
hideModal={hideDatasetRenameModal}
onOk={onDatasetRenameOk}
initialName={initialDatasetName}
loading={datasetRenameLoading}
></RenameDialog>
)}
</>
);
}

View File

@ -225,7 +225,7 @@ export function FilesTable({
id: 'actions',
header: t('action'),
meta: {
headerCellClassName: 'w-0',
headerCellClassName: 'w-0 whitespace-nowrap',
},
enableHiding: false,
enablePinning: true,
@ -278,8 +278,8 @@ export function FilesTable({
return (
<>
<div className="w-full">
<Table rootClassName="max-h-[calc(100vh-242px)] overflow-auto">
<div className="flex-1 h-0 size-full">
<Table rootClassName="max-h-full overflow-auto">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
@ -332,17 +332,17 @@ export function FilesTable({
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end py-4">
<div className="space-x-2">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={total}
onChange={(page, pageSize) => {
setPagination({ page, pageSize });
}}
></RAGFlowPagination>
</div>
</div>
<footer className="flex items-center justify-end pb-5 mt-4">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={total}
onChange={(page, pageSize) => {
setPagination({ page, pageSize });
}}
/>
</footer>
{connectToKnowledgeVisible && (
<LinkToDatasetDialog
hideModal={hideConnectToKnowledgeModal}

View File

@ -87,10 +87,9 @@ export default function Files() {
);
return (
<article className="p-8">
<header>
<article className="size-full flex flex-col" data-testid="files-list">
<header className="px-5 pt-8 mb-4">
<ListFilterBar
className="mb-4"
leftPanel={leftPanel}
searchString={searchString}
onSearchChange={handleInputChange}
@ -115,20 +114,25 @@ export default function Files() {
</DropdownMenuContent>
</DropdownMenu>
</ListFilterBar>
{!rowSelectionIsEmpty && (
<BulkOperateBar className="mb-4" list={list} count={selectedCount} />
<BulkOperateBar className="mt-4" list={list} count={selectedCount} />
)}
</header>
<FilesTable
files={files}
total={total}
pagination={pagination}
setPagination={setPagination}
loading={loading}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
showMoveFileModal={showMoveFileModal}
></FilesTable>
<div className="flex-1 px-5 flex flex-col overflow-hidden">
<FilesTable
files={files}
total={total}
pagination={pagination}
setPagination={setPagination}
loading={loading}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
showMoveFileModal={showMoveFileModal}
/>
</div>
{fileUploadVisible && (
<FileUploadDialog
hideModal={hideFileUploadModal}

View File

@ -69,24 +69,10 @@ export default function MemoryList() {
}, [isCreate, openCreateModalFun, searchUrl, setMemoryUrl]);
return (
<section className="w-full h-full flex flex-col">
{(!list?.data?.memory_list?.length ||
list?.data?.memory_list?.length <= 0) &&
!searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Memory}
onClick={() => openCreateModalFun()}
/>
</div>
)}
{(!!list?.data?.memory_list?.length || searchString) && (
<>
<div className="px-8 pt-8">
<>
{list?.data?.memory_list?.length || searchString ? (
<article className="size-full flex flex-col" data-testid="memory-list">
<header className="px-5 pt-8 mb-4">
<ListFilterBar
icon="memory"
title={t('memory')}
@ -96,35 +82,17 @@ export default function MemoryList() {
onChange={handleFilterSubmit}
value={filterValue}
>
<Button
variant={'default'}
onClick={() => {
openCreateModalFun();
}}
>
<Plus className=" h-4 w-4" />
<Button onClick={() => openCreateModalFun()}>
<Plus className="size-[1em]" />
{t('createMemory')}
</Button>
</ListFilterBar>
</div>
{(!list?.data?.memory_list?.length ||
list?.data?.memory_list?.length <= 0) &&
searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Memory}
onClick={() => openCreateModalFun()}
/>
</div>
)}
<div className="flex-1">
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
{list?.data.memory_list.map((x) => {
return (
</header>
{list?.data?.memory_list?.length ? (
<>
<CardContainer className="flex-1 overflow-auto px-5">
{list?.data.memory_list.map((x) => (
<MemoryCard
key={x.id}
data={x}
@ -132,22 +100,44 @@ export default function MemoryList() {
setAddOrEditType('edit');
showMemoryRenameModal(x);
}}
></MemoryCard>
);
})}
</CardContainer>
</div>
{list?.data.total_count && list?.data.total_count > 0 && (
<div className="px-8 mb-4">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
// total={pagination.total}
total={list?.data.total_count}
onChange={handlePageChange}
/>
))}
</CardContainer>
<footer className="mt-4 px-5 pb-5">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={list?.data.total_count}
onChange={handlePageChange}
/>
</footer>
</>
) : (
<div className="flex-1 flex items-center justify-center">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch
type={EmptyCardType.Memory}
onClick={() => openCreateModalFun()}
/>
</div>
)}
</>
</article>
) : (
<article
className="size-full flex items-center justify-center"
data-testid="memory-list"
>
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
type={EmptyCardType.Memory}
onClick={() => openCreateModalFun()}
/>
</article>
)}
{/* {openCreateModal && (
<RenameDialog
@ -168,6 +158,6 @@ export default function MemoryList() {
onSubmit={onMemoryConfirm}
/>
)}
</section>
</>
);
}

View File

@ -49,70 +49,72 @@ export default function ChatList() {
}, [isCreate, handleShowCreateModal, searchParams, setSearchParams]);
return (
<section className="flex flex-col w-full flex-1" data-testid="chats-list">
{data.dialogs?.length <= 0 && !searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Chat}
onClick={() => handleShowCreateModal()}
testId="chats-empty-create"
/>
</div>
)}
{(data.dialogs?.length > 0 || searchString) && (
<>
<div className="px-8 pt-8">
<>
{data.dialogs?.length || searchString ? (
<article className="size-full flex flex-col" data-testid="chats-list">
<header className="px-5 pt-8 mb-4">
<ListFilterBar
title={t('chat.chatApps')}
icon="chats"
onSearchChange={handleInputChange}
searchString={searchString}
>
<Button onClick={handleShowCreateModal} data-testid="create-chat">
<Plus className="h-4 w-4" />
<Button data-testid="create-chat" onClick={handleShowCreateModal}>
<Plus className="size-[1em]" />
{t('chat.createChat')}
</Button>
</ListFilterBar>
</div>
{data.dialogs?.length <= 0 && searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Chat}
onClick={() => handleShowCreateModal()}
testId="chats-empty-create"
/>
</div>
)}
<div className="flex-1 overflow-auto">
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
{data.dialogs.map((x) => {
return (
</header>
{data.dialogs?.length ? (
<>
<CardContainer className="flex-1 overflow-auto px-5">
{data.dialogs.map((x) => (
<ChatCard
key={x.id}
data={x}
showChatRenameModal={showChatRenameModal}
></ChatCard>
);
})}
</CardContainer>
</div>
<div className="mt-8 px-8 pb-8">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={pagination.total}
onChange={handlePageChange}
></RAGFlowPagination>
</div>
</>
/>
))}
</CardContainer>
<footer className="mt-4 px-5 pb-5">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={pagination.total}
onChange={handlePageChange}
/>
</footer>
</>
) : (
<div className="flex-1 flex items-center justify-center">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch
type={EmptyCardType.Chat}
testId="chats-empty-create"
/>
</div>
)}
</article>
) : (
<article
className="size-full flex items-center justify-center"
data-testid="chats-list"
>
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
type={EmptyCardType.Chat}
onClick={() => handleShowCreateModal()}
testId="chats-empty-create"
/>
</article>
)}
{chatRenameVisible && (
<RenameDialog
hideModal={hideChatRenameModal}
@ -122,6 +124,6 @@ export default function ChatList() {
title={initialChatName || t('chat.createChat')}
></RenameDialog>
)}
</section>
</>
);
}

View File

@ -65,25 +65,10 @@ export default function SearchList() {
}, [isCreate, openCreateModalFun, searchUrl, setSearchUrl]);
return (
<section className="w-full h-full flex flex-col" data-testid="search-list">
{(!list?.data?.search_apps?.length ||
list?.data?.search_apps?.length <= 0) &&
!searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
type={EmptyCardType.Search}
isSearch={!!searchString}
onClick={() => openCreateModalFun()}
testId="search-empty-create"
/>
</div>
)}
{(!!list?.data?.search_apps?.length || searchString) && (
<>
<div className="px-8 pt-8">
<>
{list?.data?.search_apps?.length || searchString ? (
<article className="size-full flex flex-col" data-testid="search-list">
<header className="px-5 pt-8 mb-4">
<ListFilterBar
icon="searches"
title={t('searchApps')}
@ -92,58 +77,66 @@ export default function SearchList() {
onSearchChange={handleInputChange}
>
<Button
variant={'default'}
data-testid="create-search"
onClick={() => {
openCreateModalFun();
}}
onClick={() => openCreateModalFun()}
>
<Plus className="h-4 w-4" />
<Plus className="size-[1em]" />
{t('createSearch')}
</Button>
</ListFilterBar>
</div>
{(!list?.data?.search_apps?.length ||
list?.data?.search_apps?.length <= 0) &&
searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
type={EmptyCardType.Search}
isSearch={!!searchString}
onClick={() => openCreateModalFun()}
testId="search-empty-create"
</header>
{list?.data?.search_apps?.length ? (
<>
<CardContainer className="flex-1 overflow-auto px-5">
{list?.data.search_apps.map((x) => {
return (
<SearchCard
key={x.id}
data={x}
showSearchRenameModal={() => {
showSearchRenameModal(x);
}}
/>
);
})}
</CardContainer>
<footer className="mt-4 px-5 pb-5">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={list?.data.total}
onChange={handlePageChange}
/>
</div>
)}
<div className="flex-1">
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
{list?.data.search_apps.map((x) => {
return (
<SearchCard
key={x.id}
data={x}
showSearchRenameModal={() => {
showSearchRenameModal(x);
}}
></SearchCard>
);
})}
</CardContainer>
</div>
{list?.data.total && list?.data.total > 0 && (
<div className="px-8 mb-4">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
// total={pagination.total}
total={list?.data.total}
onChange={handlePageChange}
</footer>
</>
) : (
<div className="flex-1 flex items-center justify-center">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch
type={EmptyCardType.Search}
testId="search-empty-create"
/>
</div>
)}
</>
</article>
) : (
<article
className="size-full flex items-center justify-center"
data-testid="search-list"
>
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
type={EmptyCardType.Search}
onClick={() => openCreateModalFun()}
testId="search-empty-create"
/>
</article>
)}
{openCreateModal && (
<RenameDialog
@ -154,6 +147,6 @@ export default function SearchList() {
title={initialSearchName || t('createSearch')}
></RenameDialog>
)}
</section>
</>
);
}

View File

@ -6,10 +6,10 @@ import { cn } from '@/lib/utils';
const UserSetting = () => {
return (
<section className="pt-8 size-full grid grid-cols-[auto_1fr] grid-rows-1">
<SideBar></SideBar>
<SideBar />
<div className={cn('pr-6 pb-6 flex flex-1 rounded-lg overflow-hidden')}>
<Outlet></Outlet>
<Outlet />
</div>
</section>
);

View File

@ -1,4 +1,5 @@
// src/hooks/useProfile.ts
import { DEFAULT_TIMEZONE } from '@/constants/setting';
import {
useFetchUserInfo,
useSaveSetting,
@ -53,7 +54,10 @@ export const useProfile = () => {
// form.setValue('currPasswd', ''); // current password
const profile = {
userName: userInfo.nickname,
timeZone: userInfo.timezone,
timeZone:
userInfo.timezone === ' UTC+8\tAsia/Shanghai'
? DEFAULT_TIMEZONE.name
: userInfo.timezone,
avatar: userInfo.avatar || '',
email: userInfo.email,
currPasswd: userInfo.password,

View File

@ -19,12 +19,17 @@ import { TimezoneList } from '@/pages/user-setting/constants';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
import { Loader2Icon, PenLine } from 'lucide-react';
import { FC, useEffect } from 'react';
import { FC, useEffect, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { ProfileSettingWrapperCard } from '../components/user-setting-header';
import { EditType, modalTitle, useProfile } from './hooks/use-profile';
const timezoneOptions = TimezoneList.map(({ name }) => ({
value: name,
label: name,
}));
const baseSchema = z.object({
userName: z
.string()
@ -75,6 +80,7 @@ const passwordSchema = baseSchema
});
}
});
const ProfilePage: FC = () => {
const { t } = useTranslate('setting');
@ -116,6 +122,11 @@ const ProfilePage: FC = () => {
// );
// };
const timezone = useMemo(() => {
const tz = TimezoneList.find((tz) => tz.name === profile.timeZone);
return tz?.name ?? '';
}, [profile.timeZone]);
return (
// <div className="h-full w-full text-text-secondary relative flex flex-col gap-4">
<ProfileSettingWrapperCard
@ -172,8 +183,8 @@ const ProfilePage: FC = () => {
{t('timezone')}
</label>
<div className="flex-1 flex items-center gap-4">
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2">
{profile.timeZone}
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2 empty:before:content-['_'] empty:before:whitespace-pre">
{timezone}
</div>
<Button
variant="outline"
@ -276,9 +287,7 @@ const ProfilePage: FC = () => {
{t('timezone')}
</FormLabel>
<SelectWithSearch
options={TimezoneList.map((timeStr) => {
return { value: timeStr, label: timeStr };
})}
options={timezoneOptions}
placeholder="Select a timeZone"
onChange={field.onChange}
value={field.value}

View File

@ -1,23 +0,0 @@
import { translationTable } from '@/locales/config';
import TranslationTable from './translation-table';
function UserSettingLocale() {
return (
<TranslationTable
data={translationTable}
languages={[
'English',
'Rus',
'Vietnamese',
'Spanish',
'zh',
'zh-TRADITIONAL',
'ja',
'pt-br',
'German',
]}
/>
);
}
export default UserSettingLocale;

View File

@ -1,238 +0,0 @@
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react';
import { useMemo, useState } from 'react';
type TranslationTableRow = {
key: string;
[language: string]: string;
};
interface TranslationTableProps {
data: TranslationTableRow[];
languages: string[];
}
type FilterType = 'all' | 'show_empty' | 'show_non_empty';
type SortOrder = 'asc' | 'desc' | null;
interface ColumnState {
key: string;
sortOrder: SortOrder;
filter: FilterType;
}
const TranslationTable: React.FC<TranslationTableProps> = ({
data,
languages,
}) => {
const [columnStates, setColumnStates] = useState<ColumnState[]>(
[{ key: 'key', sortOrder: null, filter: 'all' as FilterType }].concat(
languages.map((lang) => ({
key: lang,
sortOrder: null,
filter: 'all' as FilterType,
})),
),
);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
// Get the active sort column
const activeSortColumn = useMemo(() => {
return columnStates.find((col) => col.sortOrder !== null);
}, [columnStates]);
// Apply sorting and filtering
const processedData = useMemo(() => {
let filtered = [...data];
// Apply filters for all columns
columnStates.forEach((colState) => {
if (colState.filter !== 'all') {
filtered = filtered.filter((record) => {
const value = record[colState.key];
if (colState.filter === 'show_empty') {
return !value || value.length === 0;
}
if (colState.filter === 'show_non_empty') {
return value && value.length > 0;
}
return true;
});
}
});
// Apply sorting
if (activeSortColumn && activeSortColumn.sortOrder) {
filtered.sort((a, b) => {
const aValue = a[activeSortColumn.key] || '';
const bValue = b[activeSortColumn.key] || '';
const comparison = String(aValue).localeCompare(String(bValue));
return activeSortColumn.sortOrder === 'asc' ? comparison : -comparison;
});
}
return filtered;
}, [data, columnStates, activeSortColumn]);
// Apply pagination
const paginatedData = useMemo(() => {
const start = (currentPage - 1) * pageSize;
const end = start + pageSize;
return processedData.slice(start, end);
}, [processedData, currentPage, pageSize]);
const handleSort = (columnKey: string) => {
setColumnStates((prev) =>
prev.map((col) => {
if (col.key === columnKey) {
let newOrder: SortOrder = 'asc';
if (col.sortOrder === 'asc') {
newOrder = 'desc';
} else if (col.sortOrder === 'desc') {
newOrder = null;
}
return { ...col, sortOrder: newOrder };
}
return { ...col, sortOrder: null };
}),
);
};
const handleFilter = (columnKey: string, filter: FilterType) => {
setColumnStates((prev) =>
prev.map((col) => (col.key === columnKey ? { ...col, filter } : col)),
);
setCurrentPage(1);
};
const renderSortIcon = (columnKey: string) => {
const colState = columnStates.find((col) => col.key === columnKey);
const sortOrder = colState?.sortOrder;
if (sortOrder === 'asc') {
return <ArrowUp className="ms-1 h-4 w-4" />;
} else if (sortOrder === 'desc') {
return <ArrowDown className="ms-1 h-4 w-4" />;
} else {
return <ArrowUpDown className="ms-1 h-4 w-4" />;
}
};
const handlePageChange = (page: number, size: number) => {
setCurrentPage(page);
setPageSize(size);
};
return (
<div className="flex flex-col gap-4">
<div className="rounded-lg bg-bg-input scrollbar-auto overflow-hidden border border-border-default">
<Table rootClassName="rounded-lg">
<TableHeader className="bg-bg-title">
<TableRow className="hover:bg-bg-title">
<TableHead
className="h-12 px-4 cursor-pointer sticky start-0 bg-bg-title"
onClick={() => handleSort('key')}
>
<div className="flex items-center min-w-[200px]">
Key
{renderSortIcon('key')}
</div>
</TableHead>
{languages.map((lang) => {
const colState = columnStates.find((col) => col.key === lang)!;
return (
<TableHead key={lang} className="h-12 px-4">
<div className="flex flex-col gap-2">
<div
className="flex items-center cursor-pointer"
onClick={() => handleSort(lang)}
>
{lang}
{renderSortIcon(lang)}
</div>
<div className="flex gap-1">
<button
className={`text-xs px-2 py-0.5 rounded ${
colState.filter === 'show_empty'
? 'bg-bg-card text-text-primary'
: 'bg-bg-title text-text-secondary hover:bg-bg-card'
}`}
onClick={() => handleFilter(lang, 'show_empty')}
>
Empty
</button>
<button
className={`text-xs px-2 py-0.5 rounded ${
colState.filter === 'show_non_empty'
? 'bg-bg-card text-text-primary'
: 'bg-bg-title text-text-secondary hover:bg-bg-card'
}`}
onClick={() => handleFilter(lang, 'show_non_empty')}
>
Non-Empty
</button>
<button
className={`text-xs px-2 py-0.5 rounded ${
colState.filter === 'all'
? 'bg-bg-card text-text-primary'
: 'bg-bg-title text-text-secondary hover:bg-bg-card'
}`}
onClick={() => handleFilter(lang, 'all')}
>
All
</button>
</div>
</div>
</TableHead>
);
})}
</TableRow>
</TableHeader>
<TableBody className="bg-bg-base">
{paginatedData.length > 0 ? (
paginatedData.map((record) => (
<TableRow key={record.key} className="hover:bg-bg-card">
<TableCell className="p-4 font-medium sticky start-0 bg-bg-base hover:bg-bg-card">
{record.key}
</TableCell>
{languages.map((lang) => (
<TableCell key={lang} className="p-4">
{record[lang] || ''}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={languages.length + 1}
className="h-24 text-center"
>
No data
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<RAGFlowPagination
current={currentPage}
pageSize={pageSize}
total={processedData.length}
onChange={handlePageChange}
/>
</div>
);
};
export default TranslationTable;

View File

@ -128,7 +128,8 @@ export function SideBar() {
<footer className="p-6 mt-auto">
<div className="flex items-center gap-2 mb-6 justify-between">
<span className="text-accent-primary">{version}</span>
<span className="text-xs text-accent-primary">{version}</span>
<ThemeSwitch />
</div>

View File

@ -147,7 +147,7 @@ const routeConfigOptions = [
path: Routes.Root,
layout: false,
Component: () => import('@/layouts/root-layout'),
loader: ({ request }) => {
loader: ({ request }: { request: Request }) => {
const url = new URL(request.url);
const auth = url.searchParams.get('auth');
if (auth) {
@ -261,10 +261,12 @@ const routeConfigOptions = [
path: `${Routes.UserSetting}/profile`,
Component: () => import('@/pages/user-setting/profile'),
},
/*
{
path: `${Routes.UserSetting}/locale`,
Component: () => import('@/pages/user-setting/setting-locale'),
},
*/
{
path: `${Routes.UserSetting}/model`,
Component: () => import('@/pages/user-setting/setting-model'),