### What problem does this PR solve? Feat: Add metadata configuration for new chats #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.2
| @@ -70,6 +70,10 @@ const KnowledgeBaseItem = ({ | |||
| export default KnowledgeBaseItem; | |||
| function buildQueryVariableOptionsByShowVariable(showVariable?: boolean) { | |||
| return showVariable ? useBuildQueryVariableOptions : () => []; | |||
| } | |||
| export function KnowledgeBaseFormField({ | |||
| showVariable = false, | |||
| }: { | |||
| @@ -84,7 +88,7 @@ export function KnowledgeBaseFormField({ | |||
| (x) => x.parser_id !== DocumentParserType.Tag, | |||
| ); | |||
| const nextOptions = useBuildQueryVariableOptions(); | |||
| const nextOptions = buildQueryVariableOptionsByShowVariable(showVariable)(); | |||
| const knowledgeOptions = filteredKnowledgeList.map((x) => ({ | |||
| label: x.name, | |||
| @@ -1,9 +1,8 @@ | |||
| import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg'; | |||
| import { MessageType } from '@/constants/chat'; | |||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { IReference, IReferenceChunk } from '@/interfaces/database/chat'; | |||
| import classNames from 'classnames'; | |||
| import { memo, useCallback, useEffect, useMemo, useState } from 'react'; | |||
| import { memo, useCallback, useEffect, useMemo } from 'react'; | |||
| import { | |||
| useFetchDocumentInfosByIds, | |||
| @@ -12,17 +11,13 @@ import { | |||
| import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks'; | |||
| import { IMessage } from '@/pages/chat/interface'; | |||
| import MarkdownContent from '@/pages/chat/markdown-content'; | |||
| import { getExtension, isImage } from '@/utils/document-util'; | |||
| import { Avatar, Button, Flex, List, Space, Typography } from 'antd'; | |||
| import FileIcon from '../file-icon'; | |||
| import IndentedTreeModal from '../indented-tree/modal'; | |||
| import NewDocumentLink from '../new-document-link'; | |||
| import { Avatar, Flex, Space } from 'antd'; | |||
| import { ReferenceDocumentList } from '../next-message-item/reference-document-list'; | |||
| import { InnerUploadedMessageFiles } from '../next-message-item/uploaded-message-files'; | |||
| import { useTheme } from '../theme-provider'; | |||
| import { AssistantGroupButton, UserGroupButton } from './group-button'; | |||
| import styles from './index.less'; | |||
| const { Text } = Typography; | |||
| interface IProps extends Partial<IRemoveMessageById>, IRegenerateMessage { | |||
| item: IMessage; | |||
| reference: IReference; | |||
| @@ -59,21 +54,11 @@ const MessageItem = ({ | |||
| const { data: documentList, setDocumentIds } = useFetchDocumentInfosByIds(); | |||
| const { data: documentThumbnails, setDocumentIds: setIds } = | |||
| useFetchDocumentThumbnailsByIds(); | |||
| const { visible, hideModal, showModal } = useSetModalState(); | |||
| const [clickedDocumentId, setClickedDocumentId] = useState(''); | |||
| const referenceDocumentList = useMemo(() => { | |||
| return reference?.doc_aggs ?? []; | |||
| }, [reference?.doc_aggs]); | |||
| const handleUserDocumentClick = useCallback( | |||
| (id: string) => () => { | |||
| setClickedDocumentId(id); | |||
| showModal(); | |||
| }, | |||
| [showModal], | |||
| ); | |||
| const handleRegenerateMessage = useCallback(() => { | |||
| regenerateMessage?.(item); | |||
| }, [regenerateMessage, item]); | |||
| @@ -160,83 +145,18 @@ const MessageItem = ({ | |||
| ></MarkdownContent> | |||
| </div> | |||
| {isAssistant && referenceDocumentList.length > 0 && ( | |||
| <List | |||
| bordered | |||
| dataSource={referenceDocumentList} | |||
| renderItem={(item) => { | |||
| return ( | |||
| <List.Item> | |||
| <Flex gap={'small'} align="center"> | |||
| <FileIcon | |||
| id={item.doc_id} | |||
| name={item.doc_name} | |||
| ></FileIcon> | |||
| <NewDocumentLink | |||
| documentId={item.doc_id} | |||
| documentName={item.doc_name} | |||
| prefix="document" | |||
| link={item.url} | |||
| > | |||
| {item.doc_name} | |||
| </NewDocumentLink> | |||
| </Flex> | |||
| </List.Item> | |||
| ); | |||
| }} | |||
| /> | |||
| <ReferenceDocumentList | |||
| list={referenceDocumentList} | |||
| ></ReferenceDocumentList> | |||
| )} | |||
| {isUser && documentList.length > 0 && ( | |||
| <List | |||
| bordered | |||
| dataSource={documentList} | |||
| renderItem={(item) => { | |||
| // TODO: | |||
| // const fileThumbnail = | |||
| // documentThumbnails[item.id] || documentThumbnails[item.id]; | |||
| const fileExtension = getExtension(item.name); | |||
| return ( | |||
| <List.Item> | |||
| <Flex gap={'small'} align="center"> | |||
| <FileIcon id={item.id} name={item.name}></FileIcon> | |||
| {isImage(fileExtension) ? ( | |||
| <NewDocumentLink | |||
| documentId={item.id} | |||
| documentName={item.name} | |||
| prefix="document" | |||
| > | |||
| {item.name} | |||
| </NewDocumentLink> | |||
| ) : ( | |||
| <Button | |||
| type={'text'} | |||
| onClick={handleUserDocumentClick(item.id)} | |||
| > | |||
| <Text | |||
| style={{ maxWidth: '40vw' }} | |||
| ellipsis={{ tooltip: item.name }} | |||
| > | |||
| {item.name} | |||
| </Text> | |||
| </Button> | |||
| )} | |||
| </Flex> | |||
| </List.Item> | |||
| ); | |||
| }} | |||
| /> | |||
| <InnerUploadedMessageFiles | |||
| files={documentList} | |||
| ></InnerUploadedMessageFiles> | |||
| )} | |||
| </Flex> | |||
| </div> | |||
| </section> | |||
| {visible && ( | |||
| <IndentedTreeModal | |||
| visible={visible} | |||
| hideModal={hideModal} | |||
| documentId={clickedDocumentId} | |||
| ></IndentedTreeModal> | |||
| )} | |||
| </div> | |||
| ); | |||
| }; | |||
| @@ -116,64 +116,6 @@ export const AssistantGroupButton = ({ | |||
| )} | |||
| </> | |||
| ); | |||
| return ( | |||
| <> | |||
| <Radio.Group size="small"> | |||
| <Radio.Button value="a"> | |||
| <CopyToClipboard text={content}></CopyToClipboard> | |||
| </Radio.Button> | |||
| {showLoudspeaker && ( | |||
| <Radio.Button value="b" onClick={handleRead}> | |||
| <Tooltip title={t('chat.read')}> | |||
| {isPlaying ? <PauseCircleOutlined /> : <SoundOutlined />} | |||
| </Tooltip> | |||
| <audio src="" ref={ref}></audio> | |||
| </Radio.Button> | |||
| )} | |||
| {showLikeButton && ( | |||
| <> | |||
| <Radio.Button value="c" onClick={handleLike}> | |||
| <LikeOutlined /> | |||
| </Radio.Button> | |||
| <Radio.Button value="d" onClick={showModal}> | |||
| <DislikeOutlined /> | |||
| </Radio.Button> | |||
| </> | |||
| )} | |||
| {prompt && ( | |||
| <Radio.Button value="e" onClick={showPromptModal}> | |||
| <PromptIcon style={{ fontSize: '16px' }} /> | |||
| </Radio.Button> | |||
| )} | |||
| <Radio.Button | |||
| value="f" | |||
| onClick={(e) => { | |||
| e.preventDefault(); | |||
| e.stopPropagation(); | |||
| handleShowLogSheet(); | |||
| }} | |||
| > | |||
| <NotebookText className="size-4" /> | |||
| </Radio.Button> | |||
| </Radio.Group> | |||
| {visible && ( | |||
| <FeedbackModal | |||
| visible={visible} | |||
| hideModal={hideModal} | |||
| onOk={onFeedbackOk} | |||
| loading={loading} | |||
| ></FeedbackModal> | |||
| )} | |||
| {promptVisible && ( | |||
| <PromptModal | |||
| visible={promptVisible} | |||
| hideModal={hidePromptModal} | |||
| prompt={prompt} | |||
| ></PromptModal> | |||
| )} | |||
| </> | |||
| ); | |||
| }; | |||
| interface UserGroupButtonProps extends Partial<IRemoveMessageById> { | |||
| @@ -1,6 +1,5 @@ | |||
| import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg'; | |||
| import { MessageType } from '@/constants/chat'; | |||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { IReferenceChunk, IReferenceObject } from '@/interfaces/database/chat'; | |||
| import classNames from 'classnames'; | |||
| import { | |||
| @@ -21,7 +20,6 @@ import { WorkFlowTimeline } from '@/pages/agent/log-sheet/workflow-timeline'; | |||
| import { IMessage } from '@/pages/chat/interface'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { Atom, ChevronDown, ChevronUp } from 'lucide-react'; | |||
| import IndentedTreeModal from '../indented-tree/modal'; | |||
| import MarkdownContent from '../next-markdown-content'; | |||
| import { RAGFlowAvatar } from '../ragflow-avatar'; | |||
| import { useTheme } from '../theme-provider'; | |||
| @@ -79,8 +77,6 @@ function MessageItem({ | |||
| const { theme } = useTheme(); | |||
| const isAssistant = item.role === MessageType.Assistant; | |||
| const isUser = item.role === MessageType.User; | |||
| const { visible, hideModal } = useSetModalState(); | |||
| const [clickedDocumentId] = useState(''); | |||
| const [showThinking, setShowThinking] = useState(false); | |||
| const { setLastSendLoadingFunc } = useContext(AgentChatContext); | |||
| @@ -200,8 +196,6 @@ function MessageItem({ | |||
| sendLoading={sendLoading} | |||
| ></UserGroupButton> | |||
| )} | |||
| {/* <b>{isAssistant ? '' : nickname}</b> */} | |||
| </div> | |||
| </div> | |||
| @@ -254,13 +248,6 @@ function MessageItem({ | |||
| </section> | |||
| </div> | |||
| </section> | |||
| {visible && ( | |||
| <IndentedTreeModal | |||
| visible={visible} | |||
| hideModal={hideModal} | |||
| documentId={clickedDocumentId} | |||
| ></IndentedTreeModal> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -8,7 +8,7 @@ export function ReferenceDocumentList({ list }: { list: Docagg[] }) { | |||
| <section className="flex gap-3 flex-wrap"> | |||
| {list.map((item) => ( | |||
| <Card key={item.doc_id}> | |||
| <CardContent className="p-2"> | |||
| <CardContent className="p-2 space-x-2"> | |||
| <FileIcon id={item.doc_id} name={item.doc_name}></FileIcon> | |||
| <NewDocumentLink | |||
| documentId={item.doc_id} | |||
| @@ -1,34 +1,65 @@ | |||
| import { IDocumentInfo } from '@/interfaces/database/document'; | |||
| import { getExtension } from '@/utils/document-util'; | |||
| import { formatBytes } from '@/utils/file-util'; | |||
| import { memo } from 'react'; | |||
| import FileIcon from '../file-icon'; | |||
| import NewDocumentLink from '../new-document-link'; | |||
| import SvgIcon from '../svg-icon'; | |||
| interface IProps { | |||
| files?: File[]; | |||
| files?: File[] | IDocumentInfo[]; | |||
| } | |||
| type NameWidgetType = { | |||
| name: string; | |||
| size: number; | |||
| id?: string; | |||
| }; | |||
| function NameWidget({ name, size, id }: NameWidgetType) { | |||
| return ( | |||
| <div className="text-xs max-w-20"> | |||
| {id ? ( | |||
| <NewDocumentLink documentId={id} documentName={name} prefix="document"> | |||
| {name} | |||
| </NewDocumentLink> | |||
| ) : ( | |||
| <div className="truncate">{name}</div> | |||
| )} | |||
| <p className="text-text-secondary pt-1">{formatBytes(size)}</p> | |||
| </div> | |||
| ); | |||
| } | |||
| export function InnerUploadedMessageFiles({ files = [] }: IProps) { | |||
| return ( | |||
| <section className="flex gap-2 pt-2"> | |||
| {files?.map((file, idx) => ( | |||
| <div key={idx} className="flex gap-1 border rounded-md p-1.5"> | |||
| {file.type.startsWith('image/') ? ( | |||
| <img | |||
| src={URL.createObjectURL(file)} | |||
| alt={file.name} | |||
| className="size-10 object-cover" | |||
| /> | |||
| ) : ( | |||
| <SvgIcon | |||
| name={`file-icon/${getExtension(file.name)}`} | |||
| width={24} | |||
| ></SvgIcon> | |||
| )} | |||
| <div className="text-xs max-w-20"> | |||
| <div className="truncate">{file.name}</div> | |||
| <p className="text-text-secondary pt-1">{formatBytes(file.size)}</p> | |||
| {files?.map((file, idx) => { | |||
| const name = file.name; | |||
| const isFile = file instanceof File; | |||
| return ( | |||
| <div key={idx} className="flex gap-1 border rounded-md p-1.5"> | |||
| {!isFile ? ( | |||
| <FileIcon id={file.id} name={name}></FileIcon> | |||
| ) : file.type.startsWith('image/') ? ( | |||
| <img | |||
| src={URL.createObjectURL(file)} | |||
| alt={name} | |||
| className="size-10 object-cover" | |||
| /> | |||
| ) : ( | |||
| <SvgIcon | |||
| name={`file-icon/${getExtension(name)}`} | |||
| width={24} | |||
| ></SvgIcon> | |||
| )} | |||
| <NameWidget | |||
| name={name} | |||
| size={file.size} | |||
| id={isFile ? undefined : file.id} | |||
| ></NameWidget> | |||
| </div> | |||
| </div> | |||
| ))} | |||
| ); | |||
| })} | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -57,6 +57,18 @@ export interface IDialog { | |||
| similarity_threshold: number; | |||
| top_k: number; | |||
| top_n: number; | |||
| meta_data_filter: MetaDataFilter; | |||
| } | |||
| interface MetaDataFilter { | |||
| manual: Manual[]; | |||
| method: string; | |||
| } | |||
| interface Manual { | |||
| key: string; | |||
| op: string; | |||
| value: string; | |||
| } | |||
| export interface IConversation { | |||
| @@ -2,6 +2,8 @@ | |||
| import { FileUploader } from '@/components/file-uploader'; | |||
| import { KnowledgeBaseFormField } from '@/components/knowledge-base-item'; | |||
| import { SelectWithSearch } from '@/components/originui/select-with-search'; | |||
| import { RAGFlowFormItem } from '@/components/ragflow-form'; | |||
| import { SwitchFormField } from '@/components/switch-fom-field'; | |||
| import { TavilyFormField } from '@/components/tavily-form-field'; | |||
| import { | |||
| @@ -14,11 +16,26 @@ import { | |||
| import { Input } from '@/components/ui/input'; | |||
| import { Textarea } from '@/components/ui/textarea'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { useFormContext } from 'react-hook-form'; | |||
| import { useFormContext, useWatch } from 'react-hook-form'; | |||
| import { DatasetMetadata } from '../../constants'; | |||
| import { MetadataFilterConditions } from './metadata-filter-conditions'; | |||
| export default function ChatBasicSetting() { | |||
| const { t } = useTranslate('chat'); | |||
| const form = useFormContext(); | |||
| const kbIds: string[] = useWatch({ control: form.control, name: 'kb_ids' }); | |||
| const metadata = useWatch({ | |||
| control: form.control, | |||
| name: 'meta_data_filter.method', | |||
| }); | |||
| const hasKnowledge = Array.isArray(kbIds) && kbIds.length > 0; | |||
| const MetadataOptions = Object.values(DatasetMetadata).map((x) => { | |||
| return { | |||
| value: x, | |||
| label: t(`meta.${x}`), | |||
| }; | |||
| }); | |||
| return ( | |||
| <div className="space-y-8"> | |||
| @@ -108,6 +125,18 @@ export default function ChatBasicSetting() { | |||
| ></SwitchFormField> | |||
| <TavilyFormField></TavilyFormField> | |||
| <KnowledgeBaseFormField></KnowledgeBaseFormField> | |||
| {hasKnowledge && ( | |||
| <RAGFlowFormItem | |||
| label={t('metadata')} | |||
| name={'meta_data_filter.method'} | |||
| tooltip={t('metadataTip')} | |||
| > | |||
| <SelectWithSearch options={MetadataOptions} /> | |||
| </RAGFlowFormItem> | |||
| )} | |||
| {hasKnowledge && metadata === DatasetMetadata.Manual && ( | |||
| <MetadataFilterConditions kbIds={kbIds}></MetadataFilterConditions> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -9,6 +9,7 @@ import { useEffect } from 'react'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { useParams } from 'umi'; | |||
| import { z } from 'zod'; | |||
| import { DatasetMetadata } from '../../constants'; | |||
| import ChatBasicSetting from './chat-basic-settings'; | |||
| import { ChatModelSettings } from './chat-model-settings'; | |||
| import { ChatPromptEngine } from './chat-prompt-engine'; | |||
| @@ -38,6 +39,10 @@ export function ChatSettings({ switchSettingVisible }: ChatSettingsProps) { | |||
| top_n: 8, | |||
| vector_similarity_weight: 0.2, | |||
| top_k: 1024, | |||
| meta_data_filter: { | |||
| method: DatasetMetadata.Disabled, | |||
| manual: [], | |||
| }, | |||
| }, | |||
| }); | |||
| @@ -0,0 +1,129 @@ | |||
| import { SelectWithSearch } from '@/components/originui/select-with-search'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { | |||
| DropdownMenu, | |||
| DropdownMenuContent, | |||
| DropdownMenuItem, | |||
| DropdownMenuTrigger, | |||
| } from '@/components/ui/dropdown-menu'; | |||
| import { | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { Input } from '@/components/ui/input'; | |||
| import { Separator } from '@/components/ui/separator'; | |||
| import { useFetchKnowledgeMetadata } from '@/hooks/use-knowledge-request'; | |||
| import { SwitchOperatorOptions } from '@/pages/agent/constant'; | |||
| import { useBuildSwitchOperatorOptions } from '@/pages/agent/form/switch-form'; | |||
| import { Plus, X } from 'lucide-react'; | |||
| import { useCallback } from 'react'; | |||
| import { useFieldArray, useFormContext } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| export function MetadataFilterConditions({ kbIds }: { kbIds: string[] }) { | |||
| const { t } = useTranslation(); | |||
| const form = useFormContext(); | |||
| const name = 'meta_data_filter.manual'; | |||
| const metadata = useFetchKnowledgeMetadata(kbIds); | |||
| const switchOperatorOptions = useBuildSwitchOperatorOptions(); | |||
| const { fields, remove, append } = useFieldArray({ | |||
| name, | |||
| control: form.control, | |||
| }); | |||
| const add = useCallback( | |||
| (key: string) => () => { | |||
| append({ | |||
| key, | |||
| value: '', | |||
| op: SwitchOperatorOptions[0].value, | |||
| }); | |||
| }, | |||
| [append], | |||
| ); | |||
| return ( | |||
| <section className="flex flex-col gap-2"> | |||
| <div className="flex items-center justify-between"> | |||
| <FormLabel>{t('chat.conditions')}</FormLabel> | |||
| <DropdownMenu> | |||
| <DropdownMenuTrigger> | |||
| <Button variant={'ghost'} type="button"> | |||
| <Plus /> | |||
| </Button> | |||
| </DropdownMenuTrigger> | |||
| <DropdownMenuContent> | |||
| {Object.keys(metadata.data).map((key, idx) => { | |||
| return ( | |||
| <DropdownMenuItem key={idx} onClick={add(key)}> | |||
| {key} | |||
| </DropdownMenuItem> | |||
| ); | |||
| })} | |||
| </DropdownMenuContent> | |||
| </DropdownMenu> | |||
| </div> | |||
| <div className="space-y-5"> | |||
| {fields.map((field, index) => { | |||
| const typeField = `${name}.${index}.key`; | |||
| return ( | |||
| <div key={field.id} className="flex w-full items-center gap-2"> | |||
| <FormField | |||
| control={form.control} | |||
| name={typeField} | |||
| render={({ field }) => ( | |||
| <FormItem className="flex-1 overflow-hidden"> | |||
| <FormControl> | |||
| <Input | |||
| {...field} | |||
| placeholder={t('common.pleaseInput')} | |||
| ></Input> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <Separator className="w-3 text-text-secondary" /> | |||
| <FormField | |||
| control={form.control} | |||
| name={`${name}.${index}.op`} | |||
| render={({ field }) => ( | |||
| <FormItem className="flex-1 overflow-hidden"> | |||
| <FormControl> | |||
| <SelectWithSearch | |||
| {...field} | |||
| options={switchOperatorOptions} | |||
| ></SelectWithSearch> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <Separator className="w-3 text-text-secondary" /> | |||
| <FormField | |||
| control={form.control} | |||
| name={`${name}.${index}.value`} | |||
| render={({ field }) => ( | |||
| <FormItem className="flex-1 overflow-hidden"> | |||
| <FormControl> | |||
| <Input placeholder={t('common.pleaseInput')} {...field} /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <Button variant={'ghost'} onClick={() => remove(index)}> | |||
| <X className="text-text-sub-title-invert " /> | |||
| </Button> | |||
| </div> | |||
| ); | |||
| })} | |||
| </div> | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -43,6 +43,18 @@ export function useChatSettingSchema() { | |||
| llm_id: z.string().optional(), | |||
| ...vectorSimilarityWeightSchema, | |||
| ...topnSchema, | |||
| meta_data_filter: z | |||
| .object({ | |||
| method: z.string().optional(), | |||
| manual: z.array( | |||
| z.object({ | |||
| key: z.string(), | |||
| op: z.string(), | |||
| value: z.string(), | |||
| }), | |||
| ), | |||
| }) | |||
| .optional(), | |||
| }); | |||
| return formSchema; | |||
| @@ -27,6 +27,7 @@ export function SingleChatBox({ controller }: IProps) { | |||
| messageContainerRef, | |||
| sendLoading, | |||
| derivedMessages, | |||
| isUploading, | |||
| handleInputChange, | |||
| handlePressEnter, | |||
| regenerateMessage, | |||
| @@ -91,6 +92,7 @@ export function SingleChatBox({ controller }: IProps) { | |||
| } | |||
| stopOutputMessage={stopOutputMessage} | |||
| onUpload={handleUploadFile} | |||
| isUploading={isUploading} | |||
| /> | |||
| </section> | |||
| ); | |||
| @@ -0,0 +1,7 @@ | |||
| export const EmptyConversationId = 'empty'; | |||
| export enum DatasetMetadata { | |||
| Disabled = 'disabled', | |||
| Automatic = 'automatic', | |||
| Manual = 'manual', | |||
| } | |||
| @@ -138,7 +138,8 @@ export const useSendMessage = (controller: AbortController) => { | |||
| const { conversationId, isNew } = useGetChatSearchParams(); | |||
| const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | |||
| const { handleUploadFile, fileIds, clearFileIds } = useUploadFile(); | |||
| const { handleUploadFile, fileIds, clearFileIds, isUploading } = | |||
| useUploadFile(); | |||
| const { send, answer, done } = useSendMessageWithSse( | |||
| api.completeConversation, | |||
| @@ -285,5 +286,6 @@ export const useSendMessage = (controller: AbortController) => { | |||
| removeMessageById, | |||
| stopOutputMessage, | |||
| handleUploadFile, | |||
| isUploading, | |||
| }; | |||
| }; | |||
| @@ -3,7 +3,7 @@ import { useUploadAndParseFile } from '@/hooks/use-chat-request'; | |||
| import { useCallback, useState } from 'react'; | |||
| export function useUploadFile() { | |||
| const { uploadAndParseFile } = useUploadAndParseFile(); | |||
| const { uploadAndParseFile, loading } = useUploadAndParseFile(); | |||
| const [fileIds, setFileIds] = useState<string[]>([]); | |||
| const handleUploadFile: NonNullable<FileUploadProps['onUpload']> = | |||
| @@ -23,5 +23,5 @@ export function useUploadFile() { | |||
| setFileIds([]); | |||
| }, []); | |||
| return { handleUploadFile, clearFileIds, fileIds }; | |||
| return { handleUploadFile, clearFileIds, fileIds, isUploading: loading }; | |||
| } | |||