### What problem does this PR solve? feat: send question with retrieval api #2247 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.11.0
| @@ -1,19 +0,0 @@ | |||
| import { api_host } from '@/utils/api'; | |||
| interface IImage { | |||
| id: string; | |||
| className: string; | |||
| } | |||
| const Image = ({ id, className, ...props }: IImage) => { | |||
| return ( | |||
| <img | |||
| {...props} | |||
| src={`${api_host}/document/image/${id}`} | |||
| alt="" | |||
| className={className} | |||
| /> | |||
| ); | |||
| }; | |||
| export default Image; | |||
| @@ -0,0 +1,10 @@ | |||
| .image { | |||
| width: 100px; | |||
| object-fit: contain; | |||
| } | |||
| .imagePreview { | |||
| display: block; | |||
| max-width: 45vw; | |||
| max-height: 40vh; | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| import { api_host } from '@/utils/api'; | |||
| import { Popover } from 'antd'; | |||
| import styles from './index.less'; | |||
| interface IImage { | |||
| id: string; | |||
| className: string; | |||
| } | |||
| const Image = ({ id, className, ...props }: IImage) => { | |||
| return ( | |||
| <img | |||
| {...props} | |||
| src={`${api_host}/document/image/${id}`} | |||
| alt="" | |||
| className={className} | |||
| /> | |||
| ); | |||
| }; | |||
| export default Image; | |||
| export const ImageWithPopover = ({ id }: { id: string }) => { | |||
| return ( | |||
| <Popover | |||
| placement="left" | |||
| content={<Image id={id} className={styles.imagePreview}></Image>} | |||
| > | |||
| <Image id={id} className={styles.image}></Image> | |||
| </Popover> | |||
| ); | |||
| }; | |||
| @@ -5,7 +5,10 @@ import { | |||
| IStats, | |||
| IToken, | |||
| } from '@/interfaces/database/chat'; | |||
| import { IFeedbackRequestBody } from '@/interfaces/request/chat'; | |||
| import { | |||
| IAskRequestBody, | |||
| IFeedbackRequestBody, | |||
| } from '@/interfaces/request/chat'; | |||
| import i18n from '@/locales/config'; | |||
| import { IClientConversation } from '@/pages/chat/interface'; | |||
| import chatService from '@/services/chat-service'; | |||
| @@ -477,3 +480,23 @@ export const useFetchNextSharedConversation = (conversationId: string) => { | |||
| }; | |||
| //#endregion | |||
| //#region search page | |||
| export const useFetchMindMap = () => { | |||
| const { | |||
| data, | |||
| isPending: loading, | |||
| mutateAsync, | |||
| } = useMutation({ | |||
| mutationKey: ['fetchMindMap'], | |||
| mutationFn: async (params: IAskRequestBody) => { | |||
| const { data } = await chatService.getMindMap(params); | |||
| return data; | |||
| }, | |||
| }); | |||
| return { data, loading, fetchMindMap: mutateAsync }; | |||
| }; | |||
| //#endregion | |||
| @@ -209,7 +209,7 @@ export const useTestChunkRetrieval = (): ResponsePostType<ITestingResult> & { | |||
| mutationFn: async (values: any) => { | |||
| const { data } = await kbService.retrieval_test({ | |||
| ...values, | |||
| kb_id: knowledgeBaseId, | |||
| kb_id: values.kb_id ?? knowledgeBaseId, | |||
| page, | |||
| size: pageSize, | |||
| }); | |||
| @@ -243,6 +243,10 @@ export const useSendMessageWithSse = ( | |||
| const x = await reader?.read(); | |||
| if (x) { | |||
| const { done, value } = x; | |||
| if (done) { | |||
| console.info('done'); | |||
| break; | |||
| } | |||
| try { | |||
| const val = JSON.parse(value?.data || ''); | |||
| const d = val?.data; | |||
| @@ -256,10 +260,6 @@ export const useSendMessageWithSse = ( | |||
| } catch (e) { | |||
| console.warn(e); | |||
| } | |||
| if (done) { | |||
| console.info('done'); | |||
| break; | |||
| } | |||
| } | |||
| } | |||
| console.info('done?'); | |||
| @@ -98,6 +98,7 @@ export interface ITestingChunk { | |||
| term_similarity: number; | |||
| vector: number[]; | |||
| vector_similarity: number; | |||
| highlight: string; | |||
| } | |||
| export interface ITestingDocument { | |||
| @@ -3,3 +3,8 @@ export interface IFeedbackRequestBody { | |||
| thumbup?: boolean; | |||
| feedback?: string; | |||
| } | |||
| export interface IAskRequestBody { | |||
| questionkb_ids: string; | |||
| kb_ids: string[]; | |||
| } | |||
| @@ -27,9 +27,9 @@ const RagHeader = () => { | |||
| () => [ | |||
| { path: '/knowledge', name: t('knowledgeBase'), icon: KnowledgeBaseIcon }, | |||
| { path: '/chat', name: t('chat'), icon: MessageOutlined }, | |||
| // { path: '/search', name: t('search'), icon: SearchOutlined }, | |||
| { path: '/flow', name: t('flow'), icon: GraphIcon }, | |||
| { path: '/file', name: t('fileManager'), icon: FileIcon }, | |||
| { path: '/search', name: t('search'), icon: FileIcon }, | |||
| ], | |||
| [t], | |||
| ); | |||
| @@ -0,0 +1,38 @@ | |||
| import { MessageType } from '@/constants/chat'; | |||
| import { useTestChunkRetrieval } from '@/hooks/knowledge-hooks'; | |||
| import { useSendMessageWithSse } from '@/hooks/logic-hooks'; | |||
| import api from '@/utils/api'; | |||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | |||
| import { IMessage } from '../chat/interface'; | |||
| export const useSendQuestion = (kbIds: string[]) => { | |||
| const { send, answer, done } = useSendMessageWithSse(api.ask); | |||
| const { testChunk, loading } = useTestChunkRetrieval(); | |||
| const [sendingLoading, setSendingLoading] = useState(false); | |||
| const message: IMessage = useMemo(() => { | |||
| return { | |||
| id: '', | |||
| content: answer.answer, | |||
| role: MessageType.Assistant, | |||
| reference: answer.reference, | |||
| }; | |||
| }, [answer]); | |||
| const sendQuestion = useCallback( | |||
| (question: string) => { | |||
| setSendingLoading(true); | |||
| send({ kb_ids: kbIds, question }); | |||
| testChunk({ kb_id: kbIds, highlight: true, question }); | |||
| }, | |||
| [send, testChunk, kbIds], | |||
| ); | |||
| useEffect(() => { | |||
| if (done) { | |||
| setSendingLoading(false); | |||
| } | |||
| }, [done]); | |||
| return { sendQuestion, message, loading, sendingLoading }; | |||
| }; | |||
| @@ -1,9 +1,17 @@ | |||
| .searchPage { | |||
| // height: 100%; | |||
| } | |||
| .searchSide { | |||
| height: calc(100vh - 72px); | |||
| position: fixed !important; | |||
| // height: calc(100vh - 72px); | |||
| position: relative; | |||
| // position: fixed !important; | |||
| // top: 72px; | |||
| // bottom: 0; | |||
| :global(.ant-layout-sider-children) { | |||
| height: auto; | |||
| } | |||
| inset-inline-start: 0; | |||
| top: 72px; | |||
| bottom: 0; | |||
| .modelForm { | |||
| display: flex; | |||
| @@ -16,13 +24,29 @@ | |||
| } | |||
| .list { | |||
| width: 100%; | |||
| height: 100%; | |||
| // height: 100%; | |||
| height: calc(100vh - 152px); | |||
| overflow: auto; | |||
| } | |||
| .checkbox { | |||
| width: 100%; | |||
| } | |||
| .knowledgeName { | |||
| width: 120px; | |||
| width: 130px; | |||
| } | |||
| } | |||
| .content { | |||
| height: 100%; | |||
| .main { | |||
| width: 60%; | |||
| // background-color: aqua; | |||
| overflow: auto; | |||
| padding: 10px; | |||
| } | |||
| .graph { | |||
| width: 40%; | |||
| background-color: bisque; | |||
| } | |||
| } | |||
| @@ -1,37 +1,65 @@ | |||
| import { Layout } from 'antd'; | |||
| import React from 'react'; | |||
| import HightLightMarkdown from '@/components/highlight-markdown'; | |||
| import { ImageWithPopover } from '@/components/image'; | |||
| import MessageItem from '@/components/message-item'; | |||
| import { useSelectTestingResult } from '@/hooks/knowledge-hooks'; | |||
| import { IReference } from '@/interfaces/database/chat'; | |||
| import { Card, Flex, Input, Layout, List, Space } from 'antd'; | |||
| import { useState } from 'react'; | |||
| import { useSendQuestion } from './hooks'; | |||
| import SearchSidebar from './sidebar'; | |||
| const { Header, Content, Footer } = Layout; | |||
| import styles from './index.less'; | |||
| const { Content } = Layout; | |||
| const { Search } = Input; | |||
| const SearchPage = () => { | |||
| const [checkedList, setCheckedList] = useState<string[]>([]); | |||
| const list = useSelectTestingResult(); | |||
| const { sendQuestion, message, sendingLoading } = | |||
| useSendQuestion(checkedList); | |||
| return ( | |||
| <Layout hasSider> | |||
| <SearchSidebar></SearchSidebar> | |||
| <Layout style={{ marginInlineStart: 200 }}> | |||
| <Header style={{ padding: 0 }} /> | |||
| <Content style={{ margin: '24px 16px 0', overflow: 'initial' }}> | |||
| <div | |||
| style={{ | |||
| padding: 24, | |||
| textAlign: 'center', | |||
| }} | |||
| > | |||
| <p>long content</p> | |||
| { | |||
| // indicates very long content | |||
| Array.from({ length: 100 }, (_, index) => ( | |||
| <React.Fragment key={index}> | |||
| {index % 20 === 0 && index ? 'more' : '...'} | |||
| <br /> | |||
| </React.Fragment> | |||
| )) | |||
| } | |||
| </div> | |||
| <Layout className={styles.searchPage}> | |||
| <SearchSidebar | |||
| checkedList={checkedList} | |||
| setCheckedList={setCheckedList} | |||
| ></SearchSidebar> | |||
| <Layout> | |||
| <Content> | |||
| <Flex className={styles.content}> | |||
| <section className={styles.main}> | |||
| <Search | |||
| placeholder="input search text" | |||
| onSearch={sendQuestion} | |||
| size="large" | |||
| /> | |||
| <MessageItem | |||
| item={message} | |||
| nickname="You" | |||
| reference={message.reference ?? ({} as IReference)} | |||
| loading={sendingLoading} | |||
| index={0} | |||
| ></MessageItem> | |||
| <List | |||
| dataSource={list.chunks} | |||
| renderItem={(item) => ( | |||
| <List.Item> | |||
| <Card> | |||
| <Space> | |||
| <ImageWithPopover id={item.img_id}></ImageWithPopover> | |||
| <HightLightMarkdown> | |||
| {item.highlight} | |||
| </HightLightMarkdown> | |||
| </Space> | |||
| </Card> | |||
| </List.Item> | |||
| )} | |||
| /> | |||
| </section> | |||
| <section className={styles.graph}></section> | |||
| </Flex> | |||
| </Content> | |||
| <Footer style={{ textAlign: 'center' }}> | |||
| Ant Design ©{new Date().getFullYear()} Created by Ant UED | |||
| </Footer> | |||
| </Layout> | |||
| </Layout> | |||
| ); | |||
| @@ -1,20 +1,30 @@ | |||
| import { useNextFetchKnowledgeList } from '@/hooks/knowledge-hooks'; | |||
| import type { CheckboxProps } from 'antd'; | |||
| import { Checkbox, Layout, List, Typography } from 'antd'; | |||
| import { Avatar, Checkbox, Layout, List, Space, Typography } from 'antd'; | |||
| import { CheckboxValueType } from 'antd/es/checkbox/Group'; | |||
| import { useCallback, useMemo, useState } from 'react'; | |||
| import { | |||
| Dispatch, | |||
| SetStateAction, | |||
| useCallback, | |||
| useEffect, | |||
| useMemo, | |||
| } from 'react'; | |||
| import { UserOutlined } from '@ant-design/icons'; | |||
| import { CheckboxChangeEvent } from 'antd/es/checkbox'; | |||
| import styles from './index.less'; | |||
| const { Sider } = Layout; | |||
| const SearchSidebar = () => { | |||
| interface IProps { | |||
| checkedList: string[]; | |||
| setCheckedList: Dispatch<SetStateAction<string[]>>; | |||
| } | |||
| const SearchSidebar = ({ checkedList, setCheckedList }: IProps) => { | |||
| const { list } = useNextFetchKnowledgeList(); | |||
| const ids = useMemo(() => list.map((x) => x.id), [list]); | |||
| const [checkedList, setCheckedList] = useState<string[]>(ids); | |||
| const checkAll = list.length === checkedList.length; | |||
| const indeterminate = | |||
| @@ -31,8 +41,12 @@ const SearchSidebar = () => { | |||
| [ids], | |||
| ); | |||
| useEffect(() => { | |||
| setCheckedList(ids); | |||
| }, [ids]); | |||
| return ( | |||
| <Sider className={styles.searchSide} theme={'light'} width={260}> | |||
| <Sider className={styles.searchSide} theme={'light'} width={240}> | |||
| <Checkbox | |||
| className={styles.modelForm} | |||
| indeterminate={indeterminate} | |||
| @@ -53,12 +67,15 @@ const SearchSidebar = () => { | |||
| renderItem={(item) => ( | |||
| <List.Item> | |||
| <Checkbox value={item.id} className={styles.checkbox}> | |||
| <Typography.Text | |||
| ellipsis={{ tooltip: item.name }} | |||
| className={styles.knowledgeName} | |||
| > | |||
| {item.name} | |||
| </Typography.Text> | |||
| <Space> | |||
| <Avatar size={30} icon={<UserOutlined />} src={item.avatar} /> | |||
| <Typography.Text | |||
| ellipsis={{ tooltip: item.name }} | |||
| className={styles.knowledgeName} | |||
| > | |||
| {item.name} | |||
| </Typography.Text> | |||
| </Space> | |||
| </Checkbox> | |||
| </List.Item> | |||
| )} | |||
| @@ -20,6 +20,9 @@ | |||
| :global(.ant-card-body) { | |||
| padding: 10px 24px; | |||
| } | |||
| .addButton { | |||
| padding: 0; | |||
| } | |||
| } | |||
| .addedCard { | |||
| border-radius: 18px; | |||
| @@ -293,13 +293,20 @@ const UserSettingModel = () => { | |||
| children: ( | |||
| <List | |||
| grid={{ | |||
| gutter: 24, | |||
| gutter: { | |||
| xs: 8, | |||
| sm: 10, | |||
| md: 12, | |||
| lg: 16, | |||
| xl: 20, | |||
| xxl: 24, | |||
| }, | |||
| xs: 1, | |||
| sm: 2, | |||
| md: 3, | |||
| lg: 4, | |||
| sm: 1, | |||
| md: 2, | |||
| lg: 3, | |||
| xl: 4, | |||
| xxl: 10, | |||
| xxl: 8, | |||
| }} | |||
| dataSource={factoryList} | |||
| renderItem={(item) => ( | |||
| @@ -315,7 +322,11 @@ const UserSettingModel = () => { | |||
| </Flex> | |||
| </Flex> | |||
| <Divider className={styles.modelDivider}></Divider> | |||
| <Button type="link" onClick={() => handleAddModel(item.name)}> | |||
| <Button | |||
| type="link" | |||
| onClick={() => handleAddModel(item.name)} | |||
| className={styles.addButton} | |||
| > | |||
| {t('addTheModel')} | |||
| </Button> | |||
| </Card> | |||
| @@ -23,6 +23,8 @@ const { | |||
| deleteMessage, | |||
| thumbup, | |||
| tts, | |||
| ask, | |||
| mindmap, | |||
| } = api; | |||
| const methods = { | |||
| @@ -106,6 +108,14 @@ const methods = { | |||
| url: tts, | |||
| method: 'post', | |||
| }, | |||
| ask: { | |||
| url: ask, | |||
| method: 'post', | |||
| }, | |||
| getMindMap: { | |||
| url: mindmap, | |||
| method: 'post', | |||
| }, | |||
| } as const; | |||
| const chatService = registerServer<keyof typeof methods>(methods, request); | |||
| @@ -66,6 +66,8 @@ export default { | |||
| deleteMessage: `${api_host}/conversation/delete_msg`, | |||
| thumbup: `${api_host}/conversation/thumbup`, | |||
| tts: `${api_host}/conversation/tts`, | |||
| ask: `${api_host}/conversation/ask`, | |||
| mindmap: `${api_host}/conversation/mindmap`, | |||
| // chat for external | |||
| createToken: `${api_host}/api/new_token`, | |||
| listToken: `${api_host}/api/token_list`, | |||