diff --git a/web/src/components/edit-tag/index.less b/web/src/components/edit-tag/index.less deleted file mode 100644 index 1c4314cfb..000000000 --- a/web/src/components/edit-tag/index.less +++ /dev/null @@ -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; - } -} diff --git a/web/src/components/edit-tag/index.tsx b/web/src/components/edit-tag/index.tsx index 676ba7ff9..44bea7296 100644 --- a/web/src/components/edit-tag/index.tsx +++ b/web/src/components/edit-tag/index.tsx @@ -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(null); + const inputRef = useRef(null); useEffect(() => { if (inputVisible) { @@ -50,34 +53,66 @@ const EditTag = ({ value = [], onChange }: EditTagsProps) => { const forMap = (tag: string) => { return ( - - { - e.preventDefault(); - handleClose(tag); - }} - > - {tag} - - + + {tag} + +
+
+
+ {tag} +
+ { + e.preventDefault(); + handleClose(tag); + }} + /> +
+
+
+
); }; const tagChild = value?.map(forMap); const tagPlusStyle: React.CSSProperties = { - background: token.colorBgContainer, borderStyle: 'dashed', }; return (
+ {inputVisible ? ( + { + if (e?.key === 'Enter') { + handleInputConfirm(); + } + }} + /> + ) : ( + + )} {Array.isArray(tagChild) && tagChild.length > 0 && ( { {tagChild} )} - {inputVisible ? ( - - ) : ( - - - - )}
); }; diff --git a/web/src/components/image/index.less b/web/src/components/image/index.less deleted file mode 100644 index 6aff0bd43..000000000 --- a/web/src/components/image/index.less +++ /dev/null @@ -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; -} diff --git a/web/src/components/image/index.tsx b/web/src/components/image/index.tsx index 14931ec95..020a910ad 100644 --- a/web/src/components/image/index.tsx +++ b/web/src/components/image/index.tsx @@ -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 ( - } - > - + + + + + + + ); }; diff --git a/web/src/components/originui/input.tsx b/web/src/components/originui/input.tsx index 18cf78f97..f048aa79c 100644 --- a/web/src/components/originui/input.tsx +++ b/web/src/components/originui/input.tsx @@ -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({ )} ); -} +}; export { Input }; diff --git a/web/src/components/originui/select-with-search.tsx b/web/src/components/originui/select-with-search.tsx index 7006fc0b5..0362d3e35 100644 --- a/web/src/components/originui/select-with-search.tsx +++ b/web/src/components/originui/select-with-search.tsx @@ -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, SelectWithSearchFlagProps ->( - ( - { value: val = '', onChange, options = countries, triggerClassName }, - ref, - ) => { - const id = useId(); - const [open, setOpen] = useState(false); - const [value, setValue] = useState(''); +>(({ value: val = '', onChange, options = [], triggerClassName }, ref) => { + const id = useId(); + const [open, setOpen] = useState(false); + const [value, setValue] = useState(''); - const handleSelect = useCallback( - (val: string) => { - setValue(val); - setOpen(false); - onChange?.(val); - }, - [onChange], - ); - - useEffect(() => { + const handleSelect = useCallback( + (val: string) => { setValue(val); - }, [val]); + setOpen(false); + onChange?.(val); + }, + [onChange], + ); - return ( - - - - - { + 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 ( + + + + + + + + + No data found. + {options.map((group) => { + if (group.options) { + return ( + + + {group.options.map((option) => ( + + + {option.label} + - {value === option.value && ( - - )} - - ))} - - - ))} - - - - - ); - }, -); + {value === option.value && ( + + )} + + ))} + + + ); + } else { + return ( + + {group.label} + + {value === group.value && ( + + )} + + ); + } + })} + + + + + ); +}); SelectWithSearch.displayName = 'SelectWithSearch'; diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx index b6bb54c2e..7a9cf053f 100644 --- a/web/src/components/ui/button.tsx +++ b/web/src/components/ui/button.tsx @@ -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( const Comp = asChild ? Slot : 'button'; return ( diff --git a/web/src/components/ui/divider.tsx b/web/src/components/ui/divider.tsx new file mode 100644 index 000000000..e887c0639 --- /dev/null +++ b/web/src/components/ui/divider.tsx @@ -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 = ({ + 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 ( +
+ {type === 'text' && ( +
+ {text} +
+ )} +
+ ); + } + + // Horizontal with text + if (type === 'text') { + return ( +
+
+
{text}
+
+
+ ); + } + + // Default horizontal + return ( +
+ ); +}; + +export default Divider; diff --git a/web/src/components/ui/hover-card.tsx b/web/src/components/ui/hover-card.tsx index c17abbcdd..fc14b40e4 100644 --- a/web/src/components/ui/hover-card.tsx +++ b/web/src/components/ui/hover-card.tsx @@ -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} diff --git a/web/src/components/ui/modal.tsx b/web/src/components/ui/modal.tsx new file mode 100644 index 000000000..4962fb8ab --- /dev/null +++ b/web/src/components/ui/modal.tsx @@ -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 = ({ + open, + onOpenChange, + title, + children, + footer, + className = '', + size = 'default', + closable = true, + closeIcon = , + 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 = ( +
+ + +
+ ); + return ( +
+ {footerTemp} +
+ ); + } + }, [footer, cancelText, t, confirmLoading, okText, handleCancel, handleOk]); + return ( + + + maskClosable && onOpenChange?.(false)} + > + e.stopPropagation()} + > + {/* title */} + {title && ( +
+ + {title} + + {closable && ( + + + + )} +
+ )} + + {/* content */} +
+ {destroyOnClose && !open ? null : children} +
+ + {/* footer */} + {footEl} +
+
+
+
+ ); +}; + +// example usage +/* +import { Modal } from '@/components/ui/modal'; + +function Demo() { + const [open, setOpen] = useState(false); + + return ( +
+ + + + + +
+ } + > +
弹窗内容区域
+ + +
弹窗内容区域
+
+
+ ); +} +*/ diff --git a/web/src/components/ui/space.tsx b/web/src/components/ui/space.tsx new file mode 100644 index 000000000..a9ffe55eb --- /dev/null +++ b/web/src/components/ui/space.tsx @@ -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 = { + small: 'gap-2', + middle: 'gap-4', + large: 'gap-8', +}; + +const directionClasses: Record = { + horizontal: 'flex-row', + vertical: 'flex-col', +}; + +const Space: React.FC = ({ + 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
{children}
; +}; + +export default Space; diff --git a/web/src/components/ui/textarea.tsx b/web/src/components/ui/textarea.tsx index f42863caf..e40076e94 100644 --- a/web/src/components/ui/textarea.tsx +++ b/web/src/components/ui/textarea.tsx @@ -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, 'autoSize'> { + autoSize?: { + minRows?: number; + maxRows?: number; + }; +} +const Textarea = forwardRef( + ({ className, autoSize, ...props }, ref) => { + const textareaRef = useRef(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 ( -