### What problem does this PR solve? remove antd from dataset-page [#3221](https://github.com/infiniflow/ragflow/issues/3221) ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)tags/v0.20.0
| @@ -1,22 +0,0 @@ | |||
| .tweenGroup { | |||
| display: flex; | |||
| gap: 8px; | |||
| flex-wrap: wrap; | |||
| // width: 100%; | |||
| margin-bottom: 8px; | |||
| } | |||
| .tag { | |||
| max-width: 100%; | |||
| margin: 0; | |||
| padding: 2px 20px 0px 4px; | |||
| height: 26px; | |||
| font-size: 14px; | |||
| .textEllipsis(); | |||
| position: relative; | |||
| :global(.ant-tag-close-icon) { | |||
| position: absolute; | |||
| top: 7px; | |||
| right: 4px; | |||
| } | |||
| } | |||
| @@ -1,21 +1,24 @@ | |||
| import { PlusOutlined } from '@ant-design/icons'; | |||
| import type { InputRef } from 'antd'; | |||
| import { Input, Tag, theme, Tooltip } from 'antd'; | |||
| import { TweenOneGroup } from 'rc-tween-one'; | |||
| import React, { useEffect, useRef, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| import { X } from 'lucide-react'; | |||
| import { Button } from '../ui/button'; | |||
| import { | |||
| HoverCard, | |||
| HoverCardContent, | |||
| HoverCardTrigger, | |||
| } from '../ui/hover-card'; | |||
| import { Input } from '../ui/input'; | |||
| interface EditTagsProps { | |||
| value?: string[]; | |||
| onChange?: (tags: string[]) => void; | |||
| } | |||
| const EditTag = ({ value = [], onChange }: EditTagsProps) => { | |||
| const { token } = theme.useToken(); | |||
| const [inputVisible, setInputVisible] = useState(false); | |||
| const [inputValue, setInputValue] = useState(''); | |||
| const inputRef = useRef<InputRef>(null); | |||
| const inputRef = useRef<HTMLInputElement>(null); | |||
| useEffect(() => { | |||
| if (inputVisible) { | |||
| @@ -50,34 +53,66 @@ const EditTag = ({ value = [], onChange }: EditTagsProps) => { | |||
| const forMap = (tag: string) => { | |||
| return ( | |||
| <Tooltip title={tag}> | |||
| <Tag | |||
| key={tag} | |||
| className={styles.tag} | |||
| closable | |||
| onClose={(e) => { | |||
| e.preventDefault(); | |||
| handleClose(tag); | |||
| }} | |||
| > | |||
| {tag} | |||
| </Tag> | |||
| </Tooltip> | |||
| <HoverCard> | |||
| <HoverCardContent side="top">{tag}</HoverCardContent> | |||
| <HoverCardTrigger> | |||
| <div | |||
| key={tag} | |||
| className="w-fit flex items-center justify-center gap-2 border-dashed border px-1 rounded-sm bg-background-card" | |||
| > | |||
| <div className="flex gap-2 items-center"> | |||
| <div className="max-w-80 overflow-hidden text-ellipsis"> | |||
| {tag} | |||
| </div> | |||
| <X | |||
| className="w-4 h-4 text-muted-foreground hover:text-primary" | |||
| onClick={(e) => { | |||
| e.preventDefault(); | |||
| handleClose(tag); | |||
| }} | |||
| /> | |||
| </div> | |||
| </div> | |||
| </HoverCardTrigger> | |||
| </HoverCard> | |||
| ); | |||
| }; | |||
| const tagChild = value?.map(forMap); | |||
| const tagPlusStyle: React.CSSProperties = { | |||
| background: token.colorBgContainer, | |||
| borderStyle: 'dashed', | |||
| }; | |||
| return ( | |||
| <div> | |||
| {inputVisible ? ( | |||
| <Input | |||
| ref={inputRef} | |||
| type="text" | |||
| className="h-8 bg-background-card" | |||
| value={inputValue} | |||
| onChange={handleInputChange} | |||
| onBlur={handleInputConfirm} | |||
| onKeyDown={(e) => { | |||
| if (e?.key === 'Enter') { | |||
| handleInputConfirm(); | |||
| } | |||
| }} | |||
| /> | |||
| ) : ( | |||
| <Button | |||
| variant="dashed" | |||
| className="w-fit flex items-center justify-center gap-2 bg-background-card" | |||
| onClick={showInput} | |||
| style={tagPlusStyle} | |||
| > | |||
| <PlusOutlined /> | |||
| </Button> | |||
| )} | |||
| {Array.isArray(tagChild) && tagChild.length > 0 && ( | |||
| <TweenOneGroup | |||
| className={styles.tweenGroup} | |||
| className="flex gap-2 flex-wrap mt-2" | |||
| enter={{ | |||
| scale: 0.8, | |||
| opacity: 0, | |||
| @@ -95,21 +130,6 @@ const EditTag = ({ value = [], onChange }: EditTagsProps) => { | |||
| {tagChild} | |||
| </TweenOneGroup> | |||
| )} | |||
| {inputVisible ? ( | |||
| <Input | |||
| ref={inputRef} | |||
| type="text" | |||
| size="small" | |||
| value={inputValue} | |||
| onChange={handleInputChange} | |||
| onBlur={handleInputConfirm} | |||
| onPressEnter={handleInputConfirm} | |||
| /> | |||
| ) : ( | |||
| <Tag onClick={showInput} style={tagPlusStyle}> | |||
| <PlusOutlined /> | |||
| </Tag> | |||
| )} | |||
| </div> | |||
| ); | |||
| }; | |||
| @@ -1,15 +0,0 @@ | |||
| .primitiveImg { | |||
| display: inline-block; | |||
| max-height: 100px; | |||
| } | |||
| .image { | |||
| max-width: 100px; | |||
| object-fit: contain; | |||
| } | |||
| .imagePreview { | |||
| display: block; | |||
| max-width: 45vw; | |||
| max-height: 40vh; | |||
| } | |||
| @@ -1,8 +1,6 @@ | |||
| import { api_host } from '@/utils/api'; | |||
| import { Popover } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import styles from './index.less'; | |||
| import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; | |||
| interface IImage { | |||
| id: string; | |||
| @@ -16,7 +14,7 @@ const Image = ({ id, className, ...props }: IImage) => { | |||
| {...props} | |||
| src={`${api_host}/document/image/${id}`} | |||
| alt="" | |||
| className={classNames(styles.primitiveImg, className)} | |||
| className={classNames('max-w-[45vw] max-h-[40wh] block', className)} | |||
| /> | |||
| ); | |||
| }; | |||
| @@ -25,11 +23,13 @@ export default Image; | |||
| export const ImageWithPopover = ({ id }: { id: string }) => { | |||
| return ( | |||
| <Popover | |||
| placement="left" | |||
| content={<Image id={id} className={styles.imagePreview}></Image>} | |||
| > | |||
| <Image id={id} className={styles.image}></Image> | |||
| <Popover> | |||
| <PopoverTrigger> | |||
| <Image id={id} className="max-h-[100px] inline-block"></Image> | |||
| </PopoverTrigger> | |||
| <PopoverContent> | |||
| <Image id={id} className="max-w-[100px] object-contain"></Image> | |||
| </PopoverContent> | |||
| </Popover> | |||
| ); | |||
| }; | |||
| @@ -6,11 +6,12 @@ type InputProps = React.ComponentProps<'input'> & { | |||
| iconPosition?: 'left' | 'right'; | |||
| }; | |||
| function Input({ | |||
| const Input = function ({ | |||
| className, | |||
| type, | |||
| icon, | |||
| iconPosition = 'left', | |||
| ref, | |||
| ...props | |||
| }: InputProps) { | |||
| return ( | |||
| @@ -27,6 +28,7 @@ function Input({ | |||
| </div> | |||
| )} | |||
| <input | |||
| ref={ref} | |||
| type={type} | |||
| data-slot="input" | |||
| className={cn( | |||
| @@ -45,6 +47,6 @@ function Input({ | |||
| /> | |||
| </div> | |||
| ); | |||
| } | |||
| }; | |||
| export { Input }; | |||
| @@ -7,6 +7,7 @@ import { | |||
| useCallback, | |||
| useEffect, | |||
| useId, | |||
| useMemo, | |||
| useState, | |||
| } from 'react'; | |||
| @@ -27,50 +28,9 @@ import { | |||
| import { cn } from '@/lib/utils'; | |||
| import { RAGFlowSelectOptionType } from '../ui/select'; | |||
| const countries = [ | |||
| { | |||
| label: 'America', | |||
| options: [ | |||
| { value: 'United States', label: '🇺🇸' }, | |||
| { value: 'Canada', label: '🇨🇦' }, | |||
| { value: 'Mexico', label: '🇲🇽' }, | |||
| ], | |||
| }, | |||
| { | |||
| label: 'Africa', | |||
| options: [ | |||
| { value: 'South Africa', label: '🇿🇦' }, | |||
| { value: 'Nigeria', label: '🇳🇬' }, | |||
| { value: 'Morocco', label: '🇲🇦' }, | |||
| ], | |||
| }, | |||
| { | |||
| label: 'Asia', | |||
| options: [ | |||
| { value: 'China', label: '🇨🇳' }, | |||
| { value: 'Japan', label: '🇯🇵' }, | |||
| { value: 'India', label: '🇮🇳' }, | |||
| ], | |||
| }, | |||
| { | |||
| label: 'Europe', | |||
| options: [ | |||
| { value: 'United Kingdom', label: '🇬🇧' }, | |||
| { value: 'France', label: '🇫🇷' }, | |||
| { value: 'Germany', label: '🇩🇪' }, | |||
| ], | |||
| }, | |||
| { | |||
| label: 'Oceania', | |||
| options: [ | |||
| { value: 'Australia', label: '🇦🇺' }, | |||
| { value: 'New Zealand', label: '🇳🇿' }, | |||
| ], | |||
| }, | |||
| ]; | |||
| export type SelectWithSearchFlagOptionType = { | |||
| label: string; | |||
| value?: string; | |||
| options: RAGFlowSelectOptionType[]; | |||
| }; | |||
| @@ -84,99 +44,113 @@ export type SelectWithSearchFlagProps = { | |||
| export const SelectWithSearch = forwardRef< | |||
| React.ElementRef<typeof Button>, | |||
| SelectWithSearchFlagProps | |||
| >( | |||
| ( | |||
| { value: val = '', onChange, options = countries, triggerClassName }, | |||
| ref, | |||
| ) => { | |||
| const id = useId(); | |||
| const [open, setOpen] = useState<boolean>(false); | |||
| const [value, setValue] = useState<string>(''); | |||
| const handleSelect = useCallback( | |||
| (val: string) => { | |||
| setValue(val); | |||
| setOpen(false); | |||
| onChange?.(val); | |||
| }, | |||
| [onChange], | |||
| ); | |||
| >(({ value: val = '', onChange, options = [], triggerClassName }, ref) => { | |||
| const id = useId(); | |||
| const [open, setOpen] = useState<boolean>(false); | |||
| const [value, setValue] = useState<string>(''); | |||
| useEffect(() => { | |||
| const handleSelect = useCallback( | |||
| (val: string) => { | |||
| setValue(val); | |||
| }, [val]); | |||
| setOpen(false); | |||
| onChange?.(val); | |||
| }, | |||
| [onChange], | |||
| ); | |||
| return ( | |||
| <Popover open={open} onOpenChange={setOpen}> | |||
| <PopoverTrigger asChild> | |||
| <Button | |||
| id={id} | |||
| variant="outline" | |||
| role="combobox" | |||
| aria-expanded={open} | |||
| ref={ref} | |||
| className={cn( | |||
| 'bg-background hover:bg-background border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px]', | |||
| triggerClassName, | |||
| )} | |||
| > | |||
| {value ? ( | |||
| <span className="flex min-w-0 options-center gap-2"> | |||
| <span className="text-lg leading-none truncate"> | |||
| { | |||
| options | |||
| .map((group) => | |||
| group.options.find((item) => item.value === value), | |||
| ) | |||
| .filter(Boolean)[0]?.label | |||
| } | |||
| </span> | |||
| </span> | |||
| ) : ( | |||
| <span className="text-muted-foreground">Select value</span> | |||
| )} | |||
| <ChevronDownIcon | |||
| size={16} | |||
| className="text-muted-foreground/80 shrink-0" | |||
| aria-hidden="true" | |||
| /> | |||
| </Button> | |||
| </PopoverTrigger> | |||
| <PopoverContent | |||
| className="border-input w-full min-w-[var(--radix-popper-anchor-width)] p-0" | |||
| align="start" | |||
| useEffect(() => { | |||
| setValue(val); | |||
| }, [val]); | |||
| const selectLabel = useMemo(() => { | |||
| const optionTemp = options[0]; | |||
| if (optionTemp?.options) { | |||
| return optionTemp.options.find((opt) => opt.value === value)?.label || ''; | |||
| } else { | |||
| return options.find((opt) => opt.value === value)?.label || ''; | |||
| } | |||
| }, [options, value]); | |||
| return ( | |||
| <Popover open={open} onOpenChange={setOpen}> | |||
| <PopoverTrigger asChild> | |||
| <Button | |||
| id={id} | |||
| variant="outline" | |||
| role="combobox" | |||
| aria-expanded={open} | |||
| ref={ref} | |||
| className={cn( | |||
| 'bg-background hover:bg-background border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px]', | |||
| triggerClassName, | |||
| )} | |||
| > | |||
| <Command> | |||
| <CommandInput placeholder="Search ..." /> | |||
| <CommandList> | |||
| <CommandEmpty>No data found.</CommandEmpty> | |||
| {options.map((group) => ( | |||
| <Fragment key={group.label}> | |||
| <CommandGroup heading={group.label}> | |||
| {group.options.map((option) => ( | |||
| <CommandItem | |||
| key={option.value} | |||
| value={option.value} | |||
| onSelect={handleSelect} | |||
| > | |||
| <span className="text-lg leading-none"> | |||
| {option.label} | |||
| </span> | |||
| {value ? ( | |||
| <span className="flex min-w-0 options-center gap-2"> | |||
| <span className="text-lg leading-none truncate"> | |||
| {selectLabel} | |||
| </span> | |||
| </span> | |||
| ) : ( | |||
| <span className="text-muted-foreground">Select value</span> | |||
| )} | |||
| <ChevronDownIcon | |||
| size={16} | |||
| className="text-muted-foreground/80 shrink-0" | |||
| aria-hidden="true" | |||
| /> | |||
| </Button> | |||
| </PopoverTrigger> | |||
| <PopoverContent | |||
| className="border-input w-full min-w-[var(--radix-popper-anchor-width)] p-0" | |||
| align="start" | |||
| > | |||
| <Command> | |||
| <CommandInput placeholder="Search ..." /> | |||
| <CommandList> | |||
| <CommandEmpty>No data found.</CommandEmpty> | |||
| {options.map((group) => { | |||
| if (group.options) { | |||
| return ( | |||
| <Fragment key={group.label}> | |||
| <CommandGroup heading={group.label}> | |||
| {group.options.map((option) => ( | |||
| <CommandItem | |||
| key={option.value} | |||
| value={option.value} | |||
| onSelect={handleSelect} | |||
| > | |||
| <span className="text-lg leading-none"> | |||
| {option.label} | |||
| </span> | |||
| {value === option.value && ( | |||
| <CheckIcon size={16} className="ml-auto" /> | |||
| )} | |||
| </CommandItem> | |||
| ))} | |||
| </CommandGroup> | |||
| </Fragment> | |||
| ); | |||
| } else { | |||
| return ( | |||
| <CommandItem | |||
| key={group.value} | |||
| value={group.value} | |||
| onSelect={handleSelect} | |||
| > | |||
| <span className="text-lg leading-none">{group.label}</span> | |||
| {value === option.value && ( | |||
| <CheckIcon size={16} className="ml-auto" /> | |||
| )} | |||
| </CommandItem> | |||
| ))} | |||
| </CommandGroup> | |||
| </Fragment> | |||
| ))} | |||
| </CommandList> | |||
| </Command> | |||
| </PopoverContent> | |||
| </Popover> | |||
| ); | |||
| }, | |||
| ); | |||
| {value === group.value && ( | |||
| <CheckIcon size={16} className="ml-auto" /> | |||
| )} | |||
| </CommandItem> | |||
| ); | |||
| } | |||
| })} | |||
| </CommandList> | |||
| </Command> | |||
| </PopoverContent> | |||
| </Popover> | |||
| ); | |||
| }); | |||
| SelectWithSearch.displayName = 'SelectWithSearch'; | |||
| @@ -22,6 +22,7 @@ const buttonVariants = cva( | |||
| tertiary: | |||
| 'bg-colors-background-sentiment-solid-primary text-colors-text-persist-light hover:bg-colors-background-sentiment-solid-primary/80', | |||
| icon: 'bg-colors-background-inverse-standard text-foreground hover:bg-colors-background-inverse-standard/80', | |||
| dashed: 'border border-dashed border-input hover:bg-accent', | |||
| }, | |||
| size: { | |||
| default: 'h-8 px-2.5 py-1.5 ', | |||
| @@ -49,7 +50,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( | |||
| const Comp = asChild ? Slot : 'button'; | |||
| return ( | |||
| <Comp | |||
| className={cn(buttonVariants({ variant, size, className }))} | |||
| className={cn( | |||
| 'bg-background-card', | |||
| buttonVariants({ variant, size, className }), | |||
| )} | |||
| ref={ref} | |||
| {...props} | |||
| /> | |||
| @@ -0,0 +1,64 @@ | |||
| // src/components/ui/divider.tsx | |||
| import React from 'react'; | |||
| type Direction = 'horizontal' | 'vertical'; | |||
| type DividerType = 'horizontal' | 'vertical' | 'text'; | |||
| interface DividerProps { | |||
| direction?: Direction; | |||
| type?: DividerType; | |||
| text?: React.ReactNode; | |||
| color?: string; | |||
| margin?: string; | |||
| className?: string; | |||
| } | |||
| const Divider: React.FC<DividerProps> = ({ | |||
| direction = 'horizontal', | |||
| type = 'horizontal', | |||
| text, | |||
| color = 'border-muted-foreground/50', | |||
| margin = 'my-4', | |||
| className = '', | |||
| }) => { | |||
| const baseClasses = 'flex items-center'; | |||
| const directionClass = direction === 'horizontal' ? 'flex-row' : 'flex-col'; | |||
| const colorClass = color.startsWith('border-') ? color : `border-${color}`; | |||
| const marginClass = margin || ''; | |||
| const textClass = 'px-4 text-sm text-muted-foreground'; | |||
| // Default vertical style | |||
| if (direction === 'vertical') { | |||
| return ( | |||
| <div | |||
| className={`h-full ${colorClass} border-l ${marginClass} ${className}`} | |||
| > | |||
| {type === 'text' && ( | |||
| <div className="transform -rotate-90 px-2 whitespace-nowrap"> | |||
| {text} | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| // Horizontal with text | |||
| if (type === 'text') { | |||
| return ( | |||
| <div | |||
| className={`${baseClasses} ${directionClass} ${marginClass} ${className}`} | |||
| > | |||
| <div className={`flex-1 ${colorClass} border-t`}></div> | |||
| <div className={textClass}>{text}</div> | |||
| <div className={`flex-1 ${colorClass} border-t`}></div> | |||
| </div> | |||
| ); | |||
| } | |||
| // Default horizontal | |||
| return ( | |||
| <div className={`${colorClass} border-t ${marginClass} ${className}`} /> | |||
| ); | |||
| }; | |||
| export default Divider; | |||
| @@ -18,7 +18,7 @@ const HoverCardContent = React.forwardRef< | |||
| align={align} | |||
| sideOffset={sideOffset} | |||
| className={cn( | |||
| 'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]', | |||
| 'z-50 w-fit max-w-96 overflow-auto break-words whitespace-pre-wrap rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]', | |||
| className, | |||
| )} | |||
| {...props} | |||
| @@ -0,0 +1,199 @@ | |||
| // src/components/ui/modal.tsx | |||
| import * as DialogPrimitive from '@radix-ui/react-dialog'; | |||
| import { Loader, X } from 'lucide-react'; | |||
| import { FC, ReactNode, useCallback, useEffect, useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| interface ModalProps { | |||
| open: boolean; | |||
| onOpenChange?: (open: boolean) => void; | |||
| title?: ReactNode; | |||
| children: ReactNode; | |||
| footer?: ReactNode; | |||
| className?: string; | |||
| size?: 'small' | 'default' | 'large'; | |||
| closable?: boolean; | |||
| closeIcon?: ReactNode; | |||
| maskClosable?: boolean; | |||
| destroyOnClose?: boolean; | |||
| full?: boolean; | |||
| confirmLoading?: boolean; | |||
| cancelText?: ReactNode | string; | |||
| okText?: ReactNode | string; | |||
| onOk?: () => void; | |||
| onCancel?: () => void; | |||
| } | |||
| export const Modal: FC<ModalProps> = ({ | |||
| open, | |||
| onOpenChange, | |||
| title, | |||
| children, | |||
| footer, | |||
| className = '', | |||
| size = 'default', | |||
| closable = true, | |||
| closeIcon = <X className="w-4 h-4" />, | |||
| maskClosable = true, | |||
| destroyOnClose = false, | |||
| full = false, | |||
| onOk, | |||
| onCancel, | |||
| confirmLoading, | |||
| cancelText, | |||
| okText, | |||
| }) => { | |||
| const sizeClasses = { | |||
| small: 'max-w-md', | |||
| default: 'max-w-2xl', | |||
| large: 'max-w-4xl', | |||
| }; | |||
| const { t } = useTranslation(); | |||
| // Handle ESC key close | |||
| useEffect(() => { | |||
| const handleKeyDown = (e: KeyboardEvent) => { | |||
| if (e.key === 'Escape' && maskClosable) { | |||
| onOpenChange?.(false); | |||
| } | |||
| }; | |||
| window.addEventListener('keydown', handleKeyDown); | |||
| return () => window.removeEventListener('keydown', handleKeyDown); | |||
| }, [maskClosable, onOpenChange]); | |||
| const handleCancel = useCallback(() => { | |||
| onOpenChange?.(false); | |||
| onCancel?.(); | |||
| }, [onOpenChange, onCancel]); | |||
| const handleOk = useCallback(() => { | |||
| onOpenChange?.(true); | |||
| onOk?.(); | |||
| }, [onOpenChange, onOk]); | |||
| const handleChange = (open: boolean) => { | |||
| onOpenChange?.(open); | |||
| if (open) { | |||
| handleOk(); | |||
| } | |||
| if (!open) { | |||
| handleCancel(); | |||
| } | |||
| }; | |||
| const footEl = useMemo(() => { | |||
| let footerTemp; | |||
| if (footer) { | |||
| footerTemp = footer; | |||
| } else { | |||
| footerTemp = ( | |||
| <div className="flex justify-end gap-2"> | |||
| <button | |||
| type="button" | |||
| onClick={() => handleCancel()} | |||
| className="px-2 py-1 border border-input rounded-md hover:bg-muted" | |||
| > | |||
| {cancelText ?? t('modal.cancelText')} | |||
| </button> | |||
| <button | |||
| type="button" | |||
| disabled={confirmLoading} | |||
| onClick={() => handleOk()} | |||
| className="px-2 py-1 bg-primary text-primary-foreground rounded-md hover:bg-primary/90" | |||
| > | |||
| {confirmLoading && ( | |||
| <Loader className="inline-block mr-2 h-4 w-4 animate-spin" /> | |||
| )} | |||
| {okText ?? t('modal.okText')} | |||
| </button> | |||
| </div> | |||
| ); | |||
| return ( | |||
| <div className="flex items-center justify-end border-t border-border px-6 py-4"> | |||
| {footerTemp} | |||
| </div> | |||
| ); | |||
| } | |||
| }, [footer, cancelText, t, confirmLoading, okText, handleCancel, handleOk]); | |||
| return ( | |||
| <DialogPrimitive.Root open={open} onOpenChange={handleChange}> | |||
| <DialogPrimitive.Portal> | |||
| <DialogPrimitive.Overlay | |||
| className="fixed inset-0 z-50 bg-colors-background-neutral-weak/50 backdrop-blur-sm flex items-center justify-center p-4" | |||
| onClick={() => maskClosable && onOpenChange?.(false)} | |||
| > | |||
| <DialogPrimitive.Content | |||
| className={`relative w-[700px] ${full ? 'max-w-full' : sizeClasses[size]} ${className} bg-colors-background-neutral-standard rounded-lg shadow-lg transition-all`} | |||
| onClick={(e) => e.stopPropagation()} | |||
| > | |||
| {/* title */} | |||
| {title && ( | |||
| <div className="flex items-center justify-between border-b border-border px-6 py-4"> | |||
| <DialogPrimitive.Title className="text-lg font-medium text-foreground"> | |||
| {title} | |||
| </DialogPrimitive.Title> | |||
| {closable && ( | |||
| <DialogPrimitive.Close asChild> | |||
| <button | |||
| type="button" | |||
| className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted" | |||
| > | |||
| {closeIcon} | |||
| </button> | |||
| </DialogPrimitive.Close> | |||
| )} | |||
| </div> | |||
| )} | |||
| {/* content */} | |||
| <div className="p-6 overflow-y-auto max-h-[80vh]"> | |||
| {destroyOnClose && !open ? null : children} | |||
| </div> | |||
| {/* footer */} | |||
| {footEl} | |||
| </DialogPrimitive.Content> | |||
| </DialogPrimitive.Overlay> | |||
| </DialogPrimitive.Portal> | |||
| </DialogPrimitive.Root> | |||
| ); | |||
| }; | |||
| // example usage | |||
| /* | |||
| import { Modal } from '@/components/ui/modal'; | |||
| function Demo() { | |||
| const [open, setOpen] = useState(false); | |||
| return ( | |||
| <div> | |||
| <button onClick={() => setOpen(true)}>open modal</button> | |||
| <Modal | |||
| open={open} | |||
| onOpenChange={setOpen} | |||
| title="title" | |||
| footer={ | |||
| <div className="flex gap-2"> | |||
| <button onClick={() => setOpen(false)} className="px-4 py-2 border rounded-md"> | |||
| cancel | |||
| </button> | |||
| <button onClick={() => setOpen(false)} className="px-4 py-2 bg-primary text-white rounded-md"> | |||
| ok | |||
| </button> | |||
| </div> | |||
| } | |||
| > | |||
| <div className="py-4">弹窗内容区域</div> | |||
| </Modal> | |||
| <Modal | |||
| title={'modal-title'} | |||
| onOk={handleOk} | |||
| confirmLoading={loading} | |||
| destroyOnClose | |||
| > | |||
| <div className="py-4">弹窗内容区域</div> | |||
| </Modal> | |||
| </div> | |||
| ); | |||
| } | |||
| */ | |||
| @@ -0,0 +1,57 @@ | |||
| // src/components/ui/space.tsx | |||
| import React from 'react'; | |||
| type Direction = 'horizontal' | 'vertical'; | |||
| type Size = 'small' | 'middle' | 'large'; | |||
| interface SpaceProps { | |||
| direction?: Direction; | |||
| size?: Size; | |||
| align?: string; | |||
| justify?: string; | |||
| wrap?: boolean; | |||
| className?: string; | |||
| children: React.ReactNode; | |||
| } | |||
| const sizeClasses: Record<Size, string> = { | |||
| small: 'gap-2', | |||
| middle: 'gap-4', | |||
| large: 'gap-8', | |||
| }; | |||
| const directionClasses: Record<Direction, string> = { | |||
| horizontal: 'flex-row', | |||
| vertical: 'flex-col', | |||
| }; | |||
| const Space: React.FC<SpaceProps> = ({ | |||
| direction = 'horizontal', | |||
| size = 'middle', | |||
| align, | |||
| justify, | |||
| wrap = false, | |||
| className = '', | |||
| children, | |||
| }) => { | |||
| const baseClasses = 'flex'; | |||
| const directionClass = directionClasses[direction]; | |||
| const sizeClass = sizeClasses[size]; | |||
| const alignClass = align ? `items-${align}` : ''; | |||
| const justifyClass = justify ? `justify-${justify}` : ''; | |||
| const wrapClass = wrap ? 'flex-wrap' : ''; | |||
| const classes = [ | |||
| baseClasses, | |||
| directionClass, | |||
| sizeClass, | |||
| alignClass, | |||
| justifyClass, | |||
| wrapClass, | |||
| className, | |||
| ].join(' '); | |||
| return <div className={classes}>{children}</div>; | |||
| }; | |||
| export default Space; | |||
| @@ -1,51 +1,105 @@ | |||
| import * as React from 'react'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { | |||
| ChangeEventHandler, | |||
| ComponentProps, | |||
| FocusEventHandler, | |||
| forwardRef, | |||
| TextareaHTMLAttributes, | |||
| useCallback, | |||
| useEffect, | |||
| useRef, | |||
| useState, | |||
| } from 'react'; | |||
| interface TextareaProps | |||
| extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'autoSize'> { | |||
| autoSize?: { | |||
| minRows?: number; | |||
| maxRows?: number; | |||
| }; | |||
| } | |||
| const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>( | |||
| ({ className, autoSize, ...props }, ref) => { | |||
| const textareaRef = useRef<HTMLTextAreaElement>(null); | |||
| const getLineHeight = (element: HTMLElement): number => { | |||
| const style = window.getComputedStyle(element); | |||
| return parseInt(style.lineHeight, 10) || 20; | |||
| }; | |||
| const adjustHeight = useCallback(() => { | |||
| if (!textareaRef.current) return; | |||
| const lineHeight = getLineHeight(textareaRef.current); | |||
| const maxHeight = (autoSize?.maxRows || 3) * lineHeight; | |||
| textareaRef.current.style.height = 'auto'; | |||
| const Textarea = React.forwardRef< | |||
| HTMLTextAreaElement, | |||
| React.ComponentProps<'textarea'> | |||
| >(({ className, ...props }, ref) => { | |||
| return ( | |||
| <textarea | |||
| className={cn( | |||
| 'flex min-h-[80px] w-full rounded-md border border-input bg-colors-background-inverse-weak px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm overflow-hidden', | |||
| className, | |||
| )} | |||
| ref={ref} | |||
| {...props} | |||
| /> | |||
| ); | |||
| }); | |||
| requestAnimationFrame(() => { | |||
| if (!textareaRef.current) return; | |||
| const scrollHeight = textareaRef.current.scrollHeight; | |||
| textareaRef.current.style.height = `${Math.min(scrollHeight, maxHeight)}px`; | |||
| }); | |||
| }, [autoSize]); | |||
| useEffect(() => { | |||
| if (autoSize) { | |||
| adjustHeight(); | |||
| } | |||
| }, [textareaRef, autoSize, adjustHeight]); | |||
| useEffect(() => { | |||
| if (typeof ref === 'function') { | |||
| ref(textareaRef.current); | |||
| } else if (ref) { | |||
| ref.current = textareaRef.current; | |||
| } | |||
| }, [ref]); | |||
| return ( | |||
| <textarea | |||
| className={cn( | |||
| 'flex min-h-[80px] w-full rounded-md border border-input bg-colors-background-inverse-weak px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm overflow-hidden', | |||
| className, | |||
| )} | |||
| rows={autoSize?.minRows ?? props.rows ?? undefined} | |||
| style={{ | |||
| maxHeight: autoSize?.maxRows | |||
| ? `${autoSize.maxRows * 20}px` | |||
| : undefined, | |||
| overflow: autoSize ? 'auto' : undefined, | |||
| }} | |||
| ref={textareaRef} | |||
| {...props} | |||
| /> | |||
| ); | |||
| }, | |||
| ); | |||
| Textarea.displayName = 'Textarea'; | |||
| export { Textarea }; | |||
| type Value = string | readonly string[] | number | undefined; | |||
| export const BlurTextarea = React.forwardRef< | |||
| export const BlurTextarea = forwardRef< | |||
| HTMLTextAreaElement, | |||
| React.ComponentProps<'textarea'> & { | |||
| ComponentProps<'textarea'> & { | |||
| value: Value; | |||
| onChange(value: Value): void; | |||
| } | |||
| >(({ value, onChange, ...props }, ref) => { | |||
| const [val, setVal] = React.useState<Value>(); | |||
| const [val, setVal] = useState<Value>(); | |||
| const handleChange: React.ChangeEventHandler<HTMLTextAreaElement> = | |||
| React.useCallback((e) => { | |||
| const handleChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback( | |||
| (e) => { | |||
| setVal(e.target.value); | |||
| }, []); | |||
| const handleBlur: React.FocusEventHandler<HTMLTextAreaElement> = | |||
| React.useCallback( | |||
| (e) => { | |||
| onChange?.(e.target.value); | |||
| }, | |||
| [onChange], | |||
| ); | |||
| }, | |||
| [], | |||
| ); | |||
| const handleBlur: FocusEventHandler<HTMLTextAreaElement> = useCallback( | |||
| (e) => { | |||
| onChange?.(e.target.value); | |||
| }, | |||
| [onChange], | |||
| ); | |||
| React.useEffect(() => { | |||
| useEffect(() => { | |||
| setVal(value); | |||
| }, [value]); | |||
| @@ -11,7 +11,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | |||
| import { useDebounce } from 'ahooks'; | |||
| import { message } from 'antd'; | |||
| import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | |||
| import { useParams } from 'umi'; | |||
| import { useParams, useSearchParams } from 'umi'; | |||
| import { | |||
| useGetPaginationWithRouter, | |||
| useHandleSearchChange, | |||
| @@ -230,6 +230,8 @@ export const useUpdateKnowledge = (shouldFetchList = false) => { | |||
| export const useFetchKnowledgeBaseConfiguration = (refreshCount?: number) => { | |||
| const { id } = useParams(); | |||
| const [searchParams] = useSearchParams(); | |||
| const knowledgeBaseId = searchParams.get('id') || id; | |||
| let queryKey: (KnowledgeApiAction | number)[] = [ | |||
| KnowledgeApiAction.FetchKnowledgeDetail, | |||
| @@ -244,7 +246,7 @@ export const useFetchKnowledgeBaseConfiguration = (refreshCount?: number) => { | |||
| gcTime: 0, | |||
| queryFn: async () => { | |||
| const { data } = await kbService.get_kb_detail({ | |||
| kb_id: id, | |||
| kb_id: knowledgeBaseId, | |||
| }); | |||
| return data?.data ?? {}; | |||
| }, | |||
| @@ -1303,6 +1303,10 @@ This delimiter is used to split the input text into several text pieces echo of | |||
| }, | |||
| }, | |||
| }, | |||
| modal: { | |||
| okText: 'Confirm', | |||
| cancelText: 'Cancel', | |||
| }, | |||
| mcp: { | |||
| export: 'Export', | |||
| import: 'Import', | |||
| @@ -1185,4 +1185,8 @@ export default { | |||
| chat: '聊天', | |||
| }, | |||
| }, | |||
| modal: { | |||
| okText: '確認', | |||
| cancelText: '取消', | |||
| }, | |||
| }; | |||
| @@ -1264,4 +1264,8 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 | |||
| }, | |||
| }, | |||
| }, | |||
| modal: { | |||
| okText: '确认', | |||
| cancelText: '取消', | |||
| }, | |||
| }; | |||
| @@ -1,10 +1,28 @@ | |||
| import EditTag from '@/components/edit-tag'; | |||
| import Divider from '@/components/ui/divider'; | |||
| import { | |||
| Form, | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { | |||
| HoverCard, | |||
| HoverCardContent, | |||
| HoverCardTrigger, | |||
| } from '@/components/ui/hover-card'; | |||
| import { Modal } from '@/components/ui/modal'; | |||
| import Space from '@/components/ui/space'; | |||
| import { Switch } from '@/components/ui/switch'; | |||
| import { Textarea } from '@/components/ui/textarea'; | |||
| import { useFetchChunk } from '@/hooks/chunk-hooks'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { IChunk } from '@/interfaces/database/knowledge'; | |||
| import { DeleteOutlined } from '@ant-design/icons'; | |||
| import { Divider, Form, Input, Modal, Space, Switch } from 'antd'; | |||
| import { Trash2 } from 'lucide-react'; | |||
| import React, { useCallback, useEffect, useState } from 'react'; | |||
| import { FieldValues, FormProvider, useForm } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { useDeleteChunkByIds } from '../../hooks'; | |||
| import { | |||
| @@ -32,28 +50,35 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({ | |||
| loading, | |||
| parserId, | |||
| }) => { | |||
| const [form] = Form.useForm(); | |||
| // const [form] = Form.useForm(); | |||
| // const form = useFormContext(); | |||
| const form = useForm<FieldValues>({ | |||
| defaultValues: { | |||
| content_with_weight: '', | |||
| tag_kwd: [], | |||
| question_kwd: [], | |||
| important_kwd: [], | |||
| tag_feas: [], | |||
| }, | |||
| }); | |||
| const [checked, setChecked] = useState(false); | |||
| const { removeChunk } = useDeleteChunkByIds(); | |||
| const { data } = useFetchChunk(chunkId); | |||
| const { t } = useTranslation(); | |||
| const isTagParser = parserId === 'tag'; | |||
| const handleOk = useCallback(async () => { | |||
| try { | |||
| const values = await form.validateFields(); | |||
| console.log('🚀 ~ handleOk ~ values:', values); | |||
| const onSubmit = useCallback( | |||
| (values: FieldValues) => { | |||
| onOk?.({ | |||
| ...values, | |||
| tag_feas: transformTagFeaturesArrayToObject(values.tag_feas), | |||
| available_int: checked ? 1 : 0, // available_int | |||
| available_int: checked ? 1 : 0, | |||
| }); | |||
| } catch (errorInfo) { | |||
| console.log('Failed:', errorInfo); | |||
| } | |||
| }, [checked, form, onOk]); | |||
| }, | |||
| [checked, onOk], | |||
| ); | |||
| const handleOk = form.handleSubmit(onSubmit); | |||
| const handleRemove = useCallback(() => { | |||
| if (chunkId) { | |||
| @@ -68,8 +93,8 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({ | |||
| useEffect(() => { | |||
| if (data?.code === 0) { | |||
| const { available_int, tag_feas } = data.data; | |||
| form.setFieldsValue({ | |||
| ...(data.data || {}), | |||
| form.reset({ | |||
| ...data.data, | |||
| tag_feas: transformTagFeaturesObjectToArray(tag_feas), | |||
| }); | |||
| @@ -83,54 +108,101 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({ | |||
| open={true} | |||
| onOk={handleOk} | |||
| onCancel={hideModal} | |||
| okButtonProps={{ loading }} | |||
| confirmLoading={loading} | |||
| destroyOnClose | |||
| > | |||
| <Form form={form} autoComplete="off" layout={'vertical'}> | |||
| <Form.Item<FieldType> | |||
| label={t('chunk.chunk')} | |||
| name="content_with_weight" | |||
| rules={[{ required: true, message: t('chunk.chunkMessage') }]} | |||
| > | |||
| <Input.TextArea autoSize={{ minRows: 4, maxRows: 10 }} /> | |||
| </Form.Item> | |||
| <Form {...form}> | |||
| <div className="flex flex-col gap-4"> | |||
| <FormField | |||
| control={form.control} | |||
| name="content_with_weight" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('chunk.chunk')}</FormLabel> | |||
| <FormControl> | |||
| <Textarea {...field} autoSize={{ minRows: 4, maxRows: 10 }} /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="important_kwd" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('chunk.keyword')}</FormLabel> | |||
| <FormControl> | |||
| <EditTag {...field} /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="question_kwd" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel className="flex justify-start items-start"> | |||
| <div className="flex items-center gap-0"> | |||
| <span>{t('chunk.question')}</span> | |||
| <HoverCard> | |||
| <HoverCardTrigger asChild> | |||
| <span className="text-xs mt-[-3px] text-center scale-[90%] font-thin text-primary cursor-pointer rounded-full w-[16px] h-[16px] border-muted-foreground/50 border"> | |||
| ? | |||
| </span> | |||
| </HoverCardTrigger> | |||
| <HoverCardContent className="w-80" side="top"> | |||
| {t('chunk.questionTip')} | |||
| </HoverCardContent> | |||
| </HoverCard> | |||
| </div> | |||
| </FormLabel> | |||
| <FormControl> | |||
| <EditTag {...field} /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <Form.Item<FieldType> label={t('chunk.keyword')} name="important_kwd"> | |||
| <EditTag></EditTag> | |||
| </Form.Item> | |||
| <Form.Item<FieldType> | |||
| label={t('chunk.question')} | |||
| name="question_kwd" | |||
| tooltip={t('chunk.questionTip')} | |||
| > | |||
| <EditTag></EditTag> | |||
| </Form.Item> | |||
| {isTagParser && ( | |||
| <Form.Item<FieldType> | |||
| label={t('knowledgeConfiguration.tagName')} | |||
| name="tag_kwd" | |||
| > | |||
| <EditTag></EditTag> | |||
| </Form.Item> | |||
| )} | |||
| {isTagParser && ( | |||
| <FormField | |||
| control={form.control} | |||
| name="tag_kwd" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('knowledgeConfiguration.tagName')}</FormLabel> | |||
| <FormControl> | |||
| <EditTag {...field} /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| )} | |||
| {!isTagParser && <TagFeatureItem></TagFeatureItem>} | |||
| {!isTagParser && ( | |||
| <FormProvider {...form}> | |||
| <TagFeatureItem /> | |||
| </FormProvider> | |||
| )} | |||
| </div> | |||
| </Form> | |||
| {chunkId && ( | |||
| <section> | |||
| <Divider></Divider> | |||
| <Divider /> | |||
| <Space size={'large'}> | |||
| <Switch | |||
| checkedChildren={t('chunk.enabled')} | |||
| unCheckedChildren={t('chunk.disabled')} | |||
| onChange={handleCheck} | |||
| checked={checked} | |||
| /> | |||
| <span onClick={handleRemove}> | |||
| <DeleteOutlined /> {t('common.delete')} | |||
| </span> | |||
| <div className="flex items-center gap-2"> | |||
| {t('chunk.enabled')} | |||
| <Switch checked={checked} onCheckedChange={handleCheck} /> | |||
| </div> | |||
| <div className="flex items-center gap-1" onClick={handleRemove}> | |||
| <Trash2 size={16} /> {t('common.delete')} | |||
| </div> | |||
| </Space> | |||
| </section> | |||
| )} | |||
| @@ -1,22 +1,28 @@ | |||
| import { SelectWithSearch } from '@/components/originui/select-with-search'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { | |||
| useFetchKnowledgeBaseConfiguration, | |||
| useFetchTagListByKnowledgeIds, | |||
| } from '@/hooks/knowledge-hooks'; | |||
| import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; | |||
| import { Button, Form, InputNumber, Select } from 'antd'; | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { NumberInput } from '@/components/ui/input'; | |||
| import { useFetchTagListByKnowledgeIds } from '@/hooks/knowledge-hooks'; | |||
| import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request'; | |||
| import { CircleMinus, Plus } from 'lucide-react'; | |||
| import { useCallback, useEffect, useMemo } from 'react'; | |||
| import { useFieldArray, useFormContext } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { FormListItem } from '../../utils'; | |||
| const FieldKey = 'tag_feas'; | |||
| export const TagFeatureItem = () => { | |||
| const form = Form.useFormInstance(); | |||
| const { t } = useTranslation(); | |||
| const { data: knowledgeConfiguration } = useFetchKnowledgeBaseConfiguration(); | |||
| const { setKnowledgeIds, list } = useFetchTagListByKnowledgeIds(); | |||
| const { data: knowledgeConfiguration } = useFetchKnowledgeBaseConfiguration(); | |||
| const form = useFormContext(); | |||
| const tagKnowledgeIds = useMemo(() => { | |||
| return knowledgeConfiguration?.parser_config?.tag_kb_ids ?? []; | |||
| }, [knowledgeConfiguration?.parser_config?.tag_kb_ids]); | |||
| @@ -30,15 +36,17 @@ export const TagFeatureItem = () => { | |||
| const filterOptions = useCallback( | |||
| (index: number) => { | |||
| const tags: FormListItem[] = form.getFieldValue(FieldKey) ?? []; | |||
| const tags: FormListItem[] = form.getValues(FieldKey) ?? []; | |||
| // Exclude it's own current data | |||
| const list = tags | |||
| .filter((x, idx) => x && index !== idx) | |||
| .map((x) => x.tag); | |||
| // Exclude the selected data from other options from one's own options. | |||
| return options.filter((x) => !list.some((y) => x.value === y)); | |||
| const resultList = options.filter( | |||
| (x) => !list.some((y) => x.value === y), | |||
| ); | |||
| return resultList; | |||
| }, | |||
| [form, options], | |||
| ); | |||
| @@ -47,61 +55,82 @@ export const TagFeatureItem = () => { | |||
| setKnowledgeIds(tagKnowledgeIds); | |||
| }, [setKnowledgeIds, tagKnowledgeIds]); | |||
| const { fields, append, remove } = useFieldArray({ | |||
| control: form.control, | |||
| name: FieldKey, | |||
| }); | |||
| return ( | |||
| <Form.Item label={t('knowledgeConfiguration.tags')}> | |||
| <Form.List name={FieldKey} initialValue={[]}> | |||
| {(fields, { add, remove }) => ( | |||
| <> | |||
| {fields.map(({ key, name, ...restField }) => ( | |||
| <div key={key} className="flex gap-3 items-center"> | |||
| <div className="flex flex-1 gap-8"> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'tag']} | |||
| rules={[ | |||
| { required: true, message: t('common.pleaseSelect') }, | |||
| ]} | |||
| className="w-2/3" | |||
| > | |||
| <Select | |||
| showSearch | |||
| placeholder={t('knowledgeConfiguration.tagName')} | |||
| options={filterOptions(name)} | |||
| <FormField | |||
| control={form.control} | |||
| name={FieldKey as any} | |||
| render={() => ( | |||
| <FormItem> | |||
| <FormLabel>{t('knowledgeConfiguration.tags')}</FormLabel> | |||
| <div> | |||
| {fields.map((item, name) => { | |||
| return ( | |||
| <div key={item.id} className="flex gap-3 items-center mb-4"> | |||
| <div className="flex flex-1 gap-8"> | |||
| <FormField | |||
| control={form.control} | |||
| name={`${FieldKey}.${name}.tag` as any} | |||
| render={({ field }) => ( | |||
| <FormItem className="w-2/3"> | |||
| <FormControl className="w-full"> | |||
| <div> | |||
| <SelectWithSearch | |||
| options={filterOptions(name)} | |||
| placeholder={t( | |||
| 'knowledgeConfiguration.tagName', | |||
| )} | |||
| value={field.value} | |||
| onChange={field.onChange} | |||
| /> | |||
| </div> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'frequency']} | |||
| rules={[ | |||
| { required: true, message: t('common.pleaseInput') }, | |||
| ]} | |||
| > | |||
| <InputNumber | |||
| placeholder={t('knowledgeConfiguration.frequency')} | |||
| max={10} | |||
| min={0} | |||
| <FormField | |||
| control={form.control} | |||
| name={`${FieldKey}.${name}.frequency`} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormControl> | |||
| <NumberInput | |||
| value={field.value} | |||
| onChange={field.onChange} | |||
| placeholder={t( | |||
| 'knowledgeConfiguration.frequency', | |||
| )} | |||
| max={10} | |||
| min={0} | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| </Form.Item> | |||
| </div> | |||
| <CircleMinus | |||
| onClick={() => remove(name)} | |||
| className="text-red-500" | |||
| /> | |||
| </div> | |||
| <MinusCircleOutlined | |||
| onClick={() => remove(name)} | |||
| className="mb-6" | |||
| /> | |||
| </div> | |||
| ))} | |||
| <Form.Item> | |||
| <Button | |||
| type="dashed" | |||
| onClick={() => add()} | |||
| block | |||
| icon={<PlusOutlined />} | |||
| > | |||
| {t('knowledgeConfiguration.addTag')} | |||
| </Button> | |||
| </Form.Item> | |||
| </> | |||
| )} | |||
| </Form.List> | |||
| </Form.Item> | |||
| ); | |||
| })} | |||
| <Button | |||
| variant="dashed" | |||
| className="w-full flex items-center justify-center gap-2" | |||
| onClick={() => append({ tag: '', frequency: 0 })} | |||
| > | |||
| <Plus size={16} /> | |||
| {t('knowledgeConfiguration.addTag')} | |||
| </Button> | |||
| </div> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| ); | |||
| }; | |||
| @@ -61,12 +61,12 @@ export default ({ | |||
| }; | |||
| return ( | |||
| <div className="flex pr-[25px]"> | |||
| <div className="flex items-center gap-4 bg-card text-muted-foreground w-fit h-[35px] rounded-md px-4 py-2 text-base"> | |||
| <div className="flex items-center gap-4 bg-background-card text-muted w-fit h-[35px] rounded-md px-4 py-2 text-base"> | |||
| {textSelectOptions.map((option) => ( | |||
| <div | |||
| key={option.value} | |||
| className={cn('flex items-center cursor-pointer', { | |||
| 'text-white': option.value === textSelectValue, | |||
| 'text-primary': option.value === textSelectValue, | |||
| })} | |||
| onClick={() => changeTextSelectValue(option.value)} | |||
| > | |||
| @@ -76,7 +76,7 @@ export default ({ | |||
| </div> | |||
| <div className="ml-auto"></div> | |||
| <Input | |||
| className="bg-card text-muted-foreground" | |||
| className="bg-background-card text-muted-foreground" | |||
| style={{ width: 200 }} | |||
| placeholder={t('search')} | |||
| icon={<SearchOutlined />} | |||
| @@ -86,7 +86,7 @@ export default ({ | |||
| <div className="w-[20px]"></div> | |||
| <Popover> | |||
| <PopoverTrigger asChild> | |||
| <Button className="bg-card text-muted-foreground hover:bg-card"> | |||
| <Button className="bg-background-card text-muted-foreground hover:bg-card"> | |||
| <ListFilter /> | |||
| </Button> | |||
| </PopoverTrigger> | |||
| @@ -95,7 +95,10 @@ export default ({ | |||
| </PopoverContent> | |||
| </Popover> | |||
| <div className="w-[20px]"></div> | |||
| <Button onClick={() => createChunk()}> | |||
| <Button | |||
| onClick={() => createChunk()} | |||
| className="bg-background-card text-primary" | |||
| > | |||
| <Plus size={44} /> | |||
| </Button> | |||
| </div> | |||
| @@ -1,4 +1,3 @@ | |||
| import { Skeleton } from 'antd'; | |||
| import { memo, useEffect, useRef } from 'react'; | |||
| import { | |||
| AreaHighlight, | |||
| @@ -11,6 +10,7 @@ import { | |||
| import { useGetDocumentUrl } from './hooks'; | |||
| import { useCatchDocumentError } from '@/components/pdf-previewer/hooks'; | |||
| import { Spin } from '@/components/ui/spin'; | |||
| import FileError from '@/pages/document-viewer/file-error'; | |||
| import styles from './index.less'; | |||
| @@ -50,7 +50,11 @@ const PdfPreview = ({ highlights: state, setWidthAndHeight }: IProps) => { | |||
| > | |||
| <PdfLoader | |||
| url={url} | |||
| beforeLoad={<Skeleton active />} | |||
| beforeLoad={ | |||
| <div className="absolute inset-0 flex items-center justify-center"> | |||
| <Spin /> | |||
| </div> | |||
| } | |||
| workerSrc="/pdfjs-dist/pdf.worker.min.js" | |||
| errorMessage={<FileError>{error}</FileError>} | |||
| > | |||
| @@ -1,6 +1,6 @@ | |||
| import message from '@/components/ui/message'; | |||
| import { Spin } from '@/components/ui/spin'; | |||
| import request from '@/utils/request'; | |||
| import { Spin } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useGetDocumentUrl } from './hooks'; | |||
| @@ -41,8 +41,7 @@ | |||
| .chunkContainer { | |||
| display: flex; | |||
| // height: calc(100vh - 332px); | |||
| height: calc(100vh - 300px); | |||
| height: calc(100vh - 332px); | |||
| } | |||
| .chunkOtherContainer { | |||
| @@ -61,7 +61,7 @@ export function ParsingStatusCell({ | |||
| <DropdownMenu> | |||
| <DropdownMenuTrigger asChild> | |||
| <Button variant={'ghost'} size={'sm'}> | |||
| {parser_id} | |||
| {parser_id === 'naive' ? 'general' : parser_id} | |||
| </Button> | |||
| </DropdownMenuTrigger> | |||
| <DropdownMenuContent> | |||
| @@ -221,79 +221,77 @@ export function TagTable() { | |||
| </ConfirmDeleteDialog> | |||
| )} | |||
| </div> | |||
| <div className="rounded-md border"> | |||
| <Table> | |||
| <TableHeader> | |||
| {table.getHeaderGroups().map((headerGroup) => ( | |||
| <TableRow key={headerGroup.id}> | |||
| {headerGroup.headers.map((header) => { | |||
| return ( | |||
| <TableHead key={header.id}> | |||
| {header.isPlaceholder | |||
| ? null | |||
| : flexRender( | |||
| header.column.columnDef.header, | |||
| header.getContext(), | |||
| )} | |||
| </TableHead> | |||
| ); | |||
| })} | |||
| </TableRow> | |||
| ))} | |||
| </TableHeader> | |||
| <TableBody> | |||
| {table.getRowModel().rows?.length ? ( | |||
| table.getRowModel().rows.map((row) => ( | |||
| <TableRow | |||
| key={row.id} | |||
| data-state={row.getIsSelected() && 'selected'} | |||
| > | |||
| {row.getVisibleCells().map((cell) => ( | |||
| <TableCell key={cell.id}> | |||
| {flexRender( | |||
| cell.column.columnDef.cell, | |||
| cell.getContext(), | |||
| )} | |||
| </TableCell> | |||
| ))} | |||
| </TableRow> | |||
| )) | |||
| ) : ( | |||
| <TableRow> | |||
| <TableCell | |||
| colSpan={columns.length} | |||
| className="h-24 text-center" | |||
| > | |||
| No results. | |||
| </TableCell> | |||
| <Table rootClassName="rounded-none border max-h-80 overflow-y-auto"> | |||
| <TableHeader className="bg-[#39393b]"> | |||
| {table.getHeaderGroups().map((headerGroup) => ( | |||
| <TableRow key={headerGroup.id}> | |||
| {headerGroup.headers.map((header) => { | |||
| return ( | |||
| <TableHead key={header.id}> | |||
| {header.isPlaceholder | |||
| ? null | |||
| : flexRender( | |||
| header.column.columnDef.header, | |||
| header.getContext(), | |||
| )} | |||
| </TableHead> | |||
| ); | |||
| })} | |||
| </TableRow> | |||
| ))} | |||
| </TableHeader> | |||
| <TableBody> | |||
| {table.getRowModel().rows?.length ? ( | |||
| table.getRowModel().rows.map((row) => ( | |||
| <TableRow | |||
| key={row.id} | |||
| data-state={row.getIsSelected() && 'selected'} | |||
| > | |||
| {row.getVisibleCells().map((cell) => ( | |||
| <TableCell key={cell.id}> | |||
| {flexRender( | |||
| cell.column.columnDef.cell, | |||
| cell.getContext(), | |||
| )} | |||
| </TableCell> | |||
| ))} | |||
| </TableRow> | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| )) | |||
| ) : ( | |||
| <TableRow> | |||
| <TableCell | |||
| colSpan={columns.length} | |||
| className="h-24 text-center" | |||
| > | |||
| No results. | |||
| </TableCell> | |||
| </TableRow> | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| </div> | |||
| <div className="flex items-center justify-end space-x-2 py-4"> | |||
| <div className="flex-1 text-sm text-muted-foreground"> | |||
| {selectedRowLength} of {table.getFilteredRowModel().rows.length}{' '} | |||
| row(s) selected. | |||
| </div> | |||
| <div className="flex items-center justify-end space-x-2 py-4"> | |||
| <div className="flex-1 text-sm text-muted-foreground"> | |||
| {selectedRowLength} of {table.getFilteredRowModel().rows.length}{' '} | |||
| row(s) selected. | |||
| </div> | |||
| <div className="space-x-2"> | |||
| <Button | |||
| variant="outline" | |||
| size="sm" | |||
| onClick={() => table.previousPage()} | |||
| disabled={!table.getCanPreviousPage()} | |||
| > | |||
| {t('common.previousPage')} | |||
| </Button> | |||
| <Button | |||
| variant="outline" | |||
| size="sm" | |||
| onClick={() => table.nextPage()} | |||
| disabled={!table.getCanNextPage()} | |||
| > | |||
| {t('common.nextPage')} | |||
| </Button> | |||
| </div> | |||
| <div className="space-x-2"> | |||
| <Button | |||
| variant="outline" | |||
| size="sm" | |||
| onClick={() => table.previousPage()} | |||
| disabled={!table.getCanPreviousPage()} | |||
| > | |||
| {t('common.previousPage')} | |||
| </Button> | |||
| <Button | |||
| variant="outline" | |||
| size="sm" | |||
| onClick={() => table.nextPage()} | |||
| disabled={!table.getCanNextPage()} | |||
| > | |||
| {t('common.nextPage')} | |||
| </Button> | |||
| </div> | |||
| </div> | |||
| {tagRenameVisible && ( | |||
| @@ -2,12 +2,12 @@ import { Images } from '@/constants/common'; | |||
| import { api_host } from '@/utils/api'; | |||
| import { Flex } from 'antd'; | |||
| import { useParams, useSearchParams } from 'umi'; | |||
| import Md from './md'; | |||
| import Text from './text'; | |||
| import Docx from './docx'; | |||
| import Excel from './excel'; | |||
| import Image from './image'; | |||
| import Md from './md'; | |||
| import Pdf from './pdf'; | |||
| import Text from './text'; | |||
| import { previewHtmlFile } from '@/utils/file-util'; | |||
| import styles from './index.less'; | |||
| @@ -19,13 +19,13 @@ const Md: React.FC<MdProps> = ({ filePath }) => { | |||
| return res.text(); | |||
| }) | |||
| .then((text) => setContent(text)) | |||
| .catch((err) => setError(err.message)) | |||
| .catch((err) => setError(err.message)); | |||
| }, [filePath]); | |||
| if (error) return (<FileError>{error}</FileError>); | |||
| if (error) return <FileError>{error}</FileError>; | |||
| return ( | |||
| <div style={{ padding: 24, height: "100vh", overflow: "scroll" }}> | |||
| <div style={{ padding: 24, height: '100vh', overflow: 'scroll' }}> | |||
| <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown> | |||
| </div> | |||
| ); | |||
| @@ -17,14 +17,14 @@ const Md: React.FC<TxtProps> = ({ filePath }) => { | |||
| return res.text(); | |||
| }) | |||
| .then((text) => setContent(text)) | |||
| .catch((err) => setError(err.message)) | |||
| .catch((err) => setError(err.message)); | |||
| }, [filePath]); | |||
| if (error) return (<FileError>{error}</FileError>); | |||
| if (error) return <FileError>{error}</FileError>; | |||
| return ( | |||
| <div style={{ padding: 24, height: "100vh", overflow: "scroll" }}> | |||
| {content} | |||
| <div style={{ padding: 24, height: '100vh', overflow: 'scroll' }}> | |||
| {content} | |||
| </div> | |||
| ); | |||
| }; | |||