Fix: Modify the style of the user center #10703 (#11419)

### What problem does this PR solve?

Fix: Modify the style of the user center

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
chanx
2025-11-21 09:33:50 +08:00
committed by GitHub
parent 971c1bcba7
commit 653b785958
27 changed files with 122 additions and 897 deletions

View File

@ -29,7 +29,10 @@ const BackButton: React.FC<BackButtonProps> = ({
return (
<Button
variant="ghost"
className={cn('gap-2 bg-bg-card border border-border-default', className)}
className={cn(
'gap-2 bg-bg-card border border-border-default hover:bg-border-button hover:text-text-primary',
className,
)}
onClick={handleClick}
{...props}
>

View File

@ -44,6 +44,7 @@ export function ConfirmDeleteDialog({
<AlertDialogContent
onSelect={(e) => e.preventDefault()}
onClick={(e) => e.stopPropagation()}
className="bg-bg-base"
>
<AlertDialogHeader>
<AlertDialogTitle>
@ -59,7 +60,7 @@ export function ConfirmDeleteDialog({
{t('common.no')}
</AlertDialogCancel>
<AlertDialogAction
className="bg-state-error text-text-primary"
className="bg-state-error text-text-primary hover:text-text-primary hover:bg-state-error"
onClick={onOk}
>
{t('common.yes')}

View File

@ -68,6 +68,7 @@ export interface FormFieldConfig {
dependencies?: string[];
schema?: ZodSchema;
shouldRender?: (formValues: any) => boolean;
labelClassName?: string;
}
// Component props interface
@ -81,6 +82,7 @@ interface DynamicFormProps<T extends FieldValues> {
fieldName: string,
updatedField: Partial<FormFieldConfig>,
) => void;
labelClassName?: string;
}
// Form ref interface
@ -295,6 +297,7 @@ const DynamicForm = {
children,
defaultValues: formDefaultValues = {} as DefaultValues<T>,
onFieldUpdate,
labelClassName,
}: DynamicFormProps<T>,
ref: React.Ref<any>,
) => {
@ -456,6 +459,7 @@ const DynamicForm = {
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
@ -481,6 +485,7 @@ const DynamicForm = {
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
@ -511,6 +516,7 @@ const DynamicForm = {
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
@ -551,7 +557,10 @@ const DynamicForm = {
{field.label && !field.horizontal && (
<div className="space-y-1 leading-none">
<FormLabel
className="font-normal"
className={cn(
'font-medium',
labelClassName || field.labelClassName,
)}
tooltip={field.tooltip}
>
{field.label}{' '}
@ -564,7 +573,10 @@ const DynamicForm = {
{field.label && field.horizontal && (
<div className="space-y-1 leading-none w-1/4">
<FormLabel
className="font-normal"
className={cn(
'font-medium',
labelClassName || field.labelClassName,
)}
tooltip={field.tooltip}
>
{field.label}{' '}
@ -600,6 +612,7 @@ const DynamicForm = {
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
@ -629,6 +642,7 @@ const DynamicForm = {
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
@ -748,7 +762,7 @@ const DynamicForm = {
<button
type="button"
onClick={() => handleCancel()}
className="px-2 py-1 border border-input rounded-md hover:bg-muted"
className="px-2 py-1 border border-border-button rounded-md text-text-secondary hover:bg-bg-card hover:text-primary"
>
{cancelText ?? t('modal.cancelText')}
</button>

View File

@ -102,8 +102,8 @@ const EditTag = React.forwardRef<HTMLDivElement, EditTagsProps>(
{Array.isArray(tagChild) && tagChild.length > 0 && <>{tagChild}</>}
{!inputVisible && (
<Button
variant="dashed"
className="w-fit flex items-center justify-center gap-2 bg-bg-card"
variant="ghost"
className="w-fit flex items-center justify-center gap-2 bg-bg-card border-dashed border"
onClick={showInput}
style={tagPlusStyle}
>

View File

@ -272,7 +272,7 @@ export function FileUploader(props: FileUploaderProps) {
<div
{...getRootProps()}
className={cn(
'group relative grid h-72 w-full cursor-pointer place-items-center rounded-lg border-2 border-dashed border-border-default px-5 py-2.5 text-center transition hover:bg-muted/25 bg-accent-primary-5',
'group relative grid h-72 w-full cursor-pointer place-items-center rounded-lg border border-dashed border-border-default px-5 py-2.5 text-center transition hover:bg-border-button bg-bg-card',
'ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
isDragActive && 'border-muted-foreground/50',
isDisabled && 'pointer-events-none opacity-60',
@ -285,11 +285,11 @@ export function FileUploader(props: FileUploaderProps) {
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
<div className="rounded-full border border-dashed p-3">
<Upload
className="size-7 text-muted-foreground"
className="size-7 text-text-secondary"
aria-hidden="true"
/>
</div>
<p className="font-medium text-muted-foreground">
<p className="font-medium text-text-secondary">
Drop the files here
</p>
</div>
@ -297,15 +297,15 @@ export function FileUploader(props: FileUploaderProps) {
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
<div className="rounded-full border border-dashed p-3">
<Upload
className="size-7 text-muted-foreground"
className="size-7 text-text-secondary"
aria-hidden="true"
/>
</div>
<div className="flex flex-col gap-px">
<p className="font-medium text-muted-foreground">
<p className="font-medium text-text-secondary">
{t('knowledgeDetails.uploadTitle')}
</p>
<p className="text-sm text-text-secondary">
<p className="text-sm text-text-disabled">
{description || t('knowledgeDetails.uploadDescription')}
{/* You can upload
{maxFileCount > 1

View File

@ -24,20 +24,6 @@ export default React.forwardRef<HTMLInputElement, InputProps>(
ref={ref}
{...props}
/>
<button
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
type="button"
onClick={toggleVisibility}
aria-label={isVisible ? 'Hide password' : 'Show password'}
aria-pressed={isVisible}
aria-controls="password"
>
{/* {isVisible ? (
<EyeOffIcon size={16} aria-hidden="true" />
) : (
<EyeIcon size={16} aria-hidden="true" />
)} */}
</button>
</div>
</div>
);

View File

@ -140,7 +140,7 @@ export const SelectWithSearch = forwardRef<
ref={ref}
disabled={disabled}
className={cn(
'!bg-bg-input hover:bg-background border-border-button w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px] [&_svg]:pointer-events-auto',
'!bg-bg-input hover:bg-background border-border-button w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px] [&_svg]:pointer-events-auto group',
triggerClassName,
)}
>
@ -155,12 +155,12 @@ export const SelectWithSearch = forwardRef<
{value && allowClear && (
<>
<XIcon
className="h-4 mx-2 cursor-pointer text-text-disabled"
className="h-4 mx-2 cursor-pointer text-text-disabled hidden group-hover:block"
onClick={handleClear}
/>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
className=" min-h-6 h-full hidden group-hover:flex"
/>
</>
)}
@ -173,12 +173,17 @@ export const SelectWithSearch = forwardRef<
</Button>
</PopoverTrigger>
<PopoverContent
className="border-input w-full min-w-[var(--radix-popper-anchor-width)] p-0"
className="border-border-button w-full min-w-[var(--radix-popper-anchor-width)] p-0"
align="start"
>
<Command>
<CommandInput placeholder={t('common.search') + '...'} />
<CommandList>
<Command className="p-5">
{options && options.length > 0 && (
<CommandInput
placeholder={t('common.search') + '...'}
className=" placeholder:text-text-disabled"
/>
)}
<CommandList className="mt-2">
<CommandEmpty>{t('common.noDataFound')}</CommandEmpty>
{options.map((group, idx) => {
if (group.options) {
@ -209,6 +214,7 @@ export const SelectWithSearch = forwardRef<
value={group.value}
disabled={group.disabled}
onSelect={handleSelect}
className="min-h-10"
>
<span className="leading-none">{group.label}</span>

View File

@ -39,7 +39,10 @@ const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" data-cmdk-input-wrapper="">
<div
className="flex items-center border rounded-md border-border-default bg-bg-input px-3"
data-cmdk-input-wrapper=""
>
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
@ -66,7 +69,10 @@ const CommandList = React.forwardRef<
*/
<CommandPrimitive.List
ref={ref}
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
className={cn(
'max-h-[300px] overflow-y-auto overflow-x-hidden scrollbar-auto',
className,
)}
onWheel={(e) => e.stopPropagation()}
onMouseEnter={(e) => e.currentTarget.focus()}
tabIndex={-1}
@ -96,7 +102,7 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group
ref={ref}
className={cn(
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-text-secondary',
className,
)}
{...props}

View File

@ -3,7 +3,6 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
import { Eye, EyeOff, Search } from 'lucide-react';
import { useState } from 'react';
import { Button } from './button';
export interface InputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
@ -50,6 +49,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
'pr-12': !!suffix || isPasswordInput,
'pr-24': !!suffix && isPasswordInput,
},
type === 'number' &&
'[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
className,
)}
value={inputValue ?? ''}
@ -77,10 +78,10 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
</span>
)}
{isPasswordInput && (
<Button
variant="transparent"
<button
type="button"
className="
p-2 text-text-secondary
absolute border-0 right-1 top-[50%] translate-y-[-50%]
dark:peer-autofill/input:text-text-secondary-inverse
dark:peer-autofill/input:hover:text-text-primary-inverse
@ -93,7 +94,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
) : (
<Eye className="size-[1em]" />
)}
</Button>
</button>
)}
</div>
);

View File

@ -27,7 +27,10 @@ export interface ModalProps {
okText?: ReactNode | string;
onOk?: () => void;
onCancel?: () => void;
okButtonClassName?: string;
cancelButtonClassName?: string;
disabled?: boolean;
style?: React.CSSProperties;
}
export interface ModalType extends FC<ModalProps> {
show: typeof modalIns.show;
@ -56,7 +59,10 @@ const Modal: ModalType = ({
confirmLoading,
cancelText,
okText,
okButtonClassName,
cancelButtonClassName,
disabled = false,
style,
}) => {
const sizeClasses = {
small: 'max-w-md',
@ -111,7 +117,10 @@ const Modal: ModalType = ({
<button
type="button"
onClick={() => handleCancel()}
className="px-2 py-1 border border-border-button rounded-md hover:bg-bg-card hover:text-text-primary "
className={cn(
'px-2 py-1 border border-border-button rounded-md hover:bg-bg-card hover:text-text-primary ',
cancelButtonClassName,
)}
>
{cancelText ?? t('modal.cancelText')}
</button>
@ -122,6 +131,7 @@ const Modal: ModalType = ({
className={cn(
'px-2 py-1 bg-primary text-primary-foreground rounded-md hover:bg-primary/90',
{ 'cursor-not-allowed': disabled },
okButtonClassName,
)}
>
{confirmLoading && (
@ -153,23 +163,26 @@ const Modal: ModalType = ({
handleOk,
showfooter,
footerClassName,
okButtonClassName,
cancelButtonClassName,
]);
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"
className="fixed inset-0 z-50 bg-bg-card 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-bg-base rounded-lg shadow-lg border border-border-default transition-all focus-visible:!outline-none`}
style={style}
onClick={(e) => e.stopPropagation()}
>
{/* title */}
{(title || closable) && (
<div
className={cn(
'flex items-center px-6 py-4',
'flex items-start px-6 py-4',
{
'justify-end': closable && !title,
'justify-between': closable && title,
@ -187,7 +200,7 @@ const Modal: ModalType = ({
<DialogPrimitive.Close asChild>
<button
type="button"
className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted focus-visible:outline-none"
className="flex h-7 w-7 items-center justify-center text-text-secondary rounded-full hover:bg-bg-card focus-visible:outline-none"
onClick={handleCancel}
>
{closeIcon}
@ -198,7 +211,7 @@ const Modal: ModalType = ({
)}
{/* content */}
<div className="py-2 px-6 overflow-y-auto scrollbar-auto max-h-[80vh] focus-visible:!outline-none">
<div className="py-2 px-6 overflow-y-auto scrollbar-auto max-h-[calc(100vh-280px)] focus-visible:!outline-none">
{destroyOnClose && !open ? null : children}
</div>

View File

@ -378,12 +378,12 @@ export const MultiSelect = React.forwardRef<
align="start"
onEscapeKeyDown={() => setIsPopoverOpen(false)}
>
<Command>
<Command className="p-5 pb-8">
<CommandInput
placeholder={t('common.search') + '...'}
onKeyDown={handleInputKeyDown}
/>
<CommandList>
<CommandList className="mt-2">
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{showSelectAll && (
@ -437,9 +437,9 @@ export const MultiSelect = React.forwardRef<
})}
</CommandGroup>
))}
<CommandSeparator />
<CommandGroup>
<div className="flex items-center justify-between">
<div className=" absolute bottom-1 left-1 right-1 flex items-center justify-between mx-5 bg-bg-base border-t border-border-button">
<CommandSeparator />
{selectedValues.length > 0 && (
<>
<CommandItem

View File

@ -59,7 +59,7 @@ const SelectScrollUpButton = React.forwardRef<
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1',
'flex cursor-default items-center justify-center py-1 text-text-secondary hover:text-text-primary',
className,
)}
{...props}
@ -76,7 +76,7 @@ const SelectScrollDownButton = React.forwardRef<
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1',
'flex cursor-default items-center justify-center py-1 text-text-secondary hover:text-text-primary',
className,
)}
{...props}

View File

@ -54,7 +54,7 @@ const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
'flex min-h-[80px] w-full bg-bg-input rounded-md border border-input px-3 py-2 text-base ring-offset-background placeholder:text-text-disabled focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm overflow-hidden',
'flex min-h-[80px] w-full bg-bg-input rounded-md border border-border-button px-3 py-2 text-base ring-offset-background placeholder:text-text-disabled focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm overflow-hidden',
className,
)}
rows={autoSize?.minRows ?? props.rows ?? undefined}

View File

@ -20,7 +20,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-auto scrollbar-auto rounded-md whitespace-pre-wrap border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 max-w-[30vw]',
'z-50 overflow-auto scrollbar-auto rounded-md whitespace-pre-wrap border bg-bg-base px-3 py-1.5 text-sm text-text-primary shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 max-w-[30vw]',
className,
)}
{...props}
@ -39,7 +39,7 @@ export const FormTooltip = ({ tooltip }: { tooltip: React.ReactNode }) => {
e.preventDefault(); // Prevent clicking the tooltip from triggering form save
}}
>
<CircleQuestionMark className="size-3 ml-2" />
<CircleQuestionMark className="size-3 ml-[2px] -translate-y-1" />
</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>

View File

@ -1,5 +1,4 @@
import { ExclamationCircleFilled } from '@ant-design/icons';
import { App } from 'antd';
import { Modal } from '@/components/ui/modal/modal';
import isEqual from 'lodash/isEqual';
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -84,20 +83,27 @@ interface IProps {
}
export const useShowDeleteConfirm = () => {
const { modal } = App.useApp();
const { t } = useTranslation();
const showDeleteConfirm = useCallback(
({ title, content, onOk, onCancel }: IProps): Promise<number> => {
return new Promise((resolve, reject) => {
modal.confirm({
Modal.show({
title: title ?? t('common.deleteModalTitle'),
icon: <ExclamationCircleFilled />,
content,
visible: true,
onVisibleChange: () => {
Modal.hide();
},
footer: null,
closable: true,
maskClosable: false,
okText: t('common.yes'),
okType: 'danger',
cancelText: t('common.no'),
async onOk() {
style: {
width: '400px',
},
okButtonClassName:
'bg-state-error text-white hover:bg-state-error hover:text-white',
onOk: async () => {
try {
const ret = await onOk?.();
resolve(ret);
@ -106,13 +112,15 @@ export const useShowDeleteConfirm = () => {
reject(error);
}
},
onCancel() {
onCancel: () => {
onCancel?.();
Modal.hide();
},
children: content,
});
});
},
[t, modal],
[t],
);
return showDeleteConfirm;

View File

@ -696,6 +696,8 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
tocEnhanceTip: ` During the parsing of the document, table of contents information was generated (see the 'Enable Table of Contents Extraction' option in the General method). This allows the large model to return table of contents items relevant to the user's query, thereby using these items to retrieve related chunks and apply weighting to these chunks during the sorting process. This approach is derived from mimicking the behavioral logic of how humans search for knowledge in books.`,
},
setting: {
seconds: 'seconds',
minutes: 'minutes',
edit: 'Edit',
cropTip:
'Drag the selection area to choose the cropping position of the image, and scroll to zoom in/out',

View File

@ -685,6 +685,8 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
tocEnhanceTip: `解析文档时生成了目录信息见General方法的启用目录抽取让大模型返回和用户问题相关的目录项从而利用目录项拿到相关chunk对这些chunk在排序中进行加权。这种方法来源于模仿人类查询书本中知识的行为逻辑`,
},
setting: {
seconds: '秒',
minutes: '分',
edit: '编辑',
cropTip: '拖动选区可以选择要图片的裁剪位置,滚动可以放大/缩小选区',
cropImage: '剪裁图片',

View File

@ -92,7 +92,7 @@ export function SideBar({ refreshCount }: PropType) {
key={itemIdx}
variant={active ? 'secondary' : 'ghost'}
className={cn(
'w-full justify-start gap-2.5 px-3 relative h-10 text-text-sub-title-invert',
'w-full justify-start gap-2.5 px-3 relative h-10 text-text-secondary',
{
'bg-bg-card': active,
'text-text-primary': active,

View File

@ -62,6 +62,7 @@ const AddDataSourceModal = ({
sourceData?.id as keyof typeof DataSourceFormDefaultValues
] as FieldValues
}
labelClassName="font-normal"
>
<div className=" absolute bottom-0 right-0 left-0 flex items-center justify-end w-full gap-2 py-6 px-6">
<DynamicForm.CancelButton

View File

@ -1,725 +0,0 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react';
import {
DefaultValues,
FieldValues,
SubmitHandler,
useForm,
useFormContext,
} from 'react-hook-form';
import { ZodSchema, z } from 'zod';
import EditTag from '@/components/edit-tag';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Checkbox } from '@/components/ui/checkbox';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { t } from 'i18next';
import { Loader } from 'lucide-react';
// Field type enumeration
export enum FormFieldType {
Text = 'text',
Email = 'email',
Password = 'password',
Number = 'number',
Textarea = 'textarea',
Select = 'select',
Checkbox = 'checkbox',
Tag = 'tag',
}
// Field configuration interface
export interface FormFieldConfig {
name: string;
label: string;
type: FormFieldType;
hidden?: boolean;
required?: boolean;
placeholder?: string;
options?: { label: string; value: string }[];
defaultValue?: any;
validation?: {
pattern?: RegExp;
minLength?: number;
maxLength?: number;
min?: number;
max?: number;
message?: string;
};
render?: (fieldProps: any) => React.ReactNode;
horizontal?: boolean;
onChange?: (value: any) => void;
}
// Component props interface
interface DynamicFormProps<T extends FieldValues> {
fields: FormFieldConfig[];
onSubmit: SubmitHandler<T>;
className?: string;
children?: React.ReactNode;
defaultValues?: DefaultValues<T>;
}
// Form ref interface
export interface DynamicFormRef {
submit: () => void;
getValues: () => any;
reset: (values?: any) => void;
}
// Generate Zod validation schema based on field configurations
const generateSchema = (fields: FormFieldConfig[]): ZodSchema<any> => {
const schema: Record<string, ZodSchema> = {};
const nestedSchemas: Record<string, Record<string, ZodSchema>> = {};
fields.forEach((field) => {
let fieldSchema: ZodSchema;
// Create base validation schema based on field type
switch (field.type) {
case FormFieldType.Email:
fieldSchema = z.string().email('Please enter a valid email address');
break;
case FormFieldType.Number:
fieldSchema = z.coerce.number();
if (field.validation?.min !== undefined) {
fieldSchema = (fieldSchema as z.ZodNumber).min(
field.validation.min,
field.validation.message ||
`Value cannot be less than ${field.validation.min}`,
);
}
if (field.validation?.max !== undefined) {
fieldSchema = (fieldSchema as z.ZodNumber).max(
field.validation.max,
field.validation.message ||
`Value cannot be greater than ${field.validation.max}`,
);
}
break;
case FormFieldType.Checkbox:
fieldSchema = z.boolean();
break;
case FormFieldType.Tag:
fieldSchema = z.array(z.string());
break;
default:
fieldSchema = z.string();
break;
}
// Handle required fields
if (field.required) {
if (field.type === FormFieldType.Checkbox) {
fieldSchema = (fieldSchema as z.ZodBoolean).refine(
(val) => val === true,
{
message: `${field.label} is required`,
},
);
} else if (field.type === FormFieldType.Tag) {
fieldSchema = (fieldSchema as z.ZodArray<z.ZodString>).min(1, {
message: `${field.label} is required`,
});
} else {
fieldSchema = (fieldSchema as z.ZodString).min(1, {
message: `${field.label} is required`,
});
}
}
if (!field.required) {
fieldSchema = fieldSchema.optional();
}
// Handle other validation rules
if (
field.type !== FormFieldType.Number &&
field.type !== FormFieldType.Checkbox &&
field.type !== FormFieldType.Tag &&
field.required
) {
fieldSchema = fieldSchema as z.ZodString;
if (field.validation?.minLength !== undefined) {
fieldSchema = (fieldSchema as z.ZodString).min(
field.validation.minLength,
field.validation.message ||
`Enter at least ${field.validation.minLength} characters`,
);
}
if (field.validation?.maxLength !== undefined) {
fieldSchema = (fieldSchema as z.ZodString).max(
field.validation.maxLength,
field.validation.message ||
`Enter up to ${field.validation.maxLength} characters`,
);
}
if (field.validation?.pattern) {
fieldSchema = (fieldSchema as z.ZodString).regex(
field.validation.pattern,
field.validation.message || 'Invalid input format',
);
}
}
if (field.name.includes('.')) {
const keys = field.name.split('.');
const firstKey = keys[0];
if (!nestedSchemas[firstKey]) {
nestedSchemas[firstKey] = {};
}
let currentSchema = nestedSchemas[firstKey];
for (let i = 1; i < keys.length - 1; i++) {
const key = keys[i];
if (!currentSchema[key]) {
currentSchema[key] = {};
}
currentSchema = currentSchema[key];
}
const lastKey = keys[keys.length - 1];
currentSchema[lastKey] = fieldSchema;
} else {
schema[field.name] = fieldSchema;
}
});
Object.keys(nestedSchemas).forEach((key) => {
const buildNestedSchema = (obj: Record<string, any>): ZodSchema => {
const nestedSchema: Record<string, ZodSchema> = {};
Object.keys(obj).forEach((subKey) => {
if (
typeof obj[subKey] === 'object' &&
!(obj[subKey] instanceof z.ZodType)
) {
nestedSchema[subKey] = buildNestedSchema(obj[subKey]);
} else {
nestedSchema[subKey] = obj[subKey];
}
});
return z.object(nestedSchema);
};
schema[key] = buildNestedSchema(nestedSchemas[key]);
});
return z.object(schema);
};
// Generate default values based on field configurations
const generateDefaultValues = <T extends FieldValues>(
fields: FormFieldConfig[],
): DefaultValues<T> => {
const defaultValues: Record<string, any> = {};
fields.forEach((field) => {
if (field.name.includes('.')) {
const keys = field.name.split('.');
let current = defaultValues;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!current[key]) {
current[key] = {};
}
current = current[key];
}
const lastKey = keys[keys.length - 1];
if (field.defaultValue !== undefined) {
current[lastKey] = field.defaultValue;
} else if (field.type === FormFieldType.Checkbox) {
current[lastKey] = false;
} else if (field.type === FormFieldType.Tag) {
current[lastKey] = [];
} else {
current[lastKey] = '';
}
} else {
if (field.defaultValue !== undefined) {
defaultValues[field.name] = field.defaultValue;
} else if (field.type === FormFieldType.Checkbox) {
defaultValues[field.name] = false;
} else if (field.type === FormFieldType.Tag) {
defaultValues[field.name] = [];
} else {
defaultValues[field.name] = '';
}
}
});
return defaultValues as DefaultValues<T>;
};
// Dynamic form component
const DynamicForm = {
Root: forwardRef(
<T extends FieldValues>(
{
fields,
onSubmit,
className = '',
children,
defaultValues: formDefaultValues = {} as DefaultValues<T>,
}: DynamicFormProps<T>,
ref: React.Ref<any>,
) => {
// Generate validation schema and default values
const schema = useMemo(() => generateSchema(fields), [fields]);
const defaultValues = useMemo(() => {
const value = {
...generateDefaultValues(fields),
...formDefaultValues,
};
console.log('generateDefaultValues', fields, value);
return value;
}, [fields, formDefaultValues]);
// Initialize form
const form = useForm<T>({
resolver: zodResolver(schema),
defaultValues,
});
// Expose form methods via ref
useImperativeHandle(ref, () => ({
submit: () => form.handleSubmit(onSubmit)(),
getValues: () => form.getValues(),
reset: (values?: T) => {
if (values) {
form.reset(values);
} else {
form.reset();
}
},
setError: form.setError,
clearErrors: form.clearErrors,
trigger: form.trigger,
}));
useEffect(() => {
if (formDefaultValues && Object.keys(formDefaultValues).length > 0) {
form.reset({
...generateDefaultValues(fields),
...formDefaultValues,
});
}
}, [form, formDefaultValues, fields]);
// Submit handler
// const handleSubmit = form.handleSubmit(onSubmit);
// Render form fields
const renderField = (field: FormFieldConfig) => {
if (field.render) {
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target?.value ?? e);
},
}
: fieldProps;
return field.render?.(finalFieldProps);
}}
</RAGFlowFormItem>
);
}
switch (field.type) {
case FormFieldType.Textarea:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target.value);
},
}
: fieldProps;
return (
<Textarea
{...finalFieldProps}
placeholder={field.placeholder}
className="resize-none"
/>
);
}}
</RAGFlowFormItem>
);
case FormFieldType.Select:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (value: string) => {
console.log('select value', value);
if (fieldProps.onChange) {
fieldProps.onChange(value);
}
field.onChange?.(value);
},
}
: fieldProps;
return (
<SelectWithSearch
triggerClassName="!shrink"
{...finalFieldProps}
options={field.options}
/>
);
}}
</RAGFlowFormItem>
);
case FormFieldType.Checkbox:
return (
<FormField
control={form.control}
name={field.name as any}
render={({ field: formField }) => (
<FormItem
className={cn('flex items-center', {
'flex-row items-start space-x-3 space-y-0':
!field.horizontal,
})}
>
{field.label && !field.horizontal && (
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
{field.label}{' '}
{field.required && (
<span className="text-destructive">*</span>
)}
</FormLabel>
</div>
)}
<FormControl>
<Checkbox
checked={formField.value}
onCheckedChange={(checked) => {
formField.onChange(checked);
field.onChange?.(checked);
}}
/>
</FormControl>
{field.label && field.horizontal && (
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
{field.label}{' '}
{field.required && (
<span className="text-destructive">*</span>
)}
</FormLabel>
</div>
)}
<FormMessage />
</FormItem>
)}
/>
);
case FormFieldType.Tag:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (value: string[]) => {
fieldProps.onChange(value);
field.onChange?.(value);
},
}
: fieldProps;
return (
// <TagInput {...fieldProps} placeholder={field.placeholder} />
<div className="w-full">
<EditTag {...finalFieldProps}></EditTag>
</div>
);
}}
</RAGFlowFormItem>
);
default:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target.value);
},
}
: fieldProps;
return (
<Input
{...finalFieldProps}
type={field.type}
placeholder={field.placeholder}
/>
);
}}
</RAGFlowFormItem>
);
}
};
return (
<Form {...form}>
<form
className={`space-y-6 ${className}`}
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit(onSubmit)(e);
}}
>
<>
{fields.map((field) => (
<div key={field.name} className={cn({ hidden: field.hidden })}>
{renderField(field)}
</div>
))}
{children}
</>
</form>
</Form>
);
},
) as <T extends FieldValues>(
props: DynamicFormProps<T> & { ref?: React.Ref<DynamicFormRef> },
) => React.ReactElement,
SavingButton: ({
submitLoading,
buttonText,
submitFunc,
}: {
submitLoading: boolean;
buttonText?: string;
submitFunc?: (values: FieldValues) => void;
}) => {
const form = useFormContext();
return (
<button
type="button"
disabled={submitLoading}
onClick={() => {
console.log('form submit');
(async () => {
console.log('form submit2');
try {
let beValid = await form.formControl.trigger();
console.log('form valid', beValid, form, form.formControl);
if (beValid) {
form.handleSubmit(async (values) => {
console.log('form values', values);
submitFunc?.(values);
})();
}
} catch (e) {
console.error(e);
} finally {
console.log('form submit3');
}
})();
}}
className={cn(
'px-2 py-1 bg-primary text-primary-foreground rounded-md hover:bg-primary/90',
)}
>
{submitLoading && (
<Loader className="inline-block mr-2 h-4 w-4 animate-spin" />
)}
{buttonText ?? t('modal.okText')}
</button>
);
},
CancelButton: ({
handleCancel,
cancelText,
}: {
handleCancel: () => void;
cancelText?: string;
}) => {
return (
<button
type="button"
onClick={() => handleCancel()}
className="px-2 py-1 border border-input rounded-md hover:bg-muted"
>
{cancelText ?? t('modal.cancelText')}
</button>
);
},
};
export { DynamicForm };
/**
* Usage Example 1: Basic Form
*
* <DynamicForm
* fields={[
* {
* name: "username",
* label: "Username",
* type: FormFieldType.Text,
* required: true,
* placeholder: "Please enter username"
* },
* {
* name: "email",
* label: "Email",
* type: FormFieldType.Email,
* required: true,
* placeholder: "Please enter email address"
* }
* ]}
* onSubmit={(data) => {
* console.log(data); // { username: "...", email: "..." }
* }}
* />
*
* Usage Example 2: Nested Object Form
*
* <DynamicForm
* fields={[
* {
* name: "user.name",
* label: "Name",
* type: FormFieldType.Text,
* required: true,
* placeholder: "Please enter name"
* },
* {
* name: "user.email",
* label: "Email",
* type: FormFieldType.Email,
* required: true,
* placeholder: "Please enter email address"
* },
* {
* name: "user.profile.age",
* label: "Age",
* type: FormFieldType.Number,
* required: true,
* validation: {
* min: 18,
* max: 100,
* message: "Age must be between 18 and 100"
* }
* },
* {
* name: "user.profile.bio",
* label: "Bio",
* type: FormFieldType.Textarea,
* placeholder: "Please briefly introduce yourself"
* },
* {
* name: "settings.notifications",
* label: "Enable Notifications",
* type: FormFieldType.Checkbox
* }
* ]}
* onSubmit={(data) => {
* console.log(data);
* // {
* // user: {
* // name: "...",
* // email: "...",
* // profile: {
* // age: ...,
* // bio: "..."
* // }
* // },
* // settings: {
* // notifications: true/false
* // }
* // }
* }}
* />
*
* Usage Example 3: Tag Type Form
*
* <DynamicForm
* fields={[
* {
* name: "skills",
* label: "Skill Tags",
* type: FormFieldType.Tag,
* required: true,
* placeholder: "Enter skill and press Enter to add tag"
* },
* {
* name: "user.hobbies",
* label: "Hobbies",
* type: FormFieldType.Tag,
* placeholder: "Enter hobby and press Enter to add tag"
* }
* ]}
* onSubmit={(data) => {
* console.log(data);
* // {
* // skills: ["JavaScript", "React", "TypeScript"],
* // user: {
* // hobbies: ["Reading", "Swimming", "Travel"]
* // }
* // }
* }}
* />
*/

View File

@ -387,101 +387,6 @@ export const DataSourceFormFields = {
tooltip: t('setting.jiraPasswordTip'),
},
],
// [DataSourceKey.GOOGLE_DRIVE]: [
// {
// label: 'Primary Admin Email',
// name: 'config.credentials.google_primary_admin',
// type: FormFieldType.Text,
// required: true,
// placeholder: 'admin@example.com',
// tooltip: t('setting.google_drivePrimaryAdminTip'),
// },
// {
// label: 'OAuth Token JSON',
// name: 'config.credentials.google_tokens',
// type: FormFieldType.Textarea,
// required: true,
// render: (fieldProps) => (
// <GoogleDriveTokenField
// value={fieldProps.value}
// onChange={fieldProps.onChange}
// placeholder='{ "token": "...", "refresh_token": "...", ... }'
// />
// ),
// tooltip: t('setting.google_driveTokenTip'),
// },
// {
// label: 'My Drive Emails',
// name: 'config.my_drive_emails',
// type: FormFieldType.Text,
// required: true,
// placeholder: 'user1@example.com,user2@example.com',
// tooltip: t('setting.google_driveMyDriveEmailsTip'),
// },
// {
// label: 'Shared Folder URLs',
// name: 'config.shared_folder_urls',
// type: FormFieldType.Textarea,
// required: true,
// placeholder:
// 'https://drive.google.com/drive/folders/XXXXX,https://drive.google.com/drive/folders/YYYYY',
// tooltip: t('setting.google_driveSharedFoldersTip'),
// },
// // The fields below are intentionally disabled for now. Uncomment them when we
// // reintroduce shared drive controls or advanced impersonation options.
// // {
// // label: 'Shared Drive URLs',
// // name: 'config.shared_drive_urls',
// // type: FormFieldType.Text,
// // required: false,
// // placeholder:
// // 'Optional: comma-separated shared drive links if you want to include them.',
// // },
// // {
// // label: 'Specific User Emails',
// // name: 'config.specific_user_emails',
// // type: FormFieldType.Text,
// // required: false,
// // placeholder:
// // 'Optional: comma-separated list of users to impersonate (overrides defaults).',
// // },
// // {
// // label: 'Include My Drive',
// // name: 'config.include_my_drives',
// // type: FormFieldType.Checkbox,
// // required: false,
// // defaultValue: true,
// // },
// // {
// // label: 'Include Shared Drives',
// // name: 'config.include_shared_drives',
// // type: FormFieldType.Checkbox,
// // required: false,
// // defaultValue: false,
// // },
// // {
// // label: 'Include “Shared with me”',
// // name: 'config.include_files_shared_with_me',
// // type: FormFieldType.Checkbox,
// // required: false,
// // defaultValue: false,
// // },
// // {
// // label: 'Allow Images',
// // name: 'config.allow_images',
// // type: FormFieldType.Checkbox,
// // required: false,
// // defaultValue: false,
// // },
// {
// label: '',
// name: 'config.credentials.authentication_method',
// type: FormFieldType.Text,
// required: false,
// hidden: true,
// defaultValue: 'uploaded',
// },
// ],
};
export const DataSourceFormDefaultValues = {

View File

@ -67,11 +67,11 @@ const SourceDetailPage = () => {
<div className="flex items-center gap-1 w-full relative">
<Input {...fieldProps} type={FormFieldType.Number} />
<span className="absolute right-0 -translate-x-[58px] text-text-secondary italic ">
minutes
{t('setting.minutes')}
</span>
<button
type="button"
className="text-text-secondary bg-bg-input rounded-sm text-xs h-full p-2 border border-border-button "
className="text-text-secondary bg-bg-input rounded-sm text-xs h-full p-2 border border-border-button hover:bg-border-button hover:text-text-primary"
onClick={() => {
runSchedule();
}}
@ -112,7 +112,7 @@ const SourceDetailPage = () => {
<div className="flex items-center gap-1 w-full relative">
<Input {...fieldProps} type={FormFieldType.Number} />
<span className="absolute right-0 -translate-x-6 text-text-secondary italic ">
seconds
{t('setting.seconds')}
</span>
</div>
),

View File

@ -150,7 +150,7 @@ export function EditMcpDialog({
</Button>
}
>
<div className="overflow-auto max-h-80 divide-y bg-bg-card rounded-md px-2.5">
<div className="overflow-auto max-h-80 divide-y-[0.5px] divide-border-button bg-bg-card rounded-md px-2.5 scrollbar-auto">
{nextTools?.map((x) => (
<McpToolCard key={x.name} data={x}></McpToolCard>
))}

View File

@ -86,7 +86,7 @@ export default function McpServer() {
{t('mcp.import')}
</Button>
<Button onClick={showEditModal('')}>
<Plus className="size-3.5" /> {t('mcp.addMCP')}
<Plus className="size-3.5 font-medium" /> {t('mcp.addMCP')}
</Button>
</div>
</section>

View File

@ -129,7 +129,9 @@ export const AvailableModels: FC<{
<div className="flex items-center space-x-3 mb-3">
<LlmIcon name={model.name} imgClass="h-8 w-8 text-text-primary" />
<div className="flex-1">
<h3 className="font-medium truncate">{model.name}</h3>
<div className="font-normal text-base truncate">
{model.name}
</div>
</div>
<Button className=" px-2 items-center gap-0 text-xs h-6 rounded-md transition-colors hidden group-hover:flex">
<Plus size={12} />

View File

@ -19,7 +19,7 @@ import { TenantRole } from '../constants';
import { useHandleDeleteUser } from './hooks';
const ColorMap: Record<string, string> = {
[TenantRole.Normal]: 'bg-transparent text-text-primary',
[TenantRole.Normal]: 'bg-transparent text-white',
[TenantRole.Invite]: 'bg-accent-primary-5 bg-accent-primary rounded-sm',
[TenantRole.Owner]: 'bg-red-100 text-red-800',
};
@ -137,7 +137,7 @@ const UserTable = ({ searchUser }: { searchUser: string }) => {
<Button
variant="ghost"
size="icon"
className="h-8 w-8 p-0"
className="h-8 w-8 p-0 hover:bg-state-error-5 hover:text-state-error"
onClick={handleDeleteTenantUser(record.user_id)}
>
<Trash2 className="h-4 w-4" />

View File

@ -100,8 +100,8 @@ export function SideBar() {
<ThemeToggle />
</div>
<Button
variant="outline"
className="w-full gap-3 !bg-bg-base border !border-border-button !text-text-secondary"
variant="ghost"
className="w-full gap-3 bg-bg-base border border-border-button"
onClick={() => {
logout();
}}