mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-08 12:24:48 +08:00
Feat: Integrate the name, avatar, and description of chat and search into a single component. (#14008)
### What problem does this PR solve? Feat: Integrate the name, avatar, and description of chat and search into a single component. ### Type of change - [x] New Feature (non-breaking change which adds functionality) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Inline-editable avatar, name, and description fields * Expandable content blocks in search results * New RAGFlow heading/logo component * **Refactor** * Replaced scattered form fields with a composed Avatar/Name/Description component * Mindmap drawer converted to a sheet-based drawer and layout cleanup * Simplified search page controls and layout; improved scroll viewport handling * **Chores** * Added/updated English and Chinese localization keys (placeholders, view more/less) <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Yingfeng <yingfeng.zhang@gmail.com>
This commit is contained in:
@@ -47,7 +47,7 @@ if (process.env.NODE_ENV === 'development') {
|
||||
whyDidYouRender(React, {
|
||||
trackAllPureComponents: true,
|
||||
trackExtraHooks: [],
|
||||
logOnDifferentValues: true,
|
||||
logOnDifferentValues: false,
|
||||
exclude: [/^RouterProvider$/],
|
||||
});
|
||||
},
|
||||
|
||||
101
web/src/components/avatar-name-description/editable-field.tsx
Normal file
101
web/src/components/avatar-name-description/editable-field.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { LucidePencil } from 'lucide-react';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEditableField } from './use-editable-field';
|
||||
|
||||
export interface EditableFieldProps {
|
||||
/** Form field name */
|
||||
name: string;
|
||||
/** Placeholder text when empty */
|
||||
placeholder?: string;
|
||||
/** Whether field is required */
|
||||
required?: boolean;
|
||||
/** Whether to show edit icon */
|
||||
showEditIcon?: boolean;
|
||||
/** Custom className for the container */
|
||||
className?: string;
|
||||
/** Custom className for the input */
|
||||
inputClassName?: string;
|
||||
/** Custom className for the display text */
|
||||
displayClassName?: string;
|
||||
/** Aria label for accessibility */
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export function EditableField({
|
||||
name,
|
||||
placeholder,
|
||||
required = true,
|
||||
showEditIcon = true,
|
||||
className,
|
||||
inputClassName,
|
||||
displayClassName,
|
||||
ariaLabel,
|
||||
}: EditableFieldProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isEditing, inputRef, handleEnterEdit, handleKeyDown, handleBlur } =
|
||||
useEditableField({ required });
|
||||
|
||||
const finalPlaceholder = placeholder ?? t('common.namePlaceholder');
|
||||
|
||||
return (
|
||||
<RAGFlowFormItem
|
||||
name={name}
|
||||
className={cn('flex items-center gap-1.5', className)}
|
||||
required={required}
|
||||
>
|
||||
{(field) =>
|
||||
isEditing ? (
|
||||
<Input
|
||||
ref={inputRef as React.RefObject<HTMLInputElement>}
|
||||
name={field.name}
|
||||
value={field.value || ''}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
field.onChange(e.target.value)
|
||||
}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
handleBlur(field.value || '', field.onChange);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={finalPlaceholder}
|
||||
className={cn(
|
||||
'h-7 text-base font-medium px-2 py-0.5',
|
||||
inputClassName,
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
'text-base font-medium text-text-primary truncate flex-1 min-w-0',
|
||||
!field.value && 'text-text-secondary italic',
|
||||
displayClassName,
|
||||
)}
|
||||
>
|
||||
{field.value || finalPlaceholder}
|
||||
</span>
|
||||
{showEditIcon && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleEnterEdit(field.value || '')}
|
||||
className="p-1 text-text-secondary hover:text-text-primary transition-colors focus:outline-none focus:ring-1 focus:ring-accent-primary rounded shrink-0"
|
||||
aria-label={ariaLabel ? `Edit ${ariaLabel}` : 'Edit'}
|
||||
>
|
||||
<LucidePencil className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditableField;
|
||||
144
web/src/components/avatar-name-description/editable-textarea.tsx
Normal file
144
web/src/components/avatar-name-description/editable-textarea.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
'use client';
|
||||
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { LucidePencil } from 'lucide-react';
|
||||
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface EditableTextareaProps {
|
||||
/** Form field name */
|
||||
name: string;
|
||||
/** Placeholder text when empty */
|
||||
placeholder?: string;
|
||||
/** Whether to show edit icon */
|
||||
showEditIcon?: boolean;
|
||||
/** Custom className for the container */
|
||||
className?: string;
|
||||
/** Custom className for the textarea */
|
||||
textareaClassName?: string;
|
||||
/** Custom className for the display text */
|
||||
displayClassName?: string;
|
||||
/** Aria label for accessibility */
|
||||
ariaLabel?: string;
|
||||
/** Minimum number of rows for textarea */
|
||||
minRows?: number;
|
||||
/** Maximum number of rows for textarea */
|
||||
maxRows?: number;
|
||||
}
|
||||
|
||||
export function EditableTextarea({
|
||||
name,
|
||||
placeholder,
|
||||
showEditIcon = true,
|
||||
className,
|
||||
textareaClassName,
|
||||
displayClassName,
|
||||
ariaLabel,
|
||||
minRows = 2,
|
||||
maxRows = 3,
|
||||
}: EditableTextareaProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const finalPlaceholder = placeholder ?? t('common.descriptionPlaceholder');
|
||||
|
||||
// Auto-focus when entering edit mode and move cursor to end
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
const frameId = requestAnimationFrame(() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
textarea.focus();
|
||||
const length = textarea.value.length;
|
||||
textarea.setSelectionRange(length, length);
|
||||
}
|
||||
});
|
||||
return () => cancelAnimationFrame(frameId);
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
const handleEnterEdit = useCallback(() => {
|
||||
setIsEditing(true);
|
||||
}, []);
|
||||
|
||||
const handleExitEdit = useCallback(() => {
|
||||
setIsEditing(false);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Escape') {
|
||||
setIsEditing(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-1.5 group', className)}>
|
||||
<RAGFlowFormItem
|
||||
name={name}
|
||||
className={cn(isEditing ? 'flex-1 w-full' : 'w-auto min-w-0')}
|
||||
>
|
||||
{(field) =>
|
||||
isEditing ? (
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
name={field.name}
|
||||
value={field.value || ''}
|
||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) =>
|
||||
field.onChange(e.target.value)
|
||||
}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
handleExitEdit();
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={finalPlaceholder}
|
||||
className={cn(
|
||||
'min-h-[28px] text-sm text-text-secondary resize-none px-2 py-0.5',
|
||||
textareaClassName,
|
||||
)}
|
||||
autoSize={{ minRows, maxRows }}
|
||||
aria-label={ariaLabel}
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
className={cn(
|
||||
'block w-full text-sm text-text-secondary line-clamp-2 text-left',
|
||||
!field.value && 'italic',
|
||||
displayClassName,
|
||||
)}
|
||||
onClick={handleEnterEdit}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleEnterEdit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{field.value || finalPlaceholder}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</RAGFlowFormItem>
|
||||
{!isEditing && showEditIcon && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleEnterEdit}
|
||||
className="p-1 text-text-secondary hover:text-text-primary transition-colors focus:outline-none focus:ring-1 focus:ring-accent-primary rounded shrink-0 opacity-0 group-hover:opacity-100"
|
||||
aria-label={ariaLabel ? `Edit ${ariaLabel}` : 'Edit'}
|
||||
>
|
||||
<LucidePencil className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditableTextarea;
|
||||
80
web/src/components/avatar-name-description/index.tsx
Normal file
80
web/src/components/avatar-name-description/index.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import { AvatarUpload } from '@/components/avatar-upload';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { EditableField } from './editable-field';
|
||||
import { EditableTextarea } from './editable-textarea';
|
||||
|
||||
export interface AvatarNameDescriptionProps {
|
||||
/** Form field name for avatar/icon (default: 'icon') */
|
||||
avatarField?: string;
|
||||
/** Form field name for name/title (default: 'name') */
|
||||
nameField?: string;
|
||||
/** Form field name for description (default: 'description') */
|
||||
descriptionField?: string;
|
||||
/** Label for name field */
|
||||
nameLabel?: string;
|
||||
/** Label for description field */
|
||||
descriptionLabel?: string;
|
||||
/** Placeholder for name input */
|
||||
namePlaceholder?: string;
|
||||
/** Placeholder for description input */
|
||||
descriptionPlaceholder?: string;
|
||||
/** Custom className for the container */
|
||||
className?: string;
|
||||
/** Whether name is required */
|
||||
nameRequired?: boolean;
|
||||
/** Whether to show edit icons */
|
||||
showEditIcons?: boolean;
|
||||
}
|
||||
|
||||
export function AvatarNameDescription({
|
||||
avatarField = 'icon',
|
||||
nameField = 'name',
|
||||
descriptionField = 'description',
|
||||
nameLabel,
|
||||
descriptionLabel,
|
||||
namePlaceholder,
|
||||
descriptionPlaceholder,
|
||||
className,
|
||||
nameRequired = true,
|
||||
showEditIcons = true,
|
||||
}: AvatarNameDescriptionProps) {
|
||||
const { t } = useTranslation();
|
||||
useFormContext(); // Ensure component is used within FormProvider
|
||||
|
||||
return (
|
||||
<div className={cn('flex gap-3', className)}>
|
||||
<RAGFlowFormItem name={avatarField}>
|
||||
<AvatarUpload tips={''} />
|
||||
</RAGFlowFormItem>
|
||||
|
||||
{/* Name & Description Section */}
|
||||
<div className="flex-1 min-w-0 pt-1 space-y-1">
|
||||
{/* Name Row - Using EditableField component */}
|
||||
<EditableField
|
||||
name={nameField}
|
||||
placeholder={namePlaceholder}
|
||||
required={nameRequired}
|
||||
showEditIcon={showEditIcons}
|
||||
ariaLabel={nameLabel ?? t('common.name')}
|
||||
/>
|
||||
|
||||
{/* Description Row - Using EditableTextarea component */}
|
||||
<div className="mt-0.5">
|
||||
<EditableTextarea
|
||||
name={descriptionField}
|
||||
placeholder={descriptionPlaceholder}
|
||||
showEditIcon={showEditIcons}
|
||||
ariaLabel={descriptionLabel ?? t('common.description')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AvatarNameDescription;
|
||||
@@ -0,0 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface UseEditableFieldOptions {
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
interface UseEditableFieldReturn {
|
||||
isEditing: boolean;
|
||||
inputRef: React.RefObject<HTMLInputElement | null>;
|
||||
previousValueRef: React.RefObject<string>;
|
||||
handleEnterEdit: (currentValue: string) => void;
|
||||
handleExitEdit: () => void;
|
||||
handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
handleBlur: (currentValue: string, onChange: (value: string) => void) => void;
|
||||
}
|
||||
|
||||
export function useEditableField(
|
||||
options: UseEditableFieldOptions = {},
|
||||
): UseEditableFieldReturn {
|
||||
const { required = true } = options;
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const previousValueRef = useRef<string>('');
|
||||
|
||||
// Auto-focus when entering edit mode
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
const frameId = requestAnimationFrame(() => {
|
||||
inputRef.current?.focus();
|
||||
});
|
||||
|
||||
return () => cancelAnimationFrame(frameId);
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
const handleEnterEdit = useCallback((currentValue: string) => {
|
||||
previousValueRef.current = currentValue;
|
||||
setIsEditing(true);
|
||||
}, []);
|
||||
|
||||
const handleExitEdit = useCallback(() => {
|
||||
setIsEditing(false);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
setIsEditing(false);
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
setIsEditing(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleBlur = useCallback(
|
||||
(currentValue: string, onChange: (value: string) => void) => {
|
||||
// If required and value is empty, restore to previous value
|
||||
if (required && !currentValue?.trim()) {
|
||||
onChange(previousValueRef.current);
|
||||
}
|
||||
setIsEditing(false);
|
||||
},
|
||||
[required],
|
||||
);
|
||||
|
||||
return {
|
||||
isEditing,
|
||||
inputRef,
|
||||
previousValueRef,
|
||||
handleEnterEdit,
|
||||
handleExitEdit,
|
||||
handleKeyDown,
|
||||
handleBlur,
|
||||
};
|
||||
}
|
||||
@@ -245,28 +245,6 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
/*
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
setTimeout(() => {
|
||||
// initCropArea();
|
||||
if (imageToCrop && container && isCropModalOpen) {
|
||||
container.addEventListener(
|
||||
'wheel',
|
||||
handleWheel as unknown as EventListener,
|
||||
{ passive: false },
|
||||
);
|
||||
return () => {
|
||||
container.removeEventListener(
|
||||
'wheel',
|
||||
handleWheel as unknown as EventListener,
|
||||
);
|
||||
};
|
||||
}
|
||||
}, 100);
|
||||
}, [handleWheel, imageToCrop, isCropModalOpen]);
|
||||
*/
|
||||
|
||||
return (
|
||||
<div className="flex justify-start items-end space-x-2">
|
||||
<div className="relative group">
|
||||
|
||||
@@ -28,16 +28,24 @@ const ScrollBar = React.forwardRef<
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
interface ScrollAreaProps extends React.ComponentPropsWithoutRef<
|
||||
typeof ScrollAreaPrimitive.Root
|
||||
> {
|
||||
viewportClassName?: string;
|
||||
}
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
ScrollAreaProps
|
||||
>(({ className, viewportClassName, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
className={cn('h-full w-full rounded-[inherit]', viewportClassName)}
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
|
||||
@@ -18,6 +18,7 @@ export default {
|
||||
name: 'Name',
|
||||
save: 'Save',
|
||||
namePlaceholder: 'Please input name',
|
||||
descriptionPlaceholder: 'Enter description',
|
||||
next: 'Next',
|
||||
create: 'Create',
|
||||
edit: 'Edit',
|
||||
@@ -43,6 +44,8 @@ export default {
|
||||
languagePlaceholder: 'select your language',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
viewMore: 'View more',
|
||||
viewLess: 'View less',
|
||||
comingSoon: 'Coming soon',
|
||||
download: 'Download',
|
||||
close: 'Close',
|
||||
|
||||
@@ -18,6 +18,7 @@ export default {
|
||||
name: '名称',
|
||||
save: '保存',
|
||||
namePlaceholder: '请输入名称',
|
||||
descriptionPlaceholder: '请输入描述',
|
||||
next: '下一步',
|
||||
create: '创建',
|
||||
edit: '编辑',
|
||||
@@ -33,6 +34,8 @@ export default {
|
||||
languagePlaceholder: '请选择语言',
|
||||
copy: '复制',
|
||||
copied: '复制成功',
|
||||
viewMore: '查看更多',
|
||||
viewLess: '收起',
|
||||
comingSoon: '即将推出',
|
||||
download: '下载',
|
||||
close: '关闭',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { AvatarUpload } from '@/components/avatar-upload';
|
||||
import { AvatarNameDescription } from '@/components/avatar-name-description';
|
||||
import { KnowledgeBaseFormField } from '@/components/knowledge-base-item';
|
||||
import { MetadataFilter } from '@/components/metadata-filter';
|
||||
import { SwitchFormField } from '@/components/switch-fom-field';
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { getDirAttribute } from '@/utils/text-direction';
|
||||
@@ -22,58 +21,12 @@ import { useFormContext } from 'react-hook-form';
|
||||
export default function ChatBasicSetting() {
|
||||
const { t } = useTranslate('chat');
|
||||
const form = useFormContext();
|
||||
const nameValue = form.watch('name');
|
||||
const descriptionValue = form.watch('description');
|
||||
const emptyResponseValue = form.watch('prompt_config.empty_response');
|
||||
const prologueValue = form.watch('prompt_config.prologue');
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'icon'}
|
||||
render={({ field }) => (
|
||||
<div className="space-y-6">
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>{t('assistantAvatar')}</FormLabel>
|
||||
<FormControl>
|
||||
<AvatarUpload {...field}></AvatarUpload>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('assistantName')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} dir={getDirAttribute(nameValue || '')}></Input>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('description')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder={t('descriptionPlaceholder')}
|
||||
dir={getDirAttribute(descriptionValue || '')}
|
||||
></Textarea>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<AvatarNameDescription />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'prompt_config.empty_response'}
|
||||
|
||||
@@ -158,7 +158,7 @@ export function ChatSettings({ hasSingleChatBox }: ChatSettingsProps) {
|
||||
onSubmit={form.handleSubmit(onSubmit, onInvalid)}
|
||||
className="flex-1 flex flex-col min-h-0"
|
||||
>
|
||||
<ScrollArea>
|
||||
<ScrollArea viewportClassName="[&>div]:!block">
|
||||
<section className="p-5 space-y-6 overflow-auto flex-1 min-h-0">
|
||||
<ChatBasicSetting></ChatBasicSetting>
|
||||
<Separator />
|
||||
|
||||
108
web/src/pages/next-search/expandable-content.tsx
Normal file
108
web/src/pages/next-search/expandable-content.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ExpandableContentProps {
|
||||
children: ReactNode;
|
||||
maxHeight?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ExpandableContent({
|
||||
children,
|
||||
maxHeight = 208, // 52 * 4 = 208px (max-h-52)
|
||||
className = '',
|
||||
}: ExpandableContentProps) {
|
||||
const { t } = useTranslation();
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const element = contentRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const checkOverflow = () => {
|
||||
setIsOverflowing(element.scrollHeight > maxHeight);
|
||||
};
|
||||
|
||||
checkOverflow();
|
||||
|
||||
// ResizeObserver handles element size changes
|
||||
const resizeObserver = new ResizeObserver(checkOverflow);
|
||||
resizeObserver.observe(element);
|
||||
|
||||
// MutationObserver handles DOM changes (useful for async markdown content)
|
||||
const mutationObserver = new MutationObserver(checkOverflow);
|
||||
mutationObserver.observe(element, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
});
|
||||
|
||||
// Listen for images to load (they affect scrollHeight)
|
||||
const images = element.querySelectorAll('img');
|
||||
const imageLoadPromises = Array.from(images).map(
|
||||
(img) =>
|
||||
new Promise<void>((resolve) => {
|
||||
if (img.complete) {
|
||||
resolve();
|
||||
} else {
|
||||
img.addEventListener('load', () => resolve(), { once: true });
|
||||
img.addEventListener('error', () => resolve(), { once: true });
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Re-check after all images are loaded
|
||||
Promise.all(imageLoadPromises).then(checkOverflow);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
mutationObserver.disconnect();
|
||||
};
|
||||
}, [maxHeight, children]);
|
||||
|
||||
const toggleExpand = () => {
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={`overflow-hidden scrollbar-thin transition-all duration-300 ${className}`}
|
||||
style={{
|
||||
maxHeight: isExpanded ? contentRef.current?.scrollHeight : maxHeight,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{!isExpanded && isOverflowing && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-bg-base to-transparent pointer-events-none" />
|
||||
)}
|
||||
|
||||
{isOverflowing && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={toggleExpand}
|
||||
className="absolute bottom-2 left-1/2 -translate-x-1/2"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<>
|
||||
<ChevronUp size={14} />
|
||||
<span>{t('common.viewLess')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown size={14} />
|
||||
<span>{t('common.viewMore')}</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,11 @@
|
||||
import { useFetchTokenListBeforeOtherStep } from '@/components/embed-dialog/use-show-embed-dialog';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SharedFrom } from '@/constants/chat';
|
||||
import {
|
||||
useFetchTenantInfo,
|
||||
useFetchUserInfo,
|
||||
} from '@/hooks/use-user-setting-request';
|
||||
import { Routes } from '@/routes';
|
||||
import { Send, Settings } from 'lucide-react';
|
||||
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
|
||||
import { Settings } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ISearchAppDetailProps,
|
||||
useFetchSearchDetail,
|
||||
} from '../next-searches/hooks';
|
||||
import EmbedAppModal from './embed-app-modal';
|
||||
import { useCheckSettings } from './hooks';
|
||||
import './index.less';
|
||||
import SearchHome from './search-home';
|
||||
@@ -24,15 +15,10 @@ import SearchingPage from './searching';
|
||||
export default function SearchPage() {
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const { data: SearchData } = useFetchSearchDetail();
|
||||
const { beta, handleOperate } = useFetchTokenListBeforeOtherStep();
|
||||
|
||||
const [openSetting, setOpenSetting] = useState(false);
|
||||
const [openEmbed, setOpenEmbed] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const { data: tenantInfo } = useFetchTenantInfo();
|
||||
const { data: userInfo } = useFetchUserInfo();
|
||||
const tenantId = tenantInfo.tenant_id;
|
||||
const { t } = useTranslation();
|
||||
const { openSetting: checkOpenSetting } = useCheckSettings(
|
||||
SearchData as ISearchAppDetailProps,
|
||||
);
|
||||
@@ -47,9 +33,12 @@ export default function SearchPage() {
|
||||
}, [isSearching]);
|
||||
|
||||
return (
|
||||
<section className="size-full relative" data-testid="search-detail">
|
||||
<div className="flex gap-3 w-full bg-bg-base">
|
||||
<div className="flex-1">
|
||||
<section
|
||||
className="size-full flex-1 relative px-5 pb-5 flex pt-4"
|
||||
data-testid="search-detail"
|
||||
>
|
||||
<div className="flex gap-3 w-full bg-bg-base border-0.5 border-border-button">
|
||||
<div className="flex-1 min-w-0">
|
||||
{!isSearching && (
|
||||
<div className="animate-fade-in-down">
|
||||
<SearchHome
|
||||
@@ -63,7 +52,7 @@ export default function SearchPage() {
|
||||
</div>
|
||||
)}
|
||||
{isSearching && (
|
||||
<div className="animate-fade-in-up">
|
||||
<div className="animate-fade-in-up h-full">
|
||||
<SearchingPage
|
||||
setIsSearching={setIsSearching}
|
||||
searchText={searchText}
|
||||
@@ -81,57 +70,15 @@ export default function SearchPage() {
|
||||
data={SearchData as ISearchAppDetailProps}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
<EmbedAppModal
|
||||
open={openEmbed}
|
||||
setOpen={setOpenEmbed}
|
||||
url={Routes.SearchShare}
|
||||
token={SearchData?.id as string}
|
||||
from={SharedFrom.Search}
|
||||
tenantId={tenantId}
|
||||
beta={beta}
|
||||
/>
|
||||
}
|
||||
{
|
||||
// <EmbedDialog
|
||||
// visible={openEmbed}
|
||||
// hideModal={setOpenEmbed}
|
||||
// token={SearchData?.id as string}
|
||||
// from={SharedFrom.Search}
|
||||
// beta={beta}
|
||||
// isAgent={false}
|
||||
// ></EmbedDialog>
|
||||
}
|
||||
</div>
|
||||
<div className="absolute end-5 top-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleOperate().then((res) => {
|
||||
console.log(res, 'res');
|
||||
if (res) {
|
||||
setOpenEmbed(!openEmbed);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Send />
|
||||
<div>{t('search.embedApp')}</div>
|
||||
</Button>
|
||||
</div>
|
||||
{!isSearching && (
|
||||
<div className="absolute start-5 bottom-12 ">
|
||||
<Button
|
||||
variant="transparent"
|
||||
className="bg-bg-card"
|
||||
onClick={() => setOpenSetting(!openSetting)}
|
||||
>
|
||||
<Settings className="text-text-secondary" />
|
||||
<div className="text-text-secondary">
|
||||
{t('search.searchSettings')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="transparent"
|
||||
className="bg-bg-card ml-5"
|
||||
onClick={() => setOpenSetting(!openSetting)}
|
||||
>
|
||||
<Settings className="text-text-secondary" />
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import IndentedTree from '@/components/indented-tree/indented-tree';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -9,41 +15,51 @@ interface IProps extends IModalProps<any> {
|
||||
data: any;
|
||||
}
|
||||
|
||||
const MindMapDrawer = ({ data, hideModal, loading }: IProps) => {
|
||||
const MindMapDrawer = ({ data, hideModal, loading, visible }: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const percent = usePendingMindMap();
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<div className="flex w-full justify-between items-center mb-2">
|
||||
<div className="text-text-primary font-medium text-base">
|
||||
{t('chunk.mind')}
|
||||
<Sheet open={visible} modal={false}>
|
||||
<SheetContent
|
||||
className="top-16 p-0 flex flex-col gap-0"
|
||||
closeIcon={false}
|
||||
>
|
||||
<SheetHeader className="border-b py-2 px-4">
|
||||
<SheetTitle className="hidden"></SheetTitle>
|
||||
<div className="flex w-full justify-between items-center">
|
||||
<div className="text-text-primary font-medium text-base">
|
||||
{t('chunk.mind')}
|
||||
</div>
|
||||
<X
|
||||
className="text-text-primary cursor-pointer"
|
||||
size={16}
|
||||
onClick={() => {
|
||||
hideModal?.();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
<div className="flex-1 p-4 overflow-hidden">
|
||||
{loading && (
|
||||
<div className="rounded-lg w-full h-full">
|
||||
<Progress value={percent} className="h-1 flex-1 min-w-10" />
|
||||
</div>
|
||||
)}
|
||||
{!loading && (
|
||||
<div className="bg-bg-card rounded-lg w-full h-full">
|
||||
<IndentedTree
|
||||
data={data}
|
||||
show
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
></IndentedTree>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<X
|
||||
className="text-text-primary cursor-pointer"
|
||||
size={16}
|
||||
onClick={() => {
|
||||
hideModal?.();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{loading && (
|
||||
<div className=" rounded-lg p-4 w-full h-full">
|
||||
<Progress value={percent} className="h-1 flex-1 min-w-10" />
|
||||
</div>
|
||||
)}
|
||||
{!loading && (
|
||||
<div className="bg-bg-card rounded-lg p-4 w-full h-full">
|
||||
<IndentedTree
|
||||
data={data}
|
||||
show
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
></IndentedTree>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
56
web/src/pages/next-search/ragflow-log.tsx
Normal file
56
web/src/pages/next-search/ragflow-log.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useFetchTokenListBeforeOtherStep } from '@/components/embed-dialog/use-show-embed-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SharedFrom } from '@/constants/chat';
|
||||
import { useFetchTenantInfo } from '@/hooks/use-user-setting-request';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Routes } from '@/routes';
|
||||
import { Send } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useFetchSearchDetail } from '../next-searches/hooks';
|
||||
import EmbedAppModal from './embed-app-modal';
|
||||
|
||||
export function RAGFlowLogo({
|
||||
onClick,
|
||||
}: {
|
||||
onClick?: React.MouseEventHandler<HTMLHeadingElement>;
|
||||
}) {
|
||||
const [openEmbed, setOpenEmbed] = useState(false);
|
||||
const { beta, handleOperate } = useFetchTokenListBeforeOtherStep();
|
||||
const { data: tenantInfo } = useFetchTenantInfo();
|
||||
const tenantId = tenantInfo.tenant_id;
|
||||
const { data: SearchData } = useFetchSearchDetail();
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 items-center">
|
||||
<h1
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'text-4xl font-bold bg-gradient-to-l from-[#40EBE3] to-[#4A51FF] bg-clip-text',
|
||||
)}
|
||||
>
|
||||
RAGFlow
|
||||
</h1>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
onClick={() => {
|
||||
handleOperate().then((res) => {
|
||||
if (res) {
|
||||
setOpenEmbed(!openEmbed);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Send />
|
||||
</Button>
|
||||
<EmbedAppModal
|
||||
open={openEmbed}
|
||||
setOpen={setOpenEmbed}
|
||||
url={Routes.SearchShare}
|
||||
token={SearchData?.id as string}
|
||||
from={SharedFrom.Search}
|
||||
tenantId={tenantId}
|
||||
beta={beta}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,11 +2,11 @@ import { Input } from '@/components/originui/input';
|
||||
import Spotlight from '@/components/spotlight';
|
||||
import message from '@/components/ui/message';
|
||||
import { IUserInfo } from '@/interfaces/database/user-setting';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Search } from 'lucide-react';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import './index.less';
|
||||
import { RAGFlowLogo } from './ragflow-log';
|
||||
|
||||
export default function SearchPage({
|
||||
isSearching,
|
||||
@@ -28,14 +28,7 @@ export default function SearchPage({
|
||||
return (
|
||||
<section className="relative w-full flex transition-all justify-center items-center mt-[15vh]">
|
||||
<div className="relative z-10 px-8 pt-8 flex text-transparent flex-col justify-center items-center w-[780px]">
|
||||
<h1
|
||||
className={cn(
|
||||
'text-4xl font-bold bg-gradient-to-l from-[#40EBE3] to-[#4A51FF] bg-clip-text',
|
||||
)}
|
||||
>
|
||||
RAGFlow
|
||||
</h1>
|
||||
|
||||
<RAGFlowLogo></RAGFlowLogo>
|
||||
<div className="rounded-lg text-primary text-xl sticky flex justify-center w-full transform scale-100 mt-8 p-6 h-[240px] border">
|
||||
{!isSearching && <Spotlight className="z-0" />}
|
||||
<div className="flex flex-col justify-center items-center w-2/3">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// src/pages/next-search/search-setting.tsx
|
||||
|
||||
import { AvatarUpload } from '@/components/avatar-upload';
|
||||
import AvatarNameDescription from '@/components/avatar-name-description';
|
||||
import { KnowledgeBaseFormField } from '@/components/knowledge-base-item';
|
||||
import {
|
||||
LlmSettingFieldItems,
|
||||
@@ -25,7 +25,6 @@ import { Input } from '@/components/ui/input';
|
||||
import { RAGFlowSelect } from '@/components/ui/select';
|
||||
import { Spin } from '@/components/ui/spin';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import {
|
||||
useComposeLlmOptionsByModelTypes,
|
||||
useSelectLlmOptionsByModelType,
|
||||
@@ -45,10 +44,6 @@ import {
|
||||
IllmSettingProps,
|
||||
useUpdateSearch,
|
||||
} from '../next-searches/hooks';
|
||||
// import {
|
||||
// LlmSettingFieldItems,
|
||||
// LlmSettingSchema,
|
||||
// } from './search-setting-aisummery-config';
|
||||
|
||||
interface SearchSettingProps {
|
||||
open: boolean;
|
||||
@@ -73,9 +68,17 @@ const SearchSettingFormSchema = z
|
||||
use_rerank: z.boolean(),
|
||||
top_k: z.number(),
|
||||
summary: z.boolean(),
|
||||
llm_setting: z.object(LlmSettingSchema),
|
||||
llm_setting: z.object({
|
||||
...LlmSettingSchema,
|
||||
parameter: z.string().optional(),
|
||||
}),
|
||||
related_search: z.boolean(),
|
||||
query_mindmap: z.boolean(),
|
||||
doc_ids: z.array(z.string()),
|
||||
chat_id: z.string(),
|
||||
highlight: z.boolean(),
|
||||
keyword: z.boolean(),
|
||||
chat_settingcross_languages: z.array(z.string()),
|
||||
...MetadataFilterSchema,
|
||||
}),
|
||||
})
|
||||
@@ -135,7 +138,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
|
||||
chat_id: search_config?.chat_id || '',
|
||||
llm_setting: {
|
||||
llm_id: search_config?.chat_id || '',
|
||||
parameter: llm_setting?.parameter,
|
||||
parameter: llm_setting?.parameter || '',
|
||||
temperature: llm_setting?.temperature || 0,
|
||||
top_p: llm_setting?.top_p || 0,
|
||||
frequency_penalty: llm_setting?.frequency_penalty || 0,
|
||||
@@ -246,7 +249,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'text-text-primary border p-4 pb-12 rounded-lg',
|
||||
'text-text-primary border p-4 pb-12 rounded-lg ',
|
||||
{
|
||||
'animate-fade-in-right': open,
|
||||
'animate-fade-out-right': !open,
|
||||
@@ -279,64 +282,8 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
|
||||
)}
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* Name */}
|
||||
<FormField
|
||||
control={formMethods.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<span className="text-destructive mr-1"> *</span>
|
||||
{t('search.name')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('search.name')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Avatar */}
|
||||
<FormField
|
||||
control={formMethods.control}
|
||||
name="avatar"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('search.avatar')}</FormLabel>
|
||||
<FormControl>
|
||||
<AvatarUpload {...field}></AvatarUpload>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Description */}
|
||||
<FormField
|
||||
control={formMethods.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('search.description')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={descriptionDefaultValue}
|
||||
{...field}
|
||||
onFocus={() => {
|
||||
if (field.value === descriptionDefaultValue) {
|
||||
field.onChange('');
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (field.value === '') {
|
||||
field.onChange(descriptionDefaultValue);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<AvatarNameDescription avatarField="avatar" />
|
||||
|
||||
<KnowledgeBaseFormField
|
||||
name="search_config.kb_ids"
|
||||
required
|
||||
|
||||
@@ -14,24 +14,20 @@ import {
|
||||
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
||||
import { IReference } from '@/interfaces/database/chat';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { citationMarkerReg } from '@/utils/citation-utils';
|
||||
import { getDirAttribute } from '@/utils/text-direction';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { BrainCircuit, Search, X } from 'lucide-react';
|
||||
import { ListTree, Search, X } from 'lucide-react';
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ISearchAppDetailProps } from '../next-searches/hooks';
|
||||
import PdfDrawer from './document-preview-modal';
|
||||
import ExpandableContent from './expandable-content';
|
||||
import { ISearchReturnProps } from './hooks';
|
||||
import './index.less';
|
||||
import MarkdownContent from './markdown-content';
|
||||
import MindMapDrawer from './mindmap-drawer';
|
||||
import { RAGFlowLogo } from './ragflow-log';
|
||||
import RetrievalDocuments from './retrieval-documents';
|
||||
|
||||
const getDirectionText = (content: string) =>
|
||||
content.replace(/<[^>]+>/g, ' ').replace(citationMarkerReg, '');
|
||||
|
||||
export default function SearchingView({
|
||||
setIsSearching,
|
||||
searchData,
|
||||
@@ -66,12 +62,7 @@ export default function SearchingView({
|
||||
searchData: ISearchAppDetailProps;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
// useEffect(() => {
|
||||
// const changeLanguage = async () => {
|
||||
// await i18n.changeLanguage('zh');
|
||||
// };
|
||||
// changeLanguage();
|
||||
// }, [i18n]);
|
||||
|
||||
const [searchtext, setSearchtext] = useState<string>('');
|
||||
const [retrievalLoading, setRetrievalLoading] = useState(false);
|
||||
|
||||
@@ -81,28 +72,23 @@ export default function SearchingView({
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'relative w-full flex transition-all justify-start items-center',
|
||||
'relative w-full flex transition-all justify-start items-center h-full',
|
||||
)}
|
||||
>
|
||||
{/* search header */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10 px-8 pt-8 flex text-transparent justify-start items-start w-full',
|
||||
'relative z-10 px-8 pt-8 flex text-transparent justify-start items-start w-full h-full',
|
||||
)}
|
||||
>
|
||||
<h1
|
||||
className={cn(
|
||||
'text-4xl font-bold bg-gradient-to-l from-[#40EBE3] to-[#4A51FF] bg-clip-text cursor-pointer',
|
||||
)}
|
||||
<RAGFlowLogo
|
||||
onClick={() => {
|
||||
setIsSearching?.(false);
|
||||
}}
|
||||
>
|
||||
RAGFlow
|
||||
</h1>
|
||||
></RAGFlowLogo>
|
||||
<div
|
||||
className={cn(
|
||||
' rounded-lg text-primary text-xl sticky flex flex-col justify-center w-2/3 max-w-[780px] transform scale-100 ml-16 ',
|
||||
' rounded-lg text-primary text-xl sticky flex flex-col justify-center w-2/3 transform scale-100 ml-16 h-full',
|
||||
)}
|
||||
>
|
||||
<div className={cn('flex flex-col justify-start items-start w-full')}>
|
||||
@@ -156,7 +142,7 @@ export default function SearchingView({
|
||||
</div>
|
||||
{/* search body */}
|
||||
<div
|
||||
className="w-full mt-5 overflow-auto scrollbar-none "
|
||||
className="w-full mt-5 overflow-auto scrollbar-thin "
|
||||
style={{ height: 'calc(100vh - 250px)' }}
|
||||
>
|
||||
{searchData.search_config.summary && !isSearchStrEmpty && (
|
||||
@@ -168,13 +154,15 @@ export default function SearchingView({
|
||||
<SkeletonCard className=" mt-2" />
|
||||
) : (
|
||||
answer.answer && (
|
||||
<div className="border rounded-lg p-4 mt-3 max-h-52 overflow-auto scrollbar-none">
|
||||
<MarkdownContent
|
||||
loading={sendingLoading}
|
||||
content={answer.answer}
|
||||
reference={answer.reference ?? ({} as IReference)}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
></MarkdownContent>
|
||||
<div className="border rounded-lg p-4 mt-3">
|
||||
<ExpandableContent maxHeight={208}>
|
||||
<MarkdownContent
|
||||
loading={sendingLoading}
|
||||
content={answer.answer}
|
||||
reference={answer.reference ?? ({} as IReference)}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
/>
|
||||
</ExpandableContent>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
@@ -206,42 +194,15 @@ export default function SearchingView({
|
||||
return (
|
||||
<div key={index}>
|
||||
<div className="w-full flex flex-col">
|
||||
<div className="w-full highlightContent">
|
||||
<div className="w-full">
|
||||
{(chunk.image_id || chunk.img_id) && (
|
||||
<ImageWithPopover
|
||||
id={chunk.image_id || chunk.img_id}
|
||||
></ImageWithPopover>
|
||||
)}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(
|
||||
`${
|
||||
chunk.highlight ??
|
||||
chunk.content_with_weight ??
|
||||
''
|
||||
}...`,
|
||||
),
|
||||
}}
|
||||
className="text-sm text-text-primary mb-1"
|
||||
dir={getDirAttribute(
|
||||
getDirectionText(
|
||||
chunk.highlight ??
|
||||
chunk.content_with_weight ??
|
||||
'',
|
||||
),
|
||||
)}
|
||||
></div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="text-text-primary !w-full max-w-lg ">
|
||||
<div className="max-h-96 overflow-auto scrollbar-thin">
|
||||
<HighLightMarkdown>
|
||||
{chunk.content_with_weight}
|
||||
</HighLightMarkdown>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<HighLightMarkdown>
|
||||
{chunk.content_with_weight}
|
||||
</HighLightMarkdown>
|
||||
</div>
|
||||
<div
|
||||
className="flex gap-2 items-center text-xs text-text-secondary border p-1 rounded-lg w-fit mt-3"
|
||||
@@ -331,13 +292,13 @@ export default function SearchingView({
|
||||
searchData.search_config.query_mindmap && (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
className="rounded-lg h-16 w-16 p-0 absolute top-28 right-3 z-30 border cursor-pointer flex justify-center items-center bg-bg-card"
|
||||
<Button
|
||||
onClick={showMindMapModal}
|
||||
variant={'outline'}
|
||||
className="absolute top-28 right-3 z-30 rounded-full size-6"
|
||||
>
|
||||
{/* <SvgIcon name="paper-clip" width={24} height={30}></SvgIcon> */}
|
||||
<BrainCircuit size={36} />
|
||||
</div>
|
||||
<ListTree />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit">{t('chunk.mind')}</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
Reference in New Issue
Block a user