mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-05-05 01:37:46 +08:00
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:
@ -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')}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: '',
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
)!;
|
||||
|
||||
@ -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 });
|
||||
};
|
||||
|
||||
|
||||
@ -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 ?? {};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@ -2300,7 +2300,7 @@ export default {
|
||||
},
|
||||
pagination: {
|
||||
total: 'الإجمالي {{total}}',
|
||||
page: '{{page}} /الصفحة',
|
||||
page: '{{page}} / الصفحة',
|
||||
},
|
||||
dataflowParser: {
|
||||
result: 'نتيجة',
|
||||
|
||||
@ -2385,7 +2385,7 @@ Important structured information may include: names, dates, locations, events, k
|
||||
},
|
||||
pagination: {
|
||||
total: 'Общо {{total}}',
|
||||
page: '{{page}} /Страница',
|
||||
page: '{{page}} / Страница',
|
||||
},
|
||||
dataflowParser: {
|
||||
result: 'Резултат',
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user