diff --git a/web/src/app.tsx b/web/src/app.tsx index 01a97f653..28f019a6d 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -47,7 +47,7 @@ if (process.env.NODE_ENV === 'development') { whyDidYouRender(React, { trackAllPureComponents: true, trackExtraHooks: [], - logOnDifferentValues: true, + logOnDifferentValues: false, exclude: [/^RouterProvider$/], }); }, diff --git a/web/src/components/avatar-name-description/editable-field.tsx b/web/src/components/avatar-name-description/editable-field.tsx new file mode 100644 index 000000000..08b52459e --- /dev/null +++ b/web/src/components/avatar-name-description/editable-field.tsx @@ -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 ( + + {(field) => + isEditing ? ( + } + name={field.name} + value={field.value || ''} + onChange={(e: ChangeEvent) => + 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} + /> + ) : ( +
+ + {field.value || finalPlaceholder} + + {showEditIcon && ( + + )} +
+ ) + } +
+ ); +} + +export default EditableField; diff --git a/web/src/components/avatar-name-description/editable-textarea.tsx b/web/src/components/avatar-name-description/editable-textarea.tsx new file mode 100644 index 000000000..cf15719f7 --- /dev/null +++ b/web/src/components/avatar-name-description/editable-textarea.tsx @@ -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(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) => { + if (e.key === 'Escape') { + setIsEditing(false); + } + }, + [], + ); + + return ( +
+ + {(field) => + isEditing ? ( + - - - - )} - /> + - +
diff --git a/web/src/pages/next-search/expandable-content.tsx b/web/src/pages/next-search/expandable-content.tsx new file mode 100644 index 000000000..310cd0587 --- /dev/null +++ b/web/src/pages/next-search/expandable-content.tsx @@ -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(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((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 ( +
+
+ {children} +
+ + {!isExpanded && isOverflowing && ( +
+ )} + + {isOverflowing && ( + + )} +
+ ); +} diff --git a/web/src/pages/next-search/index.tsx b/web/src/pages/next-search/index.tsx index 87e5aca0e..47d51cf7a 100644 --- a/web/src/pages/next-search/index.tsx +++ b/web/src/pages/next-search/index.tsx @@ -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 ( -
-
-
+
+
+
{!isSearching && (
)} {isSearching && ( -
+
)} - { - - } - { - // - }
-
- -
- {!isSearching && ( -
- -
- )} + +
); } diff --git a/web/src/pages/next-search/mindmap-drawer.tsx b/web/src/pages/next-search/mindmap-drawer.tsx index cf5853d5e..fa5a80c76 100644 --- a/web/src/pages/next-search/mindmap-drawer.tsx +++ b/web/src/pages/next-search/mindmap-drawer.tsx @@ -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 { data: any; } -const MindMapDrawer = ({ data, hideModal, loading }: IProps) => { +const MindMapDrawer = ({ data, hideModal, loading, visible }: IProps) => { const { t } = useTranslation(); const percent = usePendingMindMap(); return ( -
-
-
- {t('chunk.mind')} + + + + +
+
+ {t('chunk.mind')} +
+ { + hideModal?.(); + }} + /> +
+
+
+ {loading && ( +
+ +
+ )} + {!loading && ( +
+ +
+ )}
- { - hideModal?.(); - }} - /> -
- {loading && ( -
- -
- )} - {!loading && ( -
- -
- )} -
+ + ); }; diff --git a/web/src/pages/next-search/ragflow-log.tsx b/web/src/pages/next-search/ragflow-log.tsx new file mode 100644 index 000000000..fbe2f1952 --- /dev/null +++ b/web/src/pages/next-search/ragflow-log.tsx @@ -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; +}) { + const [openEmbed, setOpenEmbed] = useState(false); + const { beta, handleOperate } = useFetchTokenListBeforeOtherStep(); + const { data: tenantInfo } = useFetchTenantInfo(); + const tenantId = tenantInfo.tenant_id; + const { data: SearchData } = useFetchSearchDetail(); + + return ( +
+

+ RAGFlow +

+ + +
+ ); +} diff --git a/web/src/pages/next-search/search-home.tsx b/web/src/pages/next-search/search-home.tsx index 63e9f7bb3..281546c3b 100644 --- a/web/src/pages/next-search/search-home.tsx +++ b/web/src/pages/next-search/search-home.tsx @@ -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 (
-

- RAGFlow -

- +
{!isSearching && }
diff --git a/web/src/pages/next-search/search-setting.tsx b/web/src/pages/next-search/search-setting.tsx index 75fedb626..db10d2cb8 100644 --- a/web/src/pages/next-search/search-setting.tsx +++ b/web/src/pages/next-search/search-setting.tsx @@ -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 = ({ 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 = ({ return (
= ({ )} className="space-y-6" > - {/* Name */} - ( - - - * - {t('search.name')} - - - - - - - )} - /> - {/* Avatar */} - ( - - {t('search.avatar')} - - - - - - )} - /> - {/* Description */} - ( - - {t('search.description')} - -