feat: send question with retrieval api #2247 (#2272)

### 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)
This commit is contained in:
balibabu 2024-09-05 19:32:55 +08:00 committed by GitHub
parent 9377192859
commit 6ae0da92cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 264 additions and 78 deletions

View File

@ -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;

View File

@ -0,0 +1,10 @@
.image {
width: 100px;
object-fit: contain;
}
.imagePreview {
display: block;
max-width: 45vw;
max-height: 40vh;
}

View File

@ -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>
);
};

View File

@ -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

View File

@ -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,
});

View File

@ -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?');

View File

@ -98,6 +98,7 @@ export interface ITestingChunk {
term_similarity: number;
vector: number[];
vector_similarity: number;
highlight: string;
}
export interface ITestingDocument {

View File

@ -3,3 +3,8 @@ export interface IFeedbackRequestBody {
thumbup?: boolean;
feedback?: string;
}
export interface IAskRequestBody {
questionkb_ids: string;
kb_ids: string[];
}

View File

@ -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],
);

View File

@ -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 };
};

View File

@ -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;
}
}

View File

@ -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>
);

View File

@ -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>
)}

View File

@ -20,6 +20,9 @@
:global(.ant-card-body) {
padding: 10px 24px;
}
.addButton {
padding: 0;
}
}
.addedCard {
border-radius: 18px;

View File

@ -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>

View File

@ -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);

View File

@ -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`,